问题背景
在尝试使用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
的三个传入参数a1
、a2
、a3
分别储存在寄存器ECX
、EBX
、EDI
中,我们在“壳子”部分需要做的事情就是将这三个参数压入栈之后,调用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函数成功返回到了原始函数,完成了扣除阳光。
我要成为玩钩子高手

Comments NOTHING