SSDT 中文名称为系统服务描述符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行内核级的Hook,从而实现对一些核心的系统动作进行过滤、监控的目的。
通过前面的学习我们已经可以编写一个驱动程序并挂钩到指定的内核函数上了,接下来我们将一步步的通过编写驱动程序,手动的来解除 NtOpenProcess
函数的驱动保护,以此来模拟如何一步步干掉游戏保护。
一般情况下当游戏启动的时候都会加载保护,而这种保护通常都是通过在SSDT层挂钩来实现的,而一旦函数被挂钩那么通过前面的读取方式就无法读取到函数的原始地址了,如下图是一个被Hook过的函数,可以看到函数的当前地址与原始地址已经发生了明显的变化。
那么如何获取到原始函数地址呢?很简单只需要使用系统提供给我们的 MmGetSystemRoutineAddress
函数即可获取到原始函数的地址,最终测试代码如下:
#include <ntddk.h> extern "C" LONG KerServiceDescriptorTable;ULONG Get_SSDTAddr () { UNICODE_STRING NtOpen; ULONG SSDT_Addr; RtlInitUnicodeString(&NtOpen, L"NtOpenProcess" ); SSDT_Addr = (ULONG)MmGetSystemRoutineAddress(&NtOpen); DbgPrint("原始函数的地址是: %x\n" , SSDT_Addr); return SSDT_Addr; } VOID UnDriver (PDRIVER_OBJECT driver) { DbgPrint(("驱动卸载成功 ! \n" )); } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { Get_SSDTAddr(); DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
编译这段驱动代码,然后回到虚拟机并加载这段驱动,手动验证一下观察:
上方的驱动代码也可以改用汇编来实现,其效果是相同的,贴出汇编代码的实现流程,这里就不演示了。
#include <ntddk.h> extern "C" LONG KerServiceDescriptorTable;ULONG Get_SSDTAddr () { UNICODE_STRING NtOpen; ULONG SSDT_Addr; RtlInitUnicodeString(&NtOpen, L"NtOpenProcess" ); __asm { lea eax, NtOpen push eax call DWORD ptr DS:[MmGetSystemRoutineAddress] mov SSDT_Addr,eax } DbgPrint("原始函数的地址是: %x\n" , SSDT_Addr); return SSDT_Addr; } VOID UnDriver (PDRIVER_OBJECT driver) { DbgPrint(("驱动卸载成功 ! \n" )); } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { Get_SSDTAddr(); DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
通过偏移二次读取: 上面的代码运行后只能获取到一部分函数的原始地址,有些函数的地址是无法获取到的,比如我们想要获取 NtReadVirtualMemory
这个内核函数的地址时,上方的代码就会显示获取失败,如下获取结果始终显示为0。
既然无法获取到当前函数的地址,那么我们可以尝试获取NtReadVirtualMemory
函数的前一个函数的内存地址,并通过相加偏移的方式来获取该函数的地址,首先我们通过Xuetr 查询到 NtReadVirtualMemory
函数的当前地址,然后通过 WinDBG
调试器找到其对应的前一个函数的偏移。
lkd> u 83e7 f82c nt!MmCopyVirtualMemory+0x50a : 83e7 f82c 6 a18 push 18 h83e7 f82e 68285 ac783 push offset nt!NtBuildGUID+0xc9a4 (83 c75a28)83e7 f833 e870e3e1ff call nt!strchr +0x118 (83 c9dba8)83e7 f838 648b 3d24010000 mov edi,dword ptr fs:[124 h]83e7 f83f 8 a873a010000 mov al,byte ptr [edi+13 Ah]83e7 f845 8845e4 mov byte ptr [ebp-1 Ch],al83e7 f848 8b 7514 mov esi,dword ptr [ebp+14 h]83e7 f84b 84 c0 test al,al
查询结果中可以发现上一个函数的是 MmCopyVirtualMemory
而相对应的偏移地址是 0x50a
,接着直接改进上方的程序,即可实现查询,代码如下:
#include <ntddk.h> extern "C" LONG KerServiceDescriptorTable;ULONG Get_SSDTAddr () { UNICODE_STRING NtOpen; ULONG SSDT_Addr; RtlInitUnicodeString(&NtOpen, L"MmCopyVirtualMemory" ); SSDT_Addr = (ULONG)MmGetSystemRoutineAddress(&NtOpen); __asm { push eax mov eax,SSDT_Addr add eax,50 ah mov SSDT_Addr,eax } DbgPrint("原始函数的地址是: %x\n" , SSDT_Addr); return SSDT_Addr; } VOID UnDriver (PDRIVER_OBJECT driver) { KdPrint(("Uninstall Driver Is OK \n" )); } NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { Get_SSDTAddr(); DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
判断函数是否被Hook: 上方的代码中,我们可以通过使用MmGetSystemRoutineAddress
函数来获取到函数的原始地址,而在第一部分我们又通过汇编的方式得到了函数的当前地址,通过使用当前地址与原始地址做比较即可判断出函数是否被Hook。
#include <ntddk.h> #include <windef.h> extern "C" LONG KerServiceDescriptorTable;extern "C" LONG KeServiceDescriptorTable;typedef struct _JMPDATE { BYTE E9; ULONG JMPADDR; }JMPDATE; ULONG Get_Origin_SSDTAddr () { UNICODE_STRING NtOpen; ULONG SSDT_Addr; RtlInitUnicodeString(&NtOpen, L"NtOpenProcess" ); SSDT_Addr = (ULONG)MmGetSystemRoutineAddress(&NtOpen); return SSDT_Addr; } ULONG Get_Now_SSDTAddr () { ULONG SSDT_Addr; __asm{ push ebx push eax mov ebx, KeServiceDescriptorTable mov ebx, [ebx] mov eax, 0xBE imul eax, eax, 4 add ebx, eax mov ebx, [ebx] mov SSDT_Addr, ebx pop eax pop ebx } return SSDT_Addr; } VOID UnDriver (PDRIVER_OBJECT driver) { KdPrint(("驱动卸载成功 !\n" )); } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath) { ULONG Get_Origin_SSDT, Get_Now_SSDT; JMPDATE JmpDate; Get_Now_SSDT = Get_Now_SSDTAddr(); Get_Origin_SSDT = Get_Origin_SSDTAddr(); if (Get_Now_SSDT != Get_Origin_SSDT) { DbgPrint("该函数已经被Hook了! \n" ); JmpDate.E9 = 0xe9 ; JmpDate.JMPADDR = Get_Origin_SSDT - Get_Now_SSDT - 5 ; DbgPrint("写入了JMP数据=%x \n" , JmpDate.JMPADDR); }else { DbgPrint("该函数没有被Hook ! \n" ); } DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
恢复被Hook过的函数: 接下来我们通过编写驱动程序的方式恢复 NtOpenProcess
内核函数所Hook的地址,恢复Hook的原理非常的简单,只需要在函数头部添加一条Jmp xxxx
并将其跳转到原始函数地址上面去即可恢复挂钩。
在下方的代码中需要注意一条计算公式 JmpDate.JMPADDR = Get_Origin_SSDT - Get_Now_SSDT - 5;
如下使用 12345678 - 00401000 - 5
即可得到 E9
机器码后面的跳转地址 7346F411
这也是计算代码的核心。
00401000 > - E9 7346F 411 jmp 12345678
有了计算的公式,我们在前面的代码的基础上继续改进一下即可,最终代码如下:
#include <ntddk.h> #include <windef.h> extern "C" LONG KerServiceDescriptorTable;extern "C" LONG KeServiceDescriptorTable;#pragma pack(1) typedef struct _JMPDATE { BYTE E9; ULONG JMPADDR; }JMPDATE, *PJMPDATE; #pragma pack() JMPDATE Origin_Data; PJMPDATE pNow_Data; ULONG Get_Origin_SSDTAddr () { UNICODE_STRING NtOpen; ULONG SSDT_Addr; RtlInitUnicodeString(&NtOpen, L"NtOpenProcess" ); SSDT_Addr = (ULONG)MmGetSystemRoutineAddress(&NtOpen); return SSDT_Addr; } ULONG Get_Now_SSDTAddr () { ULONG SSDT_Addr; __asm{ push ebx push eax mov ebx, KeServiceDescriptorTable mov ebx, [ebx] mov eax, 0xBE imul eax, eax, 4 add ebx, eax mov ebx, [ebx] mov SSDT_Addr, ebx pop eax pop ebx } return SSDT_Addr; } VOID UnDriver (PDRIVER_OBJECT driver) { __asm { cli mov eax, cr0 and eax, not 10000 h mov cr0, eax } pNow_Data->E9 = Origin_Data.E9; pNow_Data->JMPADDR = Origin_Data.JMPADDR; __asm { mov eax, cr0 or eax, 10000 h mov cr0, eax sti } KdPrint(("驱动卸载成功 !\n" )); } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { ULONG Get_Origin_SSDT, Get_Now_SSDT; JMPDATE JmpDate; Get_Now_SSDT = Get_Now_SSDTAddr(); Get_Origin_SSDT = Get_Origin_SSDTAddr(); if (Get_Now_SSDT != Get_Origin_SSDT) { DbgPrint("该函数已经被Hook了! \n" ); pNow_Data = (PJMPDATE)(Get_Now_SSDT); Origin_Data.E9 = pNow_Data->E9; Origin_Data.JMPADDR = pNow_Data->JMPADDR; JmpDate.E9 = 0xe9 ; JmpDate.JMPADDR = Get_Origin_SSDT - Get_Now_SSDT - 5 ; DbgPrint("写入JMP的数据 = %x \n" , JmpDate.JMPADDR); __asm { cli mov eax, cr0 and eax, not 10000 h mov cr0, eax } pNow_Data->E9 = JmpDate.E9; pNow_Data->JMPADDR = JmpDate.JMPADDR; __asm { mov eax, cr0 or eax, 10000 h mov cr0, eax sti } } DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
附上汇编版本的Hook恢复代码,如下,自行替换此处不做测试了。
__asm{........} __asm { mov ebx, Get_Now_SSDT lea ecx, Origin_Data mov al, byte ptr[ebx] mov byte ptr[ecx], al mov eax, [ebx + 1 ] mov[ecx + 1 ], eax mov ebx, Get_Now_SSDT lea ecx, JmpDate mov al, byte ptr[ecx] mov byte ptr[ebx], al mov eax, [ecx + 1 ] mov[ebx + 1 ], eax } __asm{........} __asm{........} __asm { mov ebx, pNow_Data lea ecx, Origin_Data mov al, byte ptr[ecx] mov byte ptr[ebx], al mov eax, [ecx + 1 ] mov[ebx + 1 ], eax } __asm{........}
将代码编译,并拖入虚拟机加载驱动,Hook之前如图一所示,Hook之后如图二,发现程序已经跳转到了原始的代码上了,Hook被解除啦。
在任意位置写入恢复代码: 上方的代码片段虽然可以恢复浅层的Hook,但如果保护驱动Hook的较深的话需上面的代码将无法恢复,我们需要使用如下代码.
NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { BYTE Jmp_OEP[5 ] = { 0xEB ,0x28 ,0xDC ,0xC8 ,0x83 }; BYTE *NtOpen = (BYTE*)0x83C8DC28 ; __asm { cli mov eax, cr0 and eax, not 10000 h mov cr0, eax } RtlCopyMemory(NtOpen+3 , Jmp_OEP, 5 ); __asm { mov eax, cr0 or eax, 10000 h mov cr0, eax sti } DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
编译生成好代码以后,拖入虚拟机并加载驱动,观察内存变化,发现已经写入地址成功,我们可以使用该方法写入任意位置,注意堆栈平衡,否则会直接蓝屏。
给系统函数添加额外功能: 通过使用Jmp跳转指令,我们可以给相应的系统函数添加新功能,以NtOpenProcess为例
核心汇编伪代码如下,这里并没有写全,可以自行完善:
#include <ntddk.h> #include <windef.h> VOID UnDriver (PDRIVER_OBJECT driver) { KdPrint(("Uninstall Driver Is OK \n" )); } BYTE *RetAddr = NULL ; BYTE *MyHook = NULL ; __declspec(naked) VOID inline_NtOpenProcess () { __asm { mov ecx, dword ptr[ebp + 14 ] mov edx, dword ptr[ebp + 10 ] mov eax, RetAddr jmp eax } } NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { BYTE *NtOpenProcess = (BYTE*)0x83C159DC ; BYTE Jmp_Addr[6 ] = { 0xE9 , 0 , 0 , 0 , 0 ,0x90 }; __asm { push eax mov eax, NtOpenProcess add eax, 0x13 mov MyHook, eax add eax, 0x06 mov RetAddr, eax pop eax } *(ULONG *)(Jmp_Addr + 1 ) = (ULONG)inline_NtOpenProcess - ((ULONG)MyHook + 5 ); CloseProtect(); RtlCopyMemory(MyHook, Jmp_Addr, 6 ); StartProtect(); DriverObject->DriverUnload = UnDriver; return STATUS_SUCCESS; }
Hook以后截图如下,可以看到我们能够在自己的函数中为NtOpenProcess函数增加额外的功能,也可以利用该方法过掉某些游戏的驱动保护,点到为止不说了。
拓展:还原 Shadow SSDT 中被Hook的函数 Shadow SSDT的全称是 Shadow System Services Descriptor Table 影子系统服务描述符表,该表中存放的是一些与系统图形回调队列以及键盘鼠标事件相关的信息。
ServiceDescriptor中只有指向KiServiceTable的SST,是ServiceDescriptorTable是被系统所导出的表结构,而ServiceDescriptorTableShadow是未导出的,但我们依然可以通过相加偏移的方式得到其当前地址。
在网络游戏中通常会Hook挂钩 NtUserSendInput 这个内核函数,从而实现拦截用户使用能够模拟合成鼠标键盘事件操作的软件脚本精灵
,那么该怎末过保护?来直接上车。
通过WinDBG附加内核调试,然后输入以下命令,记得加载符号链接。
lkd> dd KeServiceDescriptorTable 8055 d700 80505570 00000000 0000011 c 805059e4 8055 d710 00000000 00000000 00000000 00000000 lkd> dd KeServiceDescriptorTableShadow 8055 d6c0 80505570 00000000 0000011 c 805059e4 8055 d6d0 bf9a1500 00000000 0000029b bf9a2210
KeServiceDescriptorTable - KeServiceDescriptorTableShadow 相减得到SSDT相对SSSDT的偏移地址此处的便宜地址是0x40,然后直接 dd poi(KeServiceDescriptorTable-0x40)
此处的poi命令为取出后面的内存地址。
lkd> dd KeServiceDescriptorTable - KeServiceDescriptorTableShadow 00000040 ???????? ???????? ???????? ????????00000050 ???????? ???????? ???????? ????????lkd> dd poi (KeServiceDescriptorTable-0x40 ) 80505570 805a5664 805f23ea 805f5c20 805f241c 80505580 805f5c5a 805f2452 805f5c9e 805f5ce2 lkd> u 805a5664 nt!NtAcceptConnectPort: 805a5664 689c000000 push 9Ch 805a5669 6850ab4d80 push offset nt!_real+0x118 (804 dab50) 805a566e e8cd76f9ff call nt!_SEH_prolog (8053 cd40) lkd> Dd poi (KeServiceDescriptorTable-0x40 +0x10 ) bf9a1500 bf93b025 bf94c876 bf88e421 bf9442da bf9a1510 bf94df11 bf93b2b9 bf93b35e bf839eba
上方结果显示 bf93b025
是第一个函数NtGdiAbortDoc
的地址,加上 NtUserSendInput
的序号十进制的529
转为十六进制是0x211
,然后乘以4字节即可获取到 NtUserSendInput
函数的基址,这里由于电脑管家Hook了所以显示的地址是a1ea5e9e 如果管家关闭的话这里就是了。
lkd> dd poi[KeServiceDescriptorTable-0x40 +0x10 ]+0x211 *4 bf9a1d44 a1ea5e9e bf86b7d8 bf82938b bf914622 bf9a1d54 bf80e6cb bf8921d4 bf914ae8 bf915076
既然流程都已经清楚了,还原就很简单了,附上汇编代码。
extern "C" LONG KeServiceDescriptorTable;NTSTATUS DriverEntry (PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) { ULONG Shadow_Address; ULONG NtUserSendInput; __asm { push eax push ebx push ecx mov eax, KeServiceDescriptorTable sub eax, 0x40 add eax, 0x10 mov eax, [eax] mov Shadow_Address, eax mov ecx, eax mov eax, 0x211 imul eax, eax, 4 add ecx, eax mov ebx, [ecx] mov NtUserSendInput, ebx pop ecx pop ebx pop eax } DbgPrint("KeServiceDescriptorTable地址为:%x" , Shadow_Address); DbgPrint("NtUserSendInput地址为:%x" , NtUserSendInput); driver->DriverUnload = DriverUnload; return STATUS_SUCCESS; }
恢复代码如下,只附上关键代码吧,其他的和上方基本一致。
ULONG NtUserSendInput_Now; ULONG NtUserSendInput_Ord = 0xFFFFFFFF ; __asm { cli mov eax, cr0 and eax, not 10000 h mov cr0, eax } __asm { push eax push ebx push ecx push edx mov eax, KeServiceDescriptorTable sub eax, 0x40 add eax, 0x10 mov eax, [eax] mov ecx, eax mov eax, 0x211 imul eax, eax, 4 add ecx, eax mov edx, NtUserSendInput_Ord mov[ecx], edx mov ebx, [ecx] mov NtUserSendInput_Now, ebx pop edx pop ecx pop ebx pop eax } __asm { mov eax, cr0 or eax, 10000 h mov cr0, eax sti } }