用C和Assembler拦截游戏功能,游戏不稳定,改动最小

逆向工程 C 汇编
2021-06-10 20:29:17

为了向游戏添加新内容,我正在反向设计一款非常古老的游戏(Mu 97d)。我正在做的是用 DLL 拦截游戏,并在 DLL 中进行计算,以便我可以添加新内容。

我有这段代码可以拦截一个“if”语句,我将它重定向到我自己的函数——根据输入——我的函数返回真或假。

代码:

/* Called on player load - 12 bytes */
*(BYTE*)(0x004798CD  + 0) = 0x50;  /* PUSH EAX */
*(BYTE*)(0x004798CD  + 1) = 0xE8;  /* CALL */
*(DWORD*)(0x004798CD + 2) = (DWORD)&Unk11 - (0x004798CD + 6); /* FUNCTION TO CALL */
*(BYTE*)(0x004798CD  + 6) = 0x85;  /* TEST */
*(BYTE*)(0x004798CD  + 7) = 0xC0;  /* EAX, EAX */
*(BYTE*)(0x004798CD  + 8) = 0x58;  /* POP EAX */
*(BYTE*)(0x004798CD  + 9) = 0x74;  /* JE */
*(BYTE*)(0x004798CD  + 10) = 0x15;  /* 15 */
*(BYTE*)(0x004798CD  + 11) = 0x90;  /* NOP */

我正在拦截的 ASM 部分

[...]
004798A1  |.  81E7 FF000000 AND EDI,000000FF
004798A7  |.  8D0440        LEA EAX,[EAX*2+EAX]
    004798AA  |.  C1E0 02       SHL EAX,2
004798AD  |.  99            CDQ
004798AE  |.  F7FF          IDIV EDI
004798B0  |.  8BD8          MOV EBX,EAX
004798B2  |.  B8 67666666   MOV EAX,66666667
004798B7  |.  F7EF          IMUL EDI
004798B9  |.  D1FA          SAR EDX,1
004798BB  |.  8BC2          MOV EAX,EDX
004798BD      03D3          ADD EDX,EBX
004798BF      C1E8 1F       SHR EAX,1F
004798C2      8D5410 04     LEA EDX,[EDX+EAX+4]
004798C6      66:0156 12    ADD WORD PTR DS:[ESI+12],DX
004798CA      66:8B06       MOV AX,WORD PTR DS:[ESI]
004798CD      66:3D 8301    CMP AX,183 ]---------------------- FROM HERE
004798D1      7C 1A         JL SHORT 004798ED
004798D3      66:3D 8601    CMP AX,186
004798D7      7F 14         JG SHORT 004798ED ]--------------- TO HERE
004798D9      83F9 09       CMP ECX,9
004798DC      B8 09000000   MOV EAX,9
004798E1      7F 02         JG SHORT 004798E5
004798E3      8BC1          MOV EAX,ECX
004798E5      03C0          ADD EAX,EAX
004798E7      66:0146 12    ADD WORD PTR DS:[ESI+12],AX
004798EB      EB 13         JMP SHORT 00479900
004798ED      83F9 09       CMP ECX,9
004798F0  |.  B8 09000000   MOV EAX,9
004798F5  |.  7F 02         JG SHORT 004798F9
004798F7  |.  8BC1          MOV EAX,ECX
004798F9  |>  8D1440        LEA EDX,[EAX*2+EAX]
004798FC  |.  66:0156 12    ADD WORD PTR DS:[ESI+12],DX
00479900  |>  8D51 F7       LEA EDX,[ECX-9]
00479903  |.  33C0          XOR EAX,EAX
00479905  |.  85D2          TEST EDX,EDX
00479907  |.  7E 1D         JLE SHORT 00479926
00479909  |.  BB 04000000   MOV EBX,4
0047990E  |.  BF 05000000   MOV EDI,5
00479913  |>  85C0          /TEST EAX,EAX
00479915  |.  75 06         |JNE SHORT 0047991D
00479917  |.  66:015E 12    |ADD WORD PTR DS:[ESI+12],BX
0047991B  |.  EB 04         |JMP SHORT 00479921
0047991D  |>  66:017E 12    |ADD WORD PTR DS:[ESI+12],DI
00479921  |>  40            |INC EAX
[...]

这是来自我的 DLL 的代码,该函数现在的行为与游戏一样。

BOOL Unk11(short int a1)
{
    if((a1 >= 387 && a1 <= 390))
        return TRUE;
    return FALSE;
}

但是,如果我向 IF 添加第二个条件:

BOOL Unk11(short int a1)
{
    if((a1 >= 387 && a1 <= 390) || (a1 >= 404 && a1 <= 415))
        return TRUE;
    return FALSE;
}

游戏变得不稳定,即使使用写入日志文件的简单调试功能,游戏也会变得不稳定,例如:

BOOL Unk11(short int a1)
{
    Debug("Hello");
    if((a1 >= 387 && a1 <= 390))
        return TRUE;
    return FALSE;
}

我使用 Ollydbg 执行了游戏,并检查了我函数的 ASM 部分,这就是我得到的:

CPU Disasm
Address   Hex dump          Command                                  Comments
7C721B30    55              PUSH EBP
7C721B31    8BEC            MOV EBP,ESP
7C721B33    8B45 08         MOV EAX,DWORD PTR SS:[EBP+8]
7C721B36    05 7DFEFFFF     ADD EAX,-183
7C721B3B    66:B9 0300      MOV CX,3
7C721B3F    66:3BC8         CMP CX,AX
7C721B42    1BC0            SBB EAX,EAX
7C721B44    40              INC EAX
7C721B45    5D              POP EBP
7C721B46    C3              RETN

为什么 ADD EAX 是 -183?不应该是183吗?这段代码中的“<= 390”在哪里?我尝试了不同的调用约定,但我得到了相同的行为 - 游戏不稳定。

为什么即使是最小游戏,游戏也不稳定?游戏很旧,2003 年左右 - 这可能是我的 DLL 的兼容性问题吗?我正在使用 Visual Studio 2010。

我希望这是足够的信息,如果没有,请说出来!谢谢!

3个回答

不是加-1830xFFFFFE7d等于-387原来的条件是这个

387 <= x <= 390

如果我们添加-387到所有边,我们得到

0 <= x + (-387) <= 3

这正是代码中检查的内容。

至于为什么代码不稳定?好吧,它需要更详细的分析(调试?)但我能看到的是这个。就在您要替换的代码之后,ECX使用了(参见004798D9 83F9 09 CMP ECX,9),但您的函数破坏了ECX(参见7C721B3B 66:B9 0300 MOV CX,3的值尝试保存这个值,看看它是否变得更稳定。

为了补充Paweł Łukasik 的回答,ECX 的问题围绕着所谓的“调用约定”。

调用约定规定了函数调用者和被调用函数本身的义务。

此处描述Microsoft 的 32 位调用约定

它说对被调用函数的义务是-

编译器生成 prolog 和 epilog 代码以保存和恢复 ESI、EDI、EBX 和 EBP 寄存器(如果它们在函数中使用)。

相反,这意味着函数调用者必须期望其他寄存器中的值不会在调用中幸存下来。(这对于 EAX 来说是最明显的情况 - 通常用于返回值。)

Unk11 函数的编译器正在做正确的事情,因为它正在修改 EBP,它正在保留它的价值。虽然它没有义务保留 ECX。

对于接触的另一侧,这里没有调用该函数的编译器。相反,调用的是您手工编写的二进制代码,因此您有义务在此端保留 ECX。


修复它

显然,一种解决方案是使用内联汇编器,就像您在答案中所做的那样。在这里,您正在修改 ESI,因此正确地保留了它。

但是,正如您所发现的,如果您调用另一个函数,则由您在手工制作的程序集中遵守调用约定。几乎可以肯定 Debug 正在修改 ECX 或 EDX(因为它被允许这样做)会产生您所看到的不利影响。

因此,如果您还在汇编函数中保留 ECX 和 EDX,我怀疑您会发现对 Debug 的调用将起作用(或者至少,如果它失败,则是出于其他原因。)

需要考虑的另一件事是,如果您需要在 DLL 函数中使用任何更复杂的逻辑,那么使用汇编可能会很痛苦。相反,您可能想考虑使用普通 C 编写函数,就像您最初所做的那样,但编写一个非常简单的汇编包装器,在调用更易于编码的 C 函数时保留这些附加寄存器的值。

好的,几个小时后,这就是我所做的——我用 ASM 代替了 C 风格的函数;这是结果:

Naked (Unk11)
{
    _asm
    {
        PUSH ESI
        MOV ESI, DWORD PTR SS:[ESP+8]

        CMP ESI, 0x183 //387
        JL False
        CMP ESI, 0x186 //390
        JLE True
False:
        MOV EAX, FALSE
        JMP Exit
True:   
        MOV EAX, TRUE
Exit:
        POP ESI
        RETN
    }
}

现在这工作正常,唯一我仍然无法弄清楚的是,为什么我添加了“Debug();” 功能,游戏开始表现奇怪。例如,通过添加该功能,我的护甲从 95 升至 45。