本帖最后由 Nattevak 于 2021-12-7 08:48 编辑
自己对游戏方面挺感兴趣的,但是毕竟新手入门水平,想搞搞大型游戏还没有那水平,饭还得一口一口吃,先从经典的扫雷小游戏开始尝试
破解扫雷并不是说玩个扫雷都需要去使用作弊手段,只是为了学习思路和方法
扫雷游戏大家应该都玩过吧,我这里就不献丑演示游戏玩法了
扫雷.zip
(2.78 MB, 下载次数: 1103)
其中包含 扫雷EXE源文件 以及 VS工程 (DLL文件在MFC -> Debug ->MFCSL.dll)
一、数据分析
1.雷的数量
添加后分别修改,最后发现地址为01005630的数值进行修改操作时游戏中的雷数会发生变化,故这里就确定的雷数的地址。
1.1
2.游戏时间
找到时间的地址,之后通过NOP时间可以实现0秒通关的效果。
1.2
3.雷区宽度
同样的搜索方法寻找到疑似雷区宽度的数据,之后仍进行修改验证测试。
1.3
4.雷区高度
同样的搜索方法寻找到疑似雷区高度的数据,之后仍进行修改验证测试。
1.4
5.数据汇总
1.5
二、游戏分析
1.遍历雷区
①创建DLL项目
使用Visual Studio创建MFC动态链接库项目,并选定DLL类型为静态链接(我这里使用的是Visual Studio2022)
2.1.1
②使用Spy++获取窗口信息(Visual Studio2022菜单栏 工具->Spy++ 自带的工具)
2.1.2
③查看扫雷窗口信息
2.1.3
2.此处写了一个遍历雷区的测试代码,供大家参考
[C] 纯文本查看 复制代码 if (Msg == WM_KEYDOWN && wParam == VK_F5)
{
//一键秒杀
OutputDebugString(L"F5");
int nWidth = *g_pWidth;
int nHeight = *g_pHeight;
int nMineCount = *g_pMineCount;
CString strString;
strString.Format(L"宽度: %d,高度: %d,雷数:%d ", nWidth, nHeight, nMineCount);
OutputDebugString(strString.GetBuffer());
}
3.注入DLL
①使用CE 工具->注入DLL
3.1.1
②使用DebugView工具观察注入情况,对比游戏参数验证数值是否正确
3.1.2
4.反汇编代码调试
①将扫雷程序附加到OD中查看其内存情况,可以看出边界为10,雷为8F,标识为41,42…(无数字标识的地方为40)
4.1.1
②分析雷区数组的汇编代码部分
4.1.2
③测试遍历雷区代码,并重新注入进行测试
[C] 纯文本查看 复制代码 for (size_t y = 1; y < nHeight + 1; y++)
{
CString strLine;
for (size_t x = 1; x < nWidth + 1; x++)
{
//数组基地址+(y+1)*32+x+1(y=0到高度)
BYTE byCode = *(PBYTE)((DWORD)g_pBase + x + y * 32);
if (byCode == MINE)
{
nFindCount++;
}
CString strCode;
strCode.Format(L"%02x ", byCode);
strLine += strCode;
}
OutputDebugString(strLine.GetBuffer());
}
④再次使用DebugView工具观察注入情况
4.1.4
5.坐标转换
我们希望完成一键扫雷的操作,那我们就应该有模拟点击的操作,但是我现在并不知道雷区的坐标是什么样的,所以我们需要先进行坐标的转换
①获取回调函数
找到窗口回调函数的位置,仍然通过Spy++查看,使用Spy++获取窗口信息再点击确定就会弹出该窗口
5.1.1
6.反汇编代码调试
①在汇编代码中找到窗口回调函数的位置,并设置假定参数,再设置消息断点
6.1.1
②分析鼠标事件坐标转换的汇编代码部分
6.1.2
③获取鼠标位置,反馈窗口信息
[C] 纯文本查看 复制代码 x = LOWORD(lParam);
y = HIWORD(lParam);
x = (x + 4) >> 4;
y = (y - 0x27) >> 4;
BYTE byCode = *(PBYTE)((DWORD)g_pBase + x + y * 32);
if (byCode == MINE){
SetWindowText(hWnd, L"此处有雷");
}
else{
SetWindowText(hWnd, L"扫雷");
}
④通过模拟点击事件把所有非雷区域点击,实现一键通关
[C] 纯文本查看 复制代码 xPos = (x << 4) - 4;
yPos = (y << 4) + 0x27;
SendMessage(hWnd, WM_LBUTTONDOWN, 0, MAKELPARAM(xPos, yPos));
SendMessage(hWnd, WM_LBUTTONUP, 0, MAKELPARAM(xPos, yPos));
⑤游戏效果
按下F5后实现一秒钟自动扫雷并通关游戏
6.1.5
7.NOP游戏时间
①获取地址
在CE中找出是什么访问了这个地址,再显示反汇编程序,可以观察到时间的变化
7.1
②反汇编代码调试
由CE可以看到01002FF5处的指令代码实现时间的自增,如果想要时间不增加,就需要把这里给NOP掉,即将FF 05 9C570001 这六个字节都填充为NOP
7.2.1
当我们第一次按下时,01003830会自增1,所以导致之前无法实现0秒完成扫雷,故我们应该把01003830的数据也NOP掉
7.2.2
③将时间自增语句使用NOP填充
[C] 纯文本查看 复制代码 //获取扫雷进程ID
GetWindowThreadProcessId(g_Wnd, &Pid);
//获取扫雷进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
//将时间自增的语句使用NOP填充
result1 = WriteProcessMemory(hProcess, (LPVOID)g_pTime1, &szInc, 6, 0);
result2 = WriteProcessMemory(hProcess, (LPVOID)g_pTime2, &szInc, 6, 0);
④游戏效果
7.4
三、完整代码
[C] 纯文本查看 复制代码 // MFCSL.cpp: 定义 DLL 的初始化例程。
//
#include "pch.h"
#include "framework.h"
#include "MFCSL.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 唯一的 CMFCSLApp 对象
CMFCSLApp theApp;
HWND g_Wnd;//窗口句柄
WNDPROC g_OldProc;//老的窗口回调函数
PDWORD g_pHeight = (PDWORD)0x01005338;//雷区高度
PDWORD g_pWidth = (PDWORD)0x01005334;//雷区宽度
PDWORD g_pMineCount = (PDWORD)0x01005330;//雷的数量
PBYTE g_pBase = (PBYTE)0x1005340;//雷区基地址
#define MINE 0x8F//雷区中的元素标识
DWORD Pid = 0;//进程ID
HANDLE hProcess = 0;//进程句柄
PDWORD g_pTime1 = (PDWORD)0x01002FF5;//时间自增
PDWORD g_pTime2 = (PDWORD)0x01003830;//时间首次自增
char szInc[6] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };//保存NOP指令
DWORD result1, result2;//保存结果
// CMFCSLApp 初始化
LRESULT CALLBACK WindowProc(
_In_ HWND hWnd,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam)
{
//获取扫雷进程ID
GetWindowThreadProcessId(g_Wnd, &Pid);
//获取扫雷进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
//将时间自增的语句使用NOP填充
result1 = WriteProcessMemory(hProcess, (LPVOID)g_pTime1, &szInc, 6, 0);
result2 = WriteProcessMemory(hProcess, (LPVOID)g_pTime2, &szInc, 6, 0);
if (Msg == WM_KEYDOWN && wParam == VK_F5)
{
//一键秒杀
OutputDebugString(L"F5");
int nWidth = *g_pWidth;
int nHeight = *g_pHeight;
int nMineCount = *g_pMineCount;
CString strString;
strString.Format(L"宽度: %d,高度: %d,雷数:%d ", nWidth, nHeight, nMineCount);
OutputDebugString(strString.GetBuffer());
int nFindCount = 0;
for (size_t y = 1; y < nHeight + 1; y++)
{
CString strLine;
for (size_t x = 1; x < nWidth + 1; x++)
{
//数组基地址+(y+1)*32+x+1(y=0到高度)
BYTE byCode = *(PBYTE)((DWORD)g_pBase + x + y * 32);
if (byCode == MINE)
{
nFindCount++;
}
else
{
int xPos, yPos;
xPos = (x << 4) - 4;
yPos = (y << 4) + 0x27;
SendMessage(hWnd, WM_LBUTTONDOWN, 0, MAKELPARAM(xPos, yPos));
SendMessage(hWnd, WM_LBUTTONUP, 0, MAKELPARAM(xPos, yPos));
}
CString strCode;
strCode.Format(L"%02x ", byCode);
strLine += strCode;
}
OutputDebugString(strLine.GetBuffer());
}
CString strCode;
strCode.Format(L"找到的雷数 %d ", nFindCount);
OutputDebugString(strCode.GetBuffer());
}
else if (Msg == WM_MOUSEMOVE)
{
//鼠标移动
int x, y;
x = LOWORD(lParam);
y = HIWORD(lParam);
x = (x + 4) >> 4;
y = (y - 0x27) >> 4;
BYTE byCode = *(PBYTE)((DWORD)g_pBase + x + y * 32);
if (byCode == MINE)
{
SetWindowText(hWnd, L"此处有雷");
}
else
{
SetWindowText(hWnd, L"扫雷");
}
}
return CallWindowProc(g_OldProc, hWnd, Msg, wParam, lParam);
}
BOOL CMFCSLApp::InitInstance()
{
CWinApp::InitInstance();
//1.通过查找窗口,获取窗口句柄
g_Wnd = ::FindWindow(L"扫雷", L"扫雷");//FindWindowW(_In_opt_ LPCWSTR lpClassName,_In_opt_ LPCWSTR lpWindowName);
if (g_Wnd == NULL)
{
OutputDebugString(L"无法找到 扫雷窗口");
return FALSE;
}
//2.设置窗口回调函数
g_OldProc = (WNDPROC)SetWindowLong(g_Wnd, GWL_WNDPROC, (LONG)WindowProc);
if (g_OldProc == NULL)
{
OutputDebugString(L"设置窗口回调函数失败");
return FALSE;
}
return TRUE;
} |