0x00 前言
参考文章:
https://blog.csdn.net/whatday/article/details/14160875
https://blog.csdn.net/pjz969/article/details/8615085
首先重载内核不是什么新技术,对于绕过HOOK是一种一刀切的做法,不过对于我这样初探内核的小白还是很有总结意义的。
本篇涉及的知识点:0环和3环通信,PE结构(除了基础的头外还有重定位表),用到了两种HOOK(SSDT HOOK,Inline HOOK),API函数调用(逆了一个函数KiFastCallEntry)。
流程:
1)按照字节对齐把文件中的PE结构在内存里展开
2)利用重定位表修改全局变量
3)COPY自己的SSDT表
4)函数调用时换成新的内核(HOOK KiFastCallEntry)
0x01 展开PE结构
因为我的环境是win7 32位,所以要把ntkrnlpa.exe文件在内存里展开.
1. 首先,在内核里使用RtlInitUnicodeString创建对象,打开文件使用ZwCreateFile,并且要初始化InitializeObjectAttributes。
2. 通过PE结构的知识,DOS头,PE头,节表,是紧挨在一起的无需扩展,只需确定在文件中的偏移和大小利用ZwReadFile即可获取:
1)DOS头的偏移就是0,大小为sizeof(IMAGE_DOS_HEADER);
2)PE头的偏移由DOS最后一个成员确定ImageDosHeader.e_lfanew,大小为sizeof(IMAGE_NT_HEADER);
3)节表的偏移为+= sizeof(IMAGE_NT_HEADERS),因为节表里HEADER的数量不确定,所以需要到
IMAGE_NT_HEADER->IMAGE_FILE_HEADER.NumberOfSection确定节的数目,所以大小为 sizeof(IMAGE_SECTION_HEADER) * 前者。
4)每个节的偏移由
IMAGE_SCTION_HEADER->SectionHeader[i].VirTualAddress(相对于文件开头),
节的大小需要判断:比较SectionHeader[i].Misc.VirTualSize(实际的大小)和SectionHeader[i].SizeOfRawData(按字节对齐 时文件中的大小),因为全局变量的关系,所以两者大小不一定,取大的那个。
3. 第三,就是用RtlCopyMemory一段一段COPY DOS头,PE头,节表,和每个节。
void LoadKernel()
{
NTSTATUS status;
UNICODE_STRING uFileName;
HANDLE hFile;
OBJECT_ATTRIBUTES ObjAttr;
IO_STATUS_BLOCK IoStatusBlock;
LARGE_INTEGER FileOffset;
ULONG retsize;
PVOID lpVirtualPointer;
ULONG uLoop;
ULONG SectionVirtualAddress, SectionSize;
IMAGE_DOS_HEADER ImageDosHeader;
IMAGE_NT_HEADERS ImageNtHeader;
IMAGE_SECTION_HEADER* lpImageSectionHeader;
InitializeObjectAttributes(&ObjAttr, \
& uFileName, \
OBJ_CASE_INSENSITIVE, \
NULL, \
NULL);
RtlInitUnicodeString(&uFileName, L"\\??\\C:\\WINDOWS\\system32\\ntkrnlpa.exe");
//打开文件
status = ZwCreateFile(\
& hFile, \
FILE_ALL_ACCESS, \
& ObjAttr, \
& IoStatusBlock, \
0, \
FILE_ATTRIBUTE_NORMAL, \
FILE_SHARE_READ, \
FILE_OPEN, \
FILE_NON_DIRECTORY_FILE, \
NULL, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("CreateFile Failed!"));
return;
}
//读取DOS头
FileOffset.QuadPart = 0;
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
& ImageDosHeader, \
sizeof(IMAGE_DOS_HEADER), \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageDosHeader Failed!"));
ZwClose(hFile);
return;
}
//读取NT头
FileOffset.QuadPart = ImageDosHeader.e_lfanew;
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
& ImageNtHeader, \
sizeof(IMAGE_NT_HEADERS), \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageNtHeaders Failed!"));
ZwClose(hFile);
return;
}
//读取节表
lpImageSectionHeader = (IMAGE_SECTION_HEADER*)ExAllocatePool(NonPagedPool, \
sizeof(IMAGE_SECTION_HEADER) * ImageNtHeader.FileHeader.NumberOfSections);
FileOffset.QuadPart += sizeof(IMAGE_NT_HEADERS);
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
lpImageSectionHeader, \
sizeof(IMAGE_SECTION_HEADER) * ImageNtHeader.FileHeader.NumberOfSections, \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageSectionHeader Failed!"));
ExFreePool(lpImageSectionHeader);
ZwClose(hFile);
return;
}
//COPY数据到内存
lpVirtualPointer = ExAllocatePool(NonPagedPool, \
ImageNtHeader.OptionalHeader.SizeOfImage);
if (lpVirtualPointer == 0)
{
KdPrint(("lpVirtualPointer Alloc space Failed!"));
ZwClose(hFile);
return;
}
memset(lpVirtualPointer, 0, ImageNtHeader.OptionalHeader.SizeOfImage);
//COPY DOS头
RtlCopyMemory(lpVirtualPointer, \
& ImageDosHeader, \
sizeof(IMAGE_DOS_HEADER));
//COPY NT头
RtlCopyMemory((PVOID)((ULONG)lpVirtualPointer + ImageDosHeader.e_lfanew), \
& ImageNtHeader, \
sizeof(IMAGE_NT_HEADERS));
//COPY 区表
RtlCopyMemory((PVOID)((ULONG)lpVirtualPointer + ImageDosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS)), \
lpImageSectionHeader, \
sizeof(IMAGE_SECTION_HEADER) * ImageNtHeader.FileHeader.NumberOfSections);
//依次COPY 各区段数据
for (uLoop = 0;uLoop < ImageNtHeader.FileHeader.NumberOfSections;uLoop++)
{
SectionVirtualAddress = lpImageSectionHeader[uLoop].VirtualAddress;//对应区段相对偏移
if (lpImageSectionHeader[uLoop].Misc.VirtualSize > lpImageSectionHeader[uLoop].SizeOfRawData)
SectionSize = lpImageSectionHeader[uLoop].Misc.VirtualSize;//取最大的占用空间
else
SectionSize = lpImageSectionHeader[uLoop].SizeOfRawData;
FileOffset.QuadPart = lpImageSectionHeader[uLoop].PointerToRawData;//对应区段的超始地址
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
(PVOID)((ULONG)lpVirtualPointer + SectionVirtualAddress), \
SectionSize, \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("SectionData Read Failed!"));
ExFreePool(lpImageSectionHeader);
ExFreePool(lpVirtualPointer);
ZwClose(hFile);
return;
}
}
ExFreePool(lpImageSectionHeader);//释放区段内存空间
KdPrint(("lpVirtualPointer: %X", lpVirtualPointer));
FixBaseRelocTable(lpVirtualPointer);
SetNewSSDT(lpVirtualPointer);
ZwClose(hFile);//关闭句柄
}
0x02 基址重定位
如果变量都是由镜像地址+偏移算的话,那直接用就行,但是,全局变量的地址确实直接储存的,它不管镜像在哪,反正就是那个数,所以需要通过重定位表依次手动修改。
- 首先通过PE结构定位到重定位表
IMAGE_NT_HEADER->OptionalHeader.DataDirArry[IMAGE_DIRECTORY_ENTRY_BASERELOC]
2)
它的结构:
一行是4字节,一个TyOffset成员是2字节,全局变量的地址的地址 对 镜像基地址 的偏移依次是VirtualAddress + TyOffset[i],这里储存的都是全局变量的地址(这里一种是3型的地址,这种是全局变量有用的,其他类型的只是为了使字节对齐而填充的,没用),之后依次修改这里的地址(即:加上内核首地址到imagebase的偏移)。
注意: SDK10里TypeOffset[1] 被注释掉了
这种写法是不定长数组,用0标识数组末尾,最后除了用指针也没有更好的办法。解决代码如下:
typedef struct
{
USHORT offset : 12;
USHORT type : 4;
}TypeOffset;
TypeOffset* pTypeOffset = (TypeOffset*)(pImageBaseRelocation + 1);
最后附上代码:
void FixBaseRelocTable(PVOID pNewImage)
{
ULONG OriginalImageBase;
ULONG uRelocTableSize;
ULONG* uRelocAddress;
ULONG uIndex;
PIMAGE_DOS_HEADER pImageDosHeader;
PIMAGE_NT_HEADERS pImageNtHeader;
IMAGE_DATA_DIRECTORY ImageDataDirectory;
IMAGE_BASE_RELOCATION* pImageBaseRelocation;
//将新内核地址作为一个PE头,寻找重定位头
pImageDosHeader = (PIMAGE_DOS_HEADER)pNewImage;
//定位到PE头
pImageNtHeader = (PIMAGE_NT_HEADERS)((ULONG)pNewImage + pImageDosHeader->e_lfanew);
//定位到可选PE头里拿到ImageBase
OriginalImageBase = pImageNtHeader->OptionalHeader.ImageBase;
//定位到可选PE头的DataDirArray里的重定位表
ImageDataDirectory = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
//重定位表的实际地址
pImageBaseRelocation = (PIMAGE_BASE_RELOCATION)((ULONG)pNewImage + ImageDataDirectory.VirtualAddress);
if (pImageBaseRelocation == NULL)
{
return;
}
while (pImageBaseRelocation->SizeOfBlock)
{
typedef struct
{
USHORT offset : 12;
USHORT type : 4;
}TypeOffset;
TypeOffset* pTypeOffset = (TypeOffset*)(pImageBaseRelocation + 1);
uRelocTableSize = (pImageBaseRelocation->SizeOfBlock - 8) / 2;
for (uIndex = 0; uIndex < uRelocTableSize; uIndex++)
{
if (pTypeOffset[uIndex].type == 3)
{
uRelocAddress = (ULONG*)(pTypeOffset[uIndex].offset + pImageBaseRelocation->VirtualAddress + (ULONG)pNewImage);
*uRelocAddress = *uRelocAddress + (OrigImage - OriginalImageBase);
}
}
pImageBaseRelocation = (IMAGE_BASE_RELOCATION*)((ULONG)pImageBaseRelocation + pImageBaseRelocation->SizeOfBlock);
}
}
0x03 SSDT重定位
SSDT重定位就相对比较简单了。首先通过新内核地址-老内核地址,得到相对偏移,之后老内核的ssdt指针加上相对偏移,得到新内核的ssdt指针,把系统服务表的成员依次填充:
NumberOfServices 是函数的数量,不用偏移,ServiceTableBase是函数地址表的指针,需要加上偏移,表里的每个成员依次加上偏移。
见代码:
VOID SetNewSSDT(PVOID pNewImage)
{
ULONG uNewKernelInc;
ULONG uOffset;
ULONG i;
//实际地址相对位移
uNewKernelInc = (ULONG)pNewImage - OrigImage;
//新的SSDT地址
pNewSSDT = (ServiceDescriptorTableEntry_t*)((ULONG)&KeServiceDescriptorTable + uNewKernelInc);
if (!MmIsAddressValid(pNewSSDT))//判断新的SSDT地址是否合法
{
KdPrint(("pNewSSDT is unaviable!"));
return;
}
//数量
pNewSSDT->NumberOfSerivce = KeServiceDescriptorTable.NumberOfSerivce;
//表相对的位置
uOffset = (ULONG)KeServiceDescriptorTable.ServiceTableBase - OrigImage;
//新函数地址
pNewSSDT->ServiceTableBase = (unsigned int*)((ULONG)pNewImage + uOffset);
if (!MmIsAddressValid(pNewSSDT->ServiceTableBase))
{
KdPrint(("pNewSSDT->ServiceTableBase: %X", pNewSSDT->ServiceTableBase));
return;
}
for (i = 0; i < pNewSSDT->NumberOfSerivce; i++)
{
pNewSSDT->ServiceTableBase[i] += uNewKernelInc;
}
}
0X04 函数调用时换成新的内核(HOOK KiFastCallEntry)
现在已经重载好了文件,搭好了“来时的路”,但是,当请求来的时候,依然不会上咱们的道,还是走原来的“路”,所以我们就需要“半路打劫”来HOOK KiFastCallEntry走上咱们的SSDT。那么问题来了
- 如何找到KiFasrCallEntry,来HOOK?
- 到底HOOK KiFasrCallEntry的那个位置?
为了解决这两个问题,有必要逆一下KiFasrCallEntry这个函数
(详细的分析可以参考之前写的《API函数调用的过程》那篇)
以下是逆向分析完的结果:
///////////////--KiFastCallEntry--/////////////////
nt!KiFastCallEntry+0x8d:
804df781 8bf8 mov edi,eax//取出系统服务号
804df783 c1ef08 shr edi,8 //服务号右移8为
804df786 83e730 and edi,30h//检测第12位(第13个数)
//如果为1:0011 0000 & 0001 0000为0x10
//如果为0:0011 0000 & 0000 0000为0x00
804df789 8bcf mov ecx,edi//ecx存0h或10h
804df78b 03bee0000000 add edi,dword ptr [esi+0E0h]
//_KTHREAD+0xE0 -->ServiceTable(系统服务表)
//如果ecx里是0h则指向第一张表 --->Ntoskrl.exe里的函数
//如果ecx里是10h则指向第二张表 --->Win32k.exe里的函数
804df791 8bd8 mov ebx,eax
804df793 25ff0f0000 and eax,0FFFh//只看后12位
804df798 3b4708 cmp eax,dword ptr [edi+8]
// _SYSTEM_SERVICE_TABLE
{
ServiceTableBase;//指向系统服务函数地址表
ServiceCounterTableBase;
NumberOfService;//服务函数的个数
ParamTableBase;//参数表基址
}
//+8是数量,换句话说 判断编号是否超范围
804df79b 0f8341fdffff jae nt!KiBBTUnexpectedRange (804df4e2)
//超过就越界
804df7a1 83f910 cmp ecx,10h
//判断是否是第二长系统服务表
804df7a4 751a jne nt!KiFastCallEntry+0xcc (804df7c0)
//就进win32k.exe了
//////////////////////////////////////////////////////////////////////////////
804df7a6 8b0d18f0dfff mov ecx,dword ptr ds:[0FFDFF018h]
804df7ac 33db xor ebx,ebx
804df7ae 0b99700f0000 or ebx,dword ptr [ecx+0F70h]
804df7b4 740a je nt!KiFastCallEntry+0xcc (804df7c0)
804df7b6 52 push edx
804df7b7 50 push eax
804df7b8 ff1564b25580 call dword ptr [nt!KeGdiFlushUserBatch (8055b264)]
804df7be 58 pop eax
804df7bf 5a pop edx
/////////////////////////////////////////////////////////////////////////////////////////
//如果是第一张系统服务表的话,跳到这来
804df7c0 ff0538f6dfff inc dword ptr ds:[0FFDFF638h]
804df7c6 8bf2 mov esi,edx//edx里存放着3环传入参数的指针
804df7c8 8b5f0c mov ebx,dword ptr [edi+0Ch]
//edi-->ServiceTable(系统服务表),ServiceTable(系统服务表)+0x0C-->参数表的地址
804df7cb 33c9 xor ecx,ecx
804df7cd 8a0c18 mov cl,byte ptr [eax+ebx]
//参数地址 + 编号
804df7d0 8b3f mov edi,dword ptr [edi]
//edi--->ServiceTableBase系统服务函数地址表
804df7d2 8b1c87 mov ebx,dword ptr [edi+eax*4]
//地址 + 4 * 编号 就是 要调用的0环的函数
804df7d5 2be1 sub esp,ecx
//参数有几个字节栈就擡多高
804df7d7 c1e902 shr ecx,2
//与下面的rep一起看,因为下面是dword,所以这里要/4
804df7da 8bfc mov edi,esp
//设置要copy的目的地 edi本身就有这个含义
804df7dc 3b35d40b5680 cmp esi,dword ptr [nt!MmUserProbeAddress (80560bd4)]
//判断你存放参数的地址是否是3环地址范围内
804df7e2 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (804df990)
/////////
804df7e8 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
//开始把传入的参数copy到栈里
804df7ea ffd3 call ebx//终于调用函数啦!!!
804df7ec 8be5 mov esp,ebp
804df7ee 8b0d24f1dfff mov ecx,dword ptr ds:[0FFDFF124h]
804df7f4 8b553c mov edx,dword ptr [ebp+3Ch]
804df7f7 899134010000 mov dword ptr [ecx+134h],edx
第一个问题:如何定位?
因为无论调用哪个API(就用OpenProcess),都是通过call ebx调用的,此时的栈里(ebp+0x04)已经存有下一条指令的地址,因此只需要随便HOOK一个API函数,通过这个地址我们就可以进入KiFastCallEntry这个函数
用汇编写就可以:
ULONG retaddr;
UCHAR* p = NULL;
int i = 0;
__asm
{
pushad
mov eax, [ebx + 0x04]
mov retaddr, eax
popad
}
p = (UCHAR*)retaddr;
第二个问题:HOOK的位置?
KiFastCallEntry中的指令,真是寸士寸金
首先不能在call ebx之后,那就重头看 1.首先判断是用了哪个系统服务表,这块不能动;2.之后是一层一层用系统服务号算要调用的地址,3.之后是传入3环的参数,并且寄存器也不能瞎改,看看360大厂选的位置:
804df7d5 2be1 sub esp,ecx
804df7d7 c1e902 shr ecx,2
着实厉害,eax里存的是编号,ebx是函数的地址,edi里是地址表的指针,该搭的车都搭了。
--总结一下思路:
- 先通过SSDT_HOOK HOOK OpenProcess函数找到进入KiFastCallEntry函数的地址。
- 向上搜索找到硬编码为2be1 c1e902的指令作为进行InLine_HOOK的位置来HOOK KiFastCallEntry。
- 写自己的MyKiFastCallEntry()
注意:
函数调用完毕之后栈控件布局,指令pushad将
32位的通用寄存器保存在栈中,栈空间布局为:
[esp + 00] <=> eflag
[esp + 04] <=> edi
[esp + 08] <=> esi
[esp + 0C] <=> ebp
[esp + 10] <=> esp
[esp + 14] <=> ebx <<-- 使用函数返回值来修改,把原来的函数换成自己的函数
[esp + 18] <=> edx
[esp + 1C] <=> ecx
[esp + 20] <=> eax
附上代码:
ULONG display(ULONG ServiceTableBase, ULONG FuncIndex, ULONG OrigFuncAddress)
{
if (ServiceTableBase == (ULONG)KeServiceDescriptorTable.ServiceTableBase)
{
if (!strcmp((char*)PsGetCurrentProcess() + 0x174, "cheatengine-i38"))
{
return pNewSSDT->ServiceTableBase[FuncIndex];//用咱自己的ssdt
}
}
return OrigFuncAddress;
}
void __declspec(naked) MyFastCallEntry()
{
__asm
{
pushad
pushfd
push ebx//ebx里的ssdt
push eax//ssdt [Index]编号
push edi//ServiceTable(系统服务表)
call display
mov[esp + 0x14], eax
/*
函数调用完毕之后栈控件布局,指令pushad将
32位的通用寄存器保存在栈中,栈空间布局为:
[esp + 00] <=> eflag
[esp + 04] <=> edi
[esp + 08] <=> esi
[esp + 0C] <=> ebp
[esp + 10] <=> esp
[esp + 14] <=> ebx <<-- 使用函数返回值来修改,把原来的ssdt换成自己的ssdt
[esp + 18] <=> edx
[esp + 1C] <=> ecx
[esp + 20] <=> eax
*/
popfd
popad
sub esp, ecx
shr ecx, 2
jmp jmp_ret
}
}
0x05 总结
我是跟着http://bbs.pediy.com/showthread.php?t=177555这个文章的大佬学下来的,加上了一些我学这份代码时遇到的问题(和解决方案),和作为小白不理解的地方(和解释),有不正确的地方,还请各位大佬斧正。(手动滑稽)