在逆向 pvz 的时候,有这样的一个射击函数,它必须在存在有效目标的情况下才能继续执行:
1 2 3 4 5 6 7 8 9 10 11 12
| char __userpurge Plant::FindTargetAndFire( eax = Plant * this, int theRow, PlantWeapon thePlantWeapon ) { TargetZombie = Plant::FindTargetZombie(...); if ( !TargetZombie ) return 0; ... }
|
但我其实想的是不管有没有目标,都能进行射击。话不多说看一眼控制流:

绿色的部分是 IDA 自动标注的 Plant::FindTargetZombie(...) 调用汇编指令,需要注意的是这个函数为 thiscall 调用约定。
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
| 0x0045EF10 ; 函数序言开始 0x0045EF10 push ecx ; 保存非易失性寄存器(函数序言) 0x0045EF11 mov ecx, [esp+4+8] ; 将 this 存入 ecx(函数调用) 0x0045EF15 push ebx ; 保存非易失性寄存器(函数序言) 0x0045EF16 push ebp ; 保存非易失性寄存器(函数序言) 0x0045EF17 push esi ; 保存非易失性寄存器(函数序言) 0x0045EF18 mov esi, eax ; eax 存了传给这个函数的 this,毕竟是 userpurge 0x0045EF1A mov eax, [esp+10h+4] ; 压栈参数(函数调用) 0x0045EF1E push edi ; 压栈参数(函数调用) 0x0045EF1F push eax ; 压栈参数(函数调用) 0x0045EF20 push esi ; 压栈参数(函数调用) 0x0045EF21 call Plant__FindTargetZombie ; 调用函数(函数调用) 0x0045EF21 0x0045EF26 test eax, eax ; 处理返回值 0x0045EF28 mov [esp+14h-4], eax ; 处理返回值 0x0045EF2C jnz short loc_45EF38 ; if 判断 0x0045EF2C 0x0045EF2E xor al, al ; 函数尾声 0x0045EF30 pop edi ; 函数尾声 0x0045EF31 pop esi ; 函数尾声 0x0045EF32 pop ebp ; 函数尾声 0x0045EF33 pop ebx ; 函数尾声 0x0045EF34 pop ecx ; 函数尾声 0x0045EF35 retn 8 ; 函数尾声 0x0045EF35 0x0045EF38 ; -------------------------------- 0x0045EF38 0x0045EF38 loc_45EF38: 0x0045EF38 ; 真正的射击函数
|
这段碰巧在函数最开头。也就是说,通过简单复原函数序言,就可以直接跳到这个 jnz 后边运行下面的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #[unsafe(naked)] pub extern "thiscall" fn FireWithoutTarget( this: *mut Plant, theRow: i32, thePlantWeapon: i32 ) { naked_asm!( "mov eax, ecx", "push ecx", "push ebx", "push ebp", "push esi", "mov esi, eax", "push edi", "mov edx, 0x0045EF38", "jmp edx", ) }
|
这个函数除了复原函数序言,以及额外的 ecx -> eax,根本不用管其他参数,反正是按照调用约定压栈的,一切就交给原本的函数吧!
顺带一提这个裸函数最后也没有 ret,是因为调用这个函数时 call 压栈的返回地址会被原函数的 ret 使用,于是变相成功返回。