逆向学习:扫雷游戏辅助

selph
selph
发布于 2020-08-06 / 948 阅读
0
0

逆向学习:扫雷游戏辅助

小白逆向初体验,本次学习了使用C语言打开进程,以及如何读取和修改进程的内存

本文前置知识:C/C++语言基础及CE的基本使用

本文实现的功能:

  1. 一键自动标记雷的位置
  2. 一键自动扫雷

本文所用程序:

winmine

目标1:一键自动标记雷的位置

猜测&分析

要标记雷的位置,首先就得知道雷是怎么来的,雷会出现在哪里,根据我屈指可数的逆向经验,这种时候要进行猜测。

image-20200806075209877

目前扫雷游戏可以进行精确扫描的数值有三个:宽度、高度、雷数。

扫雷游戏的地图有宽和高两种属性,宽和高决定了地图的大小,也决定了雷的数量上限,这里猜测雷的生成与宽和高有关系:

通过反复修改Height属性的数值进行精确搜索,最终定位到两个地址:

image-20200806080419762

选中上面的地址,右键,是什么访问了这个地址(此处猜测雷会根据这个属性来生成),然后再次重新生成地图开始游戏:

image-20200806080643436

image-20200806080653486

一共生成了10个雷,刚好里面有一个指令执行了10次,这可能就是雷根据高的位置来生成,点击显示反汇编程序查看,这里主要的程序片段如下图所示:

image-20200806080958653

其中出现的几个内存地址的值分别是:

//通过精确搜索得知
宽度属性:Winmine__XP.exe+5334
高度属性:Winmine__XP.exe+5338
待生成雷的数量:Winmine__XP.exe+5330
//通过进入函数得知
取随机数函数:Winmine__XP.exe+3940

本段程序的逻辑大概是:

for NemberOfLandmine > 0:
    //调用函数根据宽和高随机生成一个值
    w = rand(weight)
    w = w + 1
    h = rand(height)
    h = h + 1
    h = 32 * h
    if GetValueFromAddr(Winmine__XP.exe+5340+w+h) == 0x8F://这个地址的值为8F时
        break
    SetValueFromAddr(Winmine__XP.exe+5340+w+h,0x8F)//则设置为8F
    NumberOfLandmine--

test 0F,80
ZF=1

test 8F,80
ZF=0

这段程序的功能是雷的生成:根据宽和高分别随机生成一个值然后+1,高的值乘以32,计算根据这个随机值取地址的偏移量,看这个偏移的地方的值是否为8F(8F表示为雷),如果是,则退出循环再次进行随机生成,如果不是则设置为8F,再次进行循环,直到待生成的雷为0时,跳出循环

这里的Winmine__XP.exe+5340应该就是雷区的起始位置,跳转至该地址查看内存:

image-20200806083626476

这次设置的高度是14,宽度是10,在内存地址中不难发现,每行数据以0x10开头和结尾,每行数据占32字节,多余的空位用0x0F占位,我们整理一下可以轻易看出这就是已经生成好的地图:

0x10为边界因为至少是从第二行第二列开始放的雷,所以刚才宽和高生成的随机值要+1

image-20200806084124122

当我们用鼠标右键对雷区进行插旗子时,这个位置的内存的值就变成了0x8E

代码实现

到此位置,自动标记雷的位置的功能的实现思路逐渐清晰了起来:

  1. 打开进程
  2. 读取进程雷区位置的内存
  3. 遍历雷0x8F的位置
  4. 写入进程雷区位置的内存0x8E

直接上代码吧:

#include<stdio.h>
#include<windows.h>

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

	PBYTE pByte = NULL;
	DWORD dwHeight = 0,dwWeight = 0;

	HWND hWnd = FindWindow(NULL, L"Minesweeper");				//根据窗口标题名来获取窗口的句柄
	if (hWnd != 0) {
		GetWindowThreadProcessId(hWnd, &Pid);					//通过句柄获取进程pid
		hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);	//打开进程,获取所有权限,返回句柄
		if (hProcess == 0) {
			printf("open error");
			return 0;
		}

		DWORD dwBoomAddr = 0x01005340;							//雷区首地址
		DWORD dwSize = 832;										//雷区大小:32*25字节
		pByte = (PBYTE)malloc(dwSize);							//申请雷区大小的内存空间,返回指针
		DWORD dwTmpAddr = 0;

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

		for (i = 0; i < dwSize;i++) {//遍历雷的位置
			if (pByte[i] == 0x8F) {
				dwTmpAddr = 0x01005340 + i;
				WriteProcessMemory(hProcess, (LPVOID)dwTmpAddr, &bClear, sizeof(BYTE), 0);//将0x8E写入雷的位置

			}
		}

		//刷新扫雷的客户区
		RECT rt;//用于存储窗口大小
		GetClientRect(hWnd, &rt);//获取窗口大小
		InvalidateRect(hWnd, &rt, TRUE);//刷新窗口

		free(pByte);
		CloseHandle(hProcess);
	}
	else {
		printf("get hWnd Fail!");
	}
	return 0;
}

关于windows.h库函数,可查阅:

https://blog.csdn.net/farmwang/article/details/50603608

https://docs.microsoft.com/zh-cn/windows/win32/winmsg/windows

效果展示

开始游戏之前,运行上述程序,可在游戏开始之前将地雷给标识出来:

image-20200806091443302

目标2:一键自动化扫雷

分析&代码实现

经过查阅资料得知:通过SendMessage()函数向窗口发送指定消息,也就是发送鼠标点击的消息,需要指定点击的坐标

通过模拟鼠标点击,来帮我们快速点完不是雷的区域,但需要知道非雷位置的坐标

这就需要我们来定义一个雷区的数组了

获取高度和宽度:

		DWORD dwHeight = 0, dwWidth = 0;
		DWORD InfoAddr = 0x01005330;
		ReadProcessMemory(hProcess, (LPVOID)(InfoAddr + 4), &dwWidth, sizeof(DWORD), 0);
		ReadProcessMemory(hProcess, (LPVOID)(InfoAddr + 8), &dwHeight, sizeof(DWORD), 0);

定义数组用来定位和模拟按键:

		char result[768] = {'x'};
		int resultptr = 0;
		for (int i = 1; i <= dwHeight; i++) {
			for (int j = 1; j < 32; j++) {
				if (pByte[i * 32 + j] != 0x10) {
					if (pByte[i * 32 + j] != 0x8F)
						result[resultptr] = '1';
					else 
						result[resultptr] = '0';
					resultptr++;
				}
				else
					break;
			}
		}

		int x = 0, y = 0;
		int x1 = 0, y1 = 0;
		for (int i = 0; i < dwHeight * dwWidth; i++) {
			if (result[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));

			}
		}

这里生成数组再计算位置的程序流程可以再简化一下,直接让数组指针作为数组来用也可以:

		int x = 0, y = 0;
		int x1 = 0, y1 = 0;
		int resultptr = 0;
		for (int i = 1; i <= dwHeight; i++) {
			for (int j = 1; j < 32; j++) {
				if (pByte[i * 32 + j] != 0x10) {
					if (pByte[i * 32 + j] != 0x8F){
						x1 = resultptr % dwWidth;
						y1 = resultptr / 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));
					}
					resultptr++;
				}
				else
					break;
			}
		}

image-20200806084124122

这里是直接跳过第一行,直接从第二行第二个开始遍历,遇到0x10就跳到下一行的第二个;这样一来,就相当于把二维数组通过每行的数据个数转换成一维数组来用,如果数据不是0x8F就模拟鼠标点击。

效果展示

image-20200806142107152

完整代码

#include<stdio.h>
#include<windows.h>

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

	PBYTE pByte = NULL;

	HWND hWnd = FindWindow(NULL, L"Minesweeper");				//根据窗口标题名来获取窗口的句柄
	if (hWnd != 0) {
		GetWindowThreadProcessId(hWnd, &Pid);					//通过句柄获取进程pid
		hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);	//打开进程,获取所有权限,返回句柄
		if (hProcess == 0) {
			printf("open error");
			return 0;
		}


		DWORD dwBoomAddr = 0x01005340;							//雷区首地址
		DWORD dwSize = 832;										//雷区大小:32*25字节
		pByte = (PBYTE)malloc(dwSize);							//申请雷区大小的内存空间,返回指针
		DWORD dwTmpAddr = 0;

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

		for (i = 0; i < dwSize;i++) {//遍历雷的位置
			if (pByte[i] == 0x8F) {
				dwTmpAddr = 0x01005340 + i;
				WriteProcessMemory(hProcess, (LPVOID)dwTmpAddr, &bClear, sizeof(BYTE), 0);//将0x8E写入雷的位置
				n--;
			} 
		}

		//刷新扫雷的客户区
		RECT rt;//用于存储窗口大小
		GetClientRect(hWnd, &rt);//获取窗口大小
		InvalidateRect(hWnd, &rt, TRUE);//刷新窗口


		//-----------------------------------------------------------------------------
		
		DWORD dwHeight = 0, dwWidth = 0;
		DWORD InfoAddr = 0x01005330;
		ReadProcessMemory(hProcess, (LPVOID)(InfoAddr + 4), &dwWidth, sizeof(DWORD), 0);
		ReadProcessMemory(hProcess, (LPVOID)(InfoAddr + 8), &dwHeight, sizeof(DWORD), 0);

		int x = 0, y = 0;
		int x1 = 0, y1 = 0;
		int resultptr = 0;
		for (int i = 1; i <= dwHeight; i++) {
			for (int j = 1; j < 32; j++) {
				if (pByte[i * 32 + j] != 0x10) {
					if (pByte[i * 32 + j] != 0x8F){
						x1 = resultptr % dwWidth;
						y1 = resultptr / 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));
					}
					resultptr++;
				}
				else
					break;
			}
		}


		free(pByte);
		CloseHandle(hProcess);
	}
	else {
		printf("get hWnd Fail!");
	}
	return 0;
}

参考资料

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

https://blog.csdn.net/qq_32350131/article/details/80343890

https://blog.csdn.net/farmwang/article/details/50603608

编译好的exe文件下载地址:https://github.com/kn0sky/StudyReport/blob/master/WinmineTools.exe


评论