一个月前,曾更过一篇博客,简要介绍了使用Hook手段实现对某社交软件实时聊天内容的输出打印。那次实践使用的手段就是Inline Hook,在那篇文章中也比较详细的介绍了如何编写Inline Hook的代码。本篇不再赘述Hook具体细节,简要说明原理并演示Hook过程。
Inline Hook原理
无论是3环还是0环,Inline Hook的原理是一样的:
- 通过修改函数中的指令,改变程序执行的流程
- 跳转到自己定义的代码段中执行
- 最后再返回到原始函数中
注意:Hook期间修改掉的原始函数中的代码部分要写入到自身程序中,以保证程序能够完整运行。在Hook结束后(获取自身想要的数据或执行部分功能后),再将修改的部分字节写回到原本的地方,完成Unhook,实现无痕Hook。
Inline Hook SSDT思路
本篇通过Inline Hook SSDT,实现和SSDT Hook同样的功能(定义SSDT结构、找到SSDT表、修改物理页属性),相同部分将省略,可参考前一篇SSDT Hook中的代码。
选择Hook点
SSDT Hook和Inline Hook的最大区别在于,SSDT Hook仅需要修改系统服务表中的地址即可,而Inline Hook需要找到相应的函数,修改函数中的部分指令,让新的指令执行时会跳转到自己的函数。通常情况下,Inline Hook会根据需要修改字节的大小,选择不同的修改指令,主要有以下三种:
1
| 2. push xxxxxxxx/retn (6字节)
|
1
| 3. mov eax, xxxxxxxx/jmp eax (7字节)
|
本次实验选取的Hook函数仍然是NtOpenProcess
个人习惯使用6字节的指令进行修改,这里选取了距离NtOpenProcess函数起始地址0x14偏移处的6个字节指令,这6个字节刚好对应3行指令。适合作为Hook点。
构造自己的函数
与SSDT Hook不同,Inline Hook不需要构造一个类似的MyNtOpenProcess写入到系统服务表中,因为本身执行的就是NtOpenProcess函数,只是执行时会跳转到我们的代码中继续执行,所以我们需要构造一个自己的函数,用于实现部分功能。
通常情况下,自己实现的函数采用裸函数的形式。这样编译器不会自动帮你生成例如:
1 2 3 4 5
| _asm { push ebp mov ebp, esp sub esp, 0x?? }
|
这样的指令。因为这样会导致堆栈发生变化,获取参数会变得困难。
本次实验实现的功能,同样是打印NtOpenProcess传入进来的参数。那么就有一个新的问题?如何获取参数呢?这里涉及到3环逆向的一些基本知识,这里只做简要分析 观察NtOpenProcess部分反汇编,可以发现,在Hook点之前,并没有参数通过常用的传参寄存器(ebx,eax,ecx)传入参数,后面部分的汇编,已经给eax/ecx赋值了,说明参数不是通过寄存器传参的,所以一定是通过堆栈传参的。这样就很方便了,我们可以通过[ebp+0x8],[ebp+0xc],[ebp+0x10],[ebp+0x14]。获取到NtOpenProcess的参数,用全局变量保存参数,并在自己的函数中打印。代码如下:
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
| ULONG ProcessHandle; ULONG DesiredAccess; ULONG ObjectAttributes; ULONG ClientId;
VOID __declspec(naked) MyOwnNtFunction() { __asm { pushad pushfd mov eax, [ebp + 0x8] mov ProcessHandle, eax mov eax, [ebp + 0xc] mov DesiredAccess, eax mov eax, [ebp + 0x10] mov ObjectAttributes, eax mov eax, [ebp + 0x14] mov ClientId, eax }
DbgPrint("ProcessHandle: %x, DesiredAccess: %x, ObjectAttributes: %x, ClientId: %x\n", ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
__asm { popfd popad xor eax, eax lea edi, [ebp - 0x28] stos dword ptr es : [edi] jmp JmpBackAddr } }
|
最后一段嵌入的汇编有何用呢?也是非常重要的,因为我们修改了原始函数的6个字节,也就是3行指令,为了保证程序正常执行,需要在自己的函数中添加上这3行指令,执行后直接跳转到3行指令之后的位置执行即可保证程序正常执行。
构造修改指令
因个人习惯,Inline Hook比较爱使用6字节的方式,主要原因是懒得算Jmp造成的偏移……这部分的手法在Hook聊天内容那篇文章讲过,这里仅作简要说明:
- 初始化一个6个字节的数组
- 下标0的位置设置为”\x68”,即push 0x????????指令对应的硬编码
- 下标5的位置设置为”\xc3”,即ret指令对应的硬编码(这里采用\x,是用来表明这是个16进制数)
- 下标1~4的位置,设置成自己函数的地址
实现代码如下:
1 2 3 4
| Modify_Byte[0] = '\x68'; *(ULONG*)(Modify_Byte + 1) = (ULONG)(MyOwnNtFunction); Modify_Byte[5] = '\xc3';
|
Hook与Unhook
既然有Hook,也要有Unhook完成卸载。由于Inline Hook原理是修改指令对应的硬编码,因此使用RtlMoveMemory来实现对Hook点部分的代码进行修改,注意不能忘记修改物理页的属性。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| VOID HookNtOpenProcess() { PageProtectOff(); RtlMoveMemory((PUCHAR)ModifyAddr, Modify_Byte, 6); PageProtectOn(); }
VOID UnHookNtOpenProcess() { PageProtectOff(); RtlMoveMemory((PUCHAR)ModifyAddr, Recover_Byte, 6); PageProtectOn(); }
|
功能演示
- Hook前代码
- Hook执行后 可以在DbgView中看到不断有NtOpenProcess函数的参数在被输出,说明Hook成功,实现了和SSDT Hook同样的功能。
- 打开Windbg断下后查看NtOpenProcess函数代码 发现原本的代码已经被替换成了我们自己的代码,会跳转到自己的函数中。
- 打开PC Hunter,内核钩子选项 可以看到PC Hunter同样检测到了我们的对大小为6个字节Hook的代码
- 最后,我们选择停止驱动的执行。完成UnHook的操作,这时再去查看代码 可以看到代码已经完整的修改回去了
完整代码
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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
| #include "ntddk.h"
typedef struct _KSYSTEM_SERVICE_TABLE { PULONG ServiceTableBase; PULONG Count; ULONG ServiceLimit; PULONG ArgmentTableBase; }KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
typedef struct _KSERVICE_DESCRIPTOR_TABLE { KSYSTEM_SERVICE_TABLE ntoskrnl; KSYSTEM_SERVICE_TABLE win32k; KSYSTEM_SERVICE_TABLE notUsed1; KSYSTEM_SERVICE_TABLE notUsed2; }KSERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;
extern PKSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
VOID HookNtOpenProcess(); VOID UnHookNtOpenProcess(); VOID PageProtectOff(); VOID PageProtectOn(); VOID Driver_Unload(PDRIVER_OBJECT pDriverObj);
ULONG OriginFunctionAddr; ULONG ModifyAddr; ULONG JmpBackAddr; UCHAR Modify_Byte[6] = {0}; UCHAR Recover_Byte[6] = {0x33, 0xc0, 0x8d, 0x7d, 0xd8, 0xab};
ULONG ProcessHandle; ULONG DesiredAccess; ULONG ObjectAttributes; ULONG ClientId;
VOID __declspec(naked) MyOwnNtFunction() { __asm { pushad pushfd mov eax, [ebp + 0x8] mov ProcessHandle, eax mov eax, [ebp + 0xc] mov DesiredAccess, eax mov eax, [ebp + 0x10] mov ObjectAttributes, eax mov eax, [ebp + 0x14] mov ClientId, eax }
DbgPrint("ProcessHandle: %x, DesiredAccess: %x, ObjectAttributes: %x, ClientId: %x\n", ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
__asm { pushfd pushad xor eax, eax lea edi, [ebp - 0x28] stos dword ptr es : [edi] jmp JmpBackAddr } }
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING RegistryPath) { DbgPrint("Driver is running!\n");
OriginFunctionAddr = (KeServiceDescriptorTable->ServiceTableBase)[0x7A]; ModifyAddr = OriginFunctionAddr + 0x14; JmpBackAddr = ModifyAddr + 0x6;
DbgPrint("OriginFunctionAddr: %x\n \ ModifyAddr: %x\n \ JmpBackAddr: %x\n", OriginFunctionAddr, ModifyAddr, JmpBackAddr);
Modify_Byte[0] = '\x68'; *(ULONG*)(Modify_Byte + 1) = (ULONG)(MyOwnNtFunction); Modify_Byte[5] = '\xc3';
for (ULONG i = 0; i < 6; i++) { DbgPrint("%x ", Modify_Byte[i]); }
HookNtOpenProcess();
pDriverObj->DriverUnload = Driver_Unload; return STATUS_SUCCESS; }
VOID HookNtOpenProcess() { PageProtectOff(); RtlMoveMemory((PUCHAR)ModifyAddr, Modify_Byte, 6);
PageProtectOn(); }
VOID UnHookNtOpenProcess() { PageProtectOff(); RtlMoveMemory((PUCHAR)ModifyAddr, Recover_Byte, 6); PageProtectOn(); }
VOID PageProtectOff() { __asm { cli mov eax, cr0 and eax, not 0x10000 mov cr0, eax } }
VOID PageProtectOn() { __asm { mov eax, cr0 or eax, 0x10000 mov cr0, eax sti } }
VOID Driver_Unload(PDRIVER_OBJECT pDriverObj) { UnHookNtOpenProcess();
DbgPrint("Unload Success!\n"); }
|
总结
尽管Inline Hook相比较SSDT Hook而言,更不容易被查出来,但是PC Hunter还是可以查到我们Inline Hook的代码。到此为止,驱动这块内容就差不多了,和Win32窗口程序一样有着固定的格式,具体功能实现还是要看自己的需求。多核同步的关键在于同步,会在句柄表之后介绍。再接下来是更为复杂的APC,估计要花好一段时间来更新了。至于内核重载,可能要延一延了,涉及到的原理和3环逆向相关的较多。计划是在更新完APC后再复习3环逆向的一些内容,内核重载就更要靠后了。最近可能会确定开新坑了,把Android逆向基础的部分更一更,2月中旬以来,已经快俩月没碰Android了,手生了,需要回味一下。
参考资料
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=63
参考文章:https://blog.csdn.net/qq_41988448/article/details/103557383
参考文档:https://docs.microsoft.com/en-us/windows/win32/devnotes/rtlmovememory