逆向扫雷游戏

First Post:

Last Update:

一.注入代码

1.ollydbg打开扫雷,多按几次f9,然后打开window视窗(可以在view–windows打开,也可以直接点那个w)

2.之后右键->Follow ClassProc。这个就是窗口的处理函数,这样就定位到了窗口的处理函数,此时就是断在了该函数的起始位置:01001BC9,如图:

img

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
插个基础知识:windows的窗口处理函数:WindowProc():

LRESULT CALLBACK WindowProc ( HWND hWnd,
UINT uMsg,
WPARAM wParam,
LParam);

参数的意义如下:
HWND hWnd : 事件引起消息发生的那个窗口。(窗口的句柄)
UINT message: 消息的ID,它是32位值,指明了消息类型。(消息id)
WPARAM wParam : 32位值,包含附加信息,决定于消息的种类。例如键盘的哪个键代码。
LPARAM lParam: 32位值,同上。例如,前16位=重复数
接着8位:扫描码(决定于厂家)
第24位:为1时表示扩展键。
第25到28位:保留区
第29位为1时=alt按下,否则为0。
第30位为1时=消息前按下,否则为0。
第31位为1时=正在被释放,否则为0。

当点击菜单的时候,WindowProc会被系统调用,
uMsg 赋值为:WM_COMMAND
wParam 赋值为:对应菜单的id(我们要分析的数据)

再插一句:这个WM_COMMAND就是当用户从菜单中选择命令项、控件将通知消息发送到其父窗口或翻译快捷键击时发送的参数。

3.接下来看点菜单栏里:“初级”“中级”“高级”时菜单的id是什么,(即要找第三个参数,第二个入栈)

在WindowProc函数处下条件断点:uMsg==WM_COMMAND时

在[arg.4]参数处右键->breakpoint->conditional(条件断点),条件填写:edx==WM_COMMAND。因为第二个参数就是把参数传给了edx,所以左边是edx。

这个就是对运行到这里是不一定会断,只有满足条件也可以断下来,进行了筛选

试验:在切换初级,中级,高级时候成功在断点处断下(只有在点击菜单时才是WM_COMMAND消息),而运行其他消息指令是不会断的img

此时可以分析出点击菜单时,传入函数的四个参数的值:

在栈窗口观察,利用ebp偏移看,右键地址->address->relative to ebp

初级菜单id:0x209

可以看到第二个参数值是0x111,连着的这四个就是传入的四个参数

同理观察到:中级菜单id:0x20A , 高级菜单id:0x20B

4.注入代码:

模拟函数调用,传递四个参数:

1
2
3
4
5
push 0
push 0x20b
push 0x111
push 230526
call 01001bc9

如图:

这个效果就是可以不断变换第二个参数的值来操作扫雷的菜单

将这个过程利用mfc实现:

先添加给button,代码也简单,如下:

1
2
3
4
5
6
7
8
9
10
11
void C扫雷Dlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));//按照窗口名称找句柄
if (hWnd == NULL)
{
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
::SendMessage(hWnd, WM_COMMAND, 0x209, 0);
}

二.分析游戏基地址

1.基地址的概念

全局变量,字符常量的地址一般都是基地址,查找和验证基地址

  • 使用CE内存查找工具查找数据地址

1.首次先让要查找的数据稳定在一个范围或者精确的值(使用CE的首次搜索)

2.改变要查找的数据,根据变化再筛选出数据的地址

  • 使用OD验证是否是基地址,通过内存断点的方式来验证

内存断点:如果检测到对该内存有读或写的操作就会断下来

add dowrd ptr[1005194] eax 这种立即数寻址一般就是基地址

三.使用CE扫描具体实现

现在目标是查找雷区的内存

猜想:雷区在内存中以一个二维数组形式存放

先选”未知的初始值”,首次扫描,接着不断变换雷区的首元素,(注意,这里不可以搜索“确定的值”,因为即使显示“1”,在内存中的存储可不一定,卡了我10分钟)按照”未变动”或者是”变动”不断缩小范围,最后可以锁定雷区的首元素地址(01005361),右键(browse this memory region)查看该地址对应内存

各数据意义

随便再点点,发现41对应1,42对应2……8f对应雷,雷区每行以01结尾

此时就可以达到作弊效果了,但是没有实现自动化

四.实现自动扫雷(利用mfc实现)

准备工作

因为有初级,中级等不同模式,所以为了做出一个通用的…,要找雷区的宽和高所在地址,继续使用CE查找,步骤同上。

1
2
3
雷区数据基地址  0x1005361
宽的基地址 0x1005334
高的基地址 0x1005338

在实现自动化之前,先来一个小目标练练手:

读取当前的雷数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void C扫雷Dlg::OnBnClickedButton5()
{
// TODO: 在此添加控件通知处理程序代码
DWORD pid;
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));//按照窗口名称找窗口句柄
if (hWnd == NULL)
{
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
GetWindowThreadProcessId(hWnd, &pid);//通过窗口句柄拿到进程id
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);//通过进程id拿到进程句柄
ReadProcessMemory(hProcess, (LPCVOID)0x1005194, &m_editbase, sizeof(m_editbase), &pid);//写入
UpdateData(FALSE);
}

解释:核心函数是ReadProcessMemory(),传入的参数:进程句柄,要读取数据的起始地址,存放数据的缓存区地址,要读取的字节数,实际读取数存放地址。

  • 为了找到进程句柄,需要OpenProcess()这个函数,它通过进程id来找
  • 为了找到进程id,需要GetWindowThreadProcessId(),它通过窗口句柄找
  • 为了找到窗口句柄,需要FindWindow()函数,它通过窗口名称来找
  • 窗口名称每次运行都不会变,就是“扫雷”嘛

效果如图:image-20230402152620695

正式实现

接下来,试着把雷区打印出来:

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
void C扫雷Dlg::OnBnClickedButton6()
{
// TODO: 在此添加控件通知处理程序代码
DWORD pid;
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));//按照窗口名称找窗口句柄
if (hWnd == NULL)
{
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
GetWindowThreadProcessId(hWnd, &pid);//通过窗口句柄拿到进程id

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);//通过进程id拿到进程句柄

//雷区数据基地址 0x1005361
//宽的基地址 0x1005334
//高的基地址 0x1005338
//0x8f是雷

unsigned char gamedata[24][32] = { 0 };
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005361, &gamedata, 32 * 24, &pid))
{
::MessageBox(NULL, _T("读取扫雷游戏进程失败"), _T("错误"), MB_OK);
return;
}

//以上这些和准备工作里的是一样的



DWORD dwHight = 0;//根据模式的不同,选择对应的高
m_strshowdata.Empty();//再次输出清空原来的
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005338, &dwHight, sizeof(dwHight), &pid))//读取高
{
::MessageBox(NULL, _T("读取扫雷游戏进程失败"), _T("错误"), MB_OK);
return;
}

CString strTemp = _T("");//中间变量

short gamex = 0x13;//这里的坐标是利用vs自带的spy++一点点对应出来的,有点难受
short gamey = 0x59;

for (int i = 0; i < dwHight; ++i)
{
for (int j = 0; j < 32; ++j)//这里不用选择是因为遇到0x10就break
{
if (0x10 == gamedata[i][j])
{
break;
}

strTemp.Format(_T("%02X "), gamedata[i][j]);
m_strshowdata += strTemp;
}
m_strshowdata += _T("\r\n");//换行
}

UpdateData(FALSE);

}

效果如图:image-20230402153552559

其中8F对应的就是雷

在此基础上就可以实现自动化了,原理就是模拟鼠标点击消息发送给扫雷程序

增加的代码:

1
2
3
4
5
6
7
8
9
10
unsigned short xypos[2] = { 0 };//偏移量
xypos[0] = gamex + j * 0x18;
xypos[1] = gamey + i * 0x18;
if (0x8F != gamedata[i][j])
{
//发送消息(根据spy++钩取的消息,模拟真实的点击消息,利用坐标来控制点击位置)
::PostMessage(hWnd, WM_LBUTTONDOWN,MK_LBUTTON,*(int*)xypos);
::PostMessage(hWnd, WM_LBUTTONUP,0, *(int*)xypos);

}

附上最后的完整代码:

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
void C扫雷Dlg::OnBnClickedButton6()
{
// TODO: 在此添加控件通知处理程序代码
DWORD pid;
HWND hWnd = ::FindWindow(NULL, _T("扫雷"));//按照窗口名称找窗口句柄
if (hWnd == NULL)
{
::MessageBox(NULL, _T("扫雷游戏未打开"), _T("错误"), MB_OK);
return;
}
GetWindowThreadProcessId(hWnd, &pid);//通过窗口句柄拿到进程id

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);//通过进程id拿到进程句柄

//雷区数据基地址 0x1005361
//宽的基地址 0x1005334
//高的基地址 0x1005338
//0x8f是雷
//距离为16(大概)
unsigned char gamedata[24][32] = { 0 };
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005361, &gamedata, 32 * 24, &pid))
{
::MessageBox(NULL, _T("读取扫雷游戏进程失败"), _T("错误"), MB_OK);
return;
}

DWORD dwHight = 0;
m_strshowdata.Empty();
if (!ReadProcessMemory(hProcess, (LPCVOID)0x01005338, &dwHight, sizeof(dwHight), &pid))//读取高
{
::MessageBox(NULL, _T("读取扫雷游戏进程失败"), _T("错误"), MB_OK);
return;
}

CString strTemp = _T("");

short gamex = 0x13;
short gamey = 0x59;
unsigned short xypos[2] = { 0 };
for (int i = 0; i < dwHight; ++i)
{
for (int j = 0; j < 32; ++j)
{
if (0x10 == gamedata[i][j])
{
break;
}

xypos[0] = gamex + j * 0x18;
xypos[1] = gamey + i * 0x18;
if (0x8F != gamedata[i][j])
{
//::PostMessage(hWnd, WM_LBUTTONDOWN,MK_LBUTTON,*(int*)xypos);
//::PostMessage(hWnd, WM_LBUTTONUP,0, *(int*)xypos);

}
strTemp.Format(_T("%02X "), gamedata[i][j]);
m_strshowdata += strTemp;
}
m_strshowdata += _T("\r\n");
}

UpdateData(FALSE);

}

效果展示:img

完毕!