世界上果然没有魔法,到最后发现都是魔术
解读的项目地址:https://github.com/TsudaKageyu/minhook
公众号:https://mp.weixin.qq.com/s/Po_t-JGj0e3dMBKDd9i8cw
或许我们的公众号会有更多你感兴趣的内容
P1. Hook原理
首先使用Visual Studio中的MSVC编译器,按照Release x64 禁用代码优化 编译如下代码
1 2 3 4 5 6 7 8 9 10 11 #include <Windows.h> #include <iostream> void hello () { std::cout << "123\n" ; } int main () { hello (); return 0 ; }
我们在main
函数中的hello()
处加上断点(ps:为什么选择了release版本任然能够调试:1.没有antiDebug。2.调式符号依然保存了。3.代码量小,就算开了代码优化也不会有较大影响)
步入call
汇编
这里就是目前编译情况下的hello
函数的汇编实现了。
那么我们就可以找到hello
函数的地址,然后覆盖他的汇编,让执行流转移到我们创建的新的函数 。所以我们继续写有如下代码
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 #include <Windows.h> #include <iostream> void hello () { std::cout << "123\n" ; } void newhello () { std::cout << "This is New hello\n" ; } int main () { unsigned char jmpopcode[14 ] = { 0x68 , 0x00 , 0x00 , 0x00 , 0x00 , 0xC7 , 0x44 , 0x24 , 0x04 , 0x00 , 0x00 , 0x00 ,0x00 , 0xC3 }; DWORD64 oldFuncAddr = reinterpret_cast <DWORD64>(hello); DWORD64 newFuncAddr = reinterpret_cast <DWORD64>(newhello); DWORD old; VirtualProtect (hello, 15 , PAGE_EXECUTE_READWRITE, &old); *(DWORD32*)(jmpopcode + 1 ) = (DWORD32)newFuncAddr; *(DWORD32*)(jmpopcode + 9 ) = (DWORD32)(newFuncAddr >> 32 ); memcpy (hello, jmpopcode, 14 ); hello (); return 0 ; }
依然是调用hello()
的使用跟进去
执行到00007FF7FED8100D
,会跳转到我们的函数newhello()
这样我们就完成了一次hook,后续无论调用多少次hello
函数,都会执行为newhello
函数
关于我们这里的代码
1 2 3 4 5 6 7 unsigned char jmpopcode[14 ] = { 0x68 , 0x00 , 0x00 , 0x00 , 0x00 , 0xC7 , 0x44 , 0x24 , 0x04 , 0x00 , 0x00 , 0x00 ,0x00 , 0xC3 }; *(DWORD32*)(jmpopcode + 1 ) = (DWORD32)newFuncAddr; *(DWORD32*)(jmpopcode + 9 ) = (DWORD32)(newFuncAddr >> 32 );
jmpopcode
实际上是这样一段汇编代码,假如我们要跳转到0x 20000000 10000000
1 2 3 push 0x10000000 mov dword [rsp+4], 0x20000000 ret
这是在64位下一种特殊的跳转代码,由于jmp
指令的限制,只能2GB内内存寻址,到了x64寻址空间大大加大,单纯jmp
和call
已经无法跳转到地址,所以我们将带跳转的地址依次将低位、高位移动到栈顶(rsp),这样ret就能直接跳转了,这种好处就是不会污染栈和寄存器。
如果在x86(32位)的情况下,直接使用jmp
跳转即可
总结一下hook的步骤就是:
找到待hook函数的地址
覆盖待hook函数汇编码,让待hook函数跳转到新的函数
取消hook恢复待hook函数的汇编码即可
P2. 使用MinHook
在项目文件夹中,然后再vs中包含
1 git clone https://github.com/TsudaKageyu/minhook
现在我们使用MinHook来实现P1中的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <Windows.h> #include <iostream> #include "minhook/MinHook.h" void hello () { std::cout << "123\n" ; } void newhello () { std::cout << "This is New hello\n" ; } int main () { LPVOID *lpOldHello = nullptr ; MH_Initialize (); MH_CreateHook (hello, newhello, lpOldHello); MH_EnableHook (hello); hello (); MH_DisableHook (hello); hello (); return 0 ; }
尝试Hook系统函数Sleep
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 #include <windows.h> #include <iostream> #include "minhook/MinHook.h" typedef void (WINAPI* Sleep_t) (DWORD) ;Sleep_t fpSleep = nullptr ; void WINAPI MySleep (DWORD dwMilliseconds) { std::cout << "MySleep called with " << dwMilliseconds << " milliseconds" << std::endl; fpSleep (dwMilliseconds); } int main () { MH_Initialize (); MH_CreateHook (Sleep, MySleep, reinterpret_cast <LPVOID*>(&fpSleep)); MH_EnableHook (MH_ALL_HOOKS); Sleep (1000 ); MH_DisableHook (MH_ALL_HOOKS); MH_Uninitialize (); Sleep (1000 ); return 0 ; }
P3. 代码解读
这里按照Hook Sleep函数的顺序
MH_Initialize
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 MH_STATUS WINAPI MH_Initialize (VOID) { MH_STATUS status = MH_OK; EnterSpinLock (); if (g_hHeap == NULL ) { g_hHeap = HeapCreate (0 , 0 , 0 ); if (g_hHeap != NULL ) { InitializeBuffer (); } else { status = MH_ERROR_MEMORY_ALLOC; } } else { status = MH_ERROR_ALREADY_INITIALIZED; } LeaveSpinLock (); return status; }
EnterSpinLock:进入自旋锁,避免在多线程的hook中冲突。对应的是LeaveSpinLock
InitializeBuffer:无意义函数
g_hHeap:用于管理g_hooks的句柄,从之前的代码就可以看出来MinHook对于已经hook的函数的取消hook等等的管理一定是有一个全局变量在管理
MH_CreateHook
首先是函数的原型
1 MH_STATUS WINAPI MH_CreateHook (LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal)
pTarget:待hook的函数(旧函数)
pDetour:新的函数
ppOriginal:指向旧函数的指针
然后检查旧函数和新韩淑的可执行权限,这里又学到一个新的winapi VirtualQuery
1 2 3 4 5 6 7 8 BOOL IsExecutableAddress (LPVOID pAddress) { MEMORY_BASIC_INFORMATION mi; VirtualQuery (pAddress, &mi, sizeof (mi)); return (mi.State == MEM_COMMIT && (mi.Protect & PAGE_EXECUTE_FLAGS)); }
关于VirtualQuery
函数:https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualquery
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 UINT pos = FindHookEntry (pTarget); if (pos == INVALID_HOOK_POS) static UINT FindHookEntry (LPVOID pTarget) { UINT i; for (i = 0 ; i < g_hooks.size; ++i) { if ((ULONG_PTR)pTarget == (ULONG_PTR)g_hooks.pItems[i].pTarget) return i; } return INVALID_HOOK_POS; }
这里开始就有些复杂了,上来就是两个结构体,不过不用担心,因为FindHookEntry
中的g_hooks
就没有初始化过,所以只能返回错误:INVALID_HOOK_POS
,这样就进入了if里面
1 2 3 4 LPVOID pBuffer = AllocateBuffer (pTarget); if (pBuffer != NULL ) {
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 LPVOID AllocateBuffer (LPVOID pOrigin) { PMEMORY_SLOT pSlot; PMEMORY_BLOCK pBlock = GetMemoryBlock (pOrigin); if (pBlock == NULL ) return NULL ; pSlot = pBlock->pFree; pBlock->pFree = pSlot->pNext; pBlock->usedCount++; #ifdef _DEBUG memset (pSlot, 0xCC , sizeof (MEMORY_SLOT)); #endif return pSlot; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static PMEMORY_BLOCK GetMemoryBlock (LPVOID pOrigin) #if defined(_M_X64) || defined(__x86_64__) SYSTEM_INFO si ; GetSystemInfo (&si); minAddr = (ULONG_PTR)si.lpMinimumApplicationAddress; maxAddr = (ULONG_PTR)si.lpMaximumApplicationAddress; #endif for (pBlock = g_pMemoryBlocks; pBlock != NULL ; pBlock = pBlock->pNext) { #if defined(_M_X64) || defined(__x86_64__) if ((ULONG_PTR)pBlock < minAddr || (ULONG_PTR)pBlock >= maxAddr) continue ; #endif if (pBlock->pFree != NULL ) return pBlock; }
接着再回到创建Hook中
1 2 3 4 5 6 7 8 if (pBuffer != NULL ){ TRAMPOLINE ct; ct.pTarget = pTarget; ct.pDetour = pDetour; ct.pTrampoline = pBuffer; if (CreateTrampolineFunction (&ct))
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 BOOL CreateTrampolineFunction (PTRAMPOLINE ct) #if defined(_M_X64) || defined(__x86_64__) CALL_ABS call = { 0xFF , 0x15 , 0x00000002 , 0xEB , 0x08 , 0x0000000000000000 ULL }; JMP_ABS jmp = { 0xFF , 0x25 , 0x00000000 , 0x0000000000000000 ULL }; JCC_ABS jcc = { 0x70 , 0x0E , 0xFF , 0x25 , 0x00000000 , 0x0000000000000000 ULL }; copySize = HDE_DISASM ((LPVOID)pOldInst, &hs); if (oldPos >= sizeof (JMP_REL)) { #if defined(_M_X64) || defined(__x86_64__) jmp.address = pOldInst; #else jmp.operand = (UINT32)(pOldInst - (pNewInst + sizeof (jmp))); #endif pCopySrc = &jmp; copySize = sizeof (jmp); finished = TRUE; } #ifndef ALLOW_INTRINSICS memcpy ((LPBYTE)ct->pTrampoline + newPos, pCopySrc, copySize); #else __movsb((LPBYTE)ct->pTrampoline + newPos, (LPBYTE)pCopySrc, copySize); #endif newPos += copySize; oldPos += hs.len; } while (!finished); #if defined(_M_X64) || defined(__x86_64__) jmp.address = (ULONG_PTR)ct->pDetour; ct->pRelay = (LPBYTE)ct->pTrampoline + newPos; memcpy (ct->pRelay, &jmp, sizeof (jmp)); #endif return TRUE;
HDE_DISASM
:跟进去是解析汇编指令,计算出当前函数的汇编指令长度。使用的是作者改进过的Hacker Disassembler Engine 64
,看了下版权信息还挺古早的
pCopySrc
:如果相差很近,能使用jmp
则使用jmp
,根据条件得到对应的跳转指令
ct->pRelay
:存放跳转的指令
这边感觉作者写的有点复杂,不过确实是好用的,解析反汇编的方法又学到一些,这里看不懂的可以看看后面的动调的解释
1 2 3 4 5 6 7 PHOOK_ENTRY pHook = AddHookEntry (); if (pHook != NULL ) { memcpy (pHook->oldIPs, ct.oldIPs, ARRAYSIZE (ct.oldIPs)); memcpy (pHook->newIPs, ct.newIPs, ARRAYSIZE (ct.newIPs));
这里就存储计算出的指令和原始汇编,便于后续启用hook的时候使用
这里的AddHookEntry
的时候就已经将返回地pHook
加入到g_hook
中管理了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (ct.patchAbove) { memcpy ( pHook->backup, (LPBYTE)pTarget - sizeof (JMP_REL), sizeof (JMP_REL) + sizeof (JMP_REL_SHORT)); } else { memcpy (pHook->backup, pTarget, sizeof (JMP_REL)); } if (ppOriginal != NULL ) *ppOriginal = pHook->pTrampoline;
pHook->pTrampoline :这里指向的是旧的Sleep的jmp
1 00007FFAD228B0B0 jmp qword ptr [7FFAD22F0A10h]
hook的时候变了
1 00007FFAD228B0B0 jmp 00007FFAD2260FC7
但是要hook的地址不是jmp呢?这里用到最开始的hello的例子
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 #include <Windows.h> #include <iostream> #include "minhook/MinHook.h" typedef void (*myhello) () ;myhello lpOldHello = nullptr ; void hello () { std::cout << "123\n" ; } void newhello () { std::cout << "This is New hello\n" ; reinterpret_cast <myhello>(lpOldHello)(); } int main () { hello (); MH_Initialize (); MH_CreateHook (hello, newhello, reinterpret_cast <LPVOID*>(&lpOldHello)); MH_EnableHook (hello); hello (); MH_DisableHook (hello); hello (); return 0 ; }
正常调用hello
1 2 3 4 00007FF7F1953E64 call hello (07FF7F1953E10h) void hello() { 00007FF7F1953E10 sub rsp,28h
hook过后
1 2 3 4 00007FF6D2C13E47 call qword ptr [lpOldHello (07FF6D2C18898h)] void hello() { 00007FF7F1953E10 jmp 00007FF7F1940FD9
在hook中调用原始函数
1 2 3 4 5 6 7 00007FF7F1953E47 call qword ptr [lpOldHello (07FF7F1958898h)] 00007FF7F1940FC0 sub rsp,28h 00007FF7F1940FC4 lea rdx,[__xmm@ffffffffffffffffffffffffffffffff+10h (07FF7F1956370h)] 00007FF7F1940FCB jmp qword ptr [7FF7F1940FD1h] 00007FF7F1953E22 call std::operator<<<std::char_traits<char> > (07FF7F1953F40h)
这里就已经说明的很清楚了,我们覆盖前几个字节会污染汇编指令,MinHook会把收到污染的汇编指令复制到一个地方A,A的尾部跳转到原有函数中没有收到污染的部分。
启用hook后修改原始函数的指针到A,这样A执行完后就会执行原函数没有污染的部分
MH_EnableHook || EnableHook
其实调用的是EnableHook
,参数为true
1 2 3 4 5 6 7 8 9 10 11 if (g_hooks.pItems[pos].isEnabled != enable){ FROZEN_THREADS threads; status = Freeze (&threads, pos, ACTION_ENABLE); if (status == MH_OK) { status = EnableHookLL (pos, enable); Unfreeze (&threads); } }
EnableHookLL:不管是不是启用全部hook,最终都会来到这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 LPBYTE pPatchTarget = (LPBYTE)pHook->pTarget; if (!VirtualProtect (pPatchTarget, patchSize, PAGE_EXECUTE_READWRITE, &oldProtect)) return MH_ERROR_MEMORY_PROTECT; if (enable){ PJMP_REL pJmp = (PJMP_REL)pPatchTarget; pJmp->opcode = 0xE9 ; pJmp->operand = (UINT32)((LPBYTE)pHook->pDetour - (pPatchTarget + sizeof (JMP_REL))); if (pHook->patchAbove) { PJMP_REL_SHORT pShortJmp = (PJMP_REL_SHORT)pHook->pTarget; pShortJmp->opcode = 0xEB ; pShortJmp->operand = (UINT8)(0 - (sizeof (JMP_REL_SHORT) + sizeof (JMP_REL))); } }
首先确保有足够权限,然后转为PJMP_REL
类型的结构体,通过该结构体修改位对应的jmp
跳转
第一次jmp后的地址还有一个jmp到指针,这里的指令就是MH_CreateHook
时创造出来的
后续的就是取消hook和释放一些全局变量,取消hook用到的是EnableHook
,参数为false,因为两个都需要修改函数的汇编指令码。