【武大软件逆向课程/第一周】扫雷逆向分析批量插旗及机器指令表.exe文件字体更换

作业目标

  • 逆向分析扫雷程序winmine.exe,并编写程序对其进行批量插旗

  • 更换机器指令表.exe内的显示字体

扫雷

定位绘制雷区的代码位置

    使用Ollydbg打开winmin.exe。

使用Ollydbg打开winmin.exe

    在当前模块中右键->查找->当前模块中的名称(标签),在弹出的窗口中寻找名称为Bitblt的函数。这里可以直接右键->在每个参考上设置断点,但是我的Ollydbg会报错。因此采用后续的方法。

寻找Bitblt函数

    选中Bitblt项后,按回车键Enter,在弹出的窗口中再次按回车Enter

定位Bitblt源码

    执行上述操作后弹出的窗口即为Bitblt的源码,在源码首行按F2或者右键设置断点。

在Bitblt函数源码首行打断点

    设置断点后按F9或功能按钮运行程序,程序会暂停在断点处。此时按Ctrl + F9或者功能按钮执行到返回,程序会运行到函数Bitblt调用处的下一行。

Bitblt调用处上下文

    观察上下文程序可以发现是类似于一个循环的结构,其中esi寄存器为循环变量。

定位雷区数据地址

在这一节内需要观察不同操作下内存区域的变化。

在Ollydbg中可以通过单击任意数据来刷新数据窗口中的内容。

如果想要重新绘制扫雷窗口,需要最小化后恢复窗口。

    观察内存读取部分,代码中只有两句可能是对于数组的读取。分别为:

mov    al, byte ptr [ebx+esi]
push   dword ptr [eax*4+1005A20]

    这里可以记录一下Bitblt调用处的地址1002706,然后取消所有断点重新运行程序,将游戏切换到高级模式,随机点击一个地块。这里为了便于定位雷区数据位置,我们多次开局点击地块,直到仅连锁翻开一个地块(这样对雷区数据的影响较小,数据特征更明显)。

高级模式点击地块结果

    然后将在数据窗口中右键->转到->表达式或者按Ctrl + G,输入内存读取部分的常量地址1005A20跳转到该地址。观察数据窗口内该地址处的数据。

    发现并不符合雷区数据的特征(至少应该出现大量相同的两种数据)。实际上作为绘制雷区的函数,对数组下标的访问应该具有循环变量,也就是esi寄存器应该参与内存地址的计算,而这条指令并不符合。

    因此选择下面的指令来寻找雷区数据所在的内存地址。

mov    al, byte ptr [ebx+esi]

    在这条指令上打断点后最小化扫雷窗口,再恢复窗口,查看ebx寄存器的值,发现是内存地址1005360,在数据窗口中跟踪该地址。

查看ebx寄存器的值

    该地址处的内存数据如下。可以大致推测,未翻开的雷区和未翻开的空白对应数据为0x0F或0x8F。

雷区数据地址内容

    再次重开游戏,翻开第一个地块以定位数据区域首个元素的位置,如下图所示。

定位雷区数据

    对比可知1005361为首的地址存放雷区数据。

分析地块状态对应的内存值

非雷区地块

    从上一节的最后一个图可以得知,翻开的2地块对应的内存值为0x42。推测翻开的n对应的内存值为0x4n

    翻开相邻地块进行验证,可知推测成立,同时也得知未翻开的非雷区对应的内存值为0x0F

分析非雷区地块

雷区地块

    再次观察内存区域,在9个地块后,有一个地块的内存值为不同的0x8F,翻开该地块后再次观察内存区域变化。

分析雷区地块

    可知未翻开的雷0x8F翻开的雷0x8A踩中的雷0xCC

插旗地块与存疑地块

    重开游戏并刷新数据窗口。在任意0x8E0x0F对应的地块上右键以插旗后,刷新数据窗口,观察对应数据变化。扫雷游戏中还有一种标记,在插旗的地块上再次右键可以变为问号地块,这里称为存疑地块,与本次任务并不相关,但是这里还是给出其对应的内存值。

分析插旗地块与存疑地块

    观察上图可知,插旗地块的0x?F被替换为0x?E,存疑地块的0x?F被替换为0x?D


至此,所有地块状态对应的内存值均被我们分析出来。

    简单总结一下,地块状态对应的内存值如下:

地块状态 内存值 地块状态 内存值
未翻开的空地 0x0F 插旗的空地 0x0E
翻开的空地 0x4n(n为要显示的数字) 存疑的空地 0x0D
未翻开的地雷 0x8F 插旗的地雷 0x8E
翻开的地雷 0x8A 存疑的地雷 0x8D
踩中的地雷 0xCC

    此外,按Ctrl + 调整数据窗口首列数据从0x01偏移开始展示,可以发现在高级模式下30 * 16的布局下,每30个地块的数据后都有两个0x10,用于标注此行结束,在后面编写程序时,应当注意处理这个间隔区域。

编写程序以批量插旗地雷

    程序如下:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <tlhelp32.h>

#define GET_OFFSET(ROW, COL) ((ROW) * 32 + (COL))

void print_sign(BYTE val) {
    if (val == 0x0F || val == 0x8F) {
        printf("--");
    } else if (val == 0x0E || val == 0x8E) {
        printf("||");
    } else if (val == 0x0D || val == 0x8D) {
        printf("??");
    } else if (val == 0x40) {
        printf("..");
    } else if (val == 0x8A) {
        printf("**");
    } else if (val == 0xCC) {
        printf("XX");
    } else if (val == 0x10) {
    } else {
        printf("%01X", val & 0x0F);
        printf("%01X", val & 0x0F);
    }
    printf(" ");
}

int main() {
    DWORD pid = 0;          // Target process ID
    HANDLE hProcess = NULL;
    LPVOID targetAddress = (LPVOID)0x1005361; // Target memory address
    BYTE readBuffer[512] = {0};
    // BYTE writeData[4] = {0xAA, 0xBB, 0xCC, 0xDD}; // Example data to write
    SIZE_T bytesRead = 0;
    // SIZE_T bytesWritten = 0;

    // Create a snapshot of all processes
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create process snapshot, error code: %d\n", GetLastError());
        return 1;
    }

    PROCESSENTRY32 pe = {sizeof(PROCESSENTRY32)};
    if (Process32First(hSnapshot, &pe)) {
        do {
            if (_tcsicmp(pe.szExeFile, _T("winmine.exe")) == 0) {
                pid = pe.th32ProcessID;
                printf("Process found: winmine.exe, PID: %u\n", pid);
                break;
            }
        } while (Process32Next(hSnapshot, &pe));
    } else {
        printf("Failed to retrieve process information, error code: %d\n", GetLastError());
    }

    CloseHandle(hSnapshot);

    if (pid == 0) {
        printf("Process winmine.exe not found\n");
        return 1;
    }

    // Open the target process with required permissions for memory operations
    hProcess = OpenProcess(
        PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION,
        FALSE,
        pid
    );

    if (hProcess == NULL) {
        printf("Failed to open process, error code: %d\n", GetLastError());
        return 1;
    }

    // Read memory content
    if (!ReadProcessMemory(
        hProcess,
        targetAddress,
        readBuffer,
        sizeof(readBuffer),
        &bytesRead
    )) {
        printf("Failed to read memory, error code: %d\n", GetLastError());
        CloseHandle(hProcess);
        return 1;
    }
    printf("Read %d bytes of data:\n", bytesRead);
    for (int i = 0; i < 16; i++) {
        for (int j = 0; j < 32; j++) {
            print_sign(readBuffer[GET_OFFSET(i, j)]);
        }
        printf("\n");
    }


    printf("Start flaging all the mines..\n");
    // Write to memory
    int count = 0;
    for (int i = 0; i < 16; i++) {
        for (int j = 0; j < 32; j++) {
            BYTE flag_mine = 0x8E;
            if (readBuffer[GET_OFFSET(i, j)] == 0x8F) {
                WriteProcessMemory(
                    hProcess,
                    targetAddress + GET_OFFSET(i, j),
                    &flag_mine,
                    sizeof(flag_mine),
                    NULL
                );
                count++;
            }
        }
    }
    printf("Finish! the count of mines: %d\n", count);

    CloseHandle(hProcess);
    // system("pause");
    return 0;
}

    这段程序将会在打印雷区当前状态后,将所有0x8F修改为0x8E,即将所有的未翻开的地雷插上旗子,用户应当在新一局游戏内翻开任意地块后运行此程序,然后最小化再恢复窗口以重新绘制雷区。

    打印当前状态过程中,未翻开的所有地块将记为--,插旗地块记为||,翻开的空地记为..,翻开的数字地块记为该数字,翻开的地雷记为**,踩中的地雷记为XX

实验结果

    运行结果如下图所示:

终端输出及刷新前窗口

    刷新窗口后成功插旗。

插旗结果截图

    降低雷数为20后重新运行,便于验证结果正确性,手动翻开所有未插旗地块,中间笑脸图标带上墨镜(表示游戏胜利),可见结果正确。

游戏胜利截图

更换机器指令表.exe显示字体

定位控制字体显示的WinAPI

    使用Ollydbg打开机器指令表.exe。

使用Ollydbg打开机器指令表.exe

    在API文档及网络资料中查询其内涉及的API。


API名称 功能
GetModuleHandle 检索指定模块的模块句柄。模块必须已由调用进程加载。
GetCommandLine 检索当前进程的命令行字符串。
ExitProcess 结束调用进程及其所有线程。
GetSystemMetrics 用于获取关于显示器、鼠标、键盘等系统参数的信息。

    观察发现在程序有效的程序段中涉及的三个WinAPI均与字体显示无关。还剩下一个call 004031,推测其很可能控制字体的显示。其上的四个push可能包含参数,尝试修改为push 0D 观察程序变化。修改两个push后运行程序并无明显变化。可见这两个参数并不控制该程序的字体显示。

    在call 00401031处设断点并单步步入,依次检索遇见的到的WinAPI功能。

API名称 功能
CreateWindowEx 用于创建一个窗口(包括各种控件如按钮、文本框等)。
SetWindowLong 更改指定窗口的属性。该函数还将指定偏移量处的32位(长)值设置为额外的窗口内存。
GetStockObject 用于获取预定义的图形对象(如画笔、画刷和字体)的句柄。

    其中GetStockObject涉及字体的显示,在4010C6处调用,其上一条指令push 0B很可能控制字体的显示。

GetStockObject上下文

    将push 0B修改为push 0E后继续运行程序,可见字体明显发生变化。

修改参数后运行结果

将修改保存到文件

    选中被修改的指令,在指令窗口中右键->复制到可执行文件->选择,在弹出的新窗口中选中被修改的指令,右键->保存文件。将新可执行文件命名为机器指令表2.exe

将修改保存到文件

    双击运行修改前后的机器指令表,对比两者的字体差异。

对比两个机器指令表的差异

星藏点雪,月隐晦明
Built with Hugo
Theme Stack designed by Jimmy