查看原文
其他

**游戏逆向分析笔记

21Gun5 看雪学院 2021-03-07
本文为看雪论坛精华文章
看雪论坛作者ID:21Gun5


目录

0x01 样本概况

         1.1-分析环境及工具

         1.2-分析目标

0x02 具体分析过程

         2.1-去广告

         2.2-CE控制游戏以便测试

         2.3-实现无限指南针

                 2.3.1-找数组

                 2.3.2-找call指南针的地方

                 2.3.3-找基址

                 2.3.4-编写注入工具exe及外挂dll

         2.4-实现单次消除

         2.5-实现秒杀

         2.6-另一个思路来单消/秒杀

         2.7-最终效果


0x01 样本概况


>>>>

1.1 分析环境及工具


系统环境:Windows10-64位、Windows7-32位<!--more-->

工具:010Editor、OllyDebug、DbgView、Cheat Engine、PCHunter32、VS 2017


>>>>

1.2 分析目标

  • 找到原程序exe、去广告

  • 实现连连看外挂:无限指南针、单次消除、秒杀



0x02具体分析过程


>>>>

2.1 去广告


1. kyodai双击不可运行。

2. qqllk可运行,打开就是一个广告,点击“开始游戏”进入下一窗口。

3. 再点击“OK我知道了”再到下个窗口,时刻关注进程列表(火绒剑),发现
此时创建了一个新进程qqllk.ocx,虽然看起来后缀不是exe,但是既然出现在了进程列表,其本质就是一个exe。

4. 点击“继续”,kyodai进程创建,qqllk.ocx进程关闭,但qqllk.exe进程依然在运行。

5. 由此可判定,kyodai是真正的游戏程序,而qqllk是在其基础上,打包了许多广告的程序,去广告,也就是将k从q中分离出来。

6. OD附加那个ocx(而非qqllk.exe),因为是ocx创建出kyodai的

7. 分析:q创建k进程,必然用到创建进程API,ctrl+g搜索CreateProcessA/W并下断。

8. 运行,停在762E2082 上,看一下堆栈中的参数,CreationFlags = CREATE_SUSPENDED,创建进程后,是暂停状态。

9. 再开一个OD来附加k,用来测试。

10. 分析:直接运行k失败,但是通过q就能使其运行,推测q创建进程后,一定是修改了q进程中某些东西,才使其可以运行的。

11. 因此,搜索WriteProcessMemory-下断-运行,发现断在759246C7 ,观察堆栈中参数,得出:往目标进程43817a处、写入一个字节、00。

12. 修改完之后,再resumeThread来恢复线程,因此流程就是:创建进程-修改进程-恢复线程。

13. 根据修改进程时的参数,即往目标进程43817a处、写入一个字节、00,手动修改k程序即可达到目的。

14. 打开LordPE,拖入Kyodai,位置计算,43817a-40000=3817a得到RVA(OD附加了那个新创建的进程,E-看到其模块基址为400000),将RVA填入得到文件偏移,也是43817a。

15. 010editor打开K程序,ctrl+g跳到文件偏移出,手动将值修改为0(注:远程序只读不可写入,可file-save a copy复制一份再修改。

16. 修改后,得到新的K程序,命名为Kyodai-noAd,双击可直接运行,至此,提取成功,也就去除了广告。


>>>>

2.2 CE控制游戏以便测试

  • 0012AC5E:指南针数量不变

  • 0012A748:时间不变

  • 0012AC6E:重列道具不变


>>>>

2.3 实现无限指南针


找数组


1. ctrl+g:找rand函数,得7623C070 (rand实现处。

2. 栈回溯:得0041CAF8 (rand调用处。

3. rand调用后,在0041CB10 ,有一个memcpy,dst为0012BB50,数据窗口跟随。

4. 运行完memcpy后,内存窗口有明显变化,且较有规律,推测为连连看数组。

5. 不断点击“练习”,并对比内存窗口,空白处为00,不断测试,证实上述猜想。


找call指南针的地方


1. 数组处下内存访问断点(硬件断点无效),点击“指南针”,断在此处。

2. K-调用堆栈处,一层一层下断点,逐个测试(先随便下5、6个断点。

3. 一次测试,多次循环断下的位置先排除,效果应该是一点“指南针”就断,满足此条件的有:0040CACA -0041AF11 -0041DE5C -0041E76C ,故最外层的为0040CACA(当前测试的几个断点中最外层),最里层的为 0041E76C。

4. 由内而外,依次来看每个call的具体作用。

5. 最里层的0041E76C:两个参数,将局部变量赋值为eax再push(dword ptr,4个字节),分别为:00129D8C 、00129D94 ,将两个地址数据窗口跟随(分别M1、M2便于观察),call完成之后,返回值通过push进去的地址体现(指针间接修改),94前4个字节为20,8c前4字节为49,对比游戏界面,为两个坐标,也就是用了指南针用于提示的两个位置,故此处call,获得待连接的两个位置。

6. 再往外一层0041DE5C:虽连续三个push,但都是些没意义的参数,故推测,其仅仅是一个“使用道具”的函数, (因为参数无意义,故推测其并没有完成什么实质性功能,其调用仅仅为了进一步调用那些有用的函数),call了0041E691,推测这就是“指南针道具”的函数(因为有多个工具,call后面跟具体的哪个道具。

找基址


1. 由上,只要主动调用0041DE5C,倒数第二的那个函数即可。

2. 要调用就要手动传入正确的参数,以假装它是在正常的程序内部调用的。

3. 参数除了通过push进去那三个(栈传递的),还可能是通过ecx寄存器传递的(thiscall)即那个lea ecx,[esi+xx],事实上,也就ecx那个参数看起来靠谱,像一个地址,但是简单将ecx作为参数是不可的,其地址esi+xx=12xx,一看就是栈空间的局部变量,具有不确定性,可能每次运行都不一样,因此,追本溯源,网上找ecx究竟是哪来的(要找到一个基地址。

4. 经测试,一直向上找是找不尽的,故换思路。

5. esi是0012A1F4,CE中搜索,发现有几个绿色的地址(即基址),有了基址就能在程序外手动的调用函数(不应该用变值作为参数),几个基址:45DCF8、45DEBC(用这个)、47FDEO、777FEDE8(7开头,暂不考虑。

编写注入工具exe及外挂dll


1. 有了基址,就可以构造不变化的参数,就可以手动调用程序内部的参数。

2. 编写注入程序:Injector.exe

3. 编写被注入dll:MFCGamePlugin.dll(win10虚拟机。

4. Injector.cpp源码:
//Injector.exe#include <iostream>#include <windows.h>using namespace std;//要加载的dll路径// 最好改为相对路径(相对于连连看程序的// WCHAR szDllPath[] = L"C:\\Users\\15pb-win7\\Desktop\\MFCGamePlugin.dll";WCHAR szDllPath[] = L"../../MFCGamePlugin.dll";
int main(){ //1.要注入,需要dll文件 //2.找到要注入的进程PID DWORD dwPid=0; //HWND hwnd = FindWindow(NULL, L"new 1 - Notepad++"); //GetWindowThreadProcessId(hwnd, &dwPid); printf("please input PID>> "); scanf_s("%d", &dwPid); //3.打开进程,获取进程句柄 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); //4.在目标进程中申请空间 LPVOID pBuff = VirtualAllocEx( hProcess, 0, sizeof(szDllPath), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); //5.将路径写入到目标进程中 DWORD dwSize; WriteProcessMemory( hProcess, pBuff, //在指申请的地址上 szDllPath, //写入的内容 sizeof(szDllPath),//写入大小 &dwSize ); //6.使用关键函数加载目标dll // 利用远程创建线程函数,实现目标进程加载dll // 远程线程执行函数直接指向LoadLibaray函数,同时参数指向dll路径,完美实现加载dll HANDLE hThread = CreateRemoteThread( hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibrary, //线程执行地址指向LoadLibrary pBuff, //线程的附加参数dll路径 NULL, NULL ); //7 释放句柄 CloseHandle(hProcess); CloseHandle(hThread);
}

5. MFCGamePlugin.cpp关键代码:
if (Msg == WM_DATA1){ OutputDebugString(L"无限指南针");
//0041DE4D | . 8B86 9404000 > MOV EAX, DWORD PTR DS : [ESI + 0x494] //0041DE53 | . 8D8E 9404000 > LEA ECX, DWORD PTR DS : [ESI + 0x494] //0041DE59 | . 52 PUSH EDX //0041DE5A | . 53 PUSH EBX //0041DE5B | . 53 PUSH EBX //0041DE5C | .FF50 28 CALL DWORD PTR DS : [EAX + 0x28]; 使用指南针道具 _asm { mov ecx, 0x45DEBC mov ecx, [ecx] LEA ECX, DWORD PTR DS : [ecx + 0x494] PUSH 0xF0// 若炸弹,则F4 PUSH 0 PUSH 0 mov eax, 0x0041E691 call eax } return DefWindowProc(hWnd, Msg, wParam, lParam);}



>>>>

2.4 实现单次消除

要想消除,就需要获得可以消除的两个点,由上知,指南针call中最里面的那个0041E76C,就可以提供两个点,且可以消除。

在手动找两个可消除的两个点,消除过程中,必然会访问连连看数组(将相应位置置为0),在数组处下内存写入断点,会断下来0040FF5F ,然后删除内存断点,F2下断,通过栈回溯,不断找“消除”时会调用的call。

(注意,写入哪个会在哪个断下,以每个字节为单位,并非写入数组中任意一个位置,都会断下,所以,根据点击的那两个将要消除的点,在数组内存所在处相应位置下断)

从外到里,找到如下call:0041B4B7 -0041AB34 -0041C6C3,

就像指南针那个一样,必然不止一个,由内而外/由外而内依次检查每一个call,看其做了什么工作,检查其参数都是干嘛的(看push了谁,代码或堆栈中看),看看哪个传入了点的坐标(要想消除,就需要这两个点)。

0041B4B7:内部retn 0x1c=28=7个参数,从栈顶依次找7个参数,就参数2靠谱点,是一个地址(其他都是数,一看就不是点坐标),数据窗口中跟随,确实是两个点坐标,再看游戏窗口中点击的那两个待消除的点,确实也符合,但是是一个地址中保存了两个点,而非理想中的一个参数对应一个点,先记下,继续往后找(尽量往里找,找更满足条件的)。

0041AB34 :enter进入call的内部,在最后retn 0x18=24=6个参数,参数1-0、参数2-连连看数组地址、参数3-点1坐标、参数4-点2坐标、参数5-同上一样,有那两个点的坐标,暂记做坐标点数组、参数6-数值2,这么一看,这个call相当靠谱。

最里层那个call4个参数,不太靠谱,故从里往外,倒数第二个即为目标call,0041AB34,通过其来构造汇编代码,实现程序外调用。

难点及重点:如何构造call这个函数相应的6个参数?通过程序中汇编代码来构造,在call之前,第一个push处下断,看每一个参数的值都是怎么来的(追本溯源)。

参数2/5比较难找:参数2=12BB50,参数5=1A5DE18,看这俩值怎么构造,是这么x+y=的。

注意一点:call单次消除用到获取两点坐标功能,而后者又是在call指南针功能中调用的,就像注释中所说的。

lea ecx, DWORD PTR DS : [ecx + 0x494]// 要加上此,原程序中,此函数是在call指南针内部call的
mov ecx, DWORD PTR DS : [ecx + 0x19F0]// 即在前面的基础上调用的,因此ecx...

注意:不仅要构造模拟参数,还有注意各个寄存器的值(用不到的就不管),如ecx=0012A1F4,在基址中45DEBC存储,它是好找的。

因此,对于参数2和参数5,二者的值,可以在ecx的基础上+某个数得到,参数2+40,参数5+4,要特别注意此思路,不管他为什么要加上此数的,只要构造出这个值就行。

(这样就看出,这个ecx的值特别重要,作为一个基础,而那个基址中存储的这个ecx,可见,找到合适的基址尤其重要,是构造汇编代码的重中之重,特别注意基址的寻找,有了基址,一切都好办(哪怕同参数2/5一样,强行+x构造出某个值,只要我能构造出程序当时运行的环境就行。)


>>>>

2.5 实现秒杀


1. 就是单次消除功能的循环,设置一个停止条件即可,点坐标的x/y==0

2. 关键代码:
// MFCGamePlugin.cpp// 循环消除中,判断是否停止if (pt1.x == 0 && pt1.y == 0){ return -1;}
//CMyDlg.cppvoid CMyDlg::OnBnClickedButton3(){ // TODO: 在此添加控件通知处理程序代码
CMFCGamePluginApp* pApp = (CMFCGamePluginApp*)AfxGetApp(); // 循环消除 for (int i = 0; i < 100; i++) { int nRet = ::SendMessage(pApp->m_hWnd, WM_DATA2, 0, 0); if (nRet == -1) break; }}

>>>>

2.6 另一个思路来单消/秒杀

不断测试,当点击两个炸弹成功消除后,会出现炸弹道具。

同理,找到相应的call,观察参数,发现同指南针相比,就是F0换成了F4,二者就是一样的思路来的。

炸弹一次就相当于单次消除,加上循环便是秒杀(此时循环没有加停止条件,仅限制了循环次数,无伤大雅,有那个意思就行。

版本1的单次消除和秒杀:获取两个点+手动消除,本思路借用炸弹道具,明显简单多了,也提了个醒,逆向时,要举一反三,通过指南针道具的调用,联想其他工具,程序员一般都是按照同一个思路来做的,无非换个参数(时间有限,其他道具也是这个道理,学到思路即可。

关键代码:

else if (Msg == WM_DATA2){ // 1 获取两个点坐标 POINT pt1 = { 0 }; POINT pt2={ 0 };
// 小技巧,用于调试,当注入成功时,ctrl+s 搜索指令找到此dll地址 //_asm //{ // mov eax,eax // mov eax,eax //}
//0041E75E > \8B8E F0190000 MOV ECX, DWORD PTR DS : [ESI + 0x19F0]; Case F0(BM_GETCHECK) of switch 0041E749 //0041E764 . 8D45 D8 LEA EAX, DWORD PTR SS : [EBP - 0x28] //0041E767 . 50 PUSH EAX //0041E768 . 8D45 E0 LEA EAX, DWORD PTR SS : [EBP - 0x20] //0041E76B . 50 PUSH EAX //0041E76C.E8 CEAA0000 CALL kyodai2.0042923F; 提示待连接的两个坐标 _asm { mov ecx, 0x45DEBC mov ecx, [ecx] lea ecx, DWORD PTR DS : [ecx + 0x494]// 要加上此,原程序中,此函数是在call指南针内部call的 mov ecx, DWORD PTR DS : [ecx + 0x19F0]// 即在前面的基础上调用的,因此ecx... lea eax, pt1.x push eax// 原程序,push的是栈地址 lea eax, pt2.x push eax mov eax,0x0042923F call eax } CString strCode; strCode.Format(L"单次消除: 点1 x=%d,y=%d,点2 x=%d,y=%d", pt1.x, pt1.y, pt2.x, pt2.y); OutputDebugString(strCode.GetBuffer());
// 循环消除中,判断是否停止 if (pt1.x == 0 && pt1.y == 0) { return -1; }
// 2 调用消除call
//0041AB13 | > \57 PUSH EDI; 参数6:2(当前edi = 2 //0041AB14 | . 8D45 F4 LEA EAX, [LOCAL.3] //0041AB17 | . 53 PUSH EBX; 参数5:坐标数组( = 1A5DE18 = ?+ ? //0041AB18 | . 50 PUSH EAX; 参数4:点2坐标(eax来自local3,就是点坐标 //0041AB19 | . 8D45 EC LEA EAX, [LOCAL.5] //0041AB1C | . 8BCE MOV ECX, ESI //0041AB1E | . 50 PUSH EAX; 参数3:点1坐标(eax来自local5,就是点坐标 //0041AB1F | . 0FB645 08 MOVZX EAX, BYTE PTR SS : [EBP + 0x8]; eax = 0 //0041AB23 | . 69C0 DC000000 IMUL EAX, EAX, 0xDC; eax = 0 //0041AB29 | . 8D8430 5C1900 > LEA EAX, DWORD PTR DS : [EAX + ESI + 0x195C] //0041AB30 | . 50 PUSH EAX; 参数2:连连看数组地址( = 12BB50 = ?+ ? //0041AB31 | .FF75 08 PUSH[ARG.1]; 参数1:0(栈中可得,arg1为0 //0041AB34 | .E8 551B0000 CALL kyodai2.0041C68E; 6个参数,相当靠谱,就是他了 _asm { // 传递ecx,尤其重要,基地址!! mov ecx, 0x45DEBC mov ecx, [ecx] // 第一个参数 固定值 push 0x4 // 第二个参数 坐标点数组 lea eax, DWORD PTR DS : [ecx + 0x494] mov eax, DWORD PTR DS : [eax + 0x19F0] add eax, 0x40 push eax // 第三个参数 坐标1 lea eax, pt1.x push eax // 第四个参数 坐标2 lea eax, pt2.x push eax // 第五个参数 数组地址 lea eax, DWORD PTR DS : [ecx + 0x494] mov eax, DWORD PTR DS : [eax + 0x19F0] mov eax, DWORD PTR DS : [eax + 4] push eax // 第六个参数 0 push 0 // 调用函数 mov eax,0x0041C68E call eax }
return DefWindowProc(hWnd, Msg, wParam, lParam);// 要加此,否则运行完自动结束}else if (Msg == WM_DATA3){ OutputDebugString(L"无限炸弹"); //0041DE4D | . 8B86 9404000 > MOV EAX, DWORD PTR DS : [ESI + 0x494] //0041DE53 | . 8D8E 9404000 > LEA ECX, DWORD PTR DS : [ESI + 0x494] //0041DE59 | . 52 PUSH EDX //0041DE5A | . 53 PUSH EBX //0041DE5B | . 53 PUSH EBX //0041DE5C | .FF50 28 CALL DWORD PTR DS : [EAX + 0x28]; 使用指南针道具 _asm { mov ecx, 0x45DEBC mov ecx, [ecx] LEA ECX, DWORD PTR DS : [ecx + 0x494] PUSH 0xF4// 若指南针,则F0 PUSH 0 PUSH 0 mov eax, 0x0041E691 call eax } return DefWindowProc(hWnd, Msg, wParam, lParam);}return CallWindowProc(g_oldProc,hWnd,Msg,wParam,lParam);

>>>>

2.7 最终效果






- End -




看雪ID:21Gun5

https://bbs.pediy.com/user-868592.htm 

*本文由看雪论坛 21Gun5 原创,转载请注明来自看雪社区。




推荐文章++++

CVE-2017-11882理论以及实战样本分析

恶意代码分析之 RC4 算法学习

CVE-2017-0101-Win32k提权分析笔记

ROPEmporium全解

实战栈溢出漏洞



好书推荐



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com

“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存