0%

使用InlineHook实现进程保护(3)

简介

关于 INLINEHOOK 的技术,既可以用于驱动,又可以用于应用层,其原理本质上是汇编指令替换。
因为应用层进程之间内存空间有隔离,所以要想HOOK函数,需要先注入模块到目标进程中。

INLINEHOOK的原理

操作系统中的函数的开头多数是固定格式,这里以 NtTerminateProcess 为例,查看汇编指令

1
2
3
4
5
6
nt!NtTerminateProcess:
805d399e 8bff mov edi,edi
805d39a0 55 push ebp
805d39a1 8bec mov ebp,esp
805d39a3 83ec10 sub esp,10h
805d39a6 53 push ebx

可以看到前3条指令合起来占用5个字节的机器码,我们使用无条件跳转指令来替换它们,从而调用这个
函数时首先跳转到我们自定义的函数中,处理完毕后再跳转回来。

1
2
_asm jmp MyNtTerminateProcess;
00e1143e e9e5fbffff jmp 00e11028

以上机器码 E9 表示 JMP 指令,后边4个字节为跳转的偏移 0xFFFFFBE5,这里可以计算一下,
指令地址为 0x00E1143E 目标地址为 0x00E11028,属于往前跳,所以跳转偏移是个负数,转换完并
求绝对值为 0x41B,这里 0x00E1143E - 0x41B = 0x00E11023 并不是 0x00E11028,是因为跳转
指令本身占了 5 个字节,CPU是从执行完指令之后的地址开始算的。

所以我们在替换 NtTerminateProcess 函数首 5 个字节时,可以使用如下公式计算跳转偏移

1
跳转偏移 = 目标地址(自定义函数地址) - 指令地址(原函数地址) - 5

注意在我们自定义的函数执行完毕后,如果还需要执行原函数,需要先运行替换的原5个字节指令,
再跳转回原函数中。获取 NtTerminateProcess 函数地址的方法这里不再专门说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ULONG NtTerminateProcessNextCodeAddr = 0;

__declspec(naked) void NakeNtTerminateProcess()
{
_asm {
mov eax, [esp + 8];
push eax;
mov eax, [esp + 8];
push eax;
call ProxyNtTerminateProcess;
add esp, 8;
test eax, eax;
je oricode;
ret;
oricode:
nop;
nop;
nop;
nop;
nop;
jmp NtTerminateProcessNextCodeAddr;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NTSTATUS ProxyNtTerminateProcess(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
NTSTATUS Status = STATUS_SUCCESS;
do
{
// 检查参数
if (ProcessHandle == NULL) break;
if (ProcessHandle == (HANDLE)-1) break;
// 本次操作对应的进程
PsGetCurrentProcessId();
// 使用ZwQueryInformationProcess查询进程名
if (!NT_SUCCESS(Status))
return STATUS_ACCESS_DENIED; // 拦截操作
} while (0);
// 返回成功跳转回原函数
return STATUS_SUCCESS;
}

如下为HOOK操作函数,如果要卸载HOOK就是把替换的5个字节指令恢复回去,这里不再专门说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 函数NakeNtTerminateProcess对应的机器码
00D613D0 8B 44 24 08 mov eax,dword ptr [esp+8]
00D613D4 50 push eax
00D613D5 8B 44 24 08 mov eax,dword ptr [esp+8]
00D613D9 50 push eax
00D613DA E8 39 FD FF FF call ProxyNtTerminateProcess (0D61118h)
00D613DF 83 C4 08 add esp,8
00D613E2 85 C0 test eax,eax
00D613E4 74 01 je oricode (0D613E7h)
00D613E6 C3 ret
oricode:
00D613E7 90 nop
00D613E8 90 nop
00D613E9 90 nop
00D613EA 90 nop
00D613EB 90 nop
00D613EC FF 25 30 81 D6 00 jmp dword ptr ds:[0D68130h]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void InlineHook(IN PVOID FuncAddr, IN PVOID NakeAddr, OUT PULONG NextCodeAddr)
{
// 指令NOP地址为23个字节的偏移
PULONG NopAddr = (PULONG)((ULONG)NakeAddr + 23);
if (*NopAddr != 0x90909090) return;
// 保存返回地址
*NextCodeAddr = (ULONG)FuncAddr + 5;
WPOff(); // 关闭内存写保护
// 保存原代码
*(PUCHAR)NopAddr = *(PUCHAR)FuncAddr;
*(PULONG)((PUCHAR)NopAddr + 1) = *(PULONG)((PUCHAR)FuncAddr + 1);
// 写入跳转指令
*(PUCHAR)FuncAddr = 0xE9;
*(PULONG)((PUCHAR)FuncAddr + 1) = (ULONG)NakeAddr - (ULONG)FuncAddr - 5;
WPOn(); // 打开内存写保护
}

有些情况下,要HOOK的函数开头不是正好5个字节的指令,就需要我们内置一个反汇编引擎,反汇编
目标函数,至少找到5个字节的空间。比如7个字节的指令,仍然是相同的操作,先保存这7个字节的指令
Nake函数 预置的 0x90 中,然后保存下一条指令的地址,最后写入跳转指令,多余的空间写入 0x90

可以使用开源的minhook引擎:https://github.com/TsudaKageyu/minhook