一键扫雷小助手

对扫雷进行动态分析

扫雷的关键是找到内存中的一个二维数组,其中对应的地图坐标标记了哪里有雷哪里为空

  • 由资料得到扫雷使用了双缓存技术

双缓存是在缓存中一次性绘制,再把绘制的结果返回在界面上。比如,要在屏幕上绘制一个圆、正方形、直线,需要调用GDI的显示函数,操作显卡画一个圆,再画一个正方形和直线,它需要访问硬件三次;此时依赖硬件的访问速度,而且如果绘制错误擦除再绘制,需要反复的访问硬件,为了减少硬件操作,我们在内存中把需要绘制的图像准备好,然后一切妥当之后提交给硬件显示。

  • 在OD中找到双缓存技术的核心函数BitBlt

BitBlt是将内存中的数据提交到显示器上,该函数对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。

  • 按Ctrl+N调出Names窗口,输入BitBlt

  • 查看引用,发现存在两个引用,逐一查看,最后锁定第二个引用

因为这里存在两个循环,刚好对应了二维数组的遍历,通过分析,找到此处的EBX(0x01005360)刚好存着对应的坐标,接下来选择EBX基址寄存器,然后选择“数据窗口中跟随”

很容易就可以分析出0x10对应边界,0x0F对应空格,0x8F对应雷,进行验证

成功

用Cpp写一个扫雷辅助程序

Cpp编写鼠标坐标获取案例

  • 配置graphics.h文件

graphics.h是一个针对Windows的C语言图形库,分为像素函数、直线和线型函数、多边形函数、填充函数等。在学习Cpp游戏编程时,通常会发现VS中没有“graphics.h”头文件,因此需要配置。

直接在官网中下载exe文件即可

  • 用graphics.h画一个圆
1
2
3
4
5
6
7
8
9
10
11
12
#include<graphics.h>				// 引用图形库头文件
#include<conio.h>

int main()
{
initgraph(640, 480); // 创建绘图窗口,大小为 640x480 像素
setlinecolor(RGB(255, 0, 0)); // 设置当前线条颜色
setfillcolor(RGB(0, 255, 0)); // 设置当前填充颜色
fillcircle(200, 200, 100); // 画圆;圆心(200,200),半径 100
_getch(); // 按任意键继续
closegraph(); // 关闭图形环境
}

  • 编写鼠标事件代码
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
#include<graphics.h>				// 引用图形库头文件
#include<stdio.h>

int main()
{
// 定义鼠标
MOUSEMSG m;
// 初始化窗口,500宽度,500高度
initgraph(500, 500);

while (1) {
// 获取鼠标信息
m = GetMouseMsg();
char buff[256];

// 鼠标左键按下
if (m.uMsg == WM_LBUTTONDOWN) {
// 清空数组
memset(buff, 0, 256);
sprintf_s(buff, "X坐标:%d,Y坐标:%d", m.x, m.y);
MessageBox(NULL, buff, "坐标", MB_OK);
}

}
return 0;

}

运行前需要在项目属性设置编码方式为“使用多字节字符集”,否则会报错

运行结果:

其中GetMouseMsg函数表示获取鼠标消息,点击VS的工具选项选择Spy++可以看到Windows系统自带的鼠标操作、键盘操作、消息操作等,同时能获取鼠标是左键或右键按下以及对应坐标

在Spy++点击进程,找到MOUSEMSGAG,单击选中,再点击左上角的日志

其中WM_LBUTTONDOWN就对应鼠标左击,WM_RBUTTONDOWN对应鼠标右击

Cpp编写自动扫雷程序

接下来是通过Cpp实现一键扫雷功能,主要是模拟鼠标在雷区的点击操作,并且按下所有非雷区域从而实现一键扫雷。

利用的是Windows应用程序的消息机制,通过SendMessage函数向指定窗口发送消息,也就是在获取到扫雷的窗口句柄后,利用这个函数向该窗口发送鼠标按键消息,从而实现模拟鼠标的操作

该函数的定义如下:

1
2
3
4
5
6
LRESULT SendMessage(
HEWD hWnd, // handle to the destination window
UINT Msg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);

在这个函数中,

第1个参数hWnd是要接收消息的窗口句柄,这里是我们之前所获取到的扫雷窗口句柄

第2个参数Msg是要发送消息的消息类型,这里因为我们要模拟鼠标的按键操作,因此使用WM_LBUTTONDOWN模拟鼠标左键的按下操作,使用WM_LBUTTON模拟松开鼠标左键的操作;

第3和第4个参数是消息的两个附加参数,其中第3个参数我们这里使用MK_LBUTTON,表明是鼠标左键的操作,第4个参数是鼠标按下的坐标,也就是x轴和y轴的位置坐标

  • 分析扫雷的区域及坐标定义

资料得知MSDN中的客户区是以B点作为起始的位置,即原点坐标(0,0),而雷区中心即E点的坐标为(16,61),每一个雷区小方块的大小为16*16,于是可以知道,这里需要循环计算出雷区每一个小方块的坐标,这个坐标与保存有雷区的二维数组下标紧密相关

假设这个二维数组是mine[y1] [x1],其中y1表示的是雷区的行,x1表示雷区的列,那么每个雷区方块的坐标为:

1
2
x = x1 * 16 + 16
y = y1 * 16 + 61

获得了坐标以后,就可以通过如下语句来模拟鼠标的点击操作:

1
2
SendMessage(hWnd,WM_LBUTTONDOWN,MK_LBUTTON,MAKELONG(x,y));	 //鼠标左键点击
SendMessage(hWnd,WM_LBUTTONUP,MK_LBUTTON,MAKELONG(x,y)); //鼠标左键抬起

注意到这里用了MAKELONG 函数,原因是

SendMessage函数的第四个参数只能接收一个参数,但是我们的坐标是两个数值,因此需要将两个数值合并成一个数值

MAKELONG函数:将两个16位数据组合成一个32位的数据。

  • 获取雷区的二维数组,从而得到x1和y1的值

在之前的分析中我们已经得到了雷区在内存中的地址,进一步观察这块区域发现

0x01005330的值为0x28,这里对应的是雷的数量

0x01005334的值为0x10,这里对应的是宽度

0x01005338的值为0x10,这里对应的是高度

所以我们可以用以下代码获取雷区大小的数据:

1
2
3
4
DWORD dwInfo = 0x01005300;
DWORD dwHeight = 0,dwWidth = 0;
ReadProcessMemory(hProcess,(LPVOID)(dwInfo + 4),&dwWidth,sizeof(DWORD),0);
ReadProcessMemory(hProcess,(LPVOID)(dwInfo + 8),&dwHeight,sizeof(DWORD),0);

这样雷区的高度就保存在了dwHeight,宽度就保存在了dwWidth

接下来我们要先得到雷区的最大值的数据,并将其保存在pByte[]中,经过调试我们可以知道其实无论雷区多大,它都在固定的一块区域,然后雷区的最大范围为24*30,最后加上边界,所以最大值为832,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 存放雷区的起始地址
DWORD dwBoomAddr = 0x01005340;
// 雷区的最大值(包括边界)
DWORD dwSize = 832;
pByte = (PBYTE)malloc(dwSize);
DWORD dwTmpAddr = 0;

// 读取整个雷区的数据
ReadProcessMemory(hProcess,(LPVOID)dwBoomAddr,pByte,dwSize,0);
BYTE bClear = 0x8E;
int i = 0;
int n = dwSize;

// 将内存中的0x8F(地雷)变0x8E(小旗)
while(i<dwSize){
if(pByte[i] == 0x8F){
dwTmpAddr = 0x01005340 + i;
WriteProcessMemory(hProcess,(LPVOID)dwTmpAddr,&bClear,sizeof(BYTE),0);
n--;
}
i++;
}

下面是上面代码用到的函数的解释

malloc

1
void *malloc(size_t size)

描述:分配所需的内存空间,并返回一个指向它的指针。

参数:size – 内存块的大小,以字节为单位。

ReadProcessMemory(该函数从指定的进程中读入内存信息,被读取的区域必须具有访问权限

1
2
3
4
5
6
7
BOOL ReadProcessMemory(
HANDLE hProcess, // 目标进程句柄
LPCVOID lpBaseAddress, // 读取数据的起始地址
LPVOID lpBuffer, // 存放数据的缓存区地址
DWORD nSize, // 要读取的字节数
LPDWORD lpNumberofBytesRead // 读出数据的实际大小。如果被指定为NULL,那么将忽略此参数
);

LPVOID是一个没有类型的指针(指针),也就是说可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候再转换回来。

1
2
3
4
typedef void far	*LPVOID;
typedef const void far *LPCVOID;

// 两者差别就在 const 关键字上,const修饰入参,表示此参数不可更改,也就是说不能在ReadProcessMemory函数内部修改lpBaseAddress的值

**WriteProcessMemory(此函数能写入某一进程的内存区域,入口区必须可以访问) **

1
2
3
4
5
6
7
BOOL ReadProcessMemory(
HANDLE hProcess, // 目标进程句柄
LPVOID lpBaseAddress, // 要写的内存首地址,在写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据
LPVOID lpBuffer, // 指向要写的数据的指针
DWORD nSize, // 要写入的字节数
LPDWORD lpNumberOfBytesWritten // 写入数据的实际大小。如果被指定为NULL,那么将忽略此参数
);

————————

上面我们将整个雷区的数据,保存在了数组pByte[]中,接下来我们将根据每次的高和宽提取出实际雷区,也就是dwHeight*dwWidth的数据,所以数据中的0x10(边界)和多余的0x0F都应该删除

同样对这张图分析,我们发现其最开始都是由0x10组成的一条上边界,包含有dwWidth+2这么多的0x10,因此我们在提取实际雷区时,首先应该跳过这条边,然后接下来可能会读取到0x0F,则继续跳过,直到读取到下一个0x10为止,说明已经到了雷区的第一行,然后我们在这个0x10后提取dwWidth个字节的数据,然后跳过dwWidth+1个字节,继续往下读取即可,直到又读取到0x10,重复操作,最后提完dwHeight这么多行数据,即提取完毕,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int i=dwWidth+2
int j=0;
int h = dwHeight;
int count = 0;
PBYTE pTmpByte = NULL;
pTmpByte = (PBYTE)malloc(dwHeight*dwWidth);

while(i<dwSize){
if (pByte[i] == 0x10) {
for (j = 1; j <= dwWidth; j++){
pTmpByte[count] == pByte[i+j]
count++;
}
i = i + dwWidth + 1;
h--;
if (h == 0) break;
}
i++;
}

  • 鼠标点击具体实现

获取到雷区数据以后,它是以一维数组的形式保存在pTmpByte里面的。下面我们需要得到该数组中每一个数据在二维数组模式下的下标,即x1和y1值。其中y1是行下标,表明是第几行,行数从0开始;x1是列下标,表明是第几列,列数也是从0开始。该数组中的数据量为dwHeight*dwWidth,可以采用循环的形式逐个读取数组中的数据然后计算出其二维下标值。假设这个一维数组下标为i,则行下标可以用i除以dwWidth然后取整数商的方式获得,列下标可以用i模dwWidth的方式,也就是通过取余运算获得。包含鼠标点击的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int x1 = 0. y1 = 0;
int x = 0, y = 0;
for (i = 0; i < dwHeight*dwWidth; i++){
if (pTmpByte[i] != 0x8F){
x1 = i % dwWidth;
y1 = i / dwWidth;
x = x1 * 16 + 16;
y = y1 * 16 + 61;
SendMessage(hWnd,WM_LBUTTONDOWN,MK_LBUTTON,MAKELONG(x,y));
SendMessage(hWnd,WM_LBUTTONUP,MK_LBUTTON,MAKELONG(x,y));
}
}

综合以上,整个一键扫雷功能的完整代码如下:

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
#include <stdio.h>
#include <windows.h>


int main() {
DWORD Pid = 0;
HANDLE hProcess = 0;

// 获取扫雷游戏对应的窗口句柄
HWND hWnd = FindWindow(NULL, L"扫雷");
if (hWnd != 0) {
// 获取扫雷进程ID
GetWindowThreadProcessId(hWnd, &Pid);
// 打开扫雷游戏获取其句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
if (hProcess == 0) {
printf("Open winmine process failed.");
return 0;
}
// 存放雷区的起始地址
DWORD dwBoomAddr = 0X01005340;
// 雷区的最大值
DWORD dwSize = 832;
PBYTE pByte = NULL;
pByte = (PBYTE)malloc(dwSize);

// 读取整个雷区的数据
ReadProcessMemory(hProcess, (LPVOID)dwBoomAddr, pByte, dwSize, 0);

// 读取雷区的长和宽
DWORD dwInfo = 0x01005330;
DWORD dwHeight = 0, dwWidth = 0;
ReadProcessMemory(hProcess, (LPVOID)(dwInfo + 4), &dwWidth, sizeof(DWORD), 0);
ReadProcessMemory(hProcess, (LPVOID)(dwInfo + 8), &dwHeight, sizeof(DWORD), 0);

// 提取雷区的实际数据
int i = dwWidth + 2;
int j = 0;
int h = dwHeight;
int count = 0;
PBYTE pTmpByte = NULL;
pTmpByte = (PBYTE)malloc(dwHeight*dwWidth);

while (i < dwSize) {
if (pByte[i] == 0x10) {
for (j = 1; j <= dwWidth; j++) {
pTmpByte[count] = pByte[i + j];
count++;
}
i = i + dwWidth + 1;
h--;
if (h == 0) break;
}
i++;
}

// 获取雷区方块的坐标后,然后模拟鼠标进行点击
int x1 = 0, y1 = 0;
int x = 0, y = 0;
for (i = 0; i < dwHeight*dwWidth; i++) {
if (pTmpByte[i] != 0x8F) {
x1 = i % dwWidth;
y1 = i / dwWidth;
x = x1 * 16 + 16;
y = y1 * 16 + 61;
SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG(x, y));//鼠标左键按下
SendMessage(hWnd, WM_LBUTTONUP, MK_LBUTTON, MAKELONG(x, y));//鼠标左键抬起
}
}
MessageBox(hWnd, L"成功啦",L"扫雷killer by77",MB_OK);
free(pByte); //释放动态分配的内存空间free()可以释放由malloc、calloc、realloc分配的内存空间,以便其他程序再次使用
CloseHandle(hProcess);// 关闭一个线程句柄对象,表示不再使用该句柄,但并没有结束线程
}
else {
printf("Get hWnd failed.");
}
return 0;
}

下面是具体函数的解释:

  • FindWindow(该函数获得一个顶层窗口的句柄,该窗口的类名和窗口名与给定的字符串相匹配)

这个函数不查找子窗口。在查找时不区分大小写。

1
2
HWND FindWindow (LPCTSTR IpClassName, LPCTSTR IpWindowName) ;
HWND hWnd = FindWindow(NULL, L"扫雷");

参数:

lpClassName : 指向一个指定了类名的空结束字符串,或一个标识类名字符串的成员的指针。

lpWindowName : 指向一个指定了窗口名(窗口标题)的空结束字符串。如果该参数为空,则为所有窗口全匹配 。

返回值: 如果函数成功,返回值为具有指定类名和窗口名的窗口句柄;如果函数失败,返回值为NULL。

  • 句柄

句柄,是整个Windows编程的基础。一个句柄是指使用的一个唯一的整数值,即一个4字节(64位程序中为8字节)长的数值,来**标识应用程序中的不同对象和同类中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。应用程序能够通过句柄访问相应的对象的信息,但是句柄不是指针,程序不能利用句柄来直接阅读文件中的信息。如果句柄不在I/O文件中,它是毫无用处的。句柄是Windows用来标志应用程序中建立的或是使用的唯一整数,Windows大量使用了句柄来标识对象。

HWND 是 Handle to A Window的缩写,窗口句柄。H 是类型描述,表示句柄(handle),WND是变量对象描述,表示窗口,所以HWND表示窗口句柄。

  • GetWindowThreadProcessId(该函数返回创建指定窗口线程的标识和创建窗口的进程的标识符,后一项是可选的)
1
2
3
DWORD GetWindowThreadProcessId ( HWND hWnd, LPDWORD lpdwProcessId);

GetWindowThreadProcessId(hWnd, &Pid);

参数:

hWnd:窗口句柄

lpdwProcessld: 接收进程表示的32位值的地址。如果这个参数不为NULL,GetWindowThreadProcessld将进程标识拷贝到这个32位值中,否则不拷贝。

返回值: 创建窗口的线程标识

  • OpenProcess(打开一个已存在的进程对象,并返回进程的句柄)
1
2
3
4
5
6
7
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 想拥有的该进程访问权限
BOOL blnheritHandle, // 表示所得的进程句柄是否可以被继承
DWORD dwProcessId // 被打开进程的PID(标识符)
);
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
PROCESS_ALL_ACCESS -- 所有能获得的权限
  • MessageBox(弹出一个消息窗口)

MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)

第一个参数是消息框拥有的窗口。如果是NULL,则消息框没有拥有窗口

第二个参数是消息框的内容

第三个参数是消息框的标题

第四个参数是下列标志组中标志的组合,如 确定 取消 等

运行结果:

咳咳,真有它的哦

把杀软关掉之后再运行

image-20201214223638525

补充的知识

窗口句柄、进程ID、进程句柄、窗口与进程之间的关系

  • 窗口句柄对应着每个窗口的钥匙,句柄相当于一个指针,指向一个数据结构体,结构体里明确表示着该窗口的各种信息

  • 进程ID是当一个进程被创建出来时系统内核为其分配的一个名字/绰号

  • 进程句柄与窗口句柄不一样,它也是一个指针,指向进程下的PCB进程控制块,当我们要对进程进行I/O操作时候需要知道进程的堆栈

地址范围以及状态才能的值对应的LDT/GDT并转化为物理地址,操作系统才能为我们对该进程进行读写操作,所以一般我们会通过进程ID来获取进程句柄(临时的),来对进程进行操作

  • 窗口就是对用户进行可视化界面交互的,而进程里的数据和指令在控制着窗口应该进行怎样交互

参考资料:

https://blog.csdn.net/Kevin_Samuel/article/details/29432651

https://blog.csdn.net/ioio_jy/article/details/90577172

https://www.cnblogs.com/HeroZearin/articles/2539090.html

https://blog.csdn.net/Eastmount/article/details/108708564#comments_14206878

https://blog.csdn.net/coolszy/article/details/5523486

https://blog.csdn.net/pxm2525/article/details/39828815

https://blog.csdn.net/kmsj0x00/article/details/82354666