本篇进行一个小实验,编写一个函数,通过特征码搜索一个未导出函数(PspTerminateProcess),并调用。看着不复杂,但是涉及到了很多细节,这里逐步分析。
什么是未导出函数
先介绍一个概念,未导出函数。什么是未导出函数呢?
这个举个简单的例子,Ntoskrnl.exe中有很多内核函数,其中包括我们之前分析过的NtReadVirtualMemory函数。这个函数是ReadProcessMemory在0环的实现,但它是一个未导出函数,用IDA打开Ntoskrnl.exe,在Exports(导出表)中搜索NtReadVirtualMemory 结果发现,并没有搜索到NtReadVirtualMemory,原因是它并没有被导出,所以导出表里面,没有该函数,但是这个函数又的确存在在Ntoskrnl.exe这个模块中。
未导出函数的主要目的,是不想提供给别人使用,官方文档也不会写入相关内容。因此,我们是不能通过函数名直接调用该函数的。
如何调用未导出函数
调用未导出函数主要有两种办法:
- 特征码搜索,这也是本篇会着重介绍的。在前一篇文章中,提到过可以通过驱动结构体的DriverSection字段,找到一个_LDR_DATA_TABLE_ENTRY结构体,然后即可遍历内核的全部模块。这时,如果我们确定了未导出函数所在的模块,便可以通过在链表中找到模块对应的描述信息,例如模块基址,大小等。这时,我们再利用这些信息,结合未导出函数的特征码,搜索这个模块的内存,便能找到所需的未导出函数。这时只需定义一个函数指针,指向咱们找到的函数首地址,就可以实现对未导出函数的调用。
- 外层函数调用,如果有一个函数A调用了未导出函数B,那么函数A中一定有未导出函数B的函数地址,我们只要找到这个调用点,就可以确定函数B的地址了。然后再定义一个函数指针,就可实现这个过程。
特征码选择
在特征码的选择上也是有讲究的,拿本次实验的PspTerminateProcess函数来举例 结合图片来看。
- 橙色方框圈住的地方就不适合作为特征码,这部分的语句,比较常见,在很多内核函数中都容易看到,因此不适合作为特征码,有可能会因此找到了别的内核函数。
- 红色方框圈住的地方相对来说就比较适合作为特征码,当然,连续四行完全相同的语句,还是比较少见的,因此,选中的代码段越多,就越合适作为特征码
有一点需要说明的是,特征码不是汇编,而是硬编码,具体应用时,可以将这些特征码放入到一个字符串中,例如:
1
| UCHAR AttrCode[6] = {0x8b, 0x75, 0x08, 0x3b, 0x70, 0x44}
|
然后与指定的内存进行逐一比较,若发现相同的地方,很有可能就找对了对方,然后再减去函数内的偏移,得到函数的首地址,就可以获取该函数了
代码实现
这部分的代码实现不是非常难,并且用到了前一篇用到的知识,这里仅再重述一遍思路:
- 通过当前驱动结构体找到内核模块结构体链表
- 遍历内核模块,找到Ntoskrnl.exe(PspTerminateProcess所在模块)模块结构体
- 获取模块基址、模块大小
- 确定特征码
- 从模块基址开始的地方,与特征码依次进行比对,从而找到未导出函数PspTerminateProcess
- 用一个函数指针指向未导出函数
- 调用函数PspTerminateProcess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| #include "ntifs.h"
typedef NTSTATUS NTKERNELAPI(*PspTerminateProcess)(PEPROCESS Process, NTSTATUS ExitStatus);
VOID Driver_Unload(PDRIVER_OBJECT driver); UINT32 FindKernelModule(PDRIVER_OBJECT driver); PVOID FindAttrCode(PDRIVER_OBJECT driver); VOID TestFunc(PDRIVER_OBJECT driver);
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING RegistryPath) {
DbgPrint("Driver is running!\n"); TestFunc(driver);
driver->DriverUnload = Driver_Unload;
return STATUS_SUCCESS; }
VOID TestFunc(PDRIVER_OBJECT driver) { PspTerminateProcess pFun; PEPROCESS pEprocess;
pFun = FindAttrCode(driver); if (pFun != NULL) { PsLookupProcessByProcessId((HANDLE)1752, &pEprocess); pFun(pEprocess, 1); DbgPrint("Close notepad success!"); } else { DbgPrint("Get Failed\n"); } }
PVOID FindAttrCode(PDRIVER_OBJECT driver) { UINT32 i = 0; UINT32 ds = FindKernelModule(driver); UINT32 ModuleBase = *(PUINT32)(ds + 0x18); UINT32 ModuleSize = *(PUINT32)(ds + 0x20); UCHAR Offset[15] = { 0x8b, 0x75, 0x08, 0x3b, 0x70, 0x44, 0x75, 0x07, 0xb8, 0x0d, 0x00, 0x00, 0xc0, 0xeb, 0x5a }; PUCHAR pModuleMemory = NULL;
__try { DbgPrint("Name: %wZ, Base: %x, Size: %x\n", (PUINT32)(ds + 0x2c), ModuleBase, ModuleSize); while (i < (ModuleSize - 0xf)) { pModuleMemory = (PUCHAR)(ModuleBase + i); if (RtlCompareMemory(Offset, pModuleMemory, 0xf) == 0xf) { return pModuleMemory - 0xc; } i++; } } __except (1) {
}
return NULL; }
UINT32 FindKernelModule(PDRIVER_OBJECT driver) { UINT32 ds, dsCurrent, dsDest; UNICODE_STRING dllStr;
RtlInitUnicodeString(&dllStr, L"ntoskrnl.exe");
ds = (UINT32)(driver->DriverSection); dsCurrent = ds;
while (1) { ds = *(PUINT32)ds; if (ds == dsCurrent) { break; } if (RtlCompareUnicodeString(&dllStr, (PUINT32)(ds + 0x2c), TRUE) == 0) { break; }
} dsDest = ds; return dsDest; }
VOID Driver_Unload(PDRIVER_OBJECT driver) { DbgPrint("Unload Success!"); }
|
结果验证
- 本次实验,计划通过调用未导出函数PspTerminateProcess关掉指定进程(本次实验采用记事本)
- 在PC Hunter确定记事本的Pid
- 写入到代码中
- 生成.sys驱动文件,复制到虚拟机。通过Kmd Manager注册
- 运行该驱动,查看结果 发现成功的关掉了记事本这个进程
总结
本次实验,通过在内存中搜索特征码,调用了一个未导出函数PspTerminateProcess,并关掉了记事本这个进程,当然我们是手动将Pid填入代码后才实现的指定进程,随着进一步学习,会逐渐改进并完善这份代码
参考文章:https://blog.csdn.net/jjjyu123/article/details/13616277
参考代码:上善若水,葫芦娃救爷爷,Joney,张嘉杰