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函数具体内容:
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。
// 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可能会优先执行缓存中的指令,使得修改的指令得不到执行。所以我们需要使用一个隐藏的系统调用来刷新一下缓存,小心驶得万年船~:
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这个结构体了:
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循环的源代码如下:
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”,线程就能够顺利的执行了。源代码如下:
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位跳转,被放置在目标函数的附近。
JMP_ABS jmp = { //64位绝对地址jmp 13字节
0xFF, 0x25, 0x00000000, // FF25 00000000: JMP [RIP+6]
0x0000000000000000ULL // Absolute destination address
先来看看x64模式下的Trampoline结构体:
#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的绝对地址跳转:
#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:
// 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字节):
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了:
#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相对地址跳转指令覆盖重写目标函数时四字节的相对偏移能够成功达到目的。
#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近跳转指令的情况:
//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中有对应的情况判断与处理:
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当中:
__movsb((LPBYTE)Trampoline->MemorySlot + NewPos, (const unsigned char*)CopyData, CopyDataLength);
下一步就是构建Relay成员指向地址的内容:
#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的自定义函数.
先来看这段汇编代码:
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指令:
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指令了:
//Call指令
PVOID v4 = Asm_1(Asm_4);
Asm_4(); if (SeCreateHook(v4, &FakeSub_4,
reinterpret_cast<LPVOID*>(&__OriginalSub_4)) != STATUS_SUCCESS)
{
return;
}
(待补充)
MinHook测试分析02 (x64)的更多相关文章
- 正则表达式测试分析工具Expresso
正则表达式测试分析工具Expresso 一个正则表达式的小工具--myRegexHelper 把以前做的一个功能抽取出来做成一个小的正则表达式测试工具.没什么特色,有两点功能: 一.方便的测试正则 ...
- Hibernate一级缓存测试分析
Hibernate 一级缓存测试分析 Hibernate的一级缓存就是指Session缓存,此Session非http的session会话技术,可以理解为JDBC的Connection,连接会话,Se ...
- 【测试的艺术】+测试分析&测试计划+模板
一.项目概述 1.1.项目背景 #就是说一下为什么要做这个项目 1.2.项目目标 #这个项目最终要达到的目标是什么 二.项目整体分析 #项目分为哪些部分?各部分之间的关联是什么?各部分的目标是什么? ...
- 图形测试分析毫无头绪?HarmonyOS图形栈测试技术帮你解决
作者:huangran,图形图像技术专家 应用开发以后无法知道性能瓶颈的根因是什么?滑动卡顿.白块产生的原因是什么?代码写完之后,不知道如何优化让它表现地更好-- 我们发现,如今测试人员的需求已经不只 ...
- [原创]MinHook测试与分析(x64下 E9,EB,CALL指令测试,且逆推测试微软热补丁)
依稀记得第一次接触Hook的概念是在周伟民先生的书中-><<多任务下的数据结构与算法>>,当时觉得Hook很奇妙,有机会要学习到,正好近段日子找来了MiniHook,就一 ...
- MinHook测试与分析(x86下 E8,E9,EB,CALL指令测试,且逆推测试微软热补丁)
依稀记得第一次接触Hook的概念是在周伟民先生的书中-><<多任务下的数据结构与算法>>,当时觉得Hook的本质就是拦截,就算到现在也是如此认为. 本篇文章是在x86下测 ...
- LoadRunner测试结果分析02 转载至zhangzhe的新浪博客
LoadRunner测试结果分析之我见 上述测试过程的重点在于事务,而LoadRunner生成的测试结果图并不局限于事务上,其中还有是关于Vusers.Errors.Web Resources.Web ...
- 【测试分析】HTSM模型
◆版权声明:本文出自胖喵~的博客,转载必须注明出处. 转载请注明出处:http://www.cnblogs.com/by-dream/p/5508428.html 概述 HTSM全称Heuristic ...
- jQuery源码分析-02正则表达式-RegExp-常用正则表达式
2.4 常用正则表达式在网上找到一篇广为流传的文章<常用正则表达式>,逐一分析,不足地方进行补充和纠正. 常用的数字正则(严格匹配) 正则 含义 ^[1-9]\d*$ 匹配正整数 ^-[1 ...
随机推荐
- Java问题解决:springboot启动出现-Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package
参考资料: http://www.mamicode.com/info-detail-2101273.html https://blog.csdn.net/u012834750/article/deta ...
- inline temp 内联临时变量
double basePrice = order.getPrice(); return basePrice; 改成 return order.getPrice(); 去掉临时无用的临时变量
- SpringBoot整合Mybatis注解版---update出现org.apache.ibatis.binding.BindingException: Parameter 'XXX' not found. Available parameters are [arg1, arg0, param1, param2]
SpringBoot整合Mybatis注解版---update时出现的问题 问题描述: 1.sql建表语句 DROP TABLE IF EXISTS `department`; CREATE TABL ...
- ibm产品系列架构师技术路线
- 移动端遇到的问题小结--video
本篇主要是针对Android系统,所遇到的问题. 1. video的全屏处理: 这里说的全屏是指针对浏览器的全屏,而不是整个手机的全屏.要想全屏效果只需对video标签加 webkit-plays ...
- UnicodeEncodeError: 'gbk' codec can't encode character '\xee'
在将爬取到的内容写入文件时候报了这个错误,解决方案是在open()的时候给encoding参数传'utf-8'就好了,因为网页的编码就是utf-8. with open('douban.html',' ...
- element-ui <el-date-picker> 回显格式 yyyy-MM-dd 传值格式 yyyyMMddHHmmss
<template> <!-- 需求:使用 <el-date-picker> 日期插件 前端显示2018-10-22 后台需要传时间戳,对这个日期插件不熟悉,当时搞了好长 ...
- 2018 ACM-ICPC青岛现场赛 B题 Kawa Exam 题解 ZOJ 4059
题意:BaoBao正在进行在线考试(都是选择题),每个题都有唯一的一个正确答案,但是考试系统有m个bug(就是有m个限制),每个bug表示为第u个问题和第v个问题你必须选择相同的选项,题目问你,如果你 ...
- mysql权限参考
mysql日常管理和应用工作中,大家经常会涉及到授权问题,下面,我们就列举下权限相关的参考. 1.管理权限(Administrative Privileges) Privilege Name ...
- Python3列表(list)比较操作教程
一.相等比较 1.1 同顺序列表比较 顺序相同直接用“==”进行比较即可 list1 = ["one","two","three"] lis ...