小白逆向初体验,本次学习了使用C语言打开进程,以及如何读取和修改进程的内存
本文前置知识:C/C++语言基础及CE的基本使用
本文实现的功能:
- 一键自动标记雷的位置
- 一键自动扫雷
本文所用程序:
-
扫雷本体:Winmine__XP.exe:
-
Cheat Engine 6.7
-
Visual Studio 2019
目标1:一键自动标记雷的位置
猜测&分析
要标记雷的位置,首先就得知道雷是怎么来的,雷会出现在哪里,根据我屈指可数的逆向经验,这种时候要进行猜测。
目前扫雷游戏可以进行精确扫描的数值有三个:宽度、高度、雷数。
扫雷游戏的地图有宽和高两种属性,宽和高决定了地图的大小,也决定了雷的数量上限,这里猜测雷的生成与宽和高有关系:
通过反复修改Height属性的数值进行精确搜索,最终定位到两个地址:
选中上面的地址,右键,是什么访问了这个地址(此处猜测雷会根据这个属性来生成),然后再次重新生成地图开始游戏:
一共生成了10个雷,刚好里面有一个指令执行了10次,这可能就是雷根据高的位置来生成,点击显示反汇编程序查看,这里主要的程序片段如下图所示:
其中出现的几个内存地址的值分别是:
//通过精确搜索得知
宽度属性: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=1test 8F,80
ZF=0
这段程序的功能是雷的生成:根据宽和高分别随机生成一个值然后+1,高的值乘以32,计算根据这个随机值取地址的偏移量,看这个偏移的地方的值是否为8F(8F表示为雷),如果是,则退出循环再次进行随机生成,如果不是则设置为8F,再次进行循环,直到待生成的雷为0时,跳出循环
这里的Winmine__XP.exe+5340应该就是雷区的起始位置,跳转至该地址查看内存:
这次设置的高度是14,宽度是10,在内存地址中不难发现,每行数据以0x10
开头和结尾,每行数据占32字节,多余的空位用0x0F
占位,我们整理一下可以轻易看出这就是已经生成好的地图:
0x10为边界因为至少是从第二行第二列开始放的雷,所以刚才宽和高生成的随机值要+1
当我们用鼠标右键对雷区进行插旗子时,这个位置的内存的值就变成了0x8E
代码实现
到此位置,自动标记雷的位置的功能的实现思路逐渐清晰了起来:
- 打开进程
- 读取进程雷区位置的内存
- 遍历雷0x8F的位置
- 写入进程雷区位置的内存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
效果展示
开始游戏之前,运行上述程序,可在游戏开始之前将地雷给标识出来:
目标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;
}
}
这里是直接跳过第一行,直接从第二行第二个开始遍历,遇到0x10就跳到下一行的第二个;这样一来,就相当于把二维数组通过每行的数据个数转换成一维数组来用,如果数据不是0x8F就模拟鼠标点击。
效果展示
完整代码
#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