MinHook测试分析02 (x64)
在X64模式中,存在的问题是JMP指令和整个地址空间相比仅仅覆盖了很窄的范围。因此引入一个中继函数(Relay Function)来实现对64位Detour函数地址的跳转。
在hook的分析之前,先谈一下前一篇帖子评论中的相关话题。
之前发布的一篇Minhook分析,有大牛说没有写出多线程安全,指令缓存,以及捕获上下文,从中提取EIP / RIP的问题,实际上源码当中都是有涉及的,这里再次感谢大牛提出的问题,只是上一篇我想的是着重于单纯的hook分析所以没有将多线程安全等部分展示出来,所以在这一篇中我先来将源码中上述问题的对应的解决方案做一次分析。
(先附上github上源码的下载地址:https://github.com/TsudaKageyu/minhook)
0x01 多线程的安全问题
在MinHook中构建hook相关结构的几个函数中(比如MH_CreateHook函数中),进入函数后开始操作之前,都会首先调用EnterSpinLock()函数来确保某一确定的时间片内,只有唯一一个线程在调用当前函数,来看EnterSpinLock函数具体内容:
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 | static VOID EnterSpinLock( VOID ) { SIZE_T spinCount = 0; // Wait until the flag is FALSE. /* LONG InterlockedCompareExchange( _Inout_ LONG volatile *Destination , _In_ LONG Exchange , _In_ LONG Comparand ); 把目标操作数(第1参数所指向的内存中的数)与一个值(第3参数)比较,如果相等, 则用另一个值(第2参数)与目标操作数(第1参数所指向的内存中的数)交换; 返回值是 Destination 指针的初始值。 整个操作过程是锁定内存的,其它处理器不会同时访问内存,从而实现多处理器环境下的线程互斥 */ while (InterlockedCompareExchange(&g_isLocked, TRUE, FALSE) != FALSE) { // No need to generate a memory barrier here, since InterlockedCompareExchange() // generates a full memory barrier itself. // Prevent the loop from being too busy. if (spinCount < 32) Sleep(0); else Sleep(1); spinCount++; } } |
EnterSpinLock函数内部调用了InterlockedCompareExchange函数,这个函数的功能是把目标操作数(第1参数所指向的内存中的数)与一个值(第3参数)比较,如果相等,则用另一个值(第2参数)与目标操作数(第1参数所指向的内存中的数)交换。函数的返回值则是 Destination 指针的初始值。这整个操作过程是锁定内存的,其它处理器不会同时访问内存,从而实现多处理器环境下的线程互斥。
当我们第一次调用MH_CreateHook函数来构建HookEntry结构的时候,g_isLocked的值还是初始化时的值FALSE。
1 2 | // Spin lock flag for EnterSpinLock()/LeaveSpinLock(). volatile LONG g_isLocked = FALSE; |
所以第一次进入的时候不会进入while循环(注意InterlockedCompareExchange函数的返回值是初始值FALSE,而不是发生交换后的值TRUE,所以没有进入while循环),假定当前线程正在调用MH_CreateHook函数还没有结束,那么再次进入EnterSpinLock时,g_isLocked已经被置为TRUE了,进入while循环,假如在第二个线程while循环的过程中,g_isLocked的值迟迟不被恢复成FALSE的话(MH_CreateHook函数结束退出前会调用LeaveSpinLock函数将g_isLocked的值恢复成FALSE),那么第二个线程首先会反复地调用Sleep(0),调用次数达到32次以后,开始反复地调用Sleep(1),直至没有线程在调用MH_CreateHook函数为止(标志就是g_isLocked的值恢复成FALSE),就退出while循环,正式地进入MH_CreateHook函数。
这里有必要谈一下Sleep(0)与Sleep(1)了。
Sleep 的意思是告诉操作系统自己要休息 n 毫秒,这段时间片可以让给另一个就绪的线程。
当 n=0 的时候,意思是要当前线程放弃自己剩下的时间片,但是仍然是就绪状态。不过Sleep(0) 只允许那些优先级相等或更高的线程使用当前的CPU,其它线程只能等待了。如果没有合适的线程,那当前线程会重新使用 CPU 时间片。
当 n=1 的时候,意思是要当前线程放弃剩下的时间片,并休息 一下(这里的休息时间并不一定就是1毫秒,具体的休息时间要看系统的时间精度,比如系统的时间精度只能达到10ms的话,那么将会休息10ms)。并且所有其它就绪状态的线程都有机会竞争时间片,而不用在乎优先级。
所以在while循环的整个等待过程中,也一直在尝试让出时间片给可能需要的其他线程。
0x02 指令缓存
我们hook了原函数跳转到我们自己的Detour函数,虽然修改了内存中的指令,但有可能被修改的指令已经被缓存起来了,再执行的话,CPU可能会优先执行缓存中的指令,使得修改的指令得不到执行。所以我们需要使用一个隐藏的系统调用来刷新一下缓存,小心驶得万年船~:
1 | FlushInstructionCache(GetCurrentProcess(), pPatchTarget, patchSize); |
在这里值得一提的是,如果使用WriteProcessMemory写其他进程内存来注入亦或其他目的的话,是不需要再额外调用FlushInstructionCache函数刷新缓存的,因为WriteProcessMemory本身就会调用NtFlushInstructionCache函数来刷新缓存:
0x03 暂停线程,捕获上下文,提取EIP / RIP
当我们挂起了线程要重写目标函数时,首先要捕获其上下文。线程的上下文实际上就是其寄存器的状态。从上下文中提取我们关注的寄存器EIP / RIP。如果恰好命不好当前的EIP / RIP指向的地址是我们准备hook重写的目标函数地址处,那么就需要修正EIP / RIP中的地址值,修正的新数据将用来恢复现场保证线程的顺利执行。
在上一篇帖子中详细描述了x86中jmp + 4字节offset类型的hook,我们暂以此类型来展开具体分析。
当我们要用jmp指令去覆盖重写原函数的入口指令时,我们会先将所有线程挂起,等待我们的覆盖重写操作完成之后,再恢复线程,但是~如果实在命不好的话——可能会有某一个线程排队等待恢复的EIP/RIP正好是我们hook的5个字节的第3个字节。这个时候我们就没法让本次的函数调用被hook了,只能战战兢兢如履薄冰地期望线程恢复后能够继续执行后续的指令避免GG崩溃掉。在上一篇中我们是以挂钩MessagBox函数为例,这里就以它为例:
反汇编下的MessageBox机器指令:
现在假定某个被挂起的线程在被挂起之前已经取指执行了“8B FF”这条指令,它的EIP保存的是“55”这条指令的地址0x77068b82,那么当我们恢复这个线程的时候,就应当让它继续正确地执行后续的指令,因为我们无法期望这个线程能够执行“jmp + offset”五字节指令了——为时晚矣,不如放它一马~
这时候就应该回想起HOOK_ENTRY这个结构体了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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字节 |
回想上一篇中介绍的这三个成员变量:Index,OldIPs[8],NewIPs[8],它们记录的是,在Trampoline的构建过程当中,Index记录的是指令数目,OldIPs[8]数组记录的是目标函数的在while循环过程中记录下的当前指令长度,,NewIPs[8]数组记录的是Trampoline在每一次while循环过程中构建的指令长度,while循环的源代码如下:
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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | do { HDE hde; UINT CopyDataLength; LPVOID CopyData; //对于出现的相对偏移地址,在跳板中都要给出新的相对地址 /* 32位 MessageBox 74CA8B80 8B FF mov edi,edi 74CA8B82 55 push ebp 74CA8B83 8B EC mov ebp,esp 74CA8B85 6A 00 push 0 74CA8B87 FF 75 14 push dword ptr [ebp+14h] 74CA8B8A FF 75 10 push dword ptr [ebp+10h] 74CA8B8D FF 75 0C push dword ptr [ebp+0Ch] 74CA8B90 FF 75 08 push dword ptr [ebp+8] 74CA8B93 E8 F8 FC FF FF call _MessageBoxExW@20 (74CA8890h) 64位 MessageBox 00007FF97B4485A0 48 83 EC 38 sub rsp,38h 00007FF97B4485A4 45 33 DB xor r11d,r11d 00007FF97B4485A7 44 39 1D 7A 33 03 00 cmp dword ptr [gfEMIEnable (07FF97B47B928h)],r11d 00007FF97B4485AE 74 2E je MessageBoxW+3Eh (07FF97B4485DEh) 00007FF97B4485B0 65 48 8B 04 25 30 00 00 00 mov rax,qword ptr gs:[30h] 00007FF97B4485B9 4C 8B 50 48 mov r10,qword ptr [rax+48h] 00007FF97B4485BD 33 C0 xor eax,eax 00007FF97B4485BF F0 4C 0F B1 15 98 44 03 00 lock cmpxchg qword ptr [gdwEMIThreadID (07FF97B47CA60h)],r10 00007FF97B4485C8 4C 8B 15 99 44 03 00 mov r10,qword ptr [gpReturnAddr (07FF97B47CA68h)] 00007FF97B4485CF 41 8D 43 01 lea eax,[r11+1] 00007FF97B4485D3 4C 0F 44 D0 cmove r10,rax 00007FF97B4485D7 4C 89 15 8A 44 03 00 mov qword ptr [gpReturnAddr (07FF97B47CA68h)],r10 00007FF97B4485DE 83 4C 24 28 FF or dword ptr [rsp+28h],0FFFFFFFFh 00007FF97B4485E3 66 44 89 5C 24 20 mov word ptr [rsp+20h],r11w 00007FF97B4485E9 E8 A2 FE FF FF call MessageBoxTimeoutW (07FF97B448490h) 00007FF97B4485EE 48 83 C4 38 add rsp,38h */ ULONG_PTR OldInstance = ( ULONG_PTR )Trampoline->TargetFunctionAddress + OldPos; ULONG_PTR NewInstance = ( ULONG_PTR )Trampoline->MemorySlot + NewPos; //指令长度 CopyDataLength = HDE_DISASM(( LPVOID )OldInstance, &hde); if (hde.flags & F_ERROR) return FALSE; CopyData = ( LPVOID )OldInstance; if (OldPos >= sizeof (JMP_REL)) { // The trampoline function is long enough. #if defined(_M_X64) || defined(__x86_64__) //OldInstance = 00007FF97B4485A7; jmp.Address = OldInstance; #else //OldInstance = 74CA8B85 //目标 = 源 + Offset + 5 //Offset = 目标 - (源 + 5) jmp.Operand = ( UINT32 )(OldInstance - (NewInstance + sizeof (jmp))); //计算跳转到目标的偏移 #endif CopyData = &jmp; CopyDataLength = sizeof (jmp); IsLoop = TRUE; } #if defined(_M_X64) || defined(__x86_64__) else if ((hde.modrm & 0xC7) == 0x05) { // Instructions using RIP relative addressing. (ModR/M = 00???101B) // Modify the RIP relative address. /* PUINT32 pRelAddr; // Avoid using memcpy to reduce the footprint. #ifndef _MSC_VER memcpy(instBuf, (LPBYTE)pOldInst, copySize); #else __movsb(instBuf, (LPBYTE)pOldInst, copySize); #endif pCopySrc = instBuf; // Relative address is stored at (instruction length - immediate value length - 4). pRelAddr = (PUINT32)(instBuf + hs.len - ((hs.flags & 0x3C) >> 2) - 4); *pRelAddr = (UINT32)((pOldInst + hs.len + (INT32)hs.disp.disp32) - (pNewInst + hs.len)); // Complete the function if JMP (FF /4). if (hs.opcode == 0xFF && hs.modrm_reg == 4) finished = TRUE;*/ } #endif else if (hde.opcode == 0xE8) { // Direct relative CALL ULONG_PTR Destination = OldInstance + hde.len + ( INT32 )hde.imm.imm32; #if defined(_M_X64) || defined(__x86_64__) call.Address = Destination; #else //计算源地址和Trampoline之间的偏移值 call.Operand = ( UINT32 )(Destination - (NewInstance + sizeof (call))); #endif //CopyData 被拷贝到Trampoline中保存的内容 CopyData = &call; CopyDataLength = sizeof (call); } else if ((hde.opcode & 0xFD) == 0xE9) //F 1111 D 1101 { //E 1110 9 1001 //E 1110 B 1011 // Direct relative JMP (EB or E9) ULONG_PTR Destination = OldInstance + hde.len; // /* 0xDE EB 00 0xE0 xor eax,eax */ if (hde.opcode == 0xEB) // isShort jmp Destination += (INT8)hde.imm.imm8; else Destination += ( INT32 )hde.imm.imm32; // Simply copy an internal jump. if (( ULONG_PTR )Trampoline->TargetFunctionAddress <= Destination && Destination < (( ULONG_PTR )Trampoline->TargetFunctionAddress + sizeof (JMP_REL))) { //比较越界 /* Asm_5 PROC jmp Label1 Lable2: xor eax,eax Loop Lable2 mov eax,-5 ret Label1: mov ecx,2 jmp Lable2 Asm_5 ENDP */ if (JmpDest < Destination) JmpDest = Destination; } else { #if defined(_M_X64) || defined(__x86_64__) jmp.Address = Destination; #else // jmp.Operand = ( UINT32 )(Destination - (NewInstance + sizeof (jmp))); #endif CopyData = &jmp; CopyDataLength = sizeof (jmp); // Exit the function If it is not in the branch IsLoop = (OldInstance >= JmpDest); } } else if ((hde.opcode & 0xF0) == 0x70 || (hde.opcode & 0xFC) == 0xE0 || (hde.opcode2 & 0xF0) == 0x80) { /* & 0xF0 0x70 jo 后有一个字节的偏移 0x71 jno 后有一个字节的偏移 0x72 jb 后有一个字节的偏移 .. .. 0x7F jg 后有一个字节的偏移 & 0xFC 0xE0 loopne 后有一个字节的偏移 0xE1 0xE2 0xE3 */ // Direct relative Jcc ULONG_PTR Destination = OldInstance + hde.len; if ((hde.opcode & 0xF0) == 0x70 // Jcc || (hde.opcode & 0xFC) == 0xE0) // LOOPNZ/LOOPZ/LOOP/JECXZ Destination += (INT8)hde.imm.imm8; else Destination += ( INT32 )hde.imm.imm32; // Simply copy an internal jump. if (( ULONG_PTR )Trampoline->TargetFunctionAddress <= Destination && Destination < (( ULONG_PTR )Trampoline->TargetFunctionAddress + sizeof (JMP_REL))) { if (JmpDest < Destination) JmpDest = Destination; } else if ((hde.opcode & 0xFC) == 0xE0) { // LOOPNZ/LOOPZ/LOOP/JCXZ/JECXZ to the outside are not supported. return FALSE; } else { UINT8 v1 = ((hde.opcode != 0x0F ? hde.opcode : hde.opcode2) & 0x0F); #if defined(_M_X64) || defined(__x86_64__) // Invert the condition in x64 mode to simplify the conditional jump logic. jcc.Opcode = 0x71 ^ v1; jcc.Address = Destination; #else jcc.Opcode1 = 0x80 | v1; jcc.Operand = ( UINT32 )(Destination - (NewInstance + sizeof (jcc))); #endif CopyData = &jcc; CopyDataLength = sizeof (jcc); } } else if ((hde.opcode & 0xFE) == 0xC2) { // RET (C2 or C3) // Complete the function if not in a branch. IsLoop = (OldInstance >= JmpDest); } // Can't alter the instruction length in a branch. if (OldInstance < JmpDest && CopyDataLength != hde.len) return FALSE; // Trampoline function is too large. if ((NewPos + CopyDataLength) > TRAMPOLINE_MAX_SIZE) return FALSE; // Trampoline function has too many instructions. if (Trampoline->Index >= ARRAYSIZE(Trampoline->OldIPs)) return FALSE; Trampoline->OldIPs[Trampoline->Index] = OldPos; Trampoline->NewIPs[Trampoline->Index] = NewPos; Trampoline->Index++; // Avoid using memcpy to reduce the footprint. #ifndef _MSC_VER memcpy (( LPBYTE )Trampoline->MemorySlot + NewPos, CopyData, CopyDataLength); #else __movsb(( LPBYTE )Trampoline->MemorySlot + NewPos, ( const unsigned char *)CopyData, CopyDataLength); #endif NewPos += CopyDataLength; OldPos += hde.len; } while (!IsLoop); |
在Trampoline构建完成之后,它的内容是这样的(部分成员未写出,因为此处不需要):
在MessageBox函数将会被覆盖的五字节中,是3条指令:
所以累计起来的指令长度是2,3,5(指令长度的计算是通过反汇编引擎HDE实现的)。
当前假定的情况是我们准备实际的hook重写目标函数覆盖前5字节之前,我们挂起了线程,却发现被挂起线程已经执行了“88 FF”这条指令,当前的EIP/RIP指向了下一条指令“55”所在的地址,所以等到这个被挂起线程被我们恢复了之后,我们应当确保它能正确的执行下去,这时候来看一看MemorySlot这个结构所保存的内容:
MemorySlot当中此时已经保存好了备份的5字节,以及一个跳转到原函数开始地址后5字节的地址的跳转指令,所以当我们恢复线程的时候,只需要将EIP/RIP修正为指向MemorySlot对应的第二条指令“55”,线程就能够顺利的执行了。源代码如下:
1 2 3 4 5 6 7 8 9 10 11 | DWORD_PTR SeFindNewIP(PHOOK_ENTRY HookEntry, DWORD_PTR Ip) { UINT i; for (i = 0; i < HookEntry->Index; ++i) { if (Ip == (( DWORD_PTR )HookEntry->TargetFunctionAddress + HookEntry->OldIPs[i])) return ( DWORD_PTR )HookEntry->MemorySlot + HookEntry->NewIPs[i]; } return 0; } |
0x04 x64下各类型目标函数的hook
接下来进入x64下的hook的内容了。
1.对于普通的目标函数(函数的初始指令不涉及jmp,call等指令),MinHook采用的是构建ff 25的一个64位8字节绝对地址跳转来实现目标函数重写,这个函数叫做Relay(中继函数),它是到绕道函数的64位跳转,被放置在目标函数的附近。
1 2 3 | JMP_ABS jmp = { //64位绝对地址jmp 13字节 0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6] 0x0000000000000000ULL // Absolute destination address |
先来看看x64模式下的Trampoline结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #pragma pack(1) 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; |
可以看到其中多出的一个成员:Relay,它存放的是对绕道函数Detour的绝对地址跳转:
1 2 3 4 5 6 7 | #if defined(_M_X64) || defined(__x86_64__) // Create a relay function. jmp.Address = ( ULONG_PTR )Trampoline->FakeFunctionAddress; Trampoline->Relay = ( LPBYTE )Trampoline->MemorySlot + NewPos; memcpy (Trampoline->Relay, &jmp, sizeof (jmp)); #endif |
话不多说,调试见真章:
首先还是测试MessageBox这个API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // WindowsAPI 测试 if (MHCreateHook(&MessageBoxW, &DetourMessageBox, reinterpret_cast < LPVOID *>(&__OriginalMessageBoxW)) != STATUS_SUCCESS) { return ; } MessageBoxW(NULL, L "MessageBoxW()" , L "MessageBoxW()" , 0); //单个函数的Hook if (EnableHook(&MessageBoxW) != STATUS_SUCCESS) { printf ( "EnableHook() Error\r\n" ); return ; } int WINAPI DetourMessageBox( _In_opt_ HWND hWnd, _In_opt_ WCHAR * lpText, _In_opt_ WCHAR * lpCaption, _In_ UINT uType) { __OriginalMessageBoxW(hWnd,L "FakeMessageBox" ,L "FakeMessageBox" ,uType); return 0; } |
主要还是关注Trampoline的构建过程:
局部变量窗口找到MemorySlot的地址,准备开始通过反汇编引擎计HDE算指令长度,构建MemorySlot中的内容,这里通过对比目标函数MessageBox的反汇编指令来观察MemorySlot的构建过程:
while循环(while循环的代码上面已贴出)中第一条指令的备份保存:
第二条指令备份保存:
此时的指令备份长度达到了7字节,满足了备份的长度条件(>=5字节):
1 2 3 | if (OldPos >= sizeof (JMP_REL)) { // The trampoline function is long enough. |
开始写跳转指令了:
我们对比跳转到的地址0x00007ffa851285a7与Trampoline中已经保存好的目标函数MessageBox的绝对地址0x00007ffa851285a0
0x00007ffa851285a7-0x00007ffa851285a0 = 7,这七字节的长度正好就是已经备份到MemorySlot中的7字节,因此要成功调用已经被Hook过的目标函数,只需要执行MemorySlot中的指令即可,也就是说到了这里我们的MemorySlot成员已经构建成功了。
接下来是第二个关键成员的构造:Relay Function!
在MemorySlot构建完成的while循环推出后,就开始构造Relay Function了:
1 2 3 4 5 6 7 | #if defined(_M_X64) || defined(__x86_64__) // Create a relay function. jmp.Address = ( ULONG_PTR )Trampoline->FakeFunctionAddress; Trampoline->Relay = ( LPBYTE )Trampoline->MemorySlot + NewPos; memcpy (Trampoline->Relay, &jmp, sizeof (jmp)); #endif |
从代码中可以看出Realy的地址是紧跟在MemorySlot结构之后的,所以我们不妨直接用当前的内存窗口观察Realy所指向的地址内容:
可以看到Relay指向的地址内容中保存的是对我们的Detour(即FakeFunction)绕道函数的绝对地址(0x00007ff7cff51389)的跳转:
那么到目前为止,Trampoline结构中最关键的两个成员内容就是这样的:
随后将在MHCreateHook函数中,将Trampoline结构中的各个成员对应赋值给HookEntry结构中的各个成员,并备份好五字节的指令在Backup成员中。与x86模式下不同的一点,也是最重要的一点就是,x64模式下的FakeFunction(Detour Funciton)不再是直接由Trampoline中的FakeFunction成员直接赋值,而是由Realy成员复制给HookEntry中的FakeFunction成员,这是x64与x86的最大区别之处,也是x64hook的点睛之笔!这里值得一提的是,当初通过VirtualAlloc()API为MemorySlot申请地址的时候,就是尽量在目标函数附近申请的,而Realy的地址又紧跟在MemorySlot之后,所以才能保证了Realy的地址与目标函数地址相近,从而进一步保证了通过E9相对地址跳转指令覆盖重写目标函数时四字节的相对偏移能够成功达到目的。
1 2 3 4 | #if defined(_M_X64) || defined(__x86_64__) HookEntry->FakeFunctionAddress = Trampoline.Relay; #else HookEntry->FakeFunctionAddress = Trampoline.FakeFunctionAddress; |
最后调用EnableHook函数真正覆盖重写目标函数的时候,我们只需要用自己的Detour绕道函数地址减去原目标函数的地址,再减去5字节的指令长度,得到相对偏移地址,通过E9相对地址跳转指令使得目标函数的调用绕道到我们的Detour函数。
2.目标函数初始指令为E9 EB类型的跳转
首先分析E9近跳转指令。
这里只要通过Hook一个自定义的函数即可看到目标函数初始指令为E9近跳转指令的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //E9指令 if (MHCreateHook(&Sub_2, &DetourSub_2, reinterpret_cast < LPVOID *>(&__OriginalSub_2)) != STATUS_SUCCESS) { return ; } Sub_2(); void Sub_2() { printf ( "Sub_2\n\r" ); } void DetourSub_2() { printf ( "DetourSub_2\n\r" ); __OriginalSub_2(); } |
首先通过Trampoline结构中的目标函数Sub_2()的首地址来看它对应的反汇编指令:
不出所料第一条指令是E9跳转到真正的Sub_2()入口地址处。
这种情况下Trampoline中有对应的情况判断与处理:
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 | else if ((hde.opcode & 0xFD) == 0xE9) { //F 1111 D 1101 //E 1110 9 1001 //E 1110 B 1011 // Direct relative JMP (EB or E9) ULONG_PTR Destination = OldInstance + hde.len; // /* 0xDE EB 00 0xE0 xor eax,eax */ if (hde.opcode == 0xEB) // isShort jmp Destination += (INT8)hde.imm.imm8; else Destination += ( INT32 )hde.imm.imm32; // Simply copy an internal jump. if (( ULONG_PTR )Trampoline->TargetFunctionAddress <= Destination && Destination < (( ULONG_PTR )Trampoline->TargetFunctionAddress + sizeof (JMP_REL))) { //比较越界 if (JmpDest < Destination) JmpDest = Destination; } else { #if defined(_M_X64) || defined(__x86_64__) jmp.Address = Destination; #else // jmp.Operand = ( UINT32 )(Destination - (NewInstance + sizeof (jmp))); #endif CopyData = &jmp; CopyDataLength = sizeof (jmp); // Exit the function If it is not in the branch IsLoop = (OldInstance >= JmpDest); } } |
当第一天指令是E9或者EB时将会进入else if的判断之内,然后通过反汇编引擎HDE,将E9指令所在地址,即目标函数的地址进行修正,首先会加上E9指令的长度,然后通过反汇编引擎为目标地址加上到真正Sub_2()函数所需要的便宜,这个时候的Destination就成为真正的Sub_2()函数入口地址了,这时候再将这个地址作为ff 25跳转的绝对地址,写入到MemorySlot当中:
1 | __movsb(( LPBYTE )Trampoline->MemorySlot + NewPos, ( const unsigned char *)CopyData, CopyDataLength); |
下一步就是构建Relay成员指向地址的内容:
1 2 3 4 5 6 7 | #if defined(_M_X64) || defined(__x86_64__) // Create a relay function. jmp.Address = ( ULONG_PTR )Trampoline->FakeFunctionAddress; Trampoline->Relay = ( LPBYTE )Trampoline->MemorySlot + NewPos; memcpy (Trampoline->Relay, &jmp, sizeof (jmp)); #endif |
此时MemorySlot和Relay就构建好了,当前,它们的内容是这样的:
接下来的步骤就与之前叙述的相同了,这里就不再赘述,而且EB类型的情况与E9情况相同,也不再赘述了。进入下一种call指令类型的hook。
3.目标函数初始指令为call类型的跳转
为了能够使目标函数第一条指令是call,方便Minhook的测试,在这里将通过汇编指令来构造第一条指令是call的自定义函数.
先来看这段汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Asm_1 PROC mov qword ptr[rsp+8h],rcx push rbp push rdi sub rsp,28h xor rbx,rbx mov rax,qword ptr[rsp+28h+8h+8h+8h] mov ebx,dword ptr[rax+1] add rax,rbx add rax,5 add rsp,28h pop rdi pop rbp ret Asm_1 ENDP |
这段汇编代码的作用就在于抹去调用函数时jmp + 4字节偏移的指令,返回被调用函数真正地址。
当我们在main函数中调用自定义的函数时,当代码执行到被调用函数处,第一条指令将是一条jmp指令,跳转到真正的被调用函数入口地址,这一点在上述的自定义函数Sub_2()中也有所体现。
这段汇编指令非常简单,它首先将Asm_1传进来的第一个参数放到rsp+8字节的位置(这里涉及到x64下寄存器的传参问题,x64下函数调用的参数传递中,前四个参数分别用这四个寄存器传递:rcx,rdx,r8,r9),实际上这里的参数也就是被调用函数的地址,进一步说就是第一条指令jmp的地址。为什么是将这个地址放到rsp+8字节的位置而不是直接放在栈顶处呢?这是因为Asm_1作为函数被调用时,栈顶是必须要保存函数调用结束后下一条指令的地址的,所以只能退到距离栈顶8字节处放置第一条指令jmp的地址。
随后将rbp,rdi压栈,再通过栈顶指针rsp的偏移定位到传进去的参数,第一条指令jmp的地址,将它赋值保存到rax中,再将rax保存的地址越过一个字节,也就可以得到距离真正Sub_2函数的的偏移值,将这4字节的偏移值放到ebx保存,最后用当前地址加上偏移地址,再加上5字节的指令长度,就得到了真正的Sub_2()函数入口地址了,放在rax中作为Asm_1()的返回值返出去,就此还不算大功告成,只是万事俱备只欠东风了——我们还需要构建一条call指令出来,依然用汇编硬写出call指令:
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 | Asm_4 PROC call Label0 jmp Exit; Label0: mov rcx,0; call Label1; //Call db 'H' db 0 db 'e' db 0 db 'l' db 0 db 'l' db 0 db 'o' db 0 db 'S' db 0 db 'u' db 0 db 'b' db 0 db '_' db 0 db '4' db 0 db 0 db 0 Label1: pop rdx call Label2; db 'H' db 0 db 'e' db 0 db 'l' db 0 db 'l' db 0 db 'o' db 0 db 'S' db 0 db 'u' db 0 db 'b' db 0 db '_' db 0 db '4' db 0 db 0 db 0 Label2: pop r8 mov r9,0 call MessageBoxW ret Exit: ret Asm_4 ENDP |
然后将Asm_4作为Asm_1的参数传进去,其返回值的地址内就将是我们一直搓手想要的call指令了:
1 2 3 4 5 6 7 8 9 | //Call指令 PVOID v4 = Asm_1(Asm_4); Asm_4(); if (SeCreateHook(v4, &FakeSub_4, reinterpret_cast < LPVOID *>(&__OriginalSub_4)) != STATUS_SUCCESS) { return ; } |
(待补充)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
2016-09-12 杭电2023 平均成绩