首页
论坛
课程
招聘
[原创]2021南极动物厂 游戏高赛竞赛决赛分析02
2021-4-13 04:37 4393

[原创]2021南极动物厂 游戏高赛竞赛决赛分析02

2021-4-13 04:37
4393

Part2 书接上回

上个帖子 对整个赛题进行了大概的分析 接下来就是如何实现个跟自瞄不同的功能了 那怎么样才能够更暴力 那当然是 当当当~ 子弹追踪 接下来就是讲解如何找子弹追踪 并且 怎么实现 子弹穿墙追踪。

0x01 过程

功能分析

使用工具:IDA Pro 7.2CheatEngine 7.2x64dbgPYArk

  1. 好咧 首先子弹是从枪上打出去的 也就是子弹出发的坐标是以枪口坐标为准的
    那我们第一个思路是更改子弹出发的坐标 直接把坐标改到敌人的坐标 岂不是子弹一出发就可以直接把敌人打死 那我们的关键就是如何找到 开枪的函数了

2.开枪会跟什么有关系呢->那肯定是子弹数量啦 我们先把Bot的数量设置为0 防止老被Bot打死 然后我们CE搜索出子弹数量的地址 右键查找什么改写了此地址。
子弹数量一般都存储在武器指针下面 rbx应该就是武器指针哦

 

3.看见一个地址 我们在这F5下个断返回到上一层 武器开火的函数就能在附近找到哦 (为什么武器开火的函数就在附近呢 因为开火->子弹减少)一般正常流程都是这样~ 那我们先把子弹减少这个call nop看看



 

我们可以看见 墙上依然会有弹孔(假设开火函数在子弹数量减少的这个call里面的话 那么nop掉之后 相当于 子弹不会发射 那墙上也就不会有弹孔)而且我们已知rcx是武器指针 那我们看看附近有没有用到了武器指针的函数

 

而且通常都会是虚表函数 为什么呢 因为游戏引擎基本都是基于对象去管理事件的
例如武器.开火()这样子

 

我们nop这个函数试试

 

发现弹孔消失了 只剩下枪口的特效了 那我们就要在这个call内 认真分析了 先把这个虚表函数恢复了 然后在这里下断

 

4.武器子弹弹道函数的分析

我们可以看到函数内有大量的浮点操作 此时我们慢慢单步 观察这个函数内 每一个函数的参数 返回值 看看有没有取出来什么坐标之类的 因为这个虚表函数已经被确认为开火函数 并且这个虚表函数除了this之外是没有其他参数的 所以子弹出发的坐标一定是在这个函数里面 通过其他函数取出来的 所以我们要格外认真的去查看这些函数的返回值 参数(因为可能是通过参数返回的坐标)。



没单步一会就看到这个函数返回了一个坐标(存储在rax 或参数[rsp+0x60]里)此时我们先运行起来 然后我们对着墙开枪 中断以后把返回的坐标 清0 看看效果

 

发现弹孔消失了 所以这个坐标一定跟弹道有关 但是 并不能确认这个坐标就是子弹出发的坐标 所以我们手写一个shellcode来试试

 

5.选取一个合适的位置来hook 首先我们要保证不破坏原来的上下文(就是寄存器保护好) 并且不破坏原来的执行代码

 

所以 当当当~ 我使用了 这种支持跨4gb的跳转 并且不会影响寄存器的跳转

1
2
3
push xxxx
mov [rsp+4],xxxx
ret




 

我们对着天开枪 这个Bot就死了

 

这样就实现了穿墙加子弹追踪的效果。

 

然后在后续中 发现多个Bot存在时 Bot会出现打不中我的情况 在这个函数下断分析 发现机器人开火的时候也会走这里 那我们这时候可以改改我们的shellcode
判断下武器指针是否属于我们自己 不是我们自己的话 就不修改 是我们自己的话就给Bot坐标 这样就不会影响Bot的开枪了。(懒得写了)

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <iostream>
 
void DllEntry();
char* GetName(uintptr_t Object);
 
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        AllocConsole();
        freopen("CON", "w", stdout);
        CreateThread(0, 0, (LPTHREAD_START_ROUTINE)DllEntry, 0, 0, 0);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
 
uintptr_t GameBase{ 0 };
uintptr_t BotPosPtr{ 0 };
const char BotName[10] = "BotPawn_C";
 
struct Array
{
    uintptr_t* ArrayEntry;
    int Count;
};
 
struct Vec3
{
    float x;
    float y;
    float z;
};
 
Vec3* GetPos(uintptr_t Object);
void Hook();
 
void DllEntry()
{
    GameBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(L"ShooterClient.exe"));
    Hook();
 
    while (true)
    {
        auto UOjbectArrayPtr = *(uintptr_t*)(GameBase + 0x2F71060);
        if (!UOjbectArrayPtr)
            continue;
        UOjbectArrayPtr = *(uintptr_t*)(UOjbectArrayPtr + 0x30);
        if (!UOjbectArrayPtr)
            continue;
        auto UOjbectArray = *(Array*)(UOjbectArrayPtr + 0x98);//自己定义一个结构
 
        for (int index = 0; index < UOjbectArray.Count; index++)
        {
            auto Object = UOjbectArray.ArrayEntry[index];
 
            if (Object)
            {
                auto NamePtr = GetName(Object);
                if (NamePtr)
                {
                    if (!strcmp(NamePtr, "BotPawn_C"))
                    {
                        auto pPos = GetPos(Object);
                        memcpy((void*)BotPosPtr, pPos, sizeof(Vec3));
                    }
                    //printf("Ptr:%llx Name:%s\n", Object, NamePtr);
                }
            }
        }
    }
}
 
void Hook()
{
    uint8_t BulletShellCode[] = "\x81\xC1\x6B\x63\x19\x36\x8B\xC1\x25\xFF\xFF\x7F\x00\x0D\x00\x00\x80\x3F\x89\x85\x00\x01\x00\x00\x50\x51\x48\xB8\x66\x66\x66\x66\x66\x66\x36\x12\x48\x8B\x08\x48\x89\x4C\x24\x70\x8B\x48\x08\x89\x4C\x24\x78\x59\x58\x68\x78\x56\x34\x12\xC7\x44\x24\x04\x34\x12\x00\x00\xC3";
    uint8_t JmpShellCode[] = "\x68\x78\x56\x34\x12\xC7\x44\x24\x04\x34\x12\x00\x00\xC3";
 
    BotPosPtr = (uintptr_t)malloc(sizeof(Vec3));
    auto HookMemory = (uintptr_t)VirtualAlloc(0, 0x1000, 0x1000, PAGE_EXECUTE_READWRITE);
    if (BotPosPtr && HookMemory)
    {
        auto HookAddress = GameBase + 0x51C162;
        auto ReturnAddress = GameBase + 0x51C17A;
        *(uintptr_t*)(BulletShellCode + 0x1C) = BotPosPtr;
        *(uint32_t*)(BulletShellCode + 0x36) = *(uint32_t*)(&ReturnAddress);
        *(uint32_t*)(BulletShellCode + 0x3E) = *(uint32_t*)((uint64_t)(&ReturnAddress) + 4);
 
        memcpy((void*)HookMemory, BulletShellCode, sizeof(BulletShellCode) - 1);
        *(uint32_t*)(JmpShellCode + 0x1) = *(uint32_t*)(&HookMemory);
        *(uint32_t*)(JmpShellCode + 0x9) = *(uint32_t*)((uint64_t)(&HookMemory) + 4);
 
        DWORD old{ 0 };
        VirtualProtect((void*)HookAddress, 0x100, 0x40, &old);
        memcpy((void*)HookAddress, JmpShellCode, sizeof(JmpShellCode) - 1);
        VirtualProtect((void*)HookAddress, 0x100, old, &old);
    }
    printf("HookMemory:%llx BotPosPtr:%llx\n", HookMemory, BotPosPtr);
}
 
char* GetName(uintptr_t Object)
{
    if (IsBadReadPtr((void*)Object, 8))
    {
        return 0;
    }
    auto NameIndex = *(int*)(Object + 0x18);
    if (!NameIndex)
        return NULL;
    auto NameBase = *(uintptr_t*)(GameBase + 0x2E6E0C0);
    if (!NameBase)
        return NULL;
    auto NameIndexPtr = *(uintptr_t*)(NameBase + 8 * (static_cast<uint64_t>(NameIndex) / 0x4000));
    if (!NameIndexPtr)
        return NULL;
    NameIndexPtr = *(uintptr_t*)(NameIndexPtr + 8 * (static_cast<uint64_t>(NameIndex) % 0x4000));
    if (!NameIndexPtr)
        return NULL;
    return (char*)(NameIndexPtr + 0xC);
    //v4 = (*(*(qword_1800091A0 + 8i64 * (*v3 / 0x4000)) + 8i64 * (*v3 % 0x4000)) + 0x10i64); 这里dump出来的dll最后是0x10
    //这里 0xC 为什么跟 dump出来的 那个0x10不一样呢 因为0x10取出来的名字是不完整的 不知道为什么出题人要这样写
}
 
Vec3* GetPos(uintptr_t Object)
{
    if (IsBadReadPtr((void*)Object, 8))
    {
        return 0;
    }
 
    auto PosPtr = *(uintptr_t*)(Object + 0x158);
    return (Vec3*)(PosPtr + 0x164);
 
}

FLAG

FLAG毫无技术难度 重新去看的时候 没多久就搞定了..
重新回来看FLAG 发现了这里 不清楚是个啥 于是x64dbg 设置RIP
可以看见解密出来以后的字符串 FileName = "flag:%s\n\r"
进到140001010可以看见明显的特征(分析多了 这里其实可以看出来 这是一个printf函数)
所以在v23为假的情况下会执行这个流程打印v9 v9是从hack.dat解密出来的


往上重新分析下v23是如何被更改的






 

回到加密函数 第一个参数是要被解密的BufferPtr 第二个是长度
并且经过这个解密函数后解密前后的Buffer长度是一样的
并且在大于等于0x40的时候会走上面这个分支 小于0x40会走下面这个分支
我们已经知道了FLAG的长度 为0x3E
所以我们只需要看下面这个分支就好了 (实际上 这两个分支的算法都是一样的
只不过是被编译器用SSE加速优化了)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <Windows.h>
 
int main()
{
    uint8_t flag[] = "2RSRhrofoWtLeLrJCSlTireznrtx.oeLxuehyyAwbpCOZq0tsS7MZyVdOUoE8";
 
    for (int i = 0; i < sizeof(flag); i++)
    {
        flag[i] += 0x13;
        flag[i] ^= 0x3F;
    }
 
    HANDLE hFile = CreateFileA("hack.dat", GENERIC_ALL, NULL, NULL, CREATE_ALWAYS, NULL, NULL);
    DWORD lpNumberOfBytesWritten{ 0 };
    WriteFile(hFile, &flag, sizeof(flag), &lpNumberOfBytesWritten, NULL);
 
    std::cout << "Hello World!\n";
}

0x02 总结

发的两篇帖子 都是大晚上随手写的 写的不好的地方 欢迎指正啦~

 

而且这两篇帖子在写的时候 都是从一个不太了解UE4引擎的普通的参赛选手的角度出发去写的(为了让没有做过游戏逆向的朋友们也能看懂 并且如何得出做题的思路~~)

 

所以我觉得这次的赛题无论懂UE4引擎的数据结构好 不懂UE4引擎的数据结构好
其实只要思路正确都是可以快速做出答案的哦~ 并且还可以剩余很多时间去精进自己的WriteUp(意思就是基于做完题目的前提下 疯狂内卷 写个无限血量 无限子弹 分析下hack.dll的hook点 绘制的实现 自己写个透视 漏脚打脚 漏头打头 之类的提高分数)

 

当然如果是懂UE4引擎的数据结构 或者 有相关FPS外挂经验的选手可能在做题的过程中 更容易猜出hack.dll的意图 从而更好的去解题 其实题目还有很多有意思的地方 例如hack.dll的实现 或者是比子弹穿墙更加变态的实现(模拟弹道)
UE4 SDK的生成 等等等 涉及的面太多太多 我没有在帖子中一一讲解 因为真的讲不完

 

相关代码已经贴出来了 如果有讲的不好的地方 或者 不懂的地方 欢迎跟帖~

 

赛题链接https://gslab.qq.com/html/competition/2021/race-pre.htm


[注意] 欢迎加入看雪团队!base上海,招聘安全工程师、逆向工程师多个坑位等你投递!

最后于 2021-4-23 10:22 被淡然他徒弟编辑 ,原因:
收藏
点赞6
打赏
分享
最新回复 (9)
雪    币: 4632
活跃值: 活跃值 (567)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
如斯咩咩咩 活跃值 2021-4-13 11:05
2
0
存爷
雪    币: 4751
活跃值: 活跃值 (2188)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
淡然他徒弟 活跃值 1 2021-4-13 13:31
3
0
如斯咩咩咩 存爷
积总
雪    币: 10181
活跃值: 活跃值 (3886)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
hzqst 活跃值 3 2021-4-13 15:14
4
0
卧槽!UE4的神!
雪    币: 4751
活跃值: 活跃值 (2188)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
淡然他徒弟 活跃值 1 2021-4-13 20:20
5
0
hzqst 卧槽!UE4的神!
我草 大表哥!
我草 大表哥!
我草 大表哥!
我草 大表哥!
我草 大表哥!
爱你 致我们的大表哥~
雪    币: 17
活跃值: 活跃值 (180)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
guotouck 活跃值 2021-4-14 09:39
6
0

大佬们,看了几个帖子 没一个锁时间的,这个游戏5分钟一局,,这锁不死吗  总是新开局 

最后于 2021-4-14 09:54 被guotouck编辑 ,原因:
雪    币: 4751
活跃值: 活跃值 (2188)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
淡然他徒弟 活跃值 1 2021-4-14 23:55
7
0
guotouck 大佬们,看了几个帖子 没一个锁时间的,这个游戏5分钟一局,,这锁不死吗&nbsp; 总是新开局&nbsp;

找游戏数据 很快啊 几分钟差不多 游戏新开局 没什么影响。

最后于 2021-4-14 23:57 被淡然他徒弟编辑 ,原因:
雪    币: 125
活跃值: 活跃值 (331)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
qdjytony 活跃值 2021-4-26 04:18
8
0
存爷
雪    币: 4751
活跃值: 活跃值 (2188)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
淡然他徒弟 活跃值 1 2021-4-26 05:05
9
0
qdjytony 存爷
大哥 你来了~ 三生荣幸 幸得你的回复呢~
雪    币: 640
活跃值: 活跃值 (308)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_qmomcleg 活跃值 2021-7-23 17:25
10
0
牛蛙
游客
登录 | 注册 方可回帖
返回