2020-02-20 09:53:36

**游戏逆向分析笔记

17 / 0 / 0 / 0

作者: 看雪学院 来源: 看雪学院 本文为看雪论坛精华文章
看雪论坛作者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位
工具: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指南针的地方

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

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

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

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

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

  11. 再往外一层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);

    }
  1. 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.cpp void 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 最终效果

PS: 如本文对您有疑惑,可加QQ:1752338621 进行讨论。

0 条评论

0
0
官方
微信
官方微信
Q Q
咨询
意见
反馈
返回
顶部