Joe1sn's Cabinet

【源码分析】MinHook源代码分析

世界上果然没有魔法,到最后发现都是魔术

解读的项目地址:https://github.com/TsudaKageyu/minhook

公众号:https://mp.weixin.qq.com/s/Po_t-JGj0e3dMBKDd9i8cw

或许我们的公众号会有更多你感兴趣的内容

img

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.代码量小,就算开了代码优化也不会有较大影响)

image-20240724155339931

步入call汇编

image-20240724155353773

这里就是目前编译情况下的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()的使用跟进去

image-20240724161051582

执行到00007FF7FED8100D,会跳转到我们的函数newhello()

image-20240724161136732

这样我们就完成了一次hook,后续无论调用多少次hello函数,都会执行为newhello函数

image-20240724161944509

关于我们这里的代码

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寻址空间大大加大,单纯jmpcall已经无法跳转到地址,所以我们将带跳转的地址依次将低位、高位移动到栈顶(rsp),这样ret就能直接跳转了,这种好处就是不会污染栈和寄存器。

如果在x86(32位)的情况下,直接使用jmp跳转即可

总结一下hook的步骤就是:

  1. 找到待hook函数的地址
  2. 覆盖待hook函数汇编码,让待hook函数跳转到新的函数
  3. 取消hook恢复待hook函数的汇编码即可

P2. 使用MinHook

在项目文件夹中,然后再vs中包含

1
git clone https://github.com/TsudaKageyu/minhook

image-20240724163342822

现在我们使用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;
}

image-20240724163902834

尝试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"

// 定义指向原始 Sleep 函数的指针类型
typedef void (WINAPI* Sleep_t)(DWORD);

// 定义指向原始 Sleep 函数的指针
Sleep_t fpSleep = nullptr;

// 自定义的 MySleep 函数
// 参数要保持一致
void WINAPI MySleep(DWORD dwMilliseconds)
{
std::cout << "MySleep called with " << dwMilliseconds << " milliseconds" << std::endl;

// 调用原始的 Sleep 函数
fpSleep(dwMilliseconds);
}

int main()
{
MH_Initialize();

// 创建一个 Hook
MH_CreateHook(Sleep, MySleep, reinterpret_cast<LPVOID*>(&fpSleep));

// 启用 Hook
MH_EnableHook(MH_ALL_HOOKS);

Sleep(1000);

// 清理 Hook
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();

Sleep(1000);

return 0;
}

image-20240724165152787

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)
{
// Initialize the internal function buffer.
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
//        if (IsExecutableAddress(pTarget) && IsExecutableAddress(pDetour))
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
////////   MH_CreateHook
UINT pos = FindHookEntry(pTarget);
if (pos == INVALID_HOOK_POS)

//////// FindHookEntry
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
////////   MH_CreateHook
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
////////   AllocateBuffer
LPVOID AllocateBuffer(LPVOID pOrigin)
{
PMEMORY_SLOT pSlot;
PMEMORY_BLOCK pBlock = GetMemoryBlock(pOrigin);
if (pBlock == NULL)
return NULL;

// Remove an unused slot from the list.
pSlot = pBlock->pFree;
pBlock->pFree = pSlot->pNext;
pBlock->usedCount++;
#ifdef _DEBUG
// Fill the slot with INT3 for debugging.
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
///////		GetMemoryBlock 节选
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

// Look the registered blocks for a reachable one.
for (pBlock = g_pMemoryBlocks; pBlock != NULL; pBlock = pBlock->pNext)
{
#if defined(_M_X64) || defined(__x86_64__)
// Ignore the blocks too far.
if ((ULONG_PTR)pBlock < minAddr || (ULONG_PTR)pBlock >= maxAddr)
continue;
#endif
// The block has at least one unused slot.
if (pBlock->pFree != NULL)
return pBlock;
}
/*...*/
  • GetSystemInfohttps://learn.microsoft.com/zh-cn/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsysteminfo

    1
    lpMinimumApplicationAddress

    指向应用程序和动态链接库可访问的最低内存地址的指针, (DLL) 。

    1
    lpMaximumApplicationAddress

    指向应用程序和 DLL 可访问的最高内存地址的指针。

  • 在64位下寻找一块距离参数pOrigin最近的内存地址,作者这里对这种内存自行进行了管理,用的单链表(Windows的内存管理)

接着再回到创建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, // 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
};
/*...*/

//计算汇编指令长度
copySize = HDE_DISASM((LPVOID)pOldInst, &hs);

/*...*/

if (oldPos >= sizeof(JMP_REL))
{
// The trampoline function is long enough.
// Complete the function with the jump to the target function.
#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__)
// Create a relay function.
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
////////   MH_CreateHook
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
////////   MH_CreateHook
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;

image-20240724181309357

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跳转

image-20240724184129018

第一次jmp后的地址还有一个jmp到指针,这里的指令就是MH_CreateHook时创造出来的

后续的就是取消hook和释放一些全局变量,取消hook用到的是EnableHook,参数为false,因为两个都需要修改函数的汇编指令码。