作业目标
-
逆向分析扫雷程序winmine.exe,并编写程序对其进行批量插旗
-
更换机器指令表.exe内的显示字体
扫雷
定位绘制雷区的代码位置
使用Ollydbg打开winmin.exe。

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

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

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

设置断点后按F9或功能按钮运行程序,程序会暂停在断点处。此时按Ctrl + F9或者功能按钮执行到返回,程序会运行到函数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,在数据窗口中跟踪该地址。

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

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

对比可知1005361为首的地址存放雷区数据。
分析地块状态对应的内存值
非雷区地块
从上一节的最后一个图可以得知,翻开的2地块对应的内存值为0x42。推测翻开的n对应的内存值为0x4n。
翻开相邻地块进行验证,可知推测成立,同时也得知未翻开的非雷区对应的内存值为0x0F。

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

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

观察上图可知,插旗地块的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。

在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很可能控制字体的显示。

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

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

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