首页
社区
课程
招聘
[讨论]记录一次连连看游戏的分析
2021-12-2 21:59 15300

[讨论]记录一次连连看游戏的分析

2021-12-2 21:59
15300

1.修复程序

打开的是一个启动器,加载广告,点击启动游戏会弹一个广告,去掉勾选框后点击继续,才会打开游戏本体

在任务管理器查看本体的名字叫做kyodai.exe 在文件夹中找到打开发现文件打不开。

1638330691178

在loadPe查看信息,发现本体文件的文件对齐为1000,但是一个普通exe的对齐是200,不知道是不是这里的问题。

1638329666917

去看看启动器对源文件做了些什么修改让他可以打开

打开启动器,在任务管理器查看进程

1638328279251

点击启动游戏,会弹出一个窗口,同时多了一个qqllk.ocx

uTools_1638328468758

点击继续就会启动游戏,并且多了一个kyodia.exe

1638328582794

那么,应该就是qqllk.ocx启动了游戏本体kyodia.exe它是怎么修改文件的呢?

将qqlk.ocx改为qqllk.exe,发现也可以运行。在od中打开它

首先qqllk创建了游戏的进程,那么一定会打开一个进程

 

在od中下一个createprocess断点,运行程序,果然在断点处断了下来。

 

1638329026163

 

在创建进程的参数中这个位置是挂起线程。那么应该就是在挂起线程后对内存进行了修改,修改内存会用到WriteProcessMemory。下个WriteProcessMemory断点接着运行

 

程序断在了这个位置,对参数分析

 

1638329366437

 

43817A是写入的位置,也就是游戏进程的位置,那么一会要去游戏进程看一下

 

484E70是写入的数据的缓冲区位置,数据窗口跟随查看数据,发现写入了00

 

想到之前在loadpe中查看的文件对齐大小是1000.会不会是方便修改

 

再打开一个od附加进程kyodai

 

1638329853178

 

CTRL+G去到刚刚找到的地址43817A,现在的数据是01

 

1638330004263

 

将启动器接着运行,然后再回来游戏进程,发现之前的01已经被修改成了00

 

游戏程序就可以运行了

 

那么把内存中43817A这个位置改变就可以正常的启动游戏本体。那么之前的文件对齐大小是1000,和内存对齐一致,那么对应文件的位置也是43817A,当然这个地址是一个VA地址,RVA地址就是3817A。

 

在010editor打开游戏文件,跳转到3817A的位置,将01修改为001638330407572

 

另存为一个新文件,改个名字。打开发现游戏可以正常运行了1638330519899

2.分析道具

通过Cheat Engine 搜索道具个数。定位到道具数量地址是12AC5C

 

时间条的地址是12A748(在调试的时候锁定方便分析)

 

把数值修改,发现道具的数量也改变了1638341426338

 

在0D中跳转到12AC5C这个位置发现了一些数据

 

1638341586552

 

刚验证了这个位置就是道具的数量位置,从上边的F0 09 F1 03推断F加一个数字是道具类型。根据推断改变下面的数据1638341804361

 

果然道具的类型和数量发生了变化。

3.分析棋盘结构

通过Cheat engine 搜索棋盘第一个位置,不断刷新棋盘,对比第一个位置的数据进行搜索,对比是否发生变化

 

最后得到很多数据,但是只有第一个和之前的道具位置比较接近,合格应该是棋盘的位置,再次刷新棋盘,这里的数据发生变化。1638342360609

 

在od中跳转到这个地方,对棋盘进行刷新,这里色数据都发生了变化,

 

1638342511524

 

棋盘最后面以00000结尾,多次刷新数据发现并没有固定的结束标记,所以从游戏入手,算出方块的数量是19*11=209个16进制是D1个

4.寻找关键CALL

##

4.1指南针

首先用CE修改道具的数量,并锁定。方便后面的调试

 

在地图的一个位置下一内存访问断点,当我们点击了指南针或者炸弹道具的话,肯定会来访问地图的数据寻找可以消除的两个位置1638424562808

 

点击道具触发了断点,打开调用堆栈窗口,寻找是哪一个函数调用的

 

image-20211202140317905

 

在这几个函数打上断点,在向上就是mfc库函数,只需要在本程序的函数上分析就可以了。

 

再次运行程序,使用指南针道具,断点在一个地方断了下来,分析一下栈内的参数,其中有一个F0,根据上面的分析,F0是指南针道具的标志

 

1638425687535

 

那我就换一个道具看看这里会不会变化。这次点击炸弹道具,这次F0变成F4,正好是炸弹的标志,那这个Call就可能是调用道具的CAll

 

1638425842777

 

在IDA中打开这个程序进行分析,来到我们刚刚看到的这个CALL的位置,并进入这个CAll的内部

 

分析一下这个CAll的流程图,发现这里有个switch选择,一共是8个分支,很可能是道具的种类选择,但是道具槽的位置只有7个,所以我就错过了这个位置,后来浪费了好多时间,等分析完一遍之后,再回到这个位置,感觉它在无情的嘲讽我,道具的位置有七个,但是也可能有八种

 

1638426286626

 

1638426503091

 

查看这个地址,去到OD中跳转到这里。

 

1638426848051

 

根据EAX的值来找到跳转的地址,在数据窗口跟随。

 

image-20211202143921875

 

发现了八个地址,这八个地址其实就是switch的case,根据道具不同运行不同的代码。其实OD也有给提示caseF0到F7

 

接着,重新运行,点击指南针,触发断点,将程序运行到这个位置。跳到case F0也就是指南针的case里面,遇到第一个CALL

 

image-20211202145213958

 

这里传进去了两个参数,这两个参数是两个局部变量的地址。在数据窗口跟随一下,现在应该是还没初始化。步过这个call

 

image-20211202145530250

 

里面的数据已经变成了几个数字,棋盘式一个二维数组,这几个数字很可能是数组的下标,根据数值找到游戏的棋盘这两个位置

 

image-20211202145849094

 

发现正好是两个可以消除的,那么这个call就是来获得两个可以消除的位置的call

4.2炸弹

重新运行,选择点击一个炸弹道具。断在case处

 

image-20211202152633943

 

发现又是一个和之前的找两个位置一样的结构。验证确实是寻找了两个能消除的方块位置。

 

同样利用内存写入断点,使用炸弹道具,通过堆栈调用在call处打断点,分析call的参数,找到一个把可以消除的位置当参数传进去的地方

 

image-20211202170140695

 

image-20211202170239725

 

尝试了下手动点击两个可以消除的方块,发现也会经过这个地方。通过栈回溯找到上一层。并下断点

 

image-20211202181444138

 

再次运行到这发现了这几个参数,到数据窗口跟随,确实是两个地址。还有地图的首地址。那这就是消除两个方块所所调用的call了

5.辅助工具的实现

5.1思路概述

​ 单步消除:在道具中,有指南针还有炸弹,可以通过找到 指南针的call,分析指南针的算法,的道具给提示的时候肯定会算出哪两个方块可以消除,然后在屏幕点击对应的位置或者直接调用炸弹的call,来实现消除

 

​ 一键消除:找到对应的剩余方块数量,除以2就是需要消除的次数,就调用几次单步消除就可以了

 

​ 道具数量:我们已经有的道具的数量的地址,那么直接修改这个值就可以了

5.2代码实现

​ 消除的功能的话。直接调用炸弹道具会比较方便,当然也可以用指南针的call获取到两个坐标,然后调用消除的call也可以实现,相对来说麻烦了一点。在实际情况中当然怎么简单怎么来。既然是学习,还是用复杂点的方法来让自己理解的透彻一点比较好。

5.2.1通过调用道具实现消除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
WNDPROC g_OldProc;
LRESULT
WINAPI
WindowProc(
    _In_ HWND hWnd,
    _In_ UINT Msg,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam) {
    if (Msg == WM_K7)
    {
        OutputDebugString(L"检测到指南针");
        _asm {
            mov ecx, 0x12A688
            push 0xf0
            push 0
            push 0
            mov eax, 0x41e691
            call eax
        }
 
    }
    //调用道具实现
    else if (Msg == WM_K8) {
 
        _asm {
            mov ecx, 0x12A688
            push 0xf4
            push 0
            push 0
            mov eax, 0x41e691
            call eax
        }
 
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    //调用坐标加调用消除实现
    else if (Msg == WM_K9) {
        POINT node1 = { 0 };
        POINT node2 = { 0 };
        int* p1 = (int*)&node1.x;
        int* p2 = (int*)&node2.x;
        CString str;
        str.Format(L"%d,%d--%d,%d", node1.x, node1.y, node2.x, node2.y);
        OutputDebugString(str);
        _asm {
            //寻找两个可以消除的坐标
            mov ecx, 0x45debc
            mov ecx,[ecx]
            lea ecx,DWORD PTR ds:[ecx+0x494]
            mov ecx,DWORD PTR ds:[ecx+0x19f0]
            push p1
            push p2
            mov eax, 0x42923f
            call eax
        }
 
        str.Format(L"%d,%d--%d,%d", node1.x, node1.y, node2.x, node2.y);
        OutputDebugString(str);
        _asm{
 
        //    //消除
            mov ecx, 0x45debc
            mov ecx,[ecx]
            lea eax, DWORD PTR DS : [ecx+0x494]
            mov eax, DWORD PTR DS : [eax+0x19f0]
            add eax,0x40
            push 0x4
            push eax
            push p1
            push p2
            push 0x12bb50
            push 0
            mov eax,0x41c68e
            call eax
        }
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
 
    return CallWindowProc(g_OldProc, hWnd, Msg, wParam, lParam);
}
 
unsigned __stdcall ThreadCall() {
    //弹出对话框
    MyDlg mydlg;
    mydlg.DoModal();
    return 0;
}
BOOL CLinkLinkLookApp::InitInstance()
{
    CWinApp::InitInstance();
 
    //查找窗口获取窗口句柄
    m_hwnd = FindWindow(NULL, L"QQ连连看");
    if (m_hwnd == NULL)
    {
        OutputDebugString(L"没有找到窗口");
        return FALSE;
    }
    OutputDebugString(L"打开成功");
    //设置窗口回调函数
    g_OldProc = (WNDPROC)SetWindowLong(m_hwnd, GWL_WNDPROC, (LONG)WindowProc);
    if (g_OldProc == NULL) {
        OutputDebugString(L"设置回调函数失败");
    }
    OutputDebugString(L"设置回调函数成功");
    //弹出对话框
    _beginthreadex(0, 0, (_beginthreadex_proc_type)ThreadCall, 0, 0, 0);
    return TRUE;
}

上面的代码是直接使用道具进行消除。因为游戏对消息机制有过滤,所以使用的是自定义消息。通过远程线程注入dll,在dll初始化时创建另一个线程打开一个窗口。点击按钮来向主窗口发送消息,通过消息来调用不同的功能

 

image-20211202183701936

 

image-20211202184123007

 

成功的背后是无数次的失败。尽管最后成功了,里面的原理还是有很多搞不清楚的。路还有很长。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-12-2 22:06 被wx_Van_Zovy编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 158
活跃值: (59)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
一零七零零 2021-12-3 09:18
2
0
QQ游戏大厅的联网版连连看不知道会不会生效。
雪    币: 1928
活跃值: (5864)
能力值: ( LV7,RANK:118 )
在线值:
发帖
回帖
粉丝
tutuj 2022-1-13 15:30
3
0
一零七零零 QQ游戏大厅的联网版连连看不知道会不会生效。
一样可以用,我之前搞过这种秒杀的插件,然后杀疯了
游客
登录 | 注册 方可回帖
返回