腾讯游戏安全大赛 2024 安卓初赛题解
寻找关键结构体
和 23 年的不同,24 年的题不是 unity 引擎的游戏了,而是 UE 引擎,该引擎使用 C++ 实现,有极强的反射机制。
逆向 UE 需要几个关键的结构体
GName、GUObjectArray、GWorld
GName:为了优化性能,UE 不会把在每个对象中存储字符串,而是将所有的字符串存储在一个全局的字符串池中,并给每个字符串分配一个唯一的索引
id,有了GName,就可以将在内存中看到一个个整数映射回字符串。
GUObjectArray: UE 中所有东西都继承自
UObject,引擎会维护一个全局对象数组GUObjectArray,里面存储了所有UObject对象的指针,通过这个数组可以遍历引擎中的每一个类,可以读取类的属性以及在内存中的偏移。
GWorld: UE 引擎中有一个全局变量
GWorld,代表当前游戏的世界状态,通过GWorld可以访问到当前游戏中的所有关卡、角色、物品等信息,获取运行时数据实例。
因为不同版本引擎中的结构体定义以及偏移可能不同,因此需要的先确定游戏使用的 UE 版本,才方便后续源码比对。
这个题目应该是做些了隐藏,直接在 AndroidManifest.xml
里看不到真实版本号

在 libUE4.so 里直接字符串搜索也找不到,后面是在 Strings
窗口右键选择 Setup…,并且勾选 Unicode C-style
之后再次搜索才看到了真实的版本号是 4.27

github 上找到 ue4.27 的源码进行分析:
GWorld
找到 GWorld 定义位置

在 GWorld 被赋值为 LoadedWorld 处往上翻有个
GWorld初始化为 nullptr

往下翻有个字符串 SeamlessTravel FlushLevelStreaming

据此在 ida 中寻找 Gworld 结构体,通过对比可以注意到
qword_B32D8A8,即 Gworld 偏移为
0xB32D8A8
1 | |
GUObjectArray
找到 GUObjectArray 并且附近有字符串
CloseDisregardForGC

在 ida 中搜索字符串定位到相同位置,可知 GUObjectArray
偏移为 B1B5F98
1 | |
GName
UE 4.23 及之后的版本,Epic 重构了名称存储系统,引入了
FNamePool,现在的 GName 本质上就是指向
FNamePool 实例的指针或引用,通过 FNamePool
的构造函数会初始化一些 Name,例如
None,ByteProperty,IntProperty,构造函数通过宏
#include "UObject/UnrealNames.inl"
加载了引擎预定义的硬编码名称

字符串搜索
ByteProperty,交叉引用定位到所在函数,该函数即为
FNamePool 构造函数
1 | |
在对这个函数进行交叉引用查找,传入的参数即为 GName
1 | |
1 | |
可知 GName 偏移为 0xB171CC0
Dump
Gworld:0xB32D8A8
GUObjectArray:0xB1B5F98
GName:0xB171CC0
找到关键结构体的偏移之后,使用 UE4Dumper dump 出需要的信息,方便后续分析
ue4dumper build 要先有 android ndk 环境,找到
ndk-build.cmd 拖进从 github 上拉去下来的 UE4Dumper
路径下,生成的 ue4dumper 在 lib 目录,再 adb push 到手机端使用
SDK Dump With GObjectArray Args 1
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --sdku --gname 0xB171CC0 --guobj 0xB1B5F98 --output /data/local/tmp
SDK Dump With GWorld Args 1
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --sdkw --gname 0xB171CC0 --gworld 0xB32D8A8 --output /data/local/tmp
Dump Objects Args 1
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --objs --gname 0xB171CC0 --guobj 0xB1B5F98 --output /data/local/tmp
Show ActorList With GWorld Args 1
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --actors --gname 0xB171CC0 --gworld 0xB32D8A8 --output /data/local/tmp
section0
需要成功离开房间,但是一碰到墙生命值就会归零。
Method 1
想到的第一种方式是找到修改生命值,把它改成一个极大值
在 dump 出的 SDK 中搜索找到生命值,一般是 float
类型,再由内存对齐可知,生命值偏移为 0x510

然后需要获取玩家的地址,在 dump 出的 Actor
列表里面可以找到找到玩家角色 FirstPersonCharacter_C 的地址
7915f3e040
1 | |
根据这个地址写 frida 脚本锁血
1 | |
但是这样的话每次游戏重新开始地址都会变化,需要重新获取地址,不够方便,所以下面就把获取地址的逻辑和锁血逻辑结合起来写在一个 frida 脚本里面
要获得玩家地址,需要先获取 GWorld,然后通过
GWorld 获取 PersistentLevel,再通过
PersistentLevel 获取 ActorList,最后遍历
ActorList 找到玩家角色
FirstPersonCharacter_C
脚本中的几个偏移需要通过分析 ue 4.27 源码得到
ue4.27 源码:https://github.com/EpicGames/UnrealEngine/tree/4.27
- Level Offset(相对于 GWorld):
0x30
UnrealEngine/Engine/Source/Runtime/Engine/Classes/Engine/World.h
1 | |
public UObject 是 ue
的核心基类,成员包括:虚函数表指针(8)、ObjectFlags(4)、InternalIndex(4)、ClassPrivate(8)、NamePrivate(8)、OuterPrivate(8),一共占用
0x28 字节
public FNetworkNotify 是一个接口,在 C++
中,继承一个接口会增加一个接口虚函数表指针(vptr),指针在 64 位下占
0x08 字节
此时累加 0x28 + 0x8 = 0x30 字节,所以
UWorld 自己的成员变量将从 0x30 开始摆放,而
PersistentLevel 是 UWorld
内部定义的第一个有效成员变量,故 PersistentLevel 偏移为
0x30
在 dump 出的 SDK 中也能验证到
1 | |
- ActorList Offset(相对于
Level):
0x98
UnrealEngine/Engine/Source/Runtime/Engine/Classes/Engine/Level.h
1 | |
UObject:上文已经分析过,占用 0x28 字节
IInterface_AssetUserData :一个接口类,占用
0x08 字节
参考 ue4 源码的 URL.h,一个 FURL
结构体包含以下成员:
FString Protocol (16 字节)
FString Host (16 字节)
int32 Port (4 字节)
int32 Valid (4 字节)
FString Map (16 字节)
FString RedirectURL (16 字节)
TArray<FString> Op (16 字节)
FString Portal (16 字节)
一共 104 即 0x68 字节
所以到 Actors 偏移刚好是 0x28 + 0x8 + 0x68 = 0x98
字节
而对于 GName 的解析,可以参考这两篇文章,已经写得足够详细
执行锁血完整脚本
1 | |
1 | |
Method 2
另一种思路是直接 hook 掉扣血的函数
找到了碰到墙之后会触发的函数
void ReceiveHit(PrimitiveComponent* bpp__MyComp__pf, Actor* bpp__Other__pf, PrimitiveComponent* bpp__OtherComp__pf, bool bpp__bSelfMoved__pf, Vector bpp__HitLocation__pf, Vector bpp__HitNormal__pf, Vector bpp__NormalImpulse__pf, out const HitResult bpp__Hit__pf__const);// 0x5e93094
把函数的前几个指令改成 ret 就可以实现不扣血了
1 | |
后面了解到 UE4 的脚本包装函数,为了让蓝图能够调用原生 C++
函数,引擎会自动生成一个包装层,函数名开头有
exec,而真正的函数是通过这个函数最后 return
时候进行虚表调用的
dump 下来的 SDK 中函数地址是包装后的函数地址,所以在反编译器中国根据地址找到的函数其实是包装函数,而不是真正的原生函数
根据上面的地址找到对应的函数 sub_5E93094
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
146__int64 __fastcall sub_5E93094(__int64 a1, _QWORD *a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v32[1] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v32[0] = 0LL;
sub_6F64230();
if ( a2[4] )
{
sub_6FCBF2C((__int64)a2, a2[3]);
v31 = 0LL;
sub_6F64230();
if ( a2[4] )
goto LABEL_3;
}
else
{
v8 = a2[16];
a2[16] = *(_QWORD *)(v8 + 32);
sub_6FCBF58((__int64)a2, (__int64)v32, v8);
v31 = 0LL;
sub_6F64230();
if ( a2[4] )
{
LABEL_3:
sub_6FCBF2C((__int64)a2, a2[3]);
v30 = 0LL;
sub_6F64230();
if ( a2[4] )
goto LABEL_4;
goto LABEL_11;
}
}
v9 = a2[16];
a2[16] = *(_QWORD *)(v9 + 32);
sub_6FCBF58((__int64)a2, (__int64)&v31, v9);
v30 = 0LL;
sub_6F64230();
if ( a2[4] )
{
LABEL_4:
sub_6FCBF2C((__int64)a2, a2[3]);
v29 = 0;
sub_6F65F28();
if ( a2[4] )
goto LABEL_5;
goto LABEL_12;
}
LABEL_11:
v10 = a2[16];
a2[16] = *(_QWORD *)(v10 + 32);
sub_6FCBF58((__int64)a2, (__int64)&v30, v10);
v29 = 0;
sub_6F65F28();
if ( a2[4] )
{
LABEL_5:
v4 = sub_6FCBF2C((__int64)a2, a2[3]);
v5 = v29;
sub_6F9CA90(v4);
if ( a2[4] )
goto LABEL_6;
goto LABEL_13;
}
LABEL_12:
v11 = a2[16];
a2[16] = *(_QWORD *)(v11 + 32);
v12 = sub_6FCBF58((__int64)a2, (__int64)&v29, v11);
v5 = v29;
sub_6F9CA90(v12);
if ( a2[4] )
{
LABEL_6:
v6 = sub_6FCBF2C((__int64)a2, a2[3]);
sub_6F9CA90(v6);
if ( a2[4] )
goto LABEL_7;
LABEL_14:
v15 = a2[16];
a2[16] = *(_QWORD *)(v15 + 32);
v16 = sub_6FCBF58((__int64)a2, (__int64)v27, v15);
sub_6F9CA90(v16);
if ( a2[4] )
goto LABEL_8;
goto LABEL_15;
}
LABEL_13:
v13 = a2[16];
a2[16] = *(_QWORD *)(v13 + 32);
v14 = sub_6FCBF58((__int64)a2, (__int64)v28, v13);
sub_6F9CA90(v14);
if ( !a2[4] )
goto LABEL_14;
LABEL_7:
v7 = sub_6FCBF2C((__int64)a2, a2[3]);
sub_6F9CA90(v7);
if ( a2[4] )
{
LABEL_8:
sub_6FCBF2C((__int64)a2, a2[3]);
goto LABEL_16;
}
LABEL_15:
v17 = a2[16];
a2[16] = *(_QWORD *)(v17 + 32);
sub_6FCBF58((__int64)a2, (__int64)&v26, v17);
LABEL_16:
v25 = 0LL;
memset(v24, 0, sizeof(v24));
DWORD1(v24[0]) = 1065353216;
v18 = a2[4];
a2[7] = 0LL;
if ( v18 )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v19 = a2[16];
a2[16] = *(_QWORD *)(v19 + 32);
sub_6FCBF58((__int64)a2, (__int64)v24, v19);
}
v20 = a2[4];
if ( a2[7] )
v21 = (_OWORD *)a2[7];
else
v21 = v24;
if ( v20 )
v22 = v20 + 1;
else
v22 = 0LL;
a2[4] = v22;
return (*(__int64 (__fastcall **)(__int64, _QWORD, __int64, __int64, bool, _OWORD *, float, float, float, float, float, float))(*(_QWORD *)a1 + 0x8B0LL))(
a1,
v32[0],
v31,
v30,
v5 != 0,
v21,
v28[0],
v28[1],
v28[2],
v27[0],
v27[1],
v27[2]);
}
注意到最后 return 的 a1 + 0x8B0,写 frida 脚本找到没有
exec 包裹的函数地址
1 | |

找到偏移地址对应的函数 sub_5E843D4
1 | |
进入最后 return 的函数 sub_5E840C8
1 | |
从 -100.0 可以发现这里就是真正的扣血逻辑所在函数
1 | |
可以直接 nop 掉扣血处的指令
1 | |
Method 3
修改位置坐标,但是只成功实现了瞬间的传送,很快又会被拉回原始位置
在 SDK 中找到获取玩家位置的偏移
1 | |
在 ida 中找到偏移地址处函数
sub_965DDF8 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__int64 __fastcall sub_965DDF8(__int64 result, __int64 a2, _DWORD *a3)
{
__int64 v3; // x8
__int64 v4; // x8
__int64 *v5; // x9
__int64 *v6; // x12
int *v7; // x13
bool v8; // zf
int *v9; // x8
int *v10; // x10
int v11; // w10
int v12; // w8
v3 = *(_QWORD *)(a2 + 32);
if ( v3 )
++v3;
*(_QWORD *)(a2 + 32) = v3;
v4 = *(_QWORD *)(result + 304);
v5 = &qword_B12EC90;
v6 = (__int64 *)(v4 + 464);
v7 = (int *)(v4 + 468);
v8 = v4 == 0;
v9 = (int *)(v4 + 472);
if ( v8 )
v9 = &dword_B12EC98;
else
v5 = v6;
if ( v8 )
v10 = (int *)&qword_B12EC90 + 1;
else
v10 = v7;
v11 = *v10;
v12 = *v9;
*a3 = *(_DWORD *)v5;
a3[1] = v11;
a3[2] = v12;
return result;
}
sub_965DC3C 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__int64 __fastcall sub_965DC3C(__int64 a1, __int64 *a2, _BYTE *a3)
{
__int64 v6; // x2
__int64 v7; // x2
__int64 v8; // x8
int v9; // w24
__int64 v10; // x2
_OWORD *v11; // x8
_OWORD *v12; // x22
__int64 v13; // x2
__int64 v14; // x9
_BOOL8 v15; // x3
__int64 v16; // x8
__int64 result; // x0
int v18; // [xsp+Ch] [xbp-E4h] BYREF
_OWORD v19[8]; // [xsp+10h] [xbp-E0h] BYREF
__int64 v20; // [xsp+90h] [xbp-60h]
int v21; // [xsp+A4h] [xbp-4Ch] BYREF
float v22[4]; // [xsp+A8h] [xbp-48h] BYREF
__int64 v23; // [xsp+B8h] [xbp-38h]
v23 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
sub_6F9CA90(a1);
if ( a2[4] )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v6 = a2[16];
a2[16] = *(_QWORD *)(v6 + 32);
sub_6FCBF58((__int64)a2, (__int64)v22, v6);
}
v21 = 0;
sub_6F65F28();
if ( a2[4] )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v7 = a2[16];
a2[16] = *(_QWORD *)(v7 + 32);
sub_6FCBF58((__int64)a2, (__int64)&v21, v7);
}
v20 = 0LL;
memset(v19, 0, sizeof(v19));
DWORD1(v19[0]) = 1065353216;
v8 = a2[4];
v9 = v21;
a2[7] = 0LL;
if ( v8 )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v10 = a2[16];
a2[16] = *(_QWORD *)(v10 + 32);
sub_6FCBF58((__int64)a2, (__int64)v19, v10);
}
v11 = (_OWORD *)a2[7];
v18 = 0;
if ( v11 )
v12 = v11;
else
v12 = v19;
sub_6F65F28();
if ( a2[4] )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v13 = a2[16];
a2[16] = *(_QWORD *)(v13 + 32);
sub_6FCBF58((__int64)a2, (__int64)&v18, v13);
}
v14 = a2[4];
v15 = v18 != 0;
if ( v14 )
v16 = v14 + 1;
else
v16 = 0LL;
a2[4] = v16;
result = sub_8C3181C(a1, v9 != 0, v12, v15, v22[0], v22[1], v22[2]);
*a3 = result & 1;
return result;
}
它对应的源码中的真实函数是 return 的 sub_8C3181C
sub_8C3181C 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__int64 __fastcall sub_8C3181C(__int64 a1, char a2, __int64 a3, char a4, float32x2_t a5, float a6, float a7)
{
unsigned __int64 StatusReg; // x19
__int64 v9; // x0
float v10; // s4
char v11; // w0
__int128 v13; // [xsp+0h] [xbp-40h] BYREF
unsigned __int64 v14; // [xsp+18h] [xbp-28h] BYREF
float v15; // [xsp+20h] [xbp-20h]
__int64 v16; // [xsp+28h] [xbp-18h]
StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
if ( (a2 & 1) == 0 )
a3 = 0LL;
v16 = *(_QWORD *)(StatusReg + 40);
v9 = *(_QWORD *)(a1 + 304);
if ( v9 )
{
v10 = *(float *)(v9 + 472);
a5.n64_f32[1] = a6;
v14 = vsub_f32(a5, *(float32x2_t *)(v9 + 464)).n64_u64[0];
v15 = a7 - v10;
v13 = *(_OWORD *)(v9 + 448);
v11 = (*(__int64 (__fastcall **)(__int64, unsigned __int64 *, __int128 *, _QWORD, __int64, _QWORD, _QWORD))(*(_QWORD *)v9 + 1208LL))(
v9,
&v14,
&v13,
a2 & 1,
a3,
0LL,
a4 & 1);
}
else
{
v11 = 0;
if ( a3 )
{
*(_QWORD *)a3 = 0x3F80000000000000LL;
*(_OWORD *)(a3 + 8) = 0u;
*(_OWORD *)(a3 + 24) = 0u;
*(_OWORD *)(a3 + 40) = 0u;
*(_OWORD *)(a3 + 56) = 0u;
*(_OWORD *)(a3 + 72) = 0u;
*(_OWORD *)(a3 + 88) = 0u;
*(_OWORD *)(a3 + 104) = 0u;
*(_OWORD *)(a3 + 120) = 0u;
}
}
return v11 & 1;
}
1 | |
逃出房间
最终成功出去

section1
空中有部分 flag 字段被隐藏了

猜测是透明度的原因
在 sdk 的三个不同类中看到了和透明度相关的函数
1 | |
其中 SceneComponent::SetVisibility 是用于控制游戏世界中
3D 物体是否被渲染的函数,它的参数 bNewVisibility 为 true
时,组件可见,参数 bPropagateToChildren
是一个层级传播参数,为 true
时不仅改变当前组件的可见性,还会递归地将此设置应用到所有挂载在它下面的子组件
Widget::SetVisibility 是用于控制 2D UI 元素
的显示、布局占用以及交互能力的函数
MovieSceneLevelVisibilitySection::SetVisibility 是用于在
过场动画时间轴中,控制整个关卡的加载或显示状态
所以选择 hook SceneComponent::SetVisibility
来实现透明度的修改,再把之前锁血的功能也结合在一起
1 | |

可知 section1 为 8939
section2
提示说让立方体变得不可穿透,先问 ai 了解到
SetCollisionEnabled
在 SDK 中找到该函数
1 | |
ida 中找到该地址对应的函数 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__int64 __fastcall sub_98EDB3C(__int64 a1, __int64 *a2)
{
__int64 v4; // x2
__int64 v5; // x8
_BYTE v7[4]; // [xsp+4h] [xbp-2Ch] BYREF
__int64 v8; // [xsp+8h] [xbp-28h]
v8 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v7[0] = 0;
sub_6F6758C();
if ( a2[4] )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v4 = a2[16];
a2[16] = *(_QWORD *)(v4 + 32);
sub_6FCBF58((__int64)a2, (__int64)v7, v4);
}
v5 = a2[4];
if ( v5 )
++v5;
a2[4] = v5;
return (*(__int64 (__fastcall **)(__int64, _QWORD))(*(_QWORD *)a1 + 0x660LL))(a1, v7[0]);
}
这里和上面一样,依旧需要获取 return 的函数
但是只 hook 这个函数没有成功,又去看了碰撞相关的其他函数
1 | |
sub_98EB590 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__int64 __fastcall sub_98EB590(__int64 a1, __int64 *a2)
{
__int64 v4; // x2
__int64 v5; // x8
_BYTE v7[4]; // [xsp+4h] [xbp-2Ch] BYREF
__int64 v8; // [xsp+8h] [xbp-28h]
v8 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v7[0] = 0;
sub_6F6758C();
if ( a2[4] )
{
sub_6FCBF2C((__int64)a2, a2[3]);
}
else
{
v4 = a2[16];
a2[16] = *(_QWORD *)(v4 + 32);
sub_6FCBF58((__int64)a2, (__int64)v7, v4);
}
v5 = a2[4];
if ( v5 )
++v5;
a2[4] = v5;
return (*(__int64 (__fastcall **)(__int64, _QWORD))(*(_QWORD *)a1 + 0x850LL))(a1, v7[0]);
}
1 | |
1 | |

所以 section2 是 008
section3

在 Objects.txt 搜索 flag 看到了 getlastflag
1 | |
1 | |
在 ida 中找到对应函数
1 | |
在 libplay.so 中找到 get_last_flag 函数
1 | |
上面有字符串异或和间接跳转
用 bn 看,把 data 段设为只读,发现已经分析出来了 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
5000401458 uint64_t get_last_flag(char* arg1)
00401470 uint64_t x22 = _ReadMSR(SystemReg: tpidr_el0)
00401478 int64_t x8 = *(x22 + 0x28)
00401478
00401498 if (data_403e6c == 0)
004014cc data_403e50.b = 0xa
004014d4 data_403e50:1.b = 0xc
004014d8 data_403e50:2.b = 0xe
004014e0 data_403e50:3.b = 0
00401508 data_403e50:4.b = 0x51
00401510 data_403e50:5.b = 0x16
00401518 data_403e50:6.b = 0x27
00401520 data_403e50:7.b = 0x38
00401530 data_403e50:8.b = 0x49
00401540 data_403e50:9.b = 0x1a
00401550 __builtin_strncpy(dest: &data_403e50:0xa,
00401550 src: ";\\-No", count: 5)
00401598 data_403e50:0xf.b = 0xfa
004015a8 data_403e60.b = 0xfc
004015c0 data_403e60:1.b = 0xfe
004015d0 __builtin_strncpy(dest: &data_403e30,
004015d0 src: "UT1fc0gIYDArdz80Z0Xem46J", count: 0x18)
004015d0
00401714 data_403e6c = 1
00401730 int128_t var_70
00401730 __builtin_memcpy(dest: &var_70,
00401730 src: "\xd8\x98\x54\xc1\x64\x93\x56\x84\x38\x4f\x60\xbb\xa9"
00401730 "a4\xcc\x88\x8d\x9f",
00401730 count: 0x12)
00401738 char* x0 = sub_40192c(arg1, &var_70, 0x12)
0040174c char* x0_3 = sub_4019bc(x0, sub_401970(x0))
00401768 int32_t x21 = 0
00401768
00401770 switch (sub_401a00(x0_3) == 0x18 ? 1 : 0)
00401798 case 1
00401798 x21 = 0
00401798
004017b4 switch (zx.d(*x0_3) != 0x9d ? 1 : 0)
004017bc case 0
004017bc x21 = 1
004017bc
004017d4 sub_401a4c(x0)
004017dc sub_401a9c(x0_3)
004017dc
004017ec if (*(x22 + 0x28) == x8)
00401808 return zx.q(x21)
00401808
0040180c __stack_chk_fail()
0040180c noreturn
其中的 sub_40192c 里面是一个异或,
sub_4019bc 里面是一个变表 base64
sub_40192c 处跳进去 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2200400e4c char* sub_400e4c(char* arg1, char* arg2, int64_t arg3)
00400e74 data_403e64
00400e78 data_403e64 = 1
00400e7c size_t x0 = sub_401840(arg1)
00400e88 char* result = sub_40188c(x0 + 1)
00400e9c int64_t x8 = 0
00400e9c
00400ea4 switch (0 u< x0 ? 1 : 0)
00400ec0 case 1
00400ec0 while (true)
00400ec0 result[x8] = arg2[x8 u% arg3] ^ arg1[x8]
00400ec4 x8 += 1
00400ec4
00400ed4 switch (x8 u< x0 ? 1 : 0)
00400ed4 case 0
00400ed4 break
00400ed4 case 1
00400ed4 continue
00400ed4
00400ed8 result[x0] = 0
00400ee8 return result
sub_4019bc 处跳进去 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
9300400eec void* sub_400eec(void* arg1, int64_t arg2)
00400f18 if (data_403e68 == 0)
00400f4c __builtin_strncpy(dest: &data_403de0,
00400f4c src: "ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/",
00400f4c count: 0x41)
00400f4c
004012e4 data_403e68 = 1
004012f0 void* result = sub_4018dc(1 | (0x3fffffffffffffff & ((arg2 + 2) u/ 3)) << 2)
004012f4 char* x8 = result + 1
00401308 int64_t x12 = 0
00401308
00401310 switch (0 u< arg2 ? 1 : 0)
00401324 case 1
00401324 while (true)
00401324 uint64_t x13_1 = zx.q(x12 + 1 u< arg2 ? 1 : 0)
00401330 uint64_t x14_1 = zx.q(zx.d(*(arg1 + x12)) << 0x10)
00401334 int64_t x10_3
00401334 uint64_t x11_2
00401334 char x11_5
00401334
00401334 switch (x13_1.d)
00401338 case 0
00401338 x10_3 = x12 + 2
00401348 x11_2 = zx.q(x10_3 u< arg2 ? 1 : 0)
00401348
00401350 switch (x11_2.d)
004013f0 case 0
004013f0 label_4013f0:
004013f0 int64_t x13_3 = jump_table_403d90[x13_1]
004013f4 char x12_7 = (&data_403de0)[zx.q(x14_1.d) u>> 0x12]
004013f8 *x8 = (&data_403de0)[x14_1 u>> 0xc & 0x3f]
004013fc x8[-1] = x12_7
004013fc
00401400 switch (x13_3)
00401400 case 0x40138c
00401400 goto label_401394
00401400 case 0x401404
00401400 goto label_401414
00401364 case 1
00401364 label_401364:
00401364 int64_t x13_2 = jump_table_403d90[x13_1]
00401368 x14_1 = zx.q(x14_1.d + zx.d(*(arg1 + x12 + 2)))
0040137c char x15_5 =
0040137c (&data_403de0)[zx.q(x14_1.d) u>> 0xc & 0x3f]
00401380 x8[-1] = (&data_403de0)[x14_1 u>> 0x12]
00401384 *x8 = x15_5
00401384
00401388 switch (x13_2)
00401394 case 0x40138c
00401394 label_401394:
00401394 int64_t x11_3 = jump_table_403da0[x11_2]
0040139c x8[1] = 0x3d
0040139c
004013a0 switch (x11_3)
004013a8 case 0x4013a4
004013a8 x11_5 = (&data_403de0)[x14_1 & 0x3f]
00401424 case 0x401424
00401424 x11_5 = 0x3d
00401414 case 0x401404
00401414 label_401414:
00401414 int64_t x11_6 = jump_table_403da0[x11_2]
0040141c x8[1] =
0040141c (&data_403de0)[zx.q(x14_1.d) u>> 6 & 0x3f]
0040141c
00401420 switch (x11_6)
004013a8 case 0x4013a4
004013a8 x11_5 = (&data_403de0)[x14_1 & 0x3f]
00401424 case 0x401424
00401424 x11_5 = 0x3d
004013b0 case 1
004013b0 x10_3 = x12 + 2
004013c0 x11_2 = zx.q(x10_3 u< arg2 ? 1 : 0)
004013d0 x14_1 = zx.q(x14_1.d) | zx.q(zx.d(*(arg1 + x12 + 1)) << 8)
004013d0
004013d4 switch (x11_2.d)
004013d4 case 0
004013d4 goto label_4013f0
004013d4 case 1
004013d4 goto label_401364
004013d4
00401428 x8[2] = x11_5
0040142c x12 = x10_3 + 1
00401430 x8 = &x8[4]
00401430
00401440 switch (x12 u< arg2 ? 1 : 0)
00401440 case 0
00401440 break
00401440 case 1
00401440 continue
00401440
00401448 x8[-1] = 0
00401454 return result
最终解密 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
26import base64
enc = "UT1fc0gIYDArdz80Z0Xem46J"
std_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
new_table = "ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/"
xor_key = bytes([
0x0a, 0x0c, 0x0e, 0x00, 0x51, 0x16, 0x27, 0x38, 0x49, 0x1a,
0x3b, 0x5c, 0x2d, 0x4e, 0x6f,
0xfa, 0xfc, 0xfe
])
trans_table = str.maketrans(new_table, std_table)
cipher = enc.translate(trans_table)
# print(f"cipher: {cipher}")
dec = base64.b64decode(cipher)
# print(f"dec: {dec.hex()}")
flag = bytearray()
for i in range(len(dec)):
key_byte = xor_key[i % len(xor_key)]
flag.append(dec[i] ^ key_byte)
print("flag: ", flag.decode('utf-8'))
# flag: _Anti_Cheat_Expert
所以 section3 是 _Anti_Cheat_Expert
三个部分连在一起的最终 flag 为
flag{8939008_Anti_Cheat_Expert}
参考文章: