1.样本概况
1.1 应用程序信息
----------------------
应用程序名称:扫雷(基于Windows xp系统)
MD5值:16A4FD569A3EB5CEBEB3DA99EF1D17E1
SHA1值:31A1A89BA067EA95F117754818429D6D8E8E59CF
简单功能介绍:
1. 游戏模式:分为初级,中级,高级,自定义模式
2. 自定义:可以设置游戏界面的高度(max=24),宽度(max=30),雷数(max=667)
3. 游戏窗口:显示有雷数,计时,和需要探测点击的雷区
4. 游戏规则:鼠标点击不为雷的区域会显示数字,提示周围的雷数,右键点击可以下标记,当所有不为雷的区域被点击即成功,点击到雷则游戏失败,点击笑脸可以重置当前雷区
1.2 分析环境及工具
系统环境:win7 Professional 32位(15pb实验环境)
工具:
1. DIE(PE文件分析)
2. Cheat Engine(搜索数据)
3. VS2019(编写DLL程序)
4. MFCInject(dll注入工具)
5. DebugView(输出调试信息)
6. OllyDbg(动态调试分析)
7. Spy++(获取窗口信息)
8. Hash(计算MD5,SHA1)
1.3 分析目标
实现扫雷外挂:
1. 鼠标移动悬停在雷区时,在窗口右上角显示雷区坐标及当前区域是否有雷
2. 实现F5一键扫雷功能,模拟点击所有不为雷的区域完成游戏
2.具体分析过程
2.1 DIE查壳
2.1.1 样本基础信息
链接器版本:Microsoft Linker7.0
编写工具:从链接器版本判断为VS2003
类型:32位

2.1.2 样本详细信息
分析样本导入库,为使用SDK编写的程序
2.2 CE搜索数据
2.2.1 获取关键数据
获取数据基本步骤
多次修改获取到三个的高,宽,雷数的数据,且在CE中地址为绿色(静态地址,全局),有多份可能为(代码冗余,实际显示需要)
2.2.2 验证数据有效性
在VS2019中编写MFC DLL(静态编译,不依赖环境),关键调试代码
- //调式获取到的高度,宽度,雷数变量地址
- PDWORD pHeigth = (PDWORD)0x01005338;
- PDWORD pWidth = (PDWORD)0x01005334;
- PDWORD pMineCount = (PDWORD)0x01005330;
- //显示错误
- BOOL CheckResult(BOOL result, WCHAR* str)
- {
- if (!result)
- {
- OutputDebugString(str);
- return FALSE;
- }
- return TRUE;
- }
- //保存窗口句柄
- HWND g_hWnd = 0;
- //原来的回调函数
- WNDPROC OldWinProc = 0;
- //新的窗口回调函数
- LRESULT CALLBACK NewWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
- {
- switch (uMsg)
- {
- //如果是键盘消息
- //一键扫雷思路:遍历扫雷数组中的元素,判断不为雷的区域,模拟鼠标点击
- case WM_KEYDOWN:
- {
- //f5
- if (wParam == VK_F5)
- {
- CString str;
- str.Format(L"高:%d,宽:%d,雷数:%d\n", *pHeigth, *pWidth, *pMineCount);
- OutputDebugString(str);
- }
- }
- }
- //CallWindowProc 不处理的消息让原来的回调函数处理
- return OldWinProc(hWnd, uMsg, wParam, lParam);
- }
- // CMFCmineApp 初始化
- BOOL CMFCmineApp::InitInstance()
- {
- CWinApp::InitInstance();
- //1. 通过查找窗口获取窗口句柄 spy++
- //通过窗口名,窗口类名获取窗口句柄
- g_hWnd = ::FindWindow(L"扫雷", L"扫雷");
- //判断是否找到
- if (CheckResult(g_hWnd != 0, L"未找到扫雷窗口\n") == 0)
- return 0;
- //2. 设置窗口回调函数
- OldWinProc = WNDPROC(SetWindowLong(g_hWnd, GWL_WNDPROC, LONG(NewWndProc)));
- //判断是否修改成功
- if (CheckResult(OldWinProc != 0, L"修改回调函数失败\n") == 0)
- return 0;
- return TRUE;
- }
使用注入工具MFCInject注入编写的dll,并打开ViewDebug查看调试信息,判断数据有效
2.3 OD分析代码逻辑
2.3.1 CE中找到访问数据位置
这时可以跟踪数据,右键选择是谁访问了这个数据(原理:下断点)
这时笑脸重置按钮就发挥作用了,点击,在CE弹出的窗口中查看
2.3.2 OD中分析
在OD中跳转到访问关键数据的地址进行分析
一共三处,其他两处查看与界面相关,不做具体分析,分析地址为0x01002EE4
对代码逻辑进行分析,判断为获取高宽对雷区进行重置的操作,这里可以得到雷区数组的初始地址,雷区总大小
2.3.3 获取雷区信息
根据内存窗口对照扫雷窗口区分雷区属性(最好宽度对应)
雷区基地址:0x01005430
空白:0x0F
雷:0x8F
边界:0x10
点击后的周围雷数:0x40+周围雷数
爆炸的雷:0x8A
2.3.4 调式输出雷区数组
在DLL中编写遍历雷区数组的代码进行调试,关键代码
- //如果是键盘消息
- //一键扫雷思路:遍历扫雷数组中的元素,判断不为雷的区域,模拟鼠标点击
- case WM_KEYDOWN:
- {
- //f5
- if (wParam == VK_F5)
- {
- int count = 0;
- //循环遍历整个雷区数组
- for (int y = 1; y < *pHeigth + 1; y++)
- {
- //保存每一行雷区信息
- CString S = 0;
- //遍历行
- for (int x = 1; x < *pWidth + 1; x++)
- {
- //获取信息
- PBYTE pCode = PBYTE(dwBaseAddr + y * 32 + x);
- //雷计数
- if (*pCode == MINE)
- count++;
- //存放每个位置信息
- CString s;
- s.Format(L"%02x ", *pCode);
- S += s;
- }
- //调试输出
- OutputDebugString(S);
- }
- //输出雷数
- str.Format(L"雷数:%d", count);
- OutputDebugString(str);
- }
- break;
- }
ViewDebug调试信息如下
2.3.5 查找消息回调
SPY++查找
也可以使用OD中W窗口右键点击窗口查看ClassProc
或者在关键API下断点回溯找到
2.3.6 分析回调函数
在OD中找到回调函数的位置,可以初步判断消息的点击消息为WM_LBUTTONDOWN
技巧:Ctrl+A - 右键分析 - 假定参数选择WndProc
设置消息断点,选择需要设置断点的消息(WM_LBUTTONDWON)
回调函数原型为
LRUSULT CALLBACK WinProc(HWND hWnd, UINT uMes, Wparam wPAram, LPARAM lParam){}
参数2为消息类型,参数4为WM_LBUTTONDWON的鼠标坐标
高位y坐标HIWORD(lParam),低位x坐标LOWORD(lParam)
消息回调中初步查看操作了保存鼠标坐标lParam的代码
该处经查看分析为判断点击的代码,不做具体分析
再次找到操作WM_LBUTTONDOWN消息操作坐标的代码,对获取到的鼠标坐标处理
根据处理完的值与点击的雷区界面对比,猜测为此处操作将客户区坐标转换为数组坐标
与扫雷窗口点击的区域对应
2.4 完成功能的实现
2.4.1 鼠标悬停提示
在注入DLL中的消息回调响应鼠标移动,根据分析的转换算法将鼠标坐标转换成数组坐标,判断当前雷区数组没区域是否为雷,在标题栏提示
主要代码如下
- //鼠标移动显示信息
- case WM_MOUSEMOVE:
- {
- CString sCaption;
- //x,y坐标
- DWORD x = LOWORD(lParam);
- DWORD y = HIWORD(lParam);
- //转换为数组坐标
- x = (x + 4) >> 4;
- y = (y - 0x27) >> 4;
- //在雷区范围内
- if (0 < x && x <= *pWidth && 0 < y && y <= *pHeigth)
- {
- //如果判断为雷
- if (*(PBYTE(dwBaseAddr + y * 32 + x)) == MINE)
- sCaption.Format(L"扫雷 (x:%d,y:%d) 有雷", x, y);
- else
- sCaption.Format(L"扫雷 (x:%d,y:%d) 无雷", x, y);
- SetWindowText(g_hWnd, sCaption);
- }
- }
2.4.2 F5一键扫雷
在注入DLL中的消息回调响应F5按键消息,根据分析的转换算法将雷区数组坐标转换成客户区坐标,判断当前区域是否为雷,如果不是则发送消息模拟点击完成游戏
主要代码如下,这里需要注意要同时发送鼠标左键按下与抬起的消息,窗口才能完成响应整个模拟点击的操作
- //如果不是雷 F5一键扫雷功能
- if (*pCode != MINE)
- {
- //首先将x,y数组坐标转换为客户区坐标
- int xPos, yPos;
- yPos = (y << 4) + 0x27;
- xPos = (x << 4) - 4;
- //前面为低字节
- DWORD lparam = MAKELPARAM(xPos, yPos);
- //给扫雷发送消息 模拟点击
- SendMessage(g_hWnd, WM_LBUTTONDOWN, 0, lparam);
- SendMessage(g_hWnd, WM_LBUTTONUP, 0, lparam);
- }
2.5 实现效果
2.5.1 效果截图
3.总结
很久之前玩扫雷,就想过一键扫雷的情景,作为第一个逆向的程序,还是从中获取到了很多的。首先第一次系统性的分析一个文件,从链接器信息,编写语言到使用的库,关键的逻辑判断及调式到实现,整个流程下来算是比较完整的,感觉逆向测试的过程还是比较需要技术的。
对逆向分析中使用的工具的了解和掌握也是比较重要的,不同的工具可以完成特定的分析功能,熟练的使用可以节约大量的时间,要让他们成为分析时的左膀右臂,还是就是对回调函数的理解及分析,基于消息驱动使得消息处理尤为重要,是用户与程序交互的重要枢纽,很多逻辑判断也是在其中实现的。
作为第一次分析是没有什么思路的,经过视频的讲解还是在脑子里有了个大概的过程,这次的扫雷是通过使用CE从数据入手,找到使用数据的地方,从而在OD动态分析逻辑,找到关键位置,再基于消息的响应处理找到是实现坐标的转换算法,期间使用了注入和Debugview的功能完成了动态调试,从而实现了整个功能(实际还是利用原程序完成自己功能的实现,总结下就是用最小的开销骗过程序来实现自己想要的效果)。
整体感觉还是需要多练习,基础要扎实,这样才可以更快的分析(基于知识和经验的猜测)到关键点做判断。