1.样本概况
1.1 应用程序信息
----------------------
应用程序名称:QQ连连看
MD5值:05C759844703E7D4822DDE966284ABAB
SHA1值:48C1D825C230196BD962AC294FA1990B6D41AD7F
简单功能介绍:
1. 游戏模式:点击练习开始
2. 规则:选中两个相同的图案,直接的连线转弯不超过2次就可以消除
3. 道具:指南针可以自动消去两个可以消除的相同图案,初始有三个
重列可以让图案重新排列,初始有三个,游戏过程中会生成其他道具
4. 有计时进度条,剩余方块计数,消除点击增加时间,完成进入下一关
1.2 分析环境及工具
系统环境:win7 Professional 32位(15pb实验环境)
工具:
0. PCHunter32(分析进程,消息钩子)
1. 010Editor(编辑器)
2. LordPe(PE位置计算)
3. DIE(PE文件分析)
4. Cheat Engine(搜索数据)
5. VS2019(编写DLL程序)
6. MFCInject(dll注入工具)
7. DebugView(输出调试信息)
8. OllyDbg(动态调试分析)
9. Spy++(获取窗口信息)
10. Hash(计算MD5,SHA1)
1.3 分析目标
实现的功能:
1. 分析算法完成连接,模拟鼠标自动点击完成消除
2. 调用道具,完成每次点击自动消除
3. 实现单消,一键全局消除功能
2.具体分析过程
2.1 分析行为
2.1.1 PCHunter分析进程
运行连连看程序qqllk.exe,取消广告选择,点击开始连连看游戏,通过PChunter查看进程,创建了子进程qqllk.ocx,qqllk.ocx创建了真正的QQ连连看进程信息为 kyodai.exe
点击继续,发现kyodai.exe执行运行了真正的QQ连连看游戏,qqllk.ocx退出
2.1.2 OD中分析行为
单独点击kyodai.exe发现并不能运行,但是通过qqllk.ocx可以
初步猜测
直接创建进程并挂起,远程线程注入(远程写入修改),恢复主线程,被动态修改过内存的exe就可以运行了
步骤
1. 创建进程CreateProcess(DWORD dwCreationFlags如果设置为CREATE_SUSPENDED,则以挂起的方式创建进程)(挂起线程 SuspendThread)
2. 远程写入内存WriteProcessMemory
3. 恢复线程 ResumeThread
将程序qqllk.exe附加到OD中在创建进程的API下断点
1. DialogBoxA/W
2. CreateProcessA/W
3. WinExec
4. ShellExecuteA/W
5. CreateWindowA/W
在调用CreateProcessW函数处中断,查看堆栈信息,发现确实创建了子进程
将程序qqllk.ocx附加到OD中并在创建进程的API下断点,点击继续
程序在调用CreateProcessA函数处中断,查看堆栈信息,分析确实是以挂起的方式创建了
创建的进程信息保持在pProcessInfo的结构体中
进程kyodai.exe
创建的进程信息保持在pProcessInfo的结构体中
- typedef struct _PROCESS_INFORMATION {
- HANDLE hProcess;
- HANDLE hThread;
- DWORD dwProcessId;
- DWORD dwThreadId;
- } PROCESS_INFORMATION;
结构体中保存的进程句柄,线程句柄,进程ID,线程ID与得到的信息对应
既然可以运行,应该是在挂起的状态下动态修改了内存信息,在WriteProcessMemory函数下断点继续之前,断下,查看堆栈获取信息
写入进程的句柄0xF4,写入进程的首地址0x43817A,写入的缓冲区地址0x00484E70,写入大小0x01
跳转到写入数据的缓冲区地址0x484E70,查看写入数据为0x00
在OD中附加进程kyodai.exe,并找到被改写的位置0x43817A为0x01
这里可以判断将创建的进程挂起后的操作只改变了一个字节的数据
在OD继续运行程序qqllk.ocx,修改完内存后会唤醒进程的主线程,kyodai.exe正常执行,弹出游戏窗口
随后的操作猜测应该是调用ExitProcess退出自己
2.2 获取可执行文件
2.2.1 LordPe获取FOA
使用LordPe根据获取到的VA计算在文件中的便宜FOA为0x3817A
2.2.2 010Editor修改文件
创建kyodai.exe的副本并在010Editor中修改数据,获取到可直接运行的QQ连连看文件
2.2.3 在内存中dump
还可以直接在内存中将程序dump下来,在qqllk.ocx写入内存,唤醒线程之前,将创建的进程附加到OD中,使用OllyDump插件,生成的文件就可以直接运行了
2.3 分析样本
2.3.1 DIE查壳
链接器版本:Microsoft Linker6.0
编写工具:从链接器版本判断为VC 6.0编写的程序
类型:32位
使用的库:MFC(4.2)
初步分析为VC6.0编写的使用MFC库的32位程序
导入表信息
2.4 CE搜索数据
2.4.1 查找页面有效数据
通过游戏界面初步分析,尝试找到设置块数的地方,应该涉及到游戏数组的访问
经过查找,得到一个变量地址与之对应
设置找到是哪个地方修改了该地址中的数据
这里猜测每次消除时会将方块数量减2,重新开始时会将数量重置,做出这两个操作后
2.4.2 OD中分析该地址
在OD中附加程序,跳转到0x00423FFA
方块数量应该是连连看对象的一个成员变量
跳转到该指针查看数据
第一个数据跳转过去应该为一个虚函数表
第二个数据,可能为连连看数组信息的结构体首地址
2.4.3 验证数组地址有效性
多次重置页面测试
2.5 OD中分析逻辑
2.5.1 找到修改数组的操作
在该地址中断0x00428FFF
跳转到该地址分析,找到函数入口点初步判断为调用随机函数rand获取随机数对数组进行设置操作,具体下面再做分析
栈回溯找到调用该函数的地方
2.5.2 另一个思路rand
既然随机重置连连看图案,应该会使用到随机函数rand,可以在该API下断点,点击练习找到调用点
2.5.3 分析代码
下断点分析地址0x41CB20,在call前后下断点观察调用前后数组情况
调用前,只有0x00,0x01两种数据
调用之后,为0x01的部分产生了不同数据,再次证实为对数组的随机设置操作
2.5.4 找到指南针call
同样在数组位置上下内存访问断点,中断下后查看堆栈调用并在调用位置上依次下断点,找到调用指南针的位置(观察参数和调用行为),需要重新运行起来再点击指南针
猜测调用得位置
继续运行,断在另一个断点处,应该是用于获取数据
根据返回值和界面的对比得到信息,该函数的作用是获取可以消除的两个图案的数组坐标
2.5.5 分析指南针调用
分析如何才调用到指南针,需要找到this指针的值是如何获取的,栈回溯向上查找了两层还是没找到
使用CE搜索ESI的值,查找是哪个地址保存了,找到三个绿色的基址
在OD中右键查找所有常量搜索这三个地址,如果把此地址当成常量,就代表是一个全局的地址给它赋值操作,应该是个有效值,找到0x45DEBC
2.6 注入DLL调式
2.6.1 编写DLL
因为连连看程序为MFC程序,本身会自带消息钩子,所有不能使用扫雷的方式
使用Spy++获取窗口名
代码
提示按钮发送自定义消息MY_MSG1调用指南针
- case MY_MSG1://提示
- {
- __asm
- {
- ;标记
- mov eax,eax
- mov eax,eax
- mov ECX, ECX_this
- mov ECX, [ECX]
- mov EAX, DWORD PTR DS : [ECX + 0x494] ; 类对象中的对象成员的虚函数表
- LEA ECX, DWORD PTR DS : [ECX + 0x494] ; 对象成员
- PUSH 0xF0
- PUSH 0
- PUSH 0
- CALL DWORD PTR DS : [EAX + 0x28] ; 指南针
- }
- break;
- }
- //线程回调
- unsigned __stdcall ThreadProc()
- {
- CMyDialog * pDlg = new CMyDialog{};
- pDlg->DoModal();
- delete pDlg;
- return 0;
- }
InitInstance
- BOOL CMFCqqllkApp::InitInstance()
- {
- CWinApp::InitInstance();
- //1. 通过查找窗口获取窗口句柄 spy++
- m_hWnd = ::FindWindow(NULL, L"QQ连连看");
- //判断是否找到
- if (CheckResult(m_hWnd != 0, L"未找到连连看窗口\n") == 0)
- return 0;
- //2. 设置窗口回调函数
- OldWinProc = WNDPROC(SetWindowLong(m_hWnd, GWL_WNDPROC, LONG(NewWndProc)));
- //判断是否修改成功
- if (CheckResult(OldWinProc != 0, L"修改回调函数失败\n") == 0)
- return 0;
- //创建线程弹出对话框
- uintptr_t hThread = _beginthreadex(0, 0, (_beginthreadex_proc_type)ThreadProc, 0, 0, 0);
- //判断线程是否创建成功
- if (CheckResult(hThread != 0, L"线程创建失败\n") == 0)
- return 0;
- return TRUE;
- }
2.6.2 注入调试
使用注入工具注入编写好的DLL,测试,点击后触发指南针
2.6.3 获取坐标
找到关键函数位置,获取可以消除的两个的图案的数组坐标
代码实现
- case MY_MSG2://单消
- {
- //用于接收坐标
- POINT p1 = {};
- POINT p2 = {};
- __asm
- {
- mov ECX, [ECX_this]
- mov ECX, [ECX]
- LEA ECX, DWORD PTR DS : [ECX + 0x494]
- MOV ECX, DWORD PTR DS : [ECX + 0x19F0]
- LEA EAX, p1.x
- PUSH EAX
- LEA EAX, p2.x
- PUSH EAX
- mov EAX, 0x0042923F
- CALL EAX; 获取可以消除的图案坐标
- ; FF15 0x0042923F
- }
- CString str;
- str.Format(L"p1:%d,%d p2:%d,%d", p1.x, p1.y, p2.x, p2.y);
- OutputDebugString(str);
- return DefWindowProc(hWnd, uMsg, wParam, lParam);
- }
调试结果
2.6.4 调用消除call
在即将消除的图案上下内存写入断点,连线消除消除
在堆栈中下依次下断点断点
依次分析,排除,推测使用到了之前获取到的两个数组坐标,并作为参数传入
这个调用使用了数组坐标,且查看内存调用结束图案消除这两个区域值置为了0
在dll中构造调用消除call的代码
- //调用消除call
- __asm
- {
- push 0x4 //参数1
- mov ECX, [ECX_this]
- mov ECX, [ECX] //this指针 0x12A1F4
- LEA EAX, DWORD PTR DS : [ECX + 0x494] //0x12C078
- MOV EAX, DWORD PTR DS : [EAX + 0x19F0] //0x0063D748
- ADD EAX, 0x30
- push EAX //参数2 变量地址 坐标数组地址
- LEA EAX, p1.x //参数3 点1地址
- push EAX
- LEA EAX, p2.x //参数4 点2地址
- push EAX
- lea EAX,[ECX+0x195C] //数组地址=0x0012BB50=[0x0063D748+0x4]
- push EAX //参数5 数组地址
- push 0x0 //参数6 0
- mov eax, 0x0041C68E //调用消除
- call eax
- }
注入dll测试,单消的调用完成
2.6.5 其他道具
在游戏过程中,会发现道具栏会有其他道具的生成,如炸弹
用找到指南针call的方式下内存断点,找到调用炸弹道具的位置,发现中断在指南针call的位置,原来这个地址是调用所有道具的地方,通过查看参数,发现有一个不同,指南针为0xF0,炸弹为0xF4
在dll编程代码注入测试,发现炸弹的功能和自己写的单消功能是一样的,可以测试不同道具调用道具call对应的参入参数,来实现对其他道具的调用
2.6.6 实现一键消除
实现就是在一个循环中调用炸弹道具来完成,直到方块数量为0停止循环
- while (*((PDWORD)0x0012F51C))
- {
- __asm
- {
- ; 标记
- mov eax, eax
- mov eax, eax
- mov ECX, ECX_this
- mov ECX, [ECX]
- mov EAX, DWORD PTR DS : [ECX + 0x494] ; 类对象中的对象成员的虚函数表
- LEA ECX, DWORD PTR DS : [ECX + 0x494] ; 对象成员
- PUSH 0xF4
- PUSH 0
- PUSH 0
- CALL DWORD PTR DS : [EAX + 0x28] ;
- }
- }
测试一键消除
2.7 使用转换坐标的方式
2.7.1 找到转换坐标的函数
在需要点击的图案上下内存断点,点击这个图案
通过栈回溯的方法在栈中观察数据,找到相似鼠标坐标的位置,单步运行,找到传入x,y坐标的函数调用点
进入转换数组坐标的函数单步执行,找到传入鼠标坐标返回数组坐标的函数调用
进入函数分析转换
分析完写入dll就可以和扫雷一样模拟点击对游戏进行操作
3.总结
这次的逆向连连看程序是用MFC编写的,与SDK不同的是,加入了面向对象的思想,所以在分析时,要把部分重点放在函数调用时的ECX也就是this指针上,对象是对数据和与之相关操作的封装,获取到this指针可以从中分析到很多信息,this指针的第一个四字节数据如果是一个全局地址可以跳转过去以地址的方式查看内存,很容易辨别是否为一个虚函数指针。
对象的数据可以大胆的去猜测,与分析的窗口界面进行对比,找到可以相互对应的地方,可以让分析更加容易,要加深对函数的理解,通过入参和返回值,对参数的操作来猜测函数的功能与逻辑,判断是传入参数(值),传出参数(缓冲区),或者传出传入参数(指针)可以快速筛除非目标函数,提高分析效率,这次的功能实现都是找到程序本身的调用,通过代码来实现对这些函数的模拟调用,总的来说比扫雷的难度提高了不少,学到了使用内存访问断点的方式进行栈回溯分析出关键调用。
同时和扫雷一样找出了对坐标转换的处理函数,与SDK不同的时,这里不能使用对回调函数下消息断点的方式来分析,而还是使用了内存访问断点,观察点击图案时会产生一个蓝色的点击显示,可以猜出,应该是做了对鼠标转化成数组坐标的操作再进行对图案的显示,从而中断下一步步找到关键位置
自己写dll注入时出了不少bug,希望能更细心,逆向程序是个需要耐心的过程。
向15PB信息安全研究院表达真挚谢意。