[原创]重载内核之二接管流程
重载内核的相关文章实在是太多了,鉴于还是有很多初学者研究这一块,本文仅作为一个引导作用,文笔不好,见谅。
我的博客:http://blog.csdn.net/sidyhe
开发环境:VS2010 + WinDDK
测试环境:VirtualDDK + VMware + Win7 sp1 x86
第一部分http://bbs.pediy.com/showthread.php?t=187863
第二部分:接管流程
在第一部分完成之后,已经重载了一份新的NT内核,接下来就是接管执行流程。
自Windows XP以后从RING3进RING0统一调用sysenter汇编指令,但仍保留了INT 2E中断,在sysenter调用之后会执行内核空间中的KiFastCallEntry函数,这个函数负责所有的应用层请求,即使是INT 2E也会走这个函数,而KiFastCallEntry在经过简单的处理后会通过KeServiceDescriptorTable或者KeServiceDescriptorTableShadow来获取内核函数指针并调用,重载内核的目的就是为了在执行内核函数的时候转移到新的模块上,从而不被其他HOOK所影响。那么需要做的就是在调用内核函数的时候转移指令,但并不是所有内核函数都由ntoskrnl实现,内核中GUI函数却是由win32k.sys实现,由于目前只重载了ntoskrnl,所以只能做到转移SSDT中的函数。额外说明一下,即使NT内核有不同的版本,但所有NT内核的导出模块名称都是ntoskrnl.exe(详见输出表),所以我所说的ntoskrnl泛指NT内核,而不是具体的文件名。
在KiFastCallEntry中调用内核函数的地方大家很容易找到资料,最先是谁提出的我记不清了,反正现在是被某出名软件所使用,下面HOOK的地方与其他软件可能会冲突,所以在测试的时候需要保证没有安装任何安全软件。
用户态程序调用内核函数的流程简述为:用户态->ntdll.ZwApi->sysenter->内核态->KiFastCallEntry->ServiceTable->ServiceRoutine。ServiceTable分为两种,上面已经提到了,接下来要做的就是在调用SSDT中函数的时候转移到新模块上,在WRK中可以找到这样的代码:
mov esi, edx ; (esi)->User arguments mov ebx, [edi]+SdNumber ; get argument table address xor ecx, ecx mov cl, byte ptr [ebx+eax] ; (ecx) = argument size mov edi, [edi]+SdBase ; get service table address mov ebx, [edi+eax*4] ; (ebx)-> service routine sub esp, ecx ; allocate space for arguments shr ecx, 2 ; (ecx) = number of argument DWORDs mov edi, esp ; (edi)->location to receive 1st arg //省略部分代码 call ebx ; call system service
显然,最后一句的call指令就是调用了内核函数,而需要hook的地方就是sub esp, ecx这一句,此时,edi指向ServiceTable,eax为函数索引,ebx为函数地址,而且这一句连同下面的两句指令共7字节,可以容纳一个JMP/CALL指令,绝佳的HOOK点。至于这个地址怎么定位,就要用字节码搜索了,实在没有什么好办法,但总比直接写地址或定偏移的那种硬编码好。
PVOID __declspec(naked) _GetKiFastCallEntryAddress()
{
__asm
{
MOV ECX, 0x00000176;
RDMSR;
RETN;
}
}
PVOID FindHookKiFastCallEntryAddress(PVOID lpKiFastCallEntry)
{
/*
sub esp, ecx
shr ecx, 2
mov edi, esp
*/
UCHAR HookBytes[] = {0x2B, 0xE1, 0xC1, 0xE9, 0x02, 0x8B, 0xFC};
return RtlFindMemory(lpKiFastCallEntry, 0x300, HookBytes, sizeof(HookBytes));
}
通过MSR寄存器获取KiFastCallEntry的函数地址,有兴趣的朋友可以翻阅WRK中系统初始化部分的代码。HOOK函数这里采用了普通的MDL方式来写入只读内存,可能会有人问了,在写入地址的一瞬间别的CPU执行到了这里怎么办,我个人认为是杞人忧天,现在的CPU都有缓存机制,也就意味着内存和CPU缓存未必同步,在你写入的一瞬间即使别的CPU执行到了这里,那也是缓存中的代码,和内存中的并不一致,有兴趣的朋友可以查找CPU的TLB、TIB的资料。当然这只是降低了写入与执行冲突的几率,并不代表完全没有可能蓝屏,但话说回来,你要修改一个可执行指令,不可避免的有几率冲突,这是完全无法避免的,所以尽可能是降低几率就可以了。
VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
__asm
{
pop edx; //save ret address
sub esp, ecx;
push edx; //restore ret address
mov edx, dword ptr [edi + eax * 0x04];
shr ecx, 2;
mov edi, esp;
add edi, 0x04;
retn;
}
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT lpDriverObject, IN PUNICODE_STRING lpRegPath)
{
PVOID lpHookKiFastCallEntryAddress;
UCHAR HookCode[7];
DbgPrint("Driver Load.\n");
InitializePsLoadedModuleList(lpDriverObject);
g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
lpHookKiFastCallEntryAddress = FindHookKiFastCallEntryAddress(GetKiFastCallEntryAddress());
HookCode[0] = 0xE8;
*(ULONG*)&HookCode[1] = (ULONG_PTR)_MyKiFastCallEntryFrame - (ULONG_PTR)lpHookKiFastCallEntryAddress - 5;
HookCode[5] = 0x90;
HookCode[6] = 0x90;
RtlCopyMemoryEx(lpHookKiFastCallEntryAddress, HookCode, sizeof(HookCode));
return STATUS_SUCCESS;
}
编译并测试,通过ARK查看效果。注意!_MyKiFastCallEntryFrame是在Windows 7中测试的,XP下并不通用,因为执行上下文并不一样,XP中是mov ebx, dword ptr [edi + eax * 0x04]而Windows 7是mov edx, dword ptr [edi + eax * 0x04],这里我就不做兼容性写法了,手里也没有XP的虚拟机。XP已经停止服务了,相信没有多少人打算再使用了,如果非要兼容XP,自己做下简单的修改即可,即把edx换成ebx。

测试一段时间后并没有蓝屏现象,说明HOOK成功,但是目前并没有写任何过滤内容,下面就来丰富一下过滤函数,即所有的SSDT都走新内核。并继续考虑兼容XP。
PVOID ServiceCallFilter(PVOID *lppServiceTableBase, ULONG_PTR ServiceIndex)
{
if (lppServiceTableBase == KeServiceDescriptorTable->ServiceTableBase)
{
if (ServiceIndex == 190)
{
DbgPrint("Call NtOpenProcess\n");
}
return (ULONG_PTR)lppServiceTableBase[ServiceIndex] - (ULONG_PTR)g_lpNtoskrnlAddress + (ULONG_PTR)g_lpNewNtoskrnlAddress;
}
return lppServiceTableBase[ServiceIndex];
}
VOID __declspec(naked) _MyKiFastCallEntryFrame()
{
__asm
{
pop ebx; //save ret address
sub esp, ecx;
push ebx; //restore ret address
push eax;
push ecx;
push edx;
push eax;
push edi;
call ServiceCallFilter;
mov ebx, eax;
pop edx;
pop ecx;
pop eax;
mov edx, ebx;
shr ecx, 2;
mov edi, esp;
add edi, 0x04;
retn;
}
}
由于仅测试效果,我就暂时写190,这是NtOpenProcess的函数索引。

至此所有的SSDT中的函数已全部转移到新模块中。但这还只是一个半成品,仅做测试使用,因为后面还需要改动很多。
