刘收获

导航

< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5

统计

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;
}

  (待补充)

posted on   沉疴  阅读(1771)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
历史上的今天:
2016-09-12 杭电2023 平均成绩
点击右上角即可分享
微信分享提示