扫雷辅助制作

扫雷辅助制作

最近对游戏安全比较感兴趣,想往这方面深造,于是便开始看战队里在腾讯游戏安全工作的大佬的博客,以便熟悉一下大佬的学习思路,发现他会从扫雷这种简单的游戏外挂开始着手学习,我也感兴趣的进行了一次扫雷辅助的实现

寻找地图首地址

容易想到的是,扫雷游戏在代码中肯定是要有对雷和非雷的数字型描述,所以用CE不断翻开雷块应该可以轻松找到地图地址,但是为了脱离对CE的依赖并且提高自己的游戏代码的审计能力,还是决定麻烦点用IDA找到地图首地址

打开IDA,发现iDa没有识别出winmain入口

image-20240322192117779

于是开始从导入表中找是否有关键函数调用,找到了一个rand(),联想到rand函数可以随机化雷的x,y坐标,应该有点用,两轮交叉引用找到关键播种函数

image-20240322192432870

这里看到利用byte_1005340数组和宽高进行寻址赋值,貌似是地图首地址,下断点IDA动调一手,找到0x1005340处内存地址

image-20240322192721725

点两个雷块确认这个是地图首地址,并且确认了整型数对应的雷块类型

1
2
3
4
5
0xF:  没有雷
0x8F: 雷
0x8a: 是地雷且格子被翻开
0x40: 格子被翻开且周围都没有雷
0x4X: 格子被翻开且周围有X个地雷

既然确认了雷块地址和整型数含义,我们就可以画出对应的地图实现透视

扫雷透视辅助代码实现

这里用MFC写了一个窗口,利用按钮的槽函数实现透视

具体实现过程描述:spy++ 查看窗口类名,标题->GetWindowHandle获取窗口句柄->GetWindowThreadProcessId获取进程id->OpenProcess打开进程->ReadProcessMemory访问并读取内存->SetWindowText对edit control控件进行地图描绘

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
void CwinMine2Dlg::OnBnClickedgetmap()
{
//spy++ 查看窗口类名,标题
HWND hwnd = GetWindowHandle("扫雷", "扫雷");
if (hwnd)
{
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
LPCVOID mapBaseAddr = (LPCVOID)0x1005340;
LPCVOID xAddr = (LPCVOID)0x1005334;
LPCVOID yAddr = (LPCVOID)0x1005338;
ReadProcessMemory(handle, mapBaseAddr, (LPVOID)mapBuffer, sizeof(mapBuffer), NULL);
ReadProcessMemory(handle, xAddr, &X, sizeof(X), NULL);
ReadProcessMemory(handle, yAddr, &Y, sizeof(Y), NULL);
m_data.Empty();
PrintMap(mapBuffer, X, Y, m_data);
mmap.SetWindowText(m_data);
}
else
{
MessageBox(TEXT("can't find window"));

}
}
//这里对FindWindowA进行了简单的封装
HWND GetWindowHandle(std::string className, std::string windowName)
{
HWND hwnd = FindWindowA(className.c_str(), windowName.c_str());
return hwnd;
}
void PrintMap(byte* mapBuffer, int X, int Y,CString& m_data)
{
CString tmp = TEXT("");
for (int i = 1; i <= Y; i++)
{
for (int j = 1; j <= X; j++)
{
if ((*(mapBuffer + i * 32 + j) & 0x80) != 0)
{
tmp.Format(TEXT("1"));//1代表雷
}
else
tmp.Format(TEXT("0"));//0代表非雷
m_data += tmp;
}
m_data += TEXT("\r\n");//一定要/r/n才能换行
printf("\n");
}
}

实现效果图:

image-20240322194907271

光有透视可不行,进一步通过模拟鼠标点击实现一键扫雷才是关键

一键扫雷辅助代码实现

具体实现过程描述:spy++ 查看窗口类名,标题->GetWindowHandle获取窗口句柄->MAKELPARAM对鼠标坐标x,y进行封装->PostMessage向窗口传入WM_LBUTTONDOWN和WM_LBUTTONUP的消息

坐标x,y可以用spy++来过滤日志消息对WM_LBUTTONDOWN和WM_LBUTTONUP落点进行粗略观察也可以进行ida代码分析得出

image-20240322203634988

这里应该是通过地图的宽高确定窗口的右边缘坐标和下边缘坐标,所以16应该是雷块间的间隔,24,67应该是第一个雷块的右边缘坐标和下边缘坐标

这里对PostMessage以及MAKELPARAM函数做个简单的记录

PostMessage 是 MFC 中的一个函数,用于向指定的窗口发送消息。让我详细介绍一下这个函数:

  1. 功能

    • PostMessage 函数将一个消息放入与创建窗口的线程相关联的消息队列中,并立即返回,不会阻塞当前线程。这使得它适用于异步消息传递。
  2. 参数

    • hwnd: 目标窗口的句柄。这是要接收消息的窗口。
    • msg: 要发送的消息类型,例如 WM_USER 或自定义消息。
    • wParamlParam: 32 位的参数,用于传递额外的消息数据。这些参数的具体含义取决于消息类型。

第四个参数是 lParam,它是一个 32 位的整数值,用于传递额外的消息参数。在 PostMessage 函数中,我们通常使用它来传递鼠标点击的坐标。

具体来说,对于鼠标消息,lParam 的高 16 位表示鼠标的 Y 坐标,低 16 位表示鼠标的 X 坐标。这样,我们可以将 X 和 Y 坐标合并到一个 32 位整数中,以便在消息处理函数中解析。

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
void CwinMine2Dlg::OnBnClickedsaolei()
{
HWND hwnd = GetWindowHandle("扫雷", "扫雷");
if (hwnd)
{
for (int i = 1; i <= Y; i++)
{
for (int j = 1; j <= X; j++)
{
int x = Firstx + Xgap * (j - 1);
int y = Firsty + Ygap * (i - 1);
LPARAM lparam = MAKELPARAM(x, y);
if ((*(mapBuffer + i * 32 + j) & 0x80) == 0)
{
::PostMessage(hwnd, WM_LBUTTONDOWN, 0 , lparam);
::PostMessage(hwnd, WM_LBUTTONUP, 0, lparam);
}
}
printf("\n");
}
}
else
{
MessageBox(TEXT("can't find window"));
}
}

实现效果图:

image-20240322195002615

dll注入调用call实现一键扫雷

通过简单的猜想可以知道,代码里肯定是会有一个函数去处理鼠标点击坐标进行判断结果的,所以可以去追溯查找一下鼠标点击坐标的引用,之前通过rand函数交叉引用到的函数继续溯源找到主要程序入口点位置

继续通过交叉引用直出现到MFC程序轮廓代码,如下图

image-20240322210905951

通过lpfnWndProc名字我们大概可以分析出sub_1001BC9函数应该是处理while(1)循环的函数,进去一探究竟

image-20240322211222112

在这块区域我们可以看到v4的高16位和低16位进行一些处理得到v12,v13,并且if循环里dword_1005334和dword_1005338我们之前分析得到这是宽和高,所以v12,v13应该就是处理过的x,y坐标,v4是原来鼠标点击的xy坐标

通过调试验证猜想成功

image-20240322211828927

对这两个变量下读写断点,动调找到可能的call指令

image-20240325152954080

image-20240325191620005

动调发现分别是在sub_1002646和sub_1003512处发生雷块按下和抬起的动画,交叉引用找到调用他们的母函数,分别是在0x10031D4,0x10037E1处,利用汇编注入工具测试验证猜测

image-20240325191951679

验证猜测成功后开始写注入代码

MFC辅助程序中添加控件和槽函数以及注入函数模版

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
79
80
81
82
83
84
85
86
87
//槽函数

void CwinMine2Dlg::OnBnClickedZhuru()
{
DWORD ProcessID = ProcessFind(_T("saolei.exe"));
WCHAR WorkPath[MAX_PATH]; //用于存放获取路径的信息

GetModuleFileName(NULL, WorkPath, MAX_PATH);
CString DllPath = WorkPath;
int pos = DllPath.ReverseFind('\\'); // 查找倒数最后一个‘\\’符号
DllPath = DllPath.Left(pos + 1);
DllPath += _T("first.dll");
bool IsInjected = InjectDll(ProcessID, DllPath);
if (IsInjected)
{
MessageBox(_T("注入成功!"));
}
else
{
MessageBox(_T("注入失败!"));
}
// TODO: 在此添加控件通知处理程序代码
}

//solve.h

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

// #1. 使用dwPID获取目标进程(notepad.exe)的HANDLE
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}

// #2. 将szDllName大小的内存分配给目标进程(notepad.exe)内存。
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

// #3. 在分配的内存中写入myhack.dll路径("c:\\myhack.dll")。
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

// #4. 获取LoadLibraryA() API地址。
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

// #5. 在notepad.exe进程中运行线程
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);
CloseHandle(hProcess);

return TRUE;
}
DWORD ProcessFind(LPCTSTR Exename)
{
HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (!hProcess)
{
return FALSE;
}
PROCESSENTRY32 info;
info.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hProcess, &info))
{
return FALSE;
}
while (true)
{
if (_tcscmp(info.szExeFile, Exename) == 0)
{
return info.th32ProcessID;
}
if (!Process32Next(hProcess, &info))
{
return FALSE;
}
}
return FALSE;
}

dll代码

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
//dllmain.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "windows.h"
#include <iostream>
#include <fstream>
#include "solve.h"
using namespace std;

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
Saolei();
}
}
return TRUE;
}

//Saolei.cpp

#include "solve.h"
#include "pch.h"
void Saolei()
{
LPCVOID mapBaseAddr = (LPCVOID)0x1005340;
LPCVOID xAddr = (LPCVOID)0x1005334;
LPCVOID yAddr = (LPCVOID)0x1005338;
int tempx = 0, tempy = 0;
int x = *(int*)xAddr;
int y = *(int*)yAddr;
for (int i = 1; i <= y; i++)
{
for (int j = 1; j <= x; j++)
{
if ((*((BYTE*)mapBaseAddr + i * 32 + j) & 0x80) == 0)
{
tempx = i;
tempy = j;
__asm
{
push tempx
push tempy
mov eax, 0x10031D4
call eax
mov eax, 0x10037E1
call eax
}
if (3 == *(int*)0x1005160) { // 判断当前游戏状态 0为正在游戏 2为扫雷失败 3为扫雷成功
return;
}
}
}
}
}

效果图

image-20240325195025840