1.样本概况
1.1 应用程序信息
应用程序名称:扫雷
MD5值:16A4FD569A3EB5CEBEB3DA99EF1D17E1
SHA1值:31A1A89BA067EA95F117754818429D6D8E8E59CF
1.2 分析环境及工具
系统环境:win7 32位
工具:Ollydbg、Cheat Engine、Spy++、PEiD、Vistual Studio
1.3 分析目标
1、鼠标悬停在棋盘上可以查出雷所在的位置
2、一键扫雷
要想实现功能,需要分析以下数据及代码:
鼠标位置
扫雷数组的高度、宽度、雷数、基地址
鼠标位置转成扫雷数组下标的代码
扫雷数组下标转成鼠标位置的代码(便于发消息)
需要使用的技术:
1、MFC DLL(新建-->MFC动态链接库-->DLL类型:静态链接)
使用MFC DLL,不需要自己写DLLMain的case,直接写在Initstance函数中即可,且调试时使用Cstring比较方便
2、SetWindowLong
修改窗口回调函数,在自己的窗口回调函数中处理快捷键响应。
3、CallWindowProc
调用指定窗口回调函数
边分析边测试:
先找到必要的数据:扫雷数组的相关数据,宽度、高度、雷数等
测试数据的可靠性:写代码测试
找到扫雷数组初始化的代码:将其转为自己的代码,写在DLL中,进行测试
寻找屏幕坐标转为扫雷数组下标的代码:找到之后,写在DLL中,测试
整合代码,完成功能。
2.具体分析过程
2.1 分析过程
2.1.1 使用PEID找出程序的版本信息
将程序拖入PEiD,发现其链接器版本是7.0,对应的编译器是VC2003
查看其输入表,发现有msvctr.dll,是VC运行的库,应该是SDK程序,再看其大小,只有100多K,如果是MFC静态编译的话会比较大,至少1M,所以判断其是SDK程序
2.1.2 使用Cheat Engine找到必要数据
首先使用Cheat Engine,找到必要的数据:宽度、高度、雷数
找到几组数据,可以写代码进行验证。代码生成后,将DLL注入到exe中,进行操作后,DebugView会有以下结果:
根据结果,可以确定宽度的地址是:0x1005334,高度的地址:0x1005338,雷数地址是:0x1005330
2.1.3 找到扫雷数组初始化的代码
根据已经得到的宽度、高度数据,选中地址右键,查看是哪些访问了这个地址,点击笑脸刷新之后,会记录一些指令:
因为宽度、高度等这些数据,会被用到,所以我们查看用到这些数据的操作:
找到0x01002EE4这个地址,使用OD打开扫雷,Ctrl+G,输入该地址进行查看,并对这一段代码进行分析。该代码块,主要有几个循环的操作,先是对雷区进行了初始化,初始化为0F:
然后将雷区的四周边界都填充为10(只是边界,不在游戏的点击范围内):
同时也可以知道雷区的基地址为:0x01005340,游戏界面可以点击到的左上角地址为:0x01005361
观察发现,虽然高度是10,但是每行中间都隔一行。运行之后,再点击雷区,对比OD中数据的变化,可以发现雷区中元素的标识。雷区中的空白,在数据中对应是0,雷对应8F,1、2、3、4分别对应41、42、43、44。
我们可以写出代码,同样的进行注入,使用DebugView,看结果进行测试验证:
由结果可以看出游戏中实际雷的位置和DebugView中的位置相对应,说明分析正确。
2.1.4 找到屏幕坐标转为扫雷数组坐标的代码
使用Spy++,查看回调函数的地址:
在OD中搜索该地址,右键->分析,假定参数:
然后右键->断点,在WinProc上消息断点
选择设置断点的消息:WM_LBUTTONDOWN
断点设置好之后,鼠标左键点击游戏雷区,就会中断。我们在OD的堆栈区进行查看:
可以看到第四个参数是鼠标的X、Y坐标,4D是77,32是50,所以高十六位是Y,低十六位是X。再看ARG.4是EBP+0x14,所以ARG.4是第四个参数,X、Y坐标。
下面开始F8单步走,看哪些操作对ECX和ARG.4进行了访问。这里用到了ARG.4,进入函数,查看:
这些操作和界面有关,和数组无关,应该是没有什么用。跳过,继续往下跟踪:
发现关键位置,该段代码,将ARG.4赋值给EAX(004D0032),EAX右移0x10位,即:去掉低16位,保留高16位(y),然后y-0x27,再右移四位得到扫雷数组的y坐标,将EAX压入栈。再得到低16位(x),将x+4再右移四位得到扫雷数组的x坐标。点击的是:第二行的第三列,屏幕坐标是:X=50,Y=77,转化后的扫雷数组坐标是:x=3,y=2。
实现一键扫雷的话,需要模拟鼠标点击的操作。按下一键扫雷的时候,开始遍历雷区数组,如果不是雷,就将该坐标的x,y,转化为屏幕的xPos,yPos,然后发送消息,自动排雷。
2.1.5 功能演示
功能一:鼠标移动,提示对应位置是否有雷
功能二:F5一键扫雷
2.2 测试代码(如有代码验证贴出关键代码)
// 唯一的 CMFCLeiPlugApp 对象
CMFCLeiPlugApp theApp;
HWND g_Wnd;
WNDPROC g_OldProc;
//宽度、高度、雷数保存地址
PDWORD g_pWidth = (PDWORD)0x1005334;
PDWORD g_pHeight = (PDWORD)0x1005338;
PDWORD g_pCount = (PDWORD)0x1005330;
//雷区的基地址
PBYTE g_pBase = (PBYTE)0x01005340;
//雷区中元素的标识:空白:0x40、雷:0x8F、1/2/3/4: 41/42/43/44
#define LEI 0x8F
//回调函数
LRESULT CALLBACK WindowProc(_In_ HWND hWnd, _In_ UINT Msg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
//一键扫雷。思路:需要遍历扫雷数组,判断是否是雷,如果不是雷,模拟点击的操作。
if (Msg == WM_KEYDOWN && wParam == VK_F5)
{
//? 验证:宽度、高度、雷数
OutputDebugString(L"F5");
int nHeight = *g_pHeight;
int nWidth = *g_pWidth;
int nCount = *g_pCount;
CString strString;
strString.Format(L"宽度:%d,高度:%d,雷数:%d", nWidth, nHeight, nCount);
OutputDebugString(strString.GetBuffer());
//? 对雷区进行遍历(不包括四周边界)
int nLeiCount = 0;
for (int y = 1; y < nHeight + 1; y++)
{
CString strLine;
for (int x = 1; x < nWidth + 1; x++)
{
BYTE Code = *(PBYTE)((DWORD)g_pBase + y * 32 + x);
if (Code == LEI)
{
nLeiCount++;
}
else
{
int xPos;
int yPos;
// x = (x + 4) >> 4;
xPos = (x << 4) - 4;
// y = (y - 0x27) >> 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 ", Code);
strLine += strCode;
}
OutputDebugString(strLine.GetBuffer());
}
CString strCount;
strCount.Format(L"找到的雷数是:%d ", nLeiCount);
OutputDebugString(strCount.GetBuffer());
}
//鼠标移动:移动鼠标时,判断鼠标对应位置是否是雷,如果是雷,在标题中进行提示。
else if (Msg == WM_MOUSEMOVE)
{
int x = 0;
int y = 0;
x = LOWORD(lParam);
y = HIWORD(lParam);
//将屏幕坐标转为雷区数组坐标
x = (x + 4) >> 4;
y = (y - 0x27) >> 4;
BYTE Code = *(PBYTE)((DWORD)g_pBase + y * 32 + x);
if (Code == LEI)
{
SetWindowText(hWnd, L"提示:此处有雷");
}
else
{
SetWindowText(hWnd, L"扫雷");
}
}
return CallWindowProc(g_OldProc, hWnd, Msg, wParam, lParam);
}
// CMFCLeiPlugApp 初始化
BOOL CMFCLeiPlugApp::InitInstance()
{
CWinApp::InitInstance();
//1、查找窗口,获取窗口句柄
g_Wnd = ::FindWindow(L"扫雷", L"扫雷");
if (NULL == g_Wnd)
{
OutputDebugString(L"无法找到扫雷窗口");
return FALSE;
}
//2、设置窗口回调函数
g_OldProc = (WNDPROC)SetWindowLong(g_Wnd, GWL_WNDPROC, (LONG)WindowProc);
//CString temp;
//temp.Format(L"窗口回调函数地址:%p", g_OldProc);
//OutputDebugString(temp.GetBuffer());
if (NULL==g_OldProc)
{
OutputDebugString(L"设置窗口回调函数失败");
return FALSE;
}
return TRUE;
}
3.总结
虽然本次分析扫雷游戏,并写出辅助插件是一个比较小的逆向项目,但整体来说还是比较复杂的。有的地方需要我们一步步进行跟踪分析,有的需要我们写出对应功能的代码进行测试,还需要运用一些工具以及各种技术进行分析。通过本次项目的分析,感觉逆向是一个复杂的工作,要想做好逆向分析,需要学好多方面的技术知识,并不断地练习。