问题背景

在尝试使用MinHook通过hook修改植物大战僵尸游戏时,遇到了这样的情况。

以下是种植植物时,减少阳光的函数。

.text:0041BA60 ; =============== S U B R O U T I N E =======================================
.text:0041BA60
.text:0041BA60
.text:0041BA60 sub_41BA60      proc near               ; CODE XREF: sub_40FD30+B41↑p
.text:0041BA60                                         ; sub_421F10+13D↓p ...
.text:0041BA60                 push    esi
.text:0041BA61                 mov     esi, [edi+5560h]
.text:0041BA67                 mov     edx, edi
.text:0041BA69                 call    sub_41B980
.text:0041BA6E                 add     eax, esi
.text:0041BA70                 cmp     ebx, eax
.text:0041BA72                 jg      short loc_41BA80
.text:0041BA74                 sub     esi, ebx
.text:0041BA76                 mov     [edi+5560h], esi
.text:0041BA7C                 mov     al, 1
.text:0041BA7E                 pop     esi
.text:0041BA7F                 retn
.text:0041BA80 ; ---------------------------------------------------------------------------
.text:0041BA80
.text:0041BA80 loc_41BA80:                             ; CODE XREF: sub_41BA60+12↑j
.text:0041BA80                 mov     ecx, [edi+8Ch]
.text:0041BA86                 mov     eax, [ecx]
.text:0041BA88                 mov     edx, dword_6A7B60
.text:0041BA8E                 mov     eax, [eax+0D8h]
.text:0041BA94                 push    edx
.text:0041BA95                 call    eax
.text:0041BA97                 mov     dword ptr [edi+5578h], 46h ; 'F'
.text:0041BAA1                 xor     al, al
.text:0041BAA3                 pop     esi
.text:0041BAA4                 retn
.text:0041BAA4 sub_41BA60      endp
.text:0041BAA4
.text:0041BAA4 ; ---------------------------------------------------------------------------

再看看伪代码

char __usercall sub_41BA60@<al>(int a1@<ecx>, int a2@<ebx>, int a3@<edi>)
{
  int v3; // esi
  char result; // al

  v3 = *(_DWORD *)(a3 + 21856);
  if ( a2 > v3 + sub_41B980(a1, a3) )
  {
    (*(void (__thiscall **)(_DWORD, int))(**(_DWORD **)(a3 + 140) + 216))(*(_DWORD *)(a3 + 140), dword_6A7B60);
    *(_DWORD *)(a3 + 21880) = 70;
    result = 0;
  }
  else
  {
    *(_DWORD *)(a3 + 21856) = v3 - a2;
    result = 1;
  }
  return result;
}

这个函数的逻辑很简单,a3 + 21856显然就是储存阳光值的位置,而传入参数a2就是要减少的值,如果想实现作弊,只需要把a2设为0,或者直接修改a3 + 21856处的内容。

但现在有一个棘手的问题,这个函数并未使用__fastcall__stdcall等常见的调用约定,而是完全使用寄存器传参,那么如果按照一般的hook方法是拿不到正确的参数的。

解决方案

Detour函数部分

可以把真正的hook函数套在一个“壳子”里,我们在壳子的部分完成函数的传递之后再调用真正的hook函数。下面是具体的实现方法。

函数sub_41BA60的三个传入参数a1a2a3分别储存在寄存器ECXEBXEDI中,我们在“壳子”部分需要做的事情就是将这三个参数压入栈之后,调用hook函数(此处hook函数使用cdecl调用约定)。

由于我们需要完全控制“壳子”函数的汇编代码,所以还需要为其添加__declspec(naked)声明(MSVC特有,GCC/Clang应使用__attribute__((naked))),以免编译器生成多余的部分破坏寄存器内容。下面是“壳子”的代码。

__declspec(naked) void hook_func()
{
	__asm {
		push ebp        // 保存基址指针
		mov ebp, esp    // 设置新的栈帧
		// 保存可能被修改的非易失寄存器
		push ebx
		push esi
		push edi
		push edi // EDI -> a3
		push ebx // EBX -> a2
		push ecx // ECX -> a1
		// 调用目标函数
		call hook_func_payload
		// 保存返回值
		movzx eax, al
		// 清理栈帧
		add esp, 12
		// 恢复寄存器
		pop edi
		pop esi
		pop ebx
		leave
		ret
	}
}

其中调用的hook_func_payload即为真正的hook函数,使用__cdecl调用约定,可以直接拿到正确的参数。下面是其代码(我直接把阳光值设为9990了hehehe)。

char __cdecl hook_func_payload(int a1, int a2, int a3) {
	*(uintptr_t*)(a3 + 21856) = 9990;

        // 返回1代表正常情况,否则游戏会判断种植失败
	return 1;
}

使用MinHook创建钩子。

MH_CreateHook(reinterpret_cast<LPVOID>(target), &hook_func, nullptr);

验证

既然是游戏,那就不用调试器了,直接进游戏验证!

完成上面的操作后,当在游戏中任意种植植物后,阳光数会变成9990。

种个不要钱的小喷菇试试

阳光已经变为9990,说明我们的钩子生效了。

蹦床函数部分

现在我们已经可以拦截一个“usercall”函数了,但对于更一般的情况,当需要调用原函数时该如何处理?

其实也不复杂,我们在上面的“壳子”函数中做的事情是将参数从寄存器压入栈,想要调用原始函数时,同样也需要一个类似的“壳子”,但做的事情需要反过来,即将参数从栈复制到寄存器。下面是具体的实现方法。

声明原始函数指针,这里如何确定调用约定以及参数类型和个数其实并不重要。

typedef char(__cdecl* original)(int a1, int a2, int a3);

初始化原始函数指针。

original hook_original = nullptr;

创建钩子时,将指针填入第三个参数。

MH_CreateHook(reinterpret_cast<LPVOID>(target), &hook_func, reinterpret_cast<LPVOID*>(&hook_original));

在需要调用原始函数的地方加入内联汇编代码,例如在我上面写的hook_func_payload函数中,我不希望直接返回1,而是返回到原始函数中,那么就可以这样写。

char __cdecl hook_func_payload(int a1, int a2, int a3) {
	*(uintptr_t*)(a3 + 21856) = 9990;
	//return 1;
	__asm {
		push ebx
		push esi
		push edi
		push ecx
		push ebx
		push edi
                // 关键:从栈中取出参数
		mov ecx, [ebp + 8]   // a1 -> ECX
		mov ebx, [ebp + 12]  // a2 -> EBX
		mov edi, [ebp + 16]  // a3 -> EDI
                
		call hook_original
		movzx eax, al  // 获取返回值
		pop edi
		pop ebx
		pop ecx
		pop edi
		pop esi
		pop ebx
		leave
		ret
	}
}

验证

经过如上操作,此时种下植物后,阳光会先被修改为9990,由于返回了原始函数,阳光会在此基础上再减去这个植物的价值,下面进行验证。

种下一颗向日葵,种植完成后,阳光值应该为9940。

与预期相符,说明hook函数成功返回到了原始函数,完成了扣除阳光。

我要成为玩钩子高手