Windows XP扫雷游戏分析以及辅助编写
Windows XP扫雷游戏分析以及辅助编写
工具:IDA、x32dbg、Ollydbg、CFF Explorer、PETools
环境:Win10,Windows XP
0x01 游戏分析
找到程序所在位置,可以发现该游戏似乎并没有编写额外的dll或者lib,就只有一个winmine.exe,而且是n多年前xp自带的东西了,所以也没加壳:
尝试最大化窗口,发现最大格子数为24x30,最多的雷数为667:
拖进IDA中,OEP:
简单看一下:
25-50的位置基本上就是PE程序的自校验,这些数字的比较默认是10进制的看不出来,转成16进制就很明显了:
52-86可以不用管:
87-100就是窗体创建了:
sub_10021F0,调用Win32 API进行窗口创建:
回调函数DialogFunc:
INT_PTR __stdcall DialogFunc(HWND hDlg, UINT a2, WPARAM a3, LPARAM a4)
{
int v4; // eax
switch ( a2 )
{
case 0x53u:
WinHelpW(*(HWND *)(a4 + 12), L"winmine.hlp", 0xCu, (ULONG_PTR)&unk_1005040);
break;
case 0x7Bu:
WinHelpW((HWND)a3, L"winmine.hlp", 0xAu, (ULONG_PTR)&unk_1005040);
break;
case 0x110u:
SetDlgItemInt(hDlg, 141, height, 0); // 第二参数为宏,将值赋到Item中
SetDlgItemInt(hDlg, 142, width, 0);
SetDlgItemInt(hDlg, 143, mine_num, 0);
return 1;
case 0x111u:
if ( (unsigned __int16)a3 != 1 )
{
if ( (unsigned __int16)a3 == 2 )
{
LABEL_13:
EndDialog(hDlg, 1);
return 1;
}
if ( (unsigned __int16)a3 != 100 )
{
if ( (unsigned __int16)a3 != 109 )
return 0;
goto LABEL_13;
}
}
height = sub_1003DF6(hDlg, 141, 9, 24); // 控制高度范围为9-24
width = sub_1003DF6(hDlg, 142, 9, 30); // 控制宽度范围为9-30
v4 = (height - 1) * (width - 1);
if ( v4 > 999 )
v4 = 999;
mine_num = sub_1003DF6(hDlg, 143, 10, v4);// 控制雷数为10,v4,由于宽高限制,最大雷数为23*29=667
goto LABEL_13;
}
return 0;
}
这个宽度和高度肯定会用来初始化雷区的,Xref,发现还有五个函数调用:
最后发现sub_100367A中,通过do-while循环拿到一个随机的x和y坐标,然后将雷放到这个坐标中,完成雷区初始化,存放雷区的位置为mine_area
void sub_100367A()
{
char v0; // bl
int v1; // esi
int v2; // eax
char v3; // [esp-4h] [ebp-10h]
dword_1005164 = 0;
if ( width == __width && height == __height )
v3 = 4;
else
v3 = 6;
v0 = v3;
__width = width;
__height = height;
sub_1002ED5();
dword_1005160 = 0;
__mine_num = mine_num;
do
{
do
{
v1 = gen_mine_location(__width) + 1;
v2 = gen_mine_location(__height) + 1;
}
while ( mine_area[32 * v2 + v1] < 0 );
mine_area[32 * v2 + v1] |= 0x80u;
--__mine_num;
}
while ( __mine_num );
dword_100579C = 0;
__mine_num = mine_num;
dword_1005194 = mine_num;
dword_10057A4 = 0;
safe_pos = __width * __height - mine_num;
dword_1005000 = 1;
sub_100346A(0);
sub_1001950(v0);
}
由于程序并未开启ASLR,所以IDA中的地址跟x32dbg中的地址是一样的,直接查地址可以发现这里就是雷区的分布,并且通过前面的分析我们知道雷区的数值是|=0x80的,所以红框中0x8E的位置就是雷,0x0F的位置就是可以点击的位置:
这里需要知道的是,在我们点击前并未对数值做计算,所以此时可以发现出了雷区其他位置都是0x0F,当点击之后,才会开始计算:
这里的位置通过分析可以发现,上两行,下两行,左一行,右一行可以理解为墙,所以真正的雷区是从1005360开始,并且根据之前随机化区雷区的结果,真正开始的位置是1005361开始,然后隔一行才是下一行雷区,
真正的雷区,可以发现就是存在10的位置,这里多出的一行0x0F是预留的宽度,就是宽度的最大长度可以理解为就是32:
可以发现其他位置的值都是0x0F,点击非雷的位置,可以发现对于点击的位置周围9格进行雷数的确认,再根据雷数设置该位置值,可以看到窗口显示2,对应内存的值为0x42,那么就可以理解为 雷数 | 0x40 = 0x42
对于周围雷数为0的,则显示为0x40:
0x02 消息机制分析
程序中右键格子时会出现旗子,这里会涉及到Win32 API中的重绘函数以及捕获鼠标按键的函数SetCapture
关于内存中雷区的坐标和矩阵中的数据时通过Rect相关API进行关联
发现BeginPaint只在sub_1001BC9进行调用,那么这里肯定就是重绘的地方
关于BeginPaint,主要通过PAINTSTRUCT结构体传递消息对格子进行重绘,该函数调用完成后返回一个设备句柄HDC,之后的sub_1002AC3才是真正进行绘图的地方:
int __stdcall sub_1002AC3(HDC hdc)
{
sub_1002A22(hdc);
sub_1002785(hdc);
sub_10028D9(hdc, dword_1005160);
sub_1002825(hdc);
return sub_10026A7(hdc);
}
0x03 辅助编写
需要实现一键找到所以的雷区并且标上🚩
这里采用MFC实现,整体内容:
可以通过该程序运行winmine.exe,Status中显示该程序的实时状态,FindMine实现一键找到雷的坐标
实现程序运行
在MineCrackDlg.h类中添加public class variable:
// Class Variable
CString appName;
STARTUPINFO si;
PROCESS_INFORMATION pi;
MineCrackDlg.cpp中MineCrack的构造方法添加:
appName.Format(L"D:\\winmine.exe");
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
MineCrackDlg.cpp的按钮回调函数中添加:
void CMineCrackDlg::OnBnClickedRunmine()
{
if (CreateProcess(appName,
NULL,
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi)) {
MessageBox(L"Fuck", L"", 0);
}
}
在按钮事件之前,需要添加一个定时器,用来判断winmine.exe
是否已经运行,这里我设定的是只能让辅助程序去运行winmine.exe
方便后续的操作,所以这里的判断是判断是否已经通过辅助程序运行,创建一个Check函数,通过CreateToolhelp32Snapshot
来创建进程快照,通过遍历的方式去寻找是否有winmine.exe
的字符串存在:
UINT CMineCrackDlg::OnCheckProgramIsRunning() {
UINT dwFlag = 0; // 程序是否运行的标志,0表示未运行,1表示已运行
HANDLE hSnapshort = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshort == INVALID_HANDLE_VALUE)
{
MessageBox(L"CreateToolhelp32Snapshot调用失败", L"Error", 0);
}
PROCESSENTRY32 stcProcessInfo;
stcProcessInfo.dwSize = sizeof(stcProcessInfo);
BOOL bRet = Process32First(hSnapshort, &stcProcessInfo);
while (bRet)
{
if (!wcscmp(stcProcessInfo.szExeFile, reAppName)) {
dwFlag = 1;
break;
}
bRet = Process32Next(hSnapshort, &stcProcessInfo);
}
return dwFlag;
}
设置定时器:
SetTimer(1, 600, NULL);
SetTimer(2, 150, NULL);
WM_TIMER
消息回调函数,定义一个RefreshControl
用来刷新IDC_EXESTATUS
的状态:
void CMineCrackDlg::OnTimer(UINT_PTR nIDEvent) {
switch (nIDEvent) {
case 1:
if (!OnCheckProgramIsRunning()) {
//若程序未运行,则修改ExeStatus
CString cstr;
GetDlgItem(IDC_EXESTATUS)->GetWindowText(cstr);
if (cstr.Compare(L"Not Run") != 0) {
GetDlgItem(IDC_EXESTATUS)->SetWindowText(L"Not Run");
}
}
break;
case 2:
RefreshControl(IDC_EXESTATUS);
break;
}
CDialog::OnTimer(nIDEvent);
}
void CMineCrackDlg::RefreshControl(UINT uCtlID) {
CRect rc;
GetDlgItem(uCtlID)->GetWindowRect(&rc);
ScreenToClient(&rc);
InvalidateRect(&rc);
}
按钮回调函数:
void CMineCrackDlg::OnBnClickedRunmine()
{
if (!OnCheckProgramIsRunning()) {
if (!CreateProcess(abAppName, NULL, NULL, NULL,
FALSE, 0, NULL, NULL, &si, &pi)) {
OutputDebugString(L"Open winmine.exe Failed!");
return;
}
GetDlgItem(IDC_EXESTATUS)->SetWindowText(L"Running!");
}
}
实现一键找出雷区
通过前面的分析了解到,这里的程序没有开启ASLR,并且x32dbg中的地址就是整个PE文件拉伸加载到内存之后的地址,那么就可以直接写死地址,进行取值:
DWORD dwMineNum = 0;
DWORD dwWidth = 0;
DWORD dwHeight = 0;
BYTE mine_area[] = { 0 };
DWORD dwBaseAddr = 0x1005330;
ReadProcessMemory(hProcess, (LPCVOID)dwBaseAddr, &dwMineNum, 4, NULL); // Read Mine Num
ReadProcessMemory(hProcess, (LPCVOID)(dwBaseAddr+4), &dwWidth, 4, NULL); // Read Width
ReadProcessMemory(hProcess, (LPCVOID)(dwBaseAddr+8), &dwHeight, 4, NULL); // Read Height
DWORD dwSize = 0x20 * (dwHeight + 2); //根据x32dbg中看到的内存分配,可以发现宽度分配是按最大的分配的,所以这里直接按最大的宽度进行分配,高度算上两边的墙即可,这里就可以适用所有模式下的内存分配了
PBYTE lpMineArea = (PBYTE)malloc(dwSize);
if (lpMineArea) {
ReadProcessMemory(hProcess, (LPCVOID)dwMineAreaAddr, lpMineArea, dwSize, NULL);
} else {
MessageBox(L"Memory Alloc Failed!", L"Error", 0);
return;
}
BYTE bClear = 0x8E;
for (DWORD i = 0, n = dwMineNum; i < dwSize && n > 0; i++) {
if (lpMineArea[i] == 0x8F) {
WriteProcessMemory(hProcess, (LPVOID)(dwMineAreaAddr + i), &bClear, 1, NULL);
n--;
}
}
将雷区的标志更换成旗子之后显示是不会改变的,需要手动刷新:
// Refresh and Rewrite window
// Need to get windows handle
HWND hMine = ::FindWindow(NULL, L"扫雷");
if (!hMine) {
MessageBox(L"FUCK");
return;
}
RECT rt;
::GetClientRect(hMine, &rt);
::InvalidateRect(hMine, &rt, TRUE);
完成:
未完待续。。。
ps:后面会补上关于Win32程序的完整运行过程、详细的消息机制分析以及一键完成该游戏