除非使用某些寄存器,否则函数挂钩会崩溃

Function hook crashes unless certain registers are used

本文关键字:函数 崩溃 寄存器      更新时间:2023-10-16

所以我试图为游戏挂接一个函数,但有一个小问题。如果eax、ebx、ecx和edx等寄存器是可互换的,那么下面的第一个代码示例为什么会破坏游戏进程,而第二个代码却没有崩溃并按预期工作?

// Crashes game process
void __declspec(naked) HOOK_UnfreezePlayer()
{
__asm push eax
if ( !state->player.frozen || !state->ready )
__asm jmp hk_Disabled
__asm
{
mov eax, g_dwBase_Addr
mov ebx, [eax + LOCAL_PLAYER_INFO_OFFSET]
add ebx, 0x4
mov ecx, [ebx]
add ecx, 0x40
lea edx, [esi + 0x0C]
cmp edx, ecx
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0
hk_Return:
pop eax
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}
}
// Works
void __declspec(naked) HOOK_UnfreezePlayer()
{
__asm push eax
if ( !state->player.frozen || !state->ready )
__asm jmp hk_Disabled
__asm
{
mov ecx, g_dwBase_Addr
mov edx, [ecx + LOCAL_PLAYER_INFO_OFFSET]
add edx, 0x4
mov ebp, [edx]
add ebp, 0x40
lea ecx, [esi + 0x0C]
cmp ecx, ebp
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0
hk_Return:
pop eax
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}
}

我认为崩溃可能是由于我的汇编代码覆盖了eax、ebx、ecx等寄存器中的重要数据造成的。例如,如果游戏在eax中存储了一个重要值,然后由于我的if语句将结构指针移动到eax中而导致数据丢失,该怎么办?有没有办法保存这些寄存器的内容,并在返回之前将其恢复到原始值?

挂接已编译的程序时,寄存器肯定是不可交换的,因为单个寄存器的含义是由挂接程序的代码和挂接在该代码中的位置定义的。因此,您必须检查挂钩代码和挂钩的位置,以确定挂钩代码是否依赖于所保留的某些寄存器的内容。

push eax指令在开始,pop eax指令在结束,您已经在保留EAX寄存器的内容,然后再恢复它。您可以对EBX和EDX寄存器执行同样的操作,也可以简单地使用PUSHAD/POPAD指令来保存所有通用寄存器。根据钩子在游戏中的位置,您可能还必须保留EFLAGS寄存器,这需要PUSHFD/POPFD指令。

保存和恢复ECX寄存器并不是那么容易,因为钩子正在使用该寄存器来计算完成后要跳转到的地址。

然而,由于您说第二个代码样本有效,而第一个代码样本导致挂钩程序崩溃,因此问题很可能只是修改了EBX寄存器。这是因为第一个代码样本修改EBX寄存器,而第二个代码样本不修改。

因此,问题的可能解决方案是以与EAX寄存器相同的方式保留EBX寄存器。为此,您只需在push eax指令的同一位置添加一条push ebx指令,并在pop eax指令的相同位置添加一个pop ebx指令。但是,请注意,由于堆栈的工作方式,推送和弹出指令必须按相反的顺序,如下所示:

挂钩启动:

push eax
push ebx

钩尾:

pop ebx
pop eax

如果eax、ebx、ecx和edx等寄存器是可互换的,为什么下面的第一个代码示例会破坏游戏进程,而第二个代码不会崩溃并按预期工作?

在这个函数跳转到g_dwBase_Addr + RETURN_UnfreezePlayer之后,无论它在哪里,您的调用者可能正在使用EBX处理重要的事情。

如果您正在挂接一个现有的函数调用,那么EAX、ECX和EDX在标准调用约定中会被调用阻塞,而保留其他整数regs调用。

当你销毁EBP时,你的呼叫者碰巧没有崩溃,这是有道理的,只有当你销毁了EBX时。

或者,如果您正在将对此代码的跳转/调用插入到根本不需要函数调用的地方,那么您应该保存/恢复您修改的每个寄存器,可能包括EFLAGS。(查看"调用站点",看看它是否在您"返回"后销毁任何寄存器;例如,addcmp只写EFLAGS,而不读,所以如果您看到这样的指令,您就知道不必保存/还原EFLAGS。同样,mov的目的地也是只写的。)


具体来说,在你做任何其他事情之前,在你的函数的顶部:

_asm {
push  eax
push  ecx
push  edx
// and whatever other register you need
}

在底部,在跳过之前,按匹配的顺序弹出它们

_asm {
// and whatever other register you need
pop   edx
pop   ecx
pop   eax
jmp   target
}

您正在使用寄存器来保存跳跃目标。您可能能够分析"调用者"并找到一个可以安全销毁的寄存器,这样您就可以在不进行/save还原的情况下使用该寄存器。或者对跳转目标地址进行硬编码,以便使用jmp rel32而不是间接jmp reg

或者(以显著的性能成本)您可以用push/ret替换jmp

_asm {
push eax    // extra dummy slot we can replace with a return address
push eax
push ecx
push edx
...
pop  edx
pop  ecx
//pop  eax
mov  eax, g_dwBase_Addr
add  eax, RETURN_UnfreezePlayer
mov  [esp+4], eax       // store into the dummy slot
pop  eax
ret                     // branch mispredict guaranteed
}

使用等效的push/ret可以保证此ret以及调用堆栈上未来ret指令的分支预测错误,因为我们得到了不匹配的调用/ret预测堆栈。在这个函数中的某个地方使用一个伪call可以修复这个问题,只会使thisret预测错误。(但请注意,call next_instruction不起作用;CPU是特殊情况,不要将其视为真正的调用。您必须实际跳过某些内容。)。http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0)

您可能很想使用xchg [esp], eax/ret,但这非常慢:带有内存操作数的xchg意味着lock前缀(全内存屏障,微码原子交换)。

在最初推送时为"返回地址"保留一个插槽似乎是最有效的,否则您可能会推送一个返回地址,mov加载保存的EAX值,然后pop [esp+4]将该返回地址最多复制4个字节。但是,在发现ret预测失误之前,额外的拷贝会增加延迟。

如果这不一定是线程安全的,则可以在存储目标地址后使用jmp [target_address]。或者,如果g_dwBase_Addr + RETURN_UnfreezePlayer是一个常数,只需将其保存在某个静态变量中,这样就可以jmp dword ptr [target_address],而不是每次都计算目标。

可以使用ESP下方的空间,但这并不完全安全。类似于恢复寄存器后的jmp [esp-4]。SEH可以介入,调试器也可以。


您可以优化函数以使用更少的寄存器

具体来说,你只需要修改一个,这样你就可以保存/恢复它。或者,如果你选择了一个,你就可以安全地获取返回地址。

在这两条指令之后,您就再也不用EAX了。

mov eax, g_dwBase_Addr
mov ebx, [eax + LOCAL_PLAYER_INFO_OFFSET]

所以你可以用EAX代替EBX:

mov eax, g_dwBase_Addr
mov eax, [eax + LOCAL_PLAYER_INFO_OFFSET]
; then use EAX everywhere you were using EBX in later instructions

因此,要保存/恢复的寄存器更少。此外,这毫无意义:

add ebx, 0x4         ;  add eax, 4        // with changes from above
mov ecx, [ebx]       ;  mov ecx, [eax]

可以在寻址模式下执行+4mov ecx, [eax + 4]

add/lea->cmp也可以进行优化。CCD_ 33与CCD_。

// no push or pop needed, destroying only ECX
_asm {
mov ecx, g_dwBase_Addr
mov ecx, [ecx + LOCAL_PLAYER_INFO_OFFSET]
mov ecx, [ecx+4]
add ecx, 0x40 - 0x0C
cmp ecx, esi               // ecx+0x40 == esi+0x0C
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0    // regs from the caller
hk_Return:
// Assuming we can destroy caller's ECX.
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}