最近对游戏安全比较感兴趣,想往这方面深造,于是便开始看战队里在腾讯游戏安全工作的大佬的博客,以便熟悉一下大佬的学习思路,发现他会从扫雷这种简单的游戏外挂开始着手学习,我也感兴趣的进行了一次扫雷辅助的实现
寻找地图首地址
容易想到的是,扫雷游戏在代码中肯定是要有对雷和非雷的数字型描述,所以用CE不断翻开雷块应该可以轻松找到地图地址,但是为了脱离对CE的依赖并且提高自己的游戏代码的审计能力,还是决定麻烦点用IDA找到地图首地址
打开IDA,发现iDa没有识别出winmain入口
于是开始从导入表中找是否有关键函数调用,找到了一个rand(),联想到rand函数可以随机化雷的x,y坐标,应该有点用,两轮交叉引用找到关键播种函数
这里看到利用byte_1005340数组和宽高进行寻址赋值,貌似是地图首地址,下断点IDA动调一手,找到0x1005340处内存地址
点两个雷块确认这个是地图首地址,并且确认了整型数对应的雷块类型
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() { 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"));
} }
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")); } else tmp.Format(TEXT("0")); m_data += tmp; } m_data += TEXT("\r\n"); printf("\n"); } }
|
实现效果图:
光有透视可不行,进一步通过模拟鼠标点击实现一键扫雷才是关键
一键扫雷辅助代码实现
具体实现过程描述:spy++ 查看窗口类名,标题->GetWindowHandle获取窗口句柄->MAKELPARAM对鼠标坐标x,y进行封装->PostMessage向窗口传入WM_LBUTTONDOWN和WM_LBUTTONUP的消息
坐标x,y可以用spy++来过滤日志消息对WM_LBUTTONDOWN和WM_LBUTTONUP落点进行粗略观察也可以进行ida代码分析得出
这里应该是通过地图的宽高确定窗口的右边缘坐标和下边缘坐标,所以16应该是雷块间的间隔,24,67应该是第一个雷块的右边缘坐标和下边缘坐标
这里对PostMessage以及MAKELPARAM函数做个简单的记录
PostMessage
是 MFC 中的一个函数,用于向指定的窗口发送消息。让我详细介绍一下这个函数:
功能:
PostMessage
函数将一个消息放入与创建窗口的线程相关联的消息队列中,并立即返回,不会阻塞当前线程。这使得它适用于异步消息传递。
参数:
hwnd
: 目标窗口的句柄。这是要接收消息的窗口。
msg
: 要发送的消息类型,例如 WM_USER
或自定义消息。
wParam
和 lParam
: 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")); } }
|
实现效果图:
dll注入调用call实现一键扫雷
通过简单的猜想可以知道,代码里肯定是会有一个函数去处理鼠标点击坐标进行判断结果的,所以可以去追溯查找一下鼠标点击坐标的引用,之前通过rand函数交叉引用到的函数继续溯源找到主要程序入口点位置
继续通过交叉引用直出现到MFC程序轮廓代码,如下图
通过lpfnWndProc名字我们大概可以分析出sub_1001BC9函数应该是处理while(1)循环的函数,进去一探究竟
在这块区域我们可以看到v4的高16位和低16位进行一些处理得到v12,v13,并且if循环里dword_1005334和dword_1005338我们之前分析得到这是宽和高,所以v12,v13应该就是处理过的x,y坐标,v4是原来鼠标点击的xy坐标
通过调试验证猜想成功
对这两个变量下读写断点,动调找到可能的call指令
动调发现分别是在sub_1002646和sub_1003512处发生雷块按下和抬起的动画,交叉引用找到调用他们的母函数,分别是在0x10031D4,0x10037E1处,利用汇编注入工具测试验证猜测
验证猜测成功后开始写注入代码
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("注入失败!")); } }
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;
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) { _tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError()); return FALSE; }
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
hMod = GetModuleHandle(L"kernel32.dll"); pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
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
|
#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; }
#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) { return; } } } } }
|
效果图