[toc]

功能

  1. 鼠标悬停在扫雷数组上可以判断是否是雷,并作出提示
  2. 一键扫雷

分析工具

  1. 动态调试工具 Ollydbg
  2. 搜索数据 Cheat Engine
  3. 寻找窗口回调函数 Spy++
  4. 查壳,查编译环境 PEiD/exeinfo
  5. 开发工具 VS2017

需要分析的数据和代码

  1. 扫雷数组的宽度、高度和雷的数量
  2. 扫雷数组的基址
  3. 鼠标位置
  4. 鼠标位置转换成扫雷数组下标的代码
  5. 扫雷数组下标转换成鼠标位置的代码(便于发消息)

扫雷游戏分析

获取扫雷数组的数据

先查看这个游戏的编写工具

image-20191223145814657

链接器版本是7.0,故可推测是使用 VC2003 编写的

image-20191223150233783

查看 API,可看到第一个运行时库 msvcrt.dll,有这个库的程序一般是 SDK 程序,且扫雷游戏大小只有 117KB,只可能是 SDK,若是静态编译的MFC程序则大小在 1M 以上

使用 CE 工具查找扫雷数组的宽度、高度和雷的数量,得到这些数据的地址

image-20191223151215534

OD中分析数据

先在 CE 中查找访问高度、宽度、雷数的地址数据的代码,应该是先取出地址再去访问,故其中只有3处符合,分别在 OD 中查找这3处的地址

image-20191223155242076

image-20191223155432004

设置扫雷的界面便于我们之后在 OD 中观察内存,高度设为10,宽度为14,雷数为10,在 OD 的数据窗口中一行为16个字节,2列的边界加14列的宽度刚好填满一行

在 OD 中附加扫雷,依次进入这3处地址 0x1002EEA,0x1003705,0x10019EB,简单地看一下汇编代码,发现第一个地址是初始化雷区的代码,后两处地址代码是和界面有关

接下来需要详细地分析第一处的汇编代码,其中数组基地址是 0x1005340,高度是 0x1005338,宽度是 0x1005334,雷数是 0x1005330

image-20191223162156788

第一次运行,雷区边界的初始化:

image-20191223161925610

之后在扫雷界面点击一下:

image-20191223162606583

image-20191223163749791

image-20191223162621753

发现雷区里面有数据填充,对比三个界面,可知:

0x10 代表边界,0x0F 代表初始值,0x40 代表周围没有雷,0x41 代表周围有1个类,依次类推,0x48 代表周围有8个类,0x8F 代表雷,数组中每一行下面都会有一行 0x0F 隔开

坐标转换

首先使用 Spy++ 查找鼠标点击的消息,先清除一些干扰消息,如 WM_NCHITTESTWM_SETCURSOR
WM_SETFOCUSWM_TIMECHANGEWM_TIMER 等,便于查找消息

image-20191223184743292

在扫雷界面点击,出现了2个消息,WM_LBUTTONDOWNWM_LBUTTONUP,参数中有坐标点信息,接下来从这2个消息着手分析

image-20191223185220126

再看到 Window Proc:01001BC9,打开 OD 进行分析

image-20191223185904923

在 OD 中的 0x1001BC9 地址处假定参数,设置消息断点

image-20191223190851676

运行,程序断下

image-20191223191558710

image-20191223191657609

37=0x25,63=0x3F,可见 ECX 高16位保存 y 坐标,低16位保存 x 坐标

因为要寻找将屏幕坐标转换为数组下标的代码,需跟踪 ARG.4,而 ECX 保存了 x,y 坐标,故之后单步往下走,看代码有没有对 ARG.4 和 ECX 的再次访问,直到找到 0x1002009 地址处

点击扫雷界面的红框位置,接着运行程序,发现程序会将鼠标点击位置处的屏幕坐标转换为扫雷界面的数组下标,x=3,y=2

image-20191223194712891

#注入程序DLL编写

VS2017 中新建 MFC DLL,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//自己编写的窗口回调函数
LRESULT CALLBACK My_DefWindowProcW(
_In_ HWND hWnd,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam)
{
if (Msg == WM_KEYDOWN && wParam == VK_F5) {//按下F5键
//F5一键秒杀,遍历扫雷数组中的元素,判断不是雷的元素,模拟点击
//用于测试
OutputDebugString(L"F5");
int nWidth = *g_pWidth;
int nHeight = *g_pHeight;
int nCounter = *g_pCounter;
CString strString;
strString.Format(L"宽度:%d, 高度:%d, 雷数:%d",
nWidth, nHeight, nCounter);
OutputDebugString(strString.GetBuffer());

int nMineCount = 0;
for (size_t y = 1; y < nHeight + 1; y++) {
CString strLine;
for (size_t x = 1; x < nWidth + 1; x++) {
//数组基地址+(y+1)*32+x+1(y=0~nHeight)
//y*32表示每次向下移动2行
BYTE byCode = *(PBYTE)((DWORD)g_pBase + y * 32 + x);
if (byCode == MINE)
nMineCount++; //雷的数量加1
else {
//点击无雷的位置
int xPos, yPos;
xPos = (x << 4) - 4;
yPos = (y << 4) + 0x27;
SendMessage(hWnd, WM_LBUTTONDOWN, 0, MAKELPARAM(xPos, yPos));
SendMessage(hWnd, WM_LBUTTONUP, 0, MAKELPARAM(xPos, yPos));
}
CString strCode;
strCode.Format(L"%02x ", byCode);
strLine += strCode;
}
OutputDebugString(strLine.GetBuffer());
}
CString strCount;
strCount.Format(L"找到的雷数:%d", nMineCount);
OutputDebugString(strCount.GetBuffer());
}
else if (Msg == WM_MOUSEMOVE) {
//鼠标移动,将鼠标在屏幕上的坐标转换为数组下标
int x, y;
x = LOWORD(lParam); //获取低位
y = HIWORD(lParam); //获取高位
x = (x + 4) >> 4;
y = (y - 0x27) >> 4;

BYTE byCode = *(PBYTE)((DWORD)g_pBase + y * 32 + x);
if (byCode == MINE)
SetWindowText(hWnd, L"此处有雷");
else
SetWindowText(hWnd, L" ");
}
return CallWindowProc(g_OldProc, hWnd, Msg, wParam, lParam);
}
BOOL CMFCsaoleiApp::InitInstance()
{
CWinApp::InitInstance();
//1.通过Spy++获取窗口名和类名,再查找窗口,获取窗口句柄
g_Wnd = ::FindWindow(L"扫雷", L"扫雷");
if (g_Wnd == NULL) {
OutputDebugString(L"找不到扫雷窗口");
return FALSE;
}
//2.修改窗口回调函数
g_OldProc = (WNDPROC)SetWindowLong(g_Wnd, GWL_WNDPROC, (LONG)My_DefWindowProcW);
if (g_OldProc == NULL) {
OutputDebugString(L"设置窗口回调函数失败");
return FALSE;
}
return TRUE;
}