彩球游戏有多个衍生版本。本章采用的是1997年发布的BallTrix版。这款程序可从http://go. yurichev.com/17311
公开下载。它的图形界面如图75.1所示。
图75.1 游戏界面
本章关注它的随机生成器,以及修改这个组件的具体方法。IDA在balltrix.exe里识别出了标准函数_rand。这个函数的地址是0x00403DA0。不仅如此,IDA还判断出该函数只会被这一处调用。
.text:00402C9C sub_402C9C proc near ; CODE XREF: sub_402ACA+52
.text:00402C9C ; sub_402ACA+64 ...
.text:00402C9C
.text:00402C9C arg_0 = dword ptr 8
.text:00402C9C
.text:00402C9C push ebp
.text:00402C9D mov ebp, esp
.text:00402C9F push ebx
.text:00402CA0 push esi
.text:00402CA1 push edi
.text:00402CA2 mov eax, dword_40D430
.text:00402CA7 imul eax, dword_40D440
.text:00402CAE add eax, dword_40D5C8
.text:00402CB4 mov ecx, 32000
.text:00402CB9 cdq
.text:00402CBA idiv ecx
.text:00402CBC mov dword_40D440, edx
.text:00402CC2 call _rand
.text:00402CC7 cdq
.text:00402CC8 idiv [ebp+arg_0]
.text:00402CCB mov dword_40D430, edx
.text:00402CD1 mov eax, dword_40D430
.text:00402CD6 jmp $+5
.text:00402CDB pop edi
.text:00402CDC pop esi
.text:00402CDD pop ebx
.text:00402CDE leave
.text:00402CDF retn
.text:00402CDF sub_402C9C endp
为了便于讨论,我们把_rand函数的调用方函数叫作“random”。在程序里有三处的代码调用了random函数。
调用random函数的前两处代码是:
.text:00402B16 mov eax, dword_40C03C ; 10 here
.text:00402B1B push eax
.text:00402B1C call random
.text:00402B21 add esp, 4
.text:00402B24 inc eax
.text:00402B25 mov [ebp+var_C], eax
.text:00402B28 mov eax, dword_40C040 ; 10 here
.text:00402B2D push eax
.text:00402B2E call random
.text:00402B33 add esp, 4
调用random函数的第三处代码是:
.text:00402BBB mov eax, dword_40C058 ; 5 here
.text:00402BC0 push eax
.text:00402BC1 call random
.text:00402BC6 add esp, 4
.text:00402BC9 inc eax
综合上述代码,我们可以判定该函数只有一个参数。前两处传递的参数是10,第三处传递的参数是5。在观察游戏的界面后,可知棋盘是10×10的方阵,而彩球的颜色总共有5种。这三处调用random的指令,必定是坐标和颜色的生成指令。标准的随机函数rand()函数会生成一个在0~0x7FFF之间的返回值,用起来并不方便。实际上,编程人员会编写自己的随机函数以获取特定区间之内的随机返回值。本例需要的随机数是0~(n−1)之间的整数,n就是函数所需的唯一参数。这一假设可由任意一种debugger验证。
本章将修改第三处调用指令,让它的第三次返回值永远是0。为此,我们可把PUSH/CALL/ADD这三条指令改为NOP,然后再添加XOR EAX,EAX指令,以清空EAX寄存器。
.00402BB8: 83C410 add esp,010
.00402BBB: A158C04000 mov eax,[00040C058]
.00402BC0: 31C0 xor eax,eax
.00402BC2: 90 nop
.00402BC3: 90 nop
.00402BC4: 90 nop
.00402BC5: 90 nop
.00402BC6: 90 nop
.00402BC7: 90 nop
.00402BC8: 90 nop
.00402BC9: 40 inc eax
.00402BCA: 8B4DF8 mov ecx,[ebp][-8]
.00402BCD: 8D0C49 lea ecx,[ecx][ecx]*2
.00402BD0: 8B15F4D54000 mov edx,[00040D5F4]
也就是说,我们修改调用random()函数的有关指令,让它的返回值固定为0。
修改程序后,它的运行界面如图75.2所示。
图75.2 作弊成功
不得不说我们修改得很成功。我在当初修改这个游戏的时候,希望我的同事明白“没有必要执迷于你肯定会赢的游戏”。可惜我的劝阻没能成功。
另外还有一个问题:为什么random()函数的参数会是全局变量?这是因为棋盘大小是可调整的变量,不能在程序里把它写成常量。本例中的5和10只是它的默认值。