MinHook 项目学习笔记

​​ MinHook是一个常用的 InlineHook 库,学习完整个项目后对 Hook 技术会有更深的认识。

项目路径TsudaKageyu/minhook: The Minimalistic x86/x64 API Hooking Library for Windows (github.com)

1. 基本原理

1)获取原函数地址

2)将原函数的前五个字节改为跳转指令跳转到 FakeFunc

3)FakeFunc 执行完后跳转到内存槽,执行原函数指令后跳回原函数的后续指令

x86特点:

​ x86,2GB 的用户空间,jmp call 指令,经过一次跳转就可以到达目标地址。

x64特点:

​ x64,128TB 的用户空间,jmp call 指令,经过一次跳转往往无法到达目标地址。

​ 由于内存空间较大,利用 E9 指令可能会跳不到 FakeFunc,即在原函数附近申请一片内存作为跳板,将原函数前五个字节指令改为跳转指令使其跳转到跳板的 Relay 中,Relay 处存放 FF 25 指令,使其跳转到 FakeFunc,FakeFunc 执行完后即跳到内存槽中执行原函数指令后跳回原函数后续指令

2. 比较重要的两个结构体

typedef struct _HOOK_ENTRY
{
	LPVOID TargetFunctionAddress;        
	LPVOID FakeFunctionAddress;           
	LPVOID MemorySlot;        
	UINT8  Backup[8];           //恢复Hook使用的存放原先数据

	UINT8  PatchAbove : 1;     // Uses the hot patch area.   位域:1位
	UINT8  IsEnabled  : 1;     // Enabled.

//	UINT8  queueEnable : 1;     // Queued for enabling/disabling when != isEnabled.

	UINT   Index : 4;            // Count of the instruction boundaries.???
	UINT8  OldIPs[8];           // Instruction boundaries of the target function.???
	UINT8  NewIPs[8];           // Instruction boundaries of the trampoline function ???

} HOOK_ENTRY, *PHOOK_ENTRY;     //44字节

typedef struct _TRAMPOLINE
{
	LPVOID TargetFunctionAddress;      
	LPVOID FakeFunctionAddress;        
	LPVOID MemorySlot;                   // MemorySlot 32字节

#if defined(_M_X64) || defined(__x86_64__)
	LPVOID Relay;           // [Out] Address of the relay function.   原函数 到 Fake函数的中转站 
#endif
	BOOL   PatchAbove;      // [Out] Should use the hot patch area?  //Patch  --->补丁   //0xA 0xB
	UINT   Index;           // [Out] Number of the instruction boundaries.
	UINT8  OldIPs[8];       // [Out] Instruction boundaries of the target function.      //恢复
	UINT8  NewIPs[8];       // [Out] Instruction boundaries of the trampoline function.  //Hook
} TRAMPOLINE, *PTRAMPOLINE;

3. MiniHook可Hook的函数类型

1)普通函数

​ 计算从 Memory 跳转到原函数后续指令的偏移值 offset,保存在 jmp 指令中作为跳转地址,将 jmp 指令写入 memoryslot

2)函数入口为 jmp

①E9 指令:保存原函数 jmp 指令跳转处的地址,保存在自己的 jmp 指令中作为跳转地址将jmp指令写入 memoryslot

②EB 指令:

​ a. 第一条短跳转地址在原函数前五个字节,将其写入 memoryslot,如果第二条指令还是 jmp 跳转地址超过前五个字节,此时把该地址保存在 jmp 指令中作为跳转地址,写入 memoryslot

​ b. 第一条指令是稍远的短跳转,构造 jmp 指令,计算此地址和 memoryslot 之间的偏移值,保存在 jmp 指令中作为跳转地址,写入memoryslot
可能会遇到比较越界问题:即第一步短跳转到不远处执行代码,后面又短跳转到前五个字节处

3)函数入口为call(E8)

①得到原函数call指令跳转处的地址,计算此地址和memoryslot之间偏移值,保存在call指令中作为跳转地址,写入memoryslot

②call指令会将下一条指令压入,不能直接跳出循环,还需要计算从Memory跳转到原函数后续指令的偏移值offset,保存在jmp指令中作为跳转地址,写入memoryslot

4)函数入口为jcc

①得到原函数指令跳转处的地址

②第一个跳转的地址在原函数的前五个字节里,保存跳转处的地址,存入memorySlot,继续循环

③若第二条指令还是jcc,跳转地址超过五个字节,进入else块,把MemorySlot后续改为jcc跳到原函数跳转地址处

可能会遇到比较越界问题:即第一步短跳转到不远处执行代码,后面又短跳转到前五个字节处

5)函数入口为ret

​ 直接返回

6)热补丁Hook

①如果原函数所有指令不足五个字节,将其写入memoryslot退出循环,判断能否进行短跳转

②如果只能写短跳转,使用热补丁技术,确定该函数前面的地址为可执行地址,前面的内容可改,设置热补丁标志

③改变原函数:构造jmp短跳转,使其可以从原函数跳到原函数之前五个字节处,前五个字节被改为jmp指令跳转到FakeFunc

4. MiniHook注意点

1)跳转地址偏移值计算公式

​ 计算公式:目标 = 源 + Offset + 5 Offset = 目标 - (源 + 5)

2)函数的真正地址

​ 当调用自定义的函数时,代码执行到被调用函数处,第一条指令将是一条jmp指令跳转到真正的被调用函数入口地址,即测试自定义函数第一步就是获得原函数的真正地址

​ (利用汇编,传入假函数地址,此地址加1即得真实地址)

3)64位构建内存槽

​ 确定Ring3层进程空间访问的范围,将MemoryBlock地址范围控制在原函数地址±1024MB,最后还要对MaxAddress进行微调:MaxAddress -= MEMORY_BLOCK_SIZE - 1;

4)ShellCode

​ x86:jmp-5个字节 call-5个字节 jcc-6个字节

​ x64:jmp-14个字节 call-16个字节 jcc-16个字节

CALL_ABS call = {
	0xFF, 0x15, 0x00000002, // FF15 00000002: CALL [RIP+8]
	0xEB, 0x08,             // EB 08:         JMP +10
	0x0000000000000000ULL   // Absolute destination address
};
JMP_ABS jmp = {
	0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6]
	0x0000000000000000ULL   // Absolute destination address
};
JCC_ABS jcc = {
	0x70, 0x0E,             // 7* 0E:         J** +16
	0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6]
	0x0000000000000000ULL   // Absolute destination address
};
CALL_REL call = {
	0xE8,                   // E8 xxxxxxxx: CALL +5 + xxxxxxxx  Push Eip    Jmp    Ret
	0x00000000              // Relative destination address
};
JMP_REL jmp = {
	0xE9,                   // E9 xxxxxxxx: JMP +5+xxxxxxxx                   
	0x00000000              // Relative destination address
};
JCC_REL jcc = {
	0x0F, 0x80,             // 0F8* xxxxxxxx: J** +6+xxxxxxxx
	0x00000000              // Relative destination address
};

5)FF 25绝对地址

​ x64跳转指令都是使用绝对地址,不需要计算偏移值

6)线程同步问题

① 加锁和Sleep

​ 第一次进入不会进入循环,第一次后 g_isLocked 赋值为 TRUE,后面其他线程进入后会进入循环

static VOID EnterSpinLock(VOID)
{
   SIZE_T spinCount = 0;
   while (InterlockedCompareExchange(&g_isLocked, TRUE, FALSE) != FALSE)
   {
       if (spinCount < 32)
           Sleep(0);
       else
           Sleep(1);
       spinCount++;
   }
}

​ 函数调用了原子操作函数,这整个操作过程是锁定内存的,其它处理器不会同时访问内存,从而实现多处理器环境下的线程互斥。

Sleep(0)与Sleep(1):

​ Sleep 的意思是告诉操作系统自己要休息 n 毫秒,这段时间片可以让给另一个就绪的线程。

​ n=0 时,当前线程放弃自己剩下的时间片,仍然是就绪。Sleep(0)只允许优先级相等或更高的线程使用当前CPU,其它线程等待。如果没有合适的线程,当前线程会重新使用CPU时间片

​ n=1 时,要当前线程放弃剩下的时间片,休息一下。且所有其它就绪状态的线程都有机会竞争时间片,而不用在乎优先级。

② 热补丁

​ Hook 原函数的前两个字节,将其改为 EB F9 短跳转指令,即跳转到原函数之前五个字节处,若此处内存被 0xcc 填充,将此处设置为 E9 指令跳转到 FakeFunc 中

解决了线程同步问题:避免 Hook 时其他线程执行前几个字节指令,热补丁 Hook 的为无效指令,所以即使有其他线程执行,也不会造成错误

7)解决了 Hook 重入问题

①设置了内存槽存放原函数被Hook的指令及跳回原函数后续指令的指令

②使用热补丁

8)钩子链

出现原因:一个函数被多次Hook,形成了Hook链

解决方法

①新Hook加入Hook链

②穿透Hook链,跳过Hook点,直接钩在原函数上(风险较大)

③重载内核模块(重新加载一遍模块,即得到新的原函数)

9)指令缓存

​ hook 原函数后跳转到 FakeFunc,虽然修改了内存中的指令,但有可能被修改的指令已经被缓存起来了,再执行,CPU 可能会优先执行缓存中的指令,使得修改的指令得不到执行

解决方法:需要调用 FlushInstructionCache(GetCurrentProcess(), pPatchTarget, patchSize);来刷新缓存

注意:如果使用WriteProcessMemory写其他进程内存,不需要再额外调用FlushInstructionCache函数刷新缓存,因为WriteProcessMemory本身就会调用NtFlushInstructionCache函数来刷新缓存

10)暂停线程,捕获上下文,提取EIP / RIP

​ 改写原函数的前五个字节指令时,需要挂起所有线程,循环遍历比对查找,如果有线程的 EIP/RIP 指向原函数的前五个字节处,将其 EIP/RIP 改为指向 MemorySlot 里保存的相应指令处(利用 HookEntry->NewIPS, 这里面存了保存在 MemorySlot 里指令的偏移长度)
在恢复 Hook 时需要对线程的EIP/RIP进行恢复:利用 HookEntry->OldIPS,里面存的该指令在原函数指令的偏移长度

posted @ 2024-03-02 16:51  修竹Kirakira  阅读(290)  评论(0编辑  收藏  举报