第76章 扫雷(Windows XP)

我的扫雷水平不高,所以干脆用debugger把雷都显示出来吧!

因为地雷的具体位置是随机的,所以扫雷程序里肯定会用随机函数安置地雷。这种随机函数不是某种自制的随机数生成函数,就是标准的C函数rand()。最为美妙的事情是,微软不仅公开了其产品的PDB文件,而且在PDB文件里提供了全部的函数名等符号信息。所以,当我们用IDA打开winmine.exe程序时,它会从微软下载PDB文件并且显示所有函数名称。

在IDA里可以看到,调用rand()函数的指令只有一处:

.text:01003940 ; __stdcall Rnd(x)
.text:01003940 _Rnd@4           proc near                ; CODE XREF: StartGame()+53
.text:01003940                                           ; StartGame()+61
.text:01003940
.text:01003940 arg_0            = dword ptr 4
.text:01003940
.text:01003940                  call     ds:__imp__rand
.text:01003946                  cdq
.text:01003947                  idiv     [esp+arg_0]
.text:0100394B                  mov      eax, edx
.text:0100394D                  retn     4
.text:0100394D _Rnd@4           endp

IDA把这个函数显示为rnd() 函数,那就是说扫雷游戏的开发人员给它起的名字就是rnd。这个函数非常简单:

int Rnd(int limit)
{
     return rand() % limit;
};

微软的PDB文件没有把参数命名为limit。为了便于讨论,本文给它起名为limit。可见,rnd()函数返回值是介于0~limit之间的整数。

Rand()函数的调用方函数也只有一个StartGame()函数。而且StartGame()函数应当就是安放地雷的函数:

.text:010036C7                  push     _xBoxMac
.text:010036CD                  call     _Rnd@4          ; Rnd(x)
.text:010036D2                  push     _yBoxMac
.text:010036D8                  mov      esi, eax
.text:010036DA                  inc      esi
.text:010036DB                  call     _Rnd@4          ; Rnd(x)
.text:010036E0                  inc      eax
.text:010036E1                  mov      ecx, eax
.text:010036E3                  shl      ecx, 5          ; ECX=ECX*32
.text:010036E6                  test     _rgBlk[ecx+esi], 80h
.text:010036EE                  jnz      short loc_10036C7
.text:010036F0                  shl      eax, 5          ; EAX=EAX*32
.text:010036F3                  lea      eax, _rgBlk[eax+esi]
.text:010036FA                  or       byte ptr [eax], 80h
.text:010036FD                  dec      _cBombStart
.text:01003703                  jnz      short loc_10036C7

因为扫雷游戏允许用户设置棋盘大小,所以棋盘的 X(xBoxMac)和 Y(yBoxMac)都是全局变量。Rnd()函数根据这两个参数生成随机坐标,而后0x10036FA处的OR指令设置地雷。如果这个坐标在以前已经设置过地雷了,那么0x010036E6的TEST和JNZ指令将再次生成一次坐标。

变量cBombStart不仅是设置地雷总数的全局变量,还是循环控制变量。

SHL/左移指令意味着棋盘宽度是32。

全局数组rgBlk的容量可通过数据段里rgBlk标签的地址与下一个数据的地址推算出来。数组容量应当是这两个地址之间的差值,即0x360(864)。

.data:01005340 _rgBlk           db 360h dup(?)    ; DATA XREF: MainWndProc(x,x,x,x)+574
.data:01005340                                    ; DisplayBlk(x,x)+23
.data:010056A0 _Preferences     dd  ?             ; DATA XREF: FixMenus()+2
...

数组的元素数量为:864(总容量)/32=27。

那么,rgBlk是否就是27×32的数组呢?当我们把棋盘设置为100×100的矩阵时,它会自动回滚为24×30的棋盘。所以棋盘盘面的最大值就是这个值,而且无论棋盘有多大,程序都把棋盘数据存储在这个数组里。

接下来,我们使用OllyDbg进行观察。在OllyDbg中运行扫雷游戏,然后在内存窗口里观察rgBlk数组(地址为0x1005340)。[1]

与这个数组有关的内存数据如下:

Address   Hex dump
01005340  10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|
01005350  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005360  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005370  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005380  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005390  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053A0  10 0F 0F 0F|0F 0F 0F 0F|8F 0F 10 0F|0F 0F 0F 0F|
010053B0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053C0  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
010053D0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053E0  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
010053F0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005400  10 0F 0F 8F|0F 0F 8F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005410  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005420  10 8F 0F 0F|8F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005430  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005440  10 8F 0F 0F|0F 0F 8F 0F|0F 8F 10 0F|0F 0F 0F 0F|
01005450  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005460  10 0F 0F 0F|0F 8F 0F 0F|0F 8F 10 0F|0F 0F 0F 0F|
01005470  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005480  10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|
01005490  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054A0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054B0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054C0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

与其他的16进制编辑程序相似,OllyDbg也采取了每行16 字节的显示风格。因此,一个32字节的数组对应着OllyDbg窗口里的两行数据。

启动程序的时候,我们把游戏设置为了“入门级”难度,所以棋盘大小是9×9。现在我们可以在每行0×10个字节的数据里观测到这种正方形结构。

接下来在OllyDbg单击“Run”以运行扫雷程序,然后随意点击、直到触碰地雷为止。此时即可看到棋盘中的全部地雷了,如图76.1所示。

..\tu\7601.tif

图76.1 地雷

在比较内存数据之后,我们可得出下列结论:

现在我们可对内存数据进行标注了。然后我们再用方括号标注地雷:

border:
01005340  10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
01005350  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #1:
01005360  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
01005370  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #2:
01005380  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
01005390  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #3:
010053A0  10 0F 0F 0F 0F 0F 0F 0F[8F]0F 10 0F 0F 0F 0F 0F
010053B0  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #4:
010053C0  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
010053D0  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #5:
010053E0  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
010053F0  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #6:
01005400  10 0F 0F[8F]0F 0F[8F]0F 0F 0F 10 0F 0F 0F 0F 0F 
01005410  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 
line #7:
01005420  10[8F]0F 0F[8F]0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F 
01005430  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #8:
01005440  10[8F]0F 0F 0F 0F[8F]0F 0F[8F]10 0F 0F 0F 0F 0F 
01005450  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #9:
01005460  10 0F 0F 0F 0F[8F]0F 0F 0F[8F]10 0F 0F 0F 0F 0F
01005470  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
border:
01005480  10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
01005490  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

把所有的边界数据(0x10)去除,即可得到地雷的确切位置:

 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F 0F 0F 0F 0F 0F[8F]0F
 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F[8F]0F 0F[8F]0F 0F 0F
[8F]0F 0F[8F]0F 0F 0F 0F 0F
[8F]0F 0F 0F 0F[8F]0F 0F[8F]
 0F 0F 0F 0F[8F]0F 0F 0F[8F]

上述数据的行和列与棋盘的相应信息一一对应。

在推导出数据结构之后,在OllyDbg里修改数据的尝试更为有趣。如果把所有的0x8F都替换成0x0F,那么扫雷游戏就可以如图76.2所示这样玩。

..\tu\7602.tif

图76.2 没有地雷的扫雷游戏

我们还可以把地雷都安置在第一行里,如图76.3所示。

..\tu\7603.tif

图76.3 用debugger设置地雷

不过,在玩游戏之前,用OllyDbg之类的debugger查看地雷分布毕竟不够方便。我们不妨写一个专用程序,专门导出棋盘上的地雷分布情况:

// Windows XP MineSweeper cheater
// written by dennis(a)yurichev.com for http://beginners.re/ book
#include <windows.h>
#include <assert.h>
#include <stdio.h>

int main (int argc, char * argv[])
{
        int i, j;
        HANDLE h;
        DWORD PID, address, rd;
        BYTE board[27][32];

        if (argc!=3)
        {
                printf ("Usage: %s <PID><address>\n", argv[0]);
                return 0; 
        };

        assert (argv[1]!=NULL);
        assert (argv[2]!=NULL);

        assert (sscanf (argv[1], "%d", &PID)==1);
        assert (sscanf (argv[2], "%x", &address)==1);
        h=OpenProcess (PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, PID);

        if (h==NULL)
        {
                DWORD e=GetLastError();
                printf ("OpenProcess error: %08X\n", e);
                return 0;
        };

        if (ReadProcessMemory (h, (LPVOID)address, board, sizeof(board), &rd)!=TRUE)
        {
                printf ("ReadProcessMemory() failed\n");
                return 0;
        };

        for (i=1; i<26; i++)
        {
                if (board[i][0]==0x10 && board[i][1]==0x10)
                        break; // end of board
                for (j=1; j<31; j++)
                {
                        if (board[i][j]==0x10)
                                break; // board border
                        if (board[i][j]==0x8F)
                                printf ("*");
                        else
                                printf (" ");

                };
                printf ("\n");
        };

        CloseHandle (h);
};

指定扫雷游戏的PID[2]之后,这个程序将会导出0x01005340处[3]的地雷分布图。

上述程序可以把自身绑定到PID指定的程序上,然后读取指定程序的数据。

76.1 练习题


[1] 本章以英文版Windows XP SP3中的扫雷游戏为例。如果调试的是其他版本的扫雷游戏,那么内存地址会与本例不同。

[2] PID即Program/process ID。Windows的任务管理器能够查看程序的PID。

[3] 不同版本的程序,其地址会发生变化。