[原创]重载内核之四蓝屏与崩溃处理
重载内核的相关文章实在是太多了,鉴于还是有很多初学者研究这一块,本文仅作为一个引导作用,文笔不好,见谅。
我的博客:http://blog.csdn.net/sidyhe
开发环境:VS2010 + WinDDK
测试环境:VirtualDDK + VMware + Win7 sp1 x86
第一部分http://bbs.pediy.com/showthread.php?t=187863
第二部分http://bbs.pediy.com/showthread.php?t=187919
第三部分http://bbs.pediy.com/showthread.php?t=187982
第四部分:最后的修正(蓝屏与崩溃)
终于走到了这里,对于新的NT内核只剩下了两个问题了(其他尚未发现),第一个就是蓝屏,最麻烦的BUG,起初在我遇到的时候也是疯掉了(当时用的还是Windows 7 x86),最后发现是因为内核异常得不到处理造成的,即POOL代码决不能异常。而造成这种现象的就是SafeSEH机制。简要说明就是在内核代码中发生了异常会多一些处理,判断SEH Handler是否有效,再执行。
现在普遍的解决思路有两个:1.废除SafeSEH机制。2.使POOL合法化。
先来研究一下这个SafeSEH到底是怎么回事,在发生异常时经过一些处理后会调用RtlDispatchException,进而调用RtlIsValidHandler来判断Handler是否有效,如果无效,你懂得。
BOOLEAN RtlIsValidHandler(IN PEXCEPTION_ROUTINE Handler)
{
PULONG FunctionTable;
ULONG FunctionTableLength;
PVOID Base;
FunctionTable = RtlLookupFunctionTable(Handler, &Base, &FunctionTableLength);
if (FunctionTable && FunctionTableLength) {
PEXCEPTION_ROUTINE FunctionEntry;
LONG High, Middle, Low;
if ((FunctionTable == LongToPtr(-1)) && (FunctionTableLength == (ULONG)-1)) {
// Address is in an image that shouldn't have any handlers (like a resource only dll).
RtlInvalidHandlerDetected((PVOID)((ULONG)Handler+(ULONG)Base), LongToPtr(-1), -1);
return FALSE;
}
// Bias the handler value down by the image base and see if the result
// is in the table
(ULONG)Handler -= (ULONG)Base;
Low = 0;
High = FunctionTableLength;
while (High >= Low) {
Middle = (Low + High) >> 1;
FunctionEntry = (PEXCEPTION_ROUTINE)FunctionTable[Middle];
if (Handler < FunctionEntry) {
High = Middle - 1;
} else if (Handler > FunctionEntry) {
Low = Middle + 1;
} else {
// found it
return TRUE;
}
}
// Didn't find it
RtlInvalidHandlerDetected((PVOID)((ULONG)Handler+(ULONG)Base), FunctionTable, FunctionTableLength);
return FALSE;
}
// Can't verify
return TRUE;
}
上面代码中的RtlLookupFunctionTable是取得一个类似函数表的东西,在PE信息中的体现则是IMAGE_NT_HEADERS.OptionalHeader[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG],用我手里的内核文件做例子,通过IDA观察到这个函数表,放着一堆RVA:

这些地址就是SEH Handler了,一共有0x12=18个,再来看KLDR的定义:
typedef struct _KLDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
PVOID ExceptionTable;
ULONG ExceptionTableSize;
// ULONG padding on IA64
PVOID GpValue;
PNON_PAGED_DEBUG_INFO NonPagedDebugInfo;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT __Unused5;
PVOID SectionPointer;
ULONG CheckSum;
// ULONG padding on IA64
PVOID LoadedImports;
PVOID PatchInformation;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
很明显KLDR中的ExceptionTable应该指向IDA图示中的首地址。注意KLDR结构只能在WRK中查看,用WinDBG也看不到(dt),KLDR和LDR并不一样。还有,这个函数表里存放的就是RVA,并不会被重定位修复为VA。
那么第一种方法就是HOOK RtlIsValidHandler,直接返回TRUE,则废掉SafeSEH。这个函数没导出,隐藏的很深,字节搜索或者重定位搜索很麻烦,但也是一种方法,不过我不用这种方式,虽然处理好之后能够很好的隐藏自己的NT内核。
第二种方式就简单多了,虽然会被检测到存在异常模块,但又不影响什么,毕竟做的不是Rootkit。方法就是直接在PsLoadedModuleList插入一个KLDR,就那么简单。
PKLDR_DATA_TABLE_ENTRY KeGetImageLdrPointer(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID lpImageAddress)
{
PKLDR_DATA_TABLE_ENTRY lpTableEntry = PsLoadedModuleList;
PKLDR_DATA_TABLE_ENTRY lpTablePointer = lpTableEntry;
do
{
if (lpTablePointer->DllBase == lpImageAddress)
{
return lpTablePointer;
}
lpTablePointer = (PKLDR_DATA_TABLE_ENTRY)(lpTablePointer->InLoadOrderLinks.Flink);
} while (lpTableEntry != lpTablePointer);
return NULL;
}
BOOLEAN KeInsertPsLoadedModuleList(PKLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID NewImage, PVOID OldImage)
{
PKLDR_DATA_TABLE_ENTRY lpNewLdr, lpSimLdr;
if (lpSimLdr = KeGetImageLdrPointer(PsLoadedModuleList, OldImage))
{
if (lpNewLdr = ExAllocatePool(NonPagedPool, sizeof(KLDR_DATA_TABLE_ENTRY)))
{
RtlCopyMemory(lpNewLdr, lpSimLdr, sizeof(KLDR_DATA_TABLE_ENTRY));
lpNewLdr->DllBase = NewImage;
InsertTailList(&PsLoadedModuleList->InLoadOrderLinks, &lpNewLdr->InLoadOrderLinks);
DbgPrint("KeInsertPsLoadedModuleList:0x%p\n", lpNewLdr);
return TRUE;
}
}
return FALSE;
}
在曾想过是否要修正KLDR中的ExceptionTable,但感觉没必要,毕竟指向的函数表里面都是RVA,又不是VA,反正SEH Handler都一样,不改也罢。
好了这样就解决了SafeSEH所带来的蓝屏。基本上这个驱动就可以拿来在本机测试了。
崩溃问题
这份代码能够很好的在大部分CPU上进行工作,但有些CPU仍会发生程序崩溃的问题,这与第三部分的文章中提到的打不开程序是不同的问题。我的CPU就是如此,否则我都不会发现这个问题。
后面的东西绝对原创,网上没有哦。
一开始遇到这个问题同样使我疯掉了一段时间,通过各种途径找问题,最后奇妙的发现了这里:

在干净的系统中,当我使用ARK恢复所有的钩子后,居然出现了我所说的崩溃问题,这就说明了在系统初始化时自己Patch了自己,很明显的大家能够观察到有一个巨大的补丁,那就是一个修改了22字节的东西,这是啥玩意?WinDBG会告诉你的:
0: kd> uf 83e7dd50 nt!KeFlushCurrentTb: 83e7dd50 0f20e0 mov eax,cr4 83e7dd53 0fbaf007 btr eax,7 83e7dd57 7309 jae nt!KeFlushCurrentTb+0x12 (83e7dd62) nt!KeFlushCurrentTb+0x9: 83e7dd59 0f22e0 mov cr4,eax 83e7dd5c 0c80 or al,80h 83e7dd5e 0f22e0 mov cr4,eax 83e7dd61 c3 ret nt!KeFlushCurrentTb+0x12: 83e7dd62 0f20d9 mov ecx,cr3 83e7dd65 0f22d9 mov cr3,ecx 83e7dd68 c3 ret
从字面上不难理解KeFlushCurrentTb是刷新了TB,TB是什么我就不知道了,个人猜测是CPU中TLB与TIB的统称,看代码也知道是刷新了页表。你也可以通过IDA来查看这部分的代码,简言之就是判断了CPU的类型,如果满足了什么条件,则Patch了这个函数。再来看恢复之后的样子:
1: kd> uf 83e7dd50 nt!KeFlushCurrentTb: 83e7dd50 0f20d8 mov eax,cr3 83e7dd53 0f22d8 mov cr3,eax 83e7dd56 c3 ret
只能说这个函数在不同的CPU上可能会有不同的代码,那如何解决?难道像它一样来判断CPU进而修改这个代码?大可没有必要,我的做法就是定位到这里,然后JMP到原模块的这个地方。我想这里不会被HOOK,没有那么逆天。定位方法我也是利用的重定位,通过找到负责Patch的代码定位KeFlushCurrentTb,再JMP过去:
VOID PatchKeFlushCurrentTb(PVOID lpNewNtoskrnlAddress, PVOID lpNtoskrnlAddress)
{
/*
mov edi, offset KeFlushCurrentTb
mov esi, offset byte_477D57
mov ecx, XXX
rep movsb
*/
IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpNewNtoskrnlAddress;
IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
IMAGE_BASE_RELOCATION *lpRelocateTable = (IMAGE_BASE_RELOCATION*)((PCHAR)lpDosHeader + lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
while (lpRelocateTable->SizeOfBlock)
{
ULONG NumberOfItems = (lpRelocateTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
USHORT *lpItem = (USHORT*)((PCHAR)lpRelocateTable + sizeof(IMAGE_BASE_RELOCATION));
ULONG i;
for (i = 0; i < NumberOfItems - 1; i++)
{
if ((lpItem[i] >> 12) == IMAGE_REL_BASED_HIGHLOW && (lpItem[i + 1] >> 12) == IMAGE_REL_BASED_HIGHLOW)
{
ULONG *lpFixAddress1 = (ULONG*)((PCHAR)lpDosHeader + lpRelocateTable->VirtualAddress + (lpItem[i] & 0x0FFF));
ULONG *lpFixAddress2 = (ULONG*)((PCHAR)lpDosHeader + lpRelocateTable->VirtualAddress + (lpItem[i + 1] & 0x0FFF));
if ((ULONG)lpFixAddress2 - (ULONG)lpFixAddress1 == 5)
{
if (*((PUCHAR)lpFixAddress1 - 1) == 0xBF && *((PUCHAR)lpFixAddress2 - 1) == 0xBE)
{
PUCHAR lpCheckBytes = (PUCHAR)lpFixAddress2 + sizeof(PVOID);
if (lpCheckBytes[0] == 0xB9 && lpCheckBytes[5] == 0xF3 && lpCheckBytes[6] == 0xA4)
{
PUCHAR lpPatchAddress = (PUCHAR)*lpFixAddress1;
lpPatchAddress[0] = 0xE9;
*(ULONG*)&lpPatchAddress[1] = (ULONG)lpNtoskrnlAddress - (ULONG)lpNewNtoskrnlAddress - 5;
return;
}
}
}
}
}
lpRelocateTable = (IMAGE_BASE_RELOCATION *)((PCHAR)lpRelocateTable + lpRelocateTable->SizeOfBlock);
}
return;
}
好了,没事儿了,新的NT内核没问题了,全部搞定,如果仍发现自己解决不了的问题可以联系我,最后的DriverEntry代码:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
PVOID lpHookKiFastCallEntryAddress;
UCHAR HookCode[7];
DbgPrint("Driver Load.\n");
InitializePsLoadedModuleList(DriverObject);
g_lpNtoskrnlAddress = KeGetModuleHandle(PsLoadedModuleList, "ntoskrnl.exe");
g_lpNewNtoskrnlAddress = ReloadNtModule(PsLoadedModuleList);
g_KeServiceTable = (PVOID*)BuildKeServiceTable(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
KeFixReloc2(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
PatchKeFlushCurrentTb(g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
KeInsertPsLoadedModuleList(PsLoadedModuleList, g_lpNewNtoskrnlAddress, g_lpNtoskrnlAddress);
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));
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
这一部分告一段落,接下来继续重载win32k,搞定SHADOW SSDT。
最后,为啥PCHunter不支持Wwindows 8.1 update 1呢?现在的最新版1.32运行不到一分钟就蓝屏,原因是触发了PatchGuard,望修复,没有ARK用真心难受。
