重载内核的相关文章实在是太多了,鉴于还是有很多初学者研究这一块,本文仅作为一个引导作用,文笔不好,见谅。
我的博客:http://blog.csdn.net/sidyhe
开发环境:VS2010 + WinDDK
测试环境:VirtualDDK + VMware + Windows 7 sp1 x86
第一部分:重载镜像
大家可以通过ARK工具来查看系统的内核模块,排在首位的一定是ntXXX.exe这个模块,这个模块就是自系统启动后加载的第一个模块,根据CPU及其不同特性的不同会加载不同的NT内核,如NTOSKRNL.EXE、NTKRNLMP.EXE、NTKRNLPA.EXE、NTKRPAMP.EXE。不同的NT模块代表不同的意义,如单CPU,多CPU,单CPU多核,是否支持PAE等都会影响所加载的NT内核,所以如果大家见到和别人不同的NT内核不要奇怪,那是因为你的CPU和别人的不一样。
本次技术研究仅仅是针对于x86系统的Windows 7以及Windows XP,Windows x64系统由于强制数字签名以及PatchGuard技术无法实现,故不做讨论,当然如果你有办法解决这两个问题就另当别论了。至于是否兼容Windows 8 x86就有待各位验证了。
既然要重载内核,肯定是NT内核(废话),上面说到了系统可能会加载不同名字的NT内核,那么就需要一些方法来确定当前系统所使用的内核,其中一个方法是使用ZwQuerySystemInformation传递SystemModuleInformation参数,不过在这里我不打算使用这个方法,因为太麻烦。我使用PsLoadedModuleList来确定NT内核,那问题来了,PsLoadedModuleList是一个未导出变量,这个变量记录了当前系统内核模块的信息,ZwQuerySystemInformation就是访问了PsLoadedModuleList来生成结果,如何定位这个东西呢?我不喜欢硬编码,所以我需要一种在不同系统上通用的方式来获取这个变量,经过收集资料发现在DriverEntry被调用时,第一个参数PDRIVER_OBJECT的PDRIVER_EXTENSION成员其实就是一个LDR_DATA_TABLE_ENTRY指针(参考WRK),这个与PsLoadedModuleList的类型是一致的,也就是说lpDriverObject->DriverSection是PsLoadedModuleList这个双向链表的其中一个节点,而PsLoadedModuleList是这个链表的头节点,根据大量的实践证明,lpDriverObject->DriverSection节点的下一个节点一定是PsLoadedModuleList,因为是双向循环链表嘛,那么定位这个东西就非常简单了,代码如下。
PLDR_DATA_TABLE_ENTRY PsLoadedModuleList = NULL;
VOID InitializePsLoadedModuleList(PDRIVER_OBJECT lpDriverObject)
{
PLDR_DATA_TABLE_ENTRY ldr = (PLDR_DATA_TABLE_ENTRY)lpDriverObject->DriverSection;
PsLoadedModuleList = (PLDR_DATA_TABLE_ENTRY)ldr->InLoadOrderLinks.Flink;
return;
}
找到了PsLoadedModuleList,那么链表的第一个节点就是NT内核了,可以取得文件路径,解决了重载内核的第一个问题。
接下来就是读文件数据,并把数据部署为镜像。部署的过程与RING3的镜像一致,不熟悉的朋友可以去恶补一下PE知识。读到文件数据后,把数据部署为镜像的核心代码如下:
PVOID ReloadNtModule(PLDR_DATA_TABLE_ENTRY PsLoadedModuleList)
{
PVOID lpImageAddress = NULL;
PLDR_DATA_TABLE_ENTRY NtLdr = (PLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->InLoadOrderLinks.Flink;
PVOID lpFileBuffer;
DbgPrint("Nt Module File is %wZ\n", &NtLdr->FullDllName);
if (lpFileBuffer = KeGetFileBuffer(&NtLdr->FullDllName))
{
PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)lpFileBuffer;
PIMAGE_NT_HEADERS lpNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
if (lpImageAddress = ExAllocatePool(NonPagedPool, lpNtHeader->OptionalHeader.SizeOfImage))
{
PUCHAR lpImageBytes = (PUCHAR)lpImageAddress;
IMAGE_SECTION_HEADER *lpSection = IMAGE_FIRST_SECTION(lpNtHeader);
ULONG i;
RtlZeroMemory(lpImageAddress, lpNtHeader->OptionalHeader.SizeOfImage);
RtlCopyMemory(lpImageBytes, lpFileBuffer, lpNtHeader->OptionalHeader.SizeOfHeaders);
for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
{
RtlCopyMemory(lpImageBytes + lpSection[i].VirtualAddress, (PCHAR)lpFileBuffer + lpSection[i].PointerToRawData, lpSection[i].SizeOfRawData);
}
//代码不完整,后续补充
}
ExFreePool(lpFileBuffer);
}
if (lpImageAddress) DbgPrint("ImageAddress:0x%p\n", lpImageAddress);
return lpImageAddress;
}
至此解决了第二个问题,镜像已具基本雏形,了解的朋友一定知道下一步就是修复镜像了,即处理重定位以及输入表(导入表)。修复输入表没什么,难就难在重定位,重定位中包含了代码重定位和变量重定位,既然我们做的是重载内核,那么肯定是需要让原本走NT模块的流程转移到我们的新模块上,那么可以肯定的是代码重定位一定要在新模块上,至于变量,我个人的做法是指向原模块,因为即使是重载内核,也不能保证所有执行单元都会走新模块,这样保险一些,也简单一些,不过需要注意的是,变量重定位也包含IAT,所以我这里把IAT也指向新模块,否则修复输入表就没意义了,也可以防范IAT HOOK。还有,如果重定位的地方属于“可废弃”的区段(节),可以不用处理,因为原模块已经废弃了。还有还有,内核模块的导入表不存在序号导入,所以处理起来更加简单。
BOOLEAN KeFixIAT(PLDR_DATA_TABLE_ENTRY PsLoadedModuleList, PVOID lpImageAddress)
{
IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)lpImageAddress;
IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR lpImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (ULONG)lpImageAddress);
PVOID lpModuleAddress;
while (lpImportDescriptor->Characteristics)
{
if (lpModuleAddress = KeGetModuleHandle(PsLoadedModuleList, (PCHAR)lpImageAddress + lpImportDescriptor->Name))
{
PIMAGE_THUNK_DATA lpThunk = (PIMAGE_THUNK_DATA)((ULONG)lpImageAddress + lpImportDescriptor->OriginalFirstThunk);
PVOID *lpFuncTable = (PVOID*)((ULONG)lpImageAddress + lpImportDescriptor->FirstThunk);
ULONG i;
for (i = 0; lpThunk->u1.Ordinal; i++)
{
if ((lpThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0)
{
PIMAGE_IMPORT_BY_NAME lpName = (PIMAGE_IMPORT_BY_NAME)((PCHAR)lpImageAddress + lpThunk->u1.AddressOfData);
PVOID lpFunc;
if (lpFunc = KeGetProcAddress(lpModuleAddress, lpName->Name))
{
lpFuncTable[i] = lpFunc;
}
else
{
DbgPrint("KeFixImageImportTable:Cannot found function : %s\n", lpName->Name);
return FALSE;
}
}
else
{
//impossible
}
lpThunk++;
}
}
else
{
DbgPrint("KeFixImageImportTable:Cannot found Module : %s\n", (PCHAR)lpImageAddress + lpImportDescriptor->Name);
return FALSE;
}
lpImportDescriptor++;
}
return TRUE;
}
下面是处理重定位的代码,相对比较复杂了,这里只贴出来核心代码,即如何处理具体重定位地址的部分。
VOID KeFixRelocEx(PVOID New, PVOID Old, PVOID *lpFixAddress)
{
IMAGE_DOS_HEADER *lpDosHeader = (IMAGE_DOS_HEADER*)New;
IMAGE_NT_HEADERS *lpNtHeader = (IMAGE_NT_HEADERS *)((PCHAR)lpDosHeader + lpDosHeader->e_lfanew);
ULONG_PTR RelocValue = (ULONG_PTR)*lpFixAddress - lpNtHeader->OptionalHeader.ImageBase;
if (KeFixRelocOfCheckIAT(New, (PCHAR)New + RelocValue))
{
*lpFixAddress = (PCHAR)New + RelocValue;
return;
}
else
{
IMAGE_SECTION_HEADER *lpSecHdr = IMAGE_FIRST_SECTION(lpNtHeader);
USHORT i;
for (i = 0; i < lpNtHeader->FileHeader.NumberOfSections; i++)
{
if (RelocValue >= lpSecHdr[i].VirtualAddress && RelocValue < lpSecHdr[i].VirtualAddress + lpSecHdr[i].SizeOfRawData)
{
if (lpSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE)
{
*lpFixAddress = (PCHAR)Old + RelocValue;
}
else
{
*lpFixAddress = (PCHAR)New + RelocValue;
}
return;
}
}
}
*lpFixAddress = (PCHAR)Old + RelocValue;
return;
}
至此,重新加载一份新的NT内核已经完成了绝大部分,还有一些细节没有处理,等到后面遇到时再告诉各位看客老爷,先卖个关子吧,涉及到的未公开结构可以从WRK中寻找,我所使用的结构在目前的x86系统中都没有改动,所以通用。具体工程代码先不打算放出来,如果大部分朋友需要的话,我会在后续文章中放出,不过我还是希望大家能够自己动手,这样收获会比纯粹的复制粘贴更多。
下面的内容应该是HOOK了,用来接管正常的执行流程,我会抽时间写后续内容的。
[课程]Linux pwn 探索篇!