xp自动扫雷程序

xp自动扫雷程序
xp自动扫雷程序

目录

1系统总体结构设计 (1)

1.1基本简介 (1)

1.2对扫雷游戏的逆向分析 (1)

1.3算法扫雷模块 (11)

1.4内存读取扫雷模块 (13)

3.4.1 主体部分 (13)

3.4.2 读出雷区模块 (13)

3.4.3 雷区显示模块 (14)

3.4.4 模拟鼠标点击模块 (15)

3.4.5 停止时间模块 (15)

3.4.6 设置时间模块 (16)

2系统设计与流程的实现 (16)

3系统测试与分析 (17)

3.1测试 (17)

1系统总体结构设计

1.1基本简介

整个系统分为两大部分,第一部分则是逆向分析找地址,第二部分则是系统功能模块部分的实现,而第二部分又分为根据内存获得雷区实现自动扫雷这部分和通过算法逻辑推理来实现自动扫雷。

1.2对扫雷游戏的逆向分析

用PEiD看看这个扫雷程序有没有加壳,如下图所示。

首先,通过OD的“查找”选择“当前模块中的名称(标签)”可以得知如图1-1中红框部分有BitBlt函数,对于扫雷这种游戏,游戏启动时便会开始绘制x*y矩阵的图形界面,而BiBlt双缓存技术(即在内存中准备一块区域,把要显示的位图都加载到内存中,然后调用BitBlt函数,把内存设备复制到显示设备上)能有效解决闪屏问题,这也是为什么优先考虑跟一下这个函数。

图1-1

其次,返回OD主界面,查看所有断点,如图1-2所示,能看到两个断点

图1-2

我们点击进入,可以发现这两个断点分别处于两个部分A部分:

B部分:

然后点击F9运行程序,会发现界面都还没正常打开就停在了第二部分的断点处,那么我们将此处断点去掉,然后重新运行程序,并且扫雷游戏画面正常启动了,那么点击一下任意一个方块,这时程序停在了第一部分的断点处,而程序界面停止响应,这时候回到OD上,开始单步跟踪,会发现这个第一部分是一个死循环,并且每次循环一部分,都会在界面上绘制一块区域,如图所示1-3所示

图1-3

这时,我们知道了此部分主要是界面的重绘。

做到这里,我们先来个小插曲

如图所示把push 10 和push 10第一个10改成5之后,运行,会发现如上图所示的情况,即雷点变小了,所以这部分

BitBlt函数的调用是关于每个雷块的。

然后,我们把第一部分的那个断点去掉,重新将第二部分BitBlt那句的断点打上,开始跟踪。可以很明显的看出第二部分有一大块都是一个跟双重循环相关的代码,考虑到之前提到的扫雷是块关于x*y的矩阵,也不难想象出这块部分是关于矩阵x*y的循环,如图1-4

图1-4

目前还不知道内外循环,哪个是矩阵宽,哪个是矩阵高,然后通过手工设置扫雷的宽高,再次跟踪可以知道上图[1005334]内存地址处存放的是矩阵宽,而[1005338]内存地址处存放的是矩阵高。因此我们可以逆出关于双重循环部分的代码,如下所示:

for(int y=1;y<=Height;y++)

{

for(int x=1;x<=Width;x++)

{

}

}

接着来看看看内循环里面的代码:

读完此段代码后可知,这段代码大部分是关于BitBlt函数的调用,通过查阅

MSDN或者直接Google,可以得知关于此函数的如下信息:

BOOL BitBlt(

HDC hDestDC, //指向目标设备环境的句柄

int xDest, //指定目标矩形区域左上角的X轴逻辑坐标。

int yDest, //指定目标矩形区域左上角的Y轴逻辑坐标。

int nDestWidth, //指定源和目标矩形区域的逻辑宽度。

int nDestHeight, //指定源和目标矩形区域的逻辑高度。

int hdcSrc, //指向源设备环境的句柄。

int xSrc, //指定源矩形区域左上角的X轴逻辑坐标。

int ySrc, //指定源矩形区域左上角的Y轴逻辑坐标。

DWORD dwROP = SRCCOPY //指定光栅操作代码。这些代码将定义源矩

形区域的颜//色数据,如何与目标矩形区域的颜色数据

组合以完成最后的颜色

)

那么,根据实际代码的情况我们就可以得到如下的形式

BitBlt(

[ebp+8]

[ebp-4]

[ebp-8]

10

10

[eax*4+1005A20]

0CC0020 )

那么有xDest=[ebp-4]和yDest=[ebp-8]。在B部分代码中,我们可以看到下面四处关于这两个值的代码语句。

关于[ebp-4]的两处是:

mov dword ptr [ebp-4], 0C

add dword ptr [ebp-4], 10

关于[ebp-8]的两处是:

mov dword ptr [ebp-8], 37

add dword ptr [ebp-8], 10

之所以会这么纠结的要赋个值再加一个数的主要原因就是,扫雷游戏不单是雷区,还有菜单,计时器等等在界面中的宽高整体显示出来的宽高才是正确的界面大小。所以有个初值和加上雷区的值(猜测是这样的)。

那么就可以得出下面的伪代码了

for(int y=1;y<=Height;y++)

{

for(int x=1;x<=Width;x++)

{

BitBlt([ebp+8],[ebp-4],[ebp-8],0x10,0x10, [eax*4+1005A20],0,0,0CC0020);

[ebp-4]= [ebp-4]+0x10;

}

[ebp-8]= [ebp-8]+0x10;

}

然后,在内循环中我们抽取出下面几句代码:

010026E0 |. 33C0 ||xor eax, eax ; |

010026E2 |. 8A0433 ||mov al, byte ptr [ebx+esi] ; |

010026E9 |.83E0 1F ||and eax, 1F ; |

010026EC |.FF3485205A00>||push dword ptr [eax*4+1005A20] ; |hSrcDC

通过位与and eax,1F来获取hSrcDC,由刚才插曲部分知道这一块代码关于是一个雷块的显示情况,因此可以推断出这里hSrcDC变化的时候会影响到所有雷块中其中一块的显示变化,在我们正常游戏时,这个变化也就是我们右键点击出现的旗子,问号,或者左键点击的数字,空白,或者是地雷等等这几种显示情况。

我们接着往下看,在外层循环中有这么一句话

0100271D |. 83C3 20 |add ebx, 20

也就是说在此每次外层循环一次就会将ebx加上0x20,而在双重循环开始前有句代码是010026C4 |. BB 60530001 mov ebx, 01005360

即,给ebx放入一个01005360的内存地址,说到这里,应该很容易发现蹊跷了吧,内层循环已经知道是表示一行所有雷块的循环,那么这里在外层每次结束前加个0x20也就是换到了下一行雷块。这时候我们查看01005360的内存地址处如下图所示

图中我圈住的10之间总共有27个0F,而之前我设置的雷区宽度也是27,所以01005360内存地址是雷区的起始地址。而0x10的意思是作为标志的边框的意思。根据上面的代码,换行时加上0x20 也可以推断出这个扫雷游戏宽度最大值只能

是30,加上边框两个,总共32刚好达成了换行是加0x20的条件。而实际测试情况,的确宽度最大只能设为30。

到这里我们找到了这个扫雷游戏的雷区在内存中的存放地址,但为了逆向的完整性,我们把逆出来的双重循环代码在补足一点。

dword address = 0x1005360

for(int y=1; y <= Height; y++)

{

for(int x=1; x<=Width; x++)

{

……//省略hSrcDC部分

BitBlt([ebp+8],[ebp-4],[ebp-8],0x10,0x10, [eax*4+1005A20],0,0,0CC0020);

[ebp-4] = [ebp-4] + 0x10;

}

[ebp-8] = [ebp-8] + 0x10;

address = address + 0x20

}

好了现在我们来验证一下这个1005360的地址,运行扫雷游戏,然后打开winhex,当然不用winhex直接用OD点F9运行程序后,在数据面板处也可以看到内存中的信息,但为了独立开来演示,所以选择用winhex来查看该程序运行所占用的内存情况,如下图

图中红框部分就对应了雷区,其中有10、0F、8F,已知10为边框,根据分布情况,0F为无雷点,8F为雷点,为了验证,我们先随便点一点,注意此处不要点到8F的块,由于扫雷游戏规则不允许第一次点就点到雷,所以如果故意去点8F 的块,雷区中的这一个雷会重新改变到另一块没有雷的块里面,而其他的雷点依然不会改变,所以我们先点0F的地方。

刚才提到了OD也可以,如下图所示

这是通过OD来看的,可以发现踩中雷是内存中的值变为了CC,而直接显示雷是8A。

由上面两图可知,

40表示空白,

41表示数字1,

42表示数字2,依次类推数字3、数字4等等。

0E表示无雷但插了旗子,

0D表示无雷但标了问号,

8E表示有雷且插了旗子,

8D表示有雷且标记了问号,

10表示雷区边框,

CC表示有雷并且被踩中,

8A表示有雷并且直接显示出,

因此可以看出整个数据的规律了吧。

最后来张很直观的整体截图,由下面这幅途中我们可以很清楚的看到扫雷的边框部分,由此可以知道雷区整体的画面显示部分是从1005340这个内存地址开始的

之后我通过OD运行扫雷,来观察比较两次游戏情况发现了如下图所示记录当前时间的地址(下图方框中所示)

同样也发现

这个地址方框中的01标识用来表示是否开始计时,当为00时表示停止计时,01时表示开始计时。

1.3算法扫雷模块

1.状态获取函数:

主要通过读取内存方式模拟读图获取信息。主要由于读图识别的方式难以完成,故使用读取内存方式代替,获取的信息包括图上的雷各说,各点状态(未点击,0-8,旗帜)、剩余雷数量、没点击区域的个数,故不会影响用算法扫雷的真实性。

函数完成的主要功能就是读取雷区的状态图,然后将它存入状表中。

case 0x0f : MineSvAI[x][y+1] = 9; unknow++; break;

case 0x8f : MineSvAI[x][y+1] = 9; unknow++; break;

case 0x40 : MineSvAI[x][y+1] = 0; break;

case 0x41 : MineSvAI[x][y+1] = 1; break;

case 0x42 : MineSvAI[x][y+1] = 2; break;

case 0x43 : MineSvAI[x][y+1] = 3; break;

case 0x44 : MineSvAI[x][y+1] = 4; break;

case 0x45 : MineSvAI[x][y+1] = 5; break;

case 0x46 : MineSvAI[x][y+1] = 6; break;

case 0x47 : MineSvAI[x][y+1] = 7; break;

case 0x48 : MineSvAI[x][y+1] = 8; break;

case 0x8e : MineSvAI[x][y+1] = 10; break;

case 0x0e : MineSvAI[x][y+1] = 10; break;

例如在内存中8e 和0e在内存中分别表示有雷插上了旗帜和无雷插上了旗帜,但在图片中都表示旗帜,故存入矩阵中都是同一符号。

同时该函数还初始化一个标记矩阵用以记录某点在一轮中是否被点击过,防止在1轮中右击标记雷时多次操作使旗帜变成问号。

2.权值计算函数:

当用逻辑无法完成推理点击时,计算所有未点击区域的有雷概率,然后点击有雷概率最低的第一个点。

wei[i][j ]=surplus/(float)unknow;

对于周围都是空白和雷的状况,有雷的概率是剩余雷个数除以未点击区域的数目。

fwei=(MineSvAI[x][y]-nMine2)/(float)nOther2;

对于有周围有数字的情况,就按数字计算当前格子的有雷概率,若周围有多个数字,则记录其中概率最大的值

最后搜索整张权值表,找第一个概率最小的点点击

3.逻辑推理函数:

按读图函数得到的矩阵进行推理,用函数推理每一个点是否能左击或右击,每轮君扫描每一个点,左击右击之后不改变记录的矩阵,一轮之后再重新读图刷新。每轮初始化一个整体的修改符号,当整轮都没有左击和右击,就调用权值计算函数来猜测,然后再读图再推理,直到全部推出或者点到雷为止。

4.左击,右击模拟函数:

模拟鼠标的左击右击来点击和标记雷,每次点击后用sleep函数等待0.5s,可以清楚看到推理的顺序和逻辑性。

1.4内存读取扫雷模块

3.4.1 主体部分

//通过查找含有“扫雷”标题的窗口以获得其句柄

HWND hWnd=::FindWindow(NULL,L"扫雷");

if(!hWnd)

{

::MessageBox(NULL,L"没有找到XP扫雷程序",L"WRONG",MB_OK);

return;

}

//调用getmine函数获得内存中的雷点分布

long w=getmine();

//调用viewMine函数将所读到的雷点显示出来

viewMine(w);

3.4.2 读出雷区模块

//通过查找含有“扫雷”标题的窗口以获得其句柄

Hwd_Mine = (HWND)::FindWindow(NULL,L"扫雷");

//获得该句柄的线程ID

::GetWindowThreadProcessId(Hwd_Mine, &pid_Mine);

//根据pid_Mine打开一个已经存在的进程,并获得对内存操作的最高权限

hand = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid_Mine);

//读取内存WIDTH_ADDRESS地址的值到变量width中,即获得雷区宽

ReadProcessMemory(hand, (const void *)(WIDTH_ADDRESS), &width, 1, 0);

//读取内存HEIGHT_ADDRESS地址的值到变量height中,即获得雷区高

ReadProcessMemory(hand, (const void *)(HEIGHT_ADDRESS), &height, 1, 0);

for(int y=0; y

{

//由于算上左右边框故加2,这是根据从内存中读出的情况做出的

for(int x=0; x

{

//雷区起始存放地址START_ADDRESS + 此行中第x个+ h获得该地址的值

ReadProcessMemory(hand, (const void *)(START_ADDRESS + x + h), &hp, 1, 0);

//规整hp,保持高6位为0,以正确显示只有低2位的值

hp = hp & 0x000000ff;

//内存雷区部分,用10表示雷区边框,这是不需要进行判断保存的

if(hp != 0x10)

{

//通过反汇编得知0x8f为有雷的标识

if(hp == 0x8f)

MineSv[flag]='X';

else

MineSv[flag]='O';

flag++;

}

}

//内存雷区的分布可看出从某一行第一列到下一行第一列间隔了0x20大小的地址

//y表示的是第几列。加上1表示下一列。

h=(y+1)*0x20;

}

CloseHandle(hand);

3.4.3 雷区显示模块

//对临时数组初始化操作,算上了最大雷区情况下包括换行符的总大小

for(int mt=0;mt<32*30;mt++)

{

temp[mt]=NULL;

}

for(int j=0;j

{

for(int i=0;i

{

//temp[此行第i个+每行个数*前面的行数+行数*2(即换行符占用的)]

temp[i+width*j+j*tp]=MineSv[i+width*j];

}

//当显示完一行之后需换行操作

temp[width*j+width+2*j]='\r';

temp[width*j+width+2*j+1]='\n';

tp=2;

}

//将包含了雷点情况的数组temp显示出来

m_ce.SetWindowText(temp);

CString str;

//显示当前雷区宽度

str.Format( L"%ld ",width);

GetDlgItem(IDC_STA TIC1)->SetWindowText(str);

//显示当前雷区高度

str.Format( L"%ld ",height);

GetDlgItem(IDC_STA TIC2)->SetWindowText(str);

3.4.4 模拟鼠标点击模块

for (int i = 0; i < height; i ++)

{

for (int j = 0; j < width; j ++)

{

if(MineSv[i * width + j] != 'X')

{

//定位到每个格子的中间

//(到左边第一个格子左侧时x的坐标+ 半个格子+ 第几个格子乘以格子宽度, y坐标同理)

::SendMessage(Hwd_Mine, WM_LBUTTONDOWN, 0,MAKELPARAM( 12 + 8 + j * 16,55 + 8 + i * 16));

::SendMessage(Hwd_Mine, WM_LBUTTONUP,0,MAKELPARAM( 12 + 8 + j * 16,55 + 8 + i * 16));

}

}

}

3.4.5 停止时间模块

//wrtFlg初始为0

if(wrtFlg == 1)

{

//已知0x01为0x01005164的值时,开始计时

a=0x01;

m_kTime.SetWindowTextW(L"停止计时");

wrtFlg=0;

}

else

{

a=0x00;

m_kTime.SetWindowTextW(L"继续计时");

wrtFlg=1;

}

WriteProcessMemory(hAndle,(LPVOID)0x01005164,(LPVOID)&a,4,&a);

3.4.6 设置时间模块

//把该时间i分为高位和地位分别赋值给t1和t2

DWORD t1 = i>>8;

DWORD t2 = i & 0x0ff;

HWND hWnd =::FindWindow(NULL,L"扫雷");

DWORD hWpid;

GetWindowThreadProcessId(hWnd,&hWpid);

HANDLE hAndle = OpenProcess(PROCESS_ALL_ACCESS,FALSE,hWpid);

WriteProcessMemory(hAndle,(LPVOID)0x0100579C,(LPVOID)&t2,1,&t2);

WriteProcessMemory(hAndle,(LPVOID)0x0100579D,(LPVOID)&t1,1,&t1); 2系统设计与流程的实现

下面如图4-1,这里展示由算法实现的流程图:

图4-1 算法实现的流程图3系统测试与分析

3.1测试

首先如下图5-1所示为程序启动的主界面。

图5-1 主界面

然后点击“显示雷区”按钮,如图5-2

图5-2 成功显示出雷区图如果没有打开扫雷游戏程序则会如图5-3提示

图5-3

接着是算法扫雷功能,如图5-4我们能看到由于通过算法实现难免在无法根据当前所知数字信息进行推理只有靠猜的时候,是会不幸中雷的!

图5-4

最后则是由读取内存的方式扫雷结果,如图5-5

图5-5

相关主题
相关文档
最新文档