系列索引
【原创】TP驱动保护分析系列一 定位TenProtect保护
【原创】TP驱动保护分析系列二 代码定位内核函数
【原创】TP驱动保护分析系列三 SSDT定位内核函数
前言
前面在【原创】TP驱动保护分析系列一 定位TenProtect保护中分析并找到了TenProtect在内核层所做的手脚,但只通过windbg等工具找到可不够,接下来继续介绍代码定位内核函数的方法
代码定位内核函数
驱动函数直接定位
在驱动开发引入的<wdm.h>
里提供了MmGetSystemRoutineAddress
函数,根据函数名可以直接获取到已导出的函数地址
该函数的官方文档直达:MmGetSystemRoutineAddress function (wdm.h) - Windows drivers | Microsoft Docs
这里再简单介绍(翻译理解)一下:
函数原型
PVOID MmGetSystemRoutineAddress(
PUNICODE_STRING SystemRoutineName
);
函数名 |
MmGetSystemRoutineAddress |
函数功能 |
驱动程序可以使用这个Routine来确定一个Routine在特定版本的Windows上是否可用。它只能用于内核或HAL导出的例程,不能用于任何驱动程序定义的Routine |
参数名 |
SystemRoutineName |
参数名说明 |
想要获取的系统Routine的名称 |
参数类型 |
PUNICODE_STRING |
参数类型说明 |
字符串,定义如下 |
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
#ifdef MIDL_PASS
[size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer;
#else // MIDL_PASS
_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
函数的使用
UNICODE_STRING s; //声明字符串
CHAR* string = L"RoutineName"; //要赋值的字符串 注意前面字符串前要加上L L告示编译器使用两个字节的 unicode 字符集。
RtlInitUnicodeString(&s, string); //初始化unicode字符串
MmGetSystemRoutineAddress(&s); //使用函数
支持直接定位的函数
前面提到了MmGetSystemRoutineAddress
只能用于内核或HAL导出的例程,也就是只能获取到内核中的导出函数
如何知道内核导出了哪些函数?
找到C:\WINDOWS\system32\ntkrnlpa.exe
的内核文件
PS:根据操作系统版本的不同,对应的内核文件也可能不同,如在虚拟机XP系统下内核文件为ntkrnlpa.exe,而在实机WIN10下对应的内核文件为ntoskrnl.exe
除此之外,还有两种内核文件:ntkrnlmp.exe 和ntkrpamp.exe
这四种内核文件的区别如下:
ntoskrnl - 单处理器,不支持PAE
ntkrnlpa - 单处理器,支持PAE
ntkrnlmp - 多处理器,不支持PAE
ntkrpamp - 多处理器,支持PAE
PAE全称Physical Address Extension,即物理地址拓展,是x86处理器的一个功能,让中央处理器在32位操作系统下访问超过4GB的物理内存
使用IDA Pro打开对应的内核文件,得到:
点击右上角的Exports,查看导出(不仅包含导出的函数,也包含了导出的变量等):
定位代码样例
下面以NtOpenProcess
函数为例,通过MmGetSystemRoutineAddress
定位函数地址
代码
#include "ntddk.h"
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸载完成!\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNICODE_STRING s;
CHAR* string = L"NtOpenProcess";
RtlInitUnicodeString(&s, string);
PVOID address=MmGetSystemRoutineAddress(&s);
DbgPrint("address:%p\n", address);
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
运行结果
验证结果
得到的地址为:805CC486
使用windbg
查看对应地址的反汇编:
u 805CC486
可以验证得到的就是NtOpenProcess
的地址
特征码定位
当想要定位的内核函数没有导出,但其在可获得地址的附近时,则可使用特征码定位法
所谓的特征码定位法,就是选一个地址作为搜索的起始地址,然后开始逐一比对 匹配的特征码(字节码)
PsTerminateProcess
下面以PsTerminateProcess
函数为例,介绍特征码定位法
确定起始地址
要使用特征码定位PsTerminateProcess
函数,首先要确定一个起始地址开始搜索该函数
于是使用IDA Pro查看PsTerminateProcess
附近的函数:
可以发现PsTerminateProcess
函数的上面一个函数PsTerminateSystemThread
正好是导出函数:
于是便可直接使用PsTerminateSystemThread
作为起始地址
确定特征码
确定了起始位置后,便要确定特征码了
所谓特征码:关键就是要突出特征,即最好能够保持unique(独一无二),并且稳定(固定不变)
使用Windbg查看PsTerminateProcess对应的反汇编
u PsTerminateProcess
得到:
即:
nt!PsTerminateProcess:
805d35c2 8bff mov edi,edi
805d35c4 55 push ebp
805d35c5 8bec mov ebp,esp
805d35c7 5d pop ebp
805d35c8 e9b5feffff jmp nt!PspTerminateProcess (805d3482)
805d35cd cc int 3
805d35ce cc int 3
805d35cf cc int 3
这里选取的特征码为:8b ff 55 8b ec 5d e9
稍微说明一下为什么不选取jmp nt!PspTerminateProcess
指令后面的b5feffff
jmp XXXX 对应的 硬编码为 e9 offset,e9代表jmp,后面的offset为偏移地址
实际要跳转的地址(XXXX) = 当前地址 + offset +当前指令长度
代入这里: 要跳转的地址(805d3482) = 805d35c8(当前地址) + fffffeb5(offset 小端存储) + 5(当前指令长度)
即 805d3482 = 805d35c8 + fffffeb5+ 5
也就是 805d3482-805d35c8-5 = fffffeb5
这里的fffffeb5是一个有符号负数,可以直接用系统自带的计算器来验证
(关于有符号数、无符号数可回顾:逆向基础笔记二 数据宽度和逻辑运算)
而偏移量是不固定的,即不满足特征码的稳定性要素,故不将后面字节码作为特征码
代码
确定了起始位置和特征码后,就可以写代码实现特征码定位了,代码如下:
#include "ntddk.h"
/*
获取指定地址的数值
base:要获取的地址
offset:偏移量
size:获取的大小
*/
PVOID GetAddrValue(PVOID* base, INT offset, INT size) {
PVOID Addr = *base;
PVOID templong = 0;
//复制内存,将指定位置的内存的值读取出来
RtlCopyMemory(&templong, (PUCHAR)Addr + offset, size);
return templong;
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸载完成!\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
//通过MmGetSystemRoutineAddress直接获取PsTerminateSystemThread地址
UNICODE_STRING s;
CHAR* string = L"PsTerminateSystemThread";
RtlInitUnicodeString(&s, string);
PVOID address = MmGetSystemRoutineAddress(&s);
DbgPrint("PsTerminateSystemThread Address:%p\n", address);
//将PsTerminateSystemThread作为起始地址
ULONG beginAdress =(ULONG) address;
//搜索范围限制为0x1000
ULONG endAdress = beginAdress + 0x1000;
//刚开始匹配地址为0,表示尚未匹配到
PVOID matchAdress = 0;
//开始循环比较 搜索特征码
for (ULONG i=beginAdress;i< endAdress;i+=1)
{
PVOID value = GetAddrValue(&i, 0, 4);
PVOID value2 = GetAddrValue(&i, 0, 3);
//特征码为8b ff 55 8b ec 5d e9 小端存储
if (value == 0x8b55ff8b && value2 == 0xe95dec) {
//匹配特征码后 赋值,终止循环
matchAdress = i;
break;
}
}
DbgPrint("PsTerminateProcess Address:%p\n", address);
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
运行结果
验证结果
得到的地址为:805D3594
使用windbg
查看对应地址的反汇编:
u 805D3594
可以验证得到的就是PsTerminateProcess
的地址
总结
本篇介绍了代码定位内核函数的两种方法:MmGetSystemRoutineAddress
直接定位和特征码定位
MmGetSystemRoutineAddress
只能定位到导出的内核函数,比较局限,但胜在方便稳定
- 特征码定位要注意特征码选取需要满足unique(独一无二)和稳定(固定不变)
限于篇幅,本章就先到这里,后续会再继续介绍SSDT定位法和符号表PDB解析法
PS:还有一种PE文件导出表扫描法在PE文件笔记十四 导出表中已说明过,就不再赘述了
在介绍完这些定位方法后,就会针对TenProtect实例进行分析定位并绕过,敬请期待( •̀ ω •́ )✧
附件
最后附上本篇中用到的用具和最后编译出的驱动文件
包括:
- 驱动加载工具:InstDrv 1.3 汉化版
- 调试信息查看工具:DbgView
- 特征码定位编译出的驱动文件:GetKernelAddress.sys
PS:特征码定位中用到了MmGetSystemRoutineAddress
,故不单独提供驱动函数直接定位的驱动文件
DbgView可直接到微软官方下载:DebugView - Windows Sysinternals | Microsoft Docs
加载工具和驱动文件下载:点我下载
务必注意,驱动运行环境为Windows XP!!!WIN7和WIN7以上版本的操作系统不支持
学习交流为主,请勿用于其他非法途径