腾讯游戏安全大赛 2024 安卓初赛题解

寻找关键结构体

和 23 年的不同,24 年的题不是 unity 引擎的游戏了,而是 UE 引擎,该引擎使用 C++ 实现,有极强的反射机制。

逆向 UE 需要几个关键的结构体 GNameGUObjectArrayGWorld

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
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
 qword_B32D8A8 = 0;
v95 = *(_QWORD *)(a1 + 128);
v96 = 0;
if ( *(int *)(v95 + 320) > 0 )
{
do
{
v97 = *(_QWORD *)(*(_QWORD *)(v95 + 312) + 8LL * v96);
if ( v97 )
{
v98 = *(_QWORD *)(v97 + 32);
if ( (*(_BYTE *)(v98 + 1560) & 1) == 0 )
{
*(_QWORD *)attr[0].__size = sub_964D250;
v158[0] = v151;
sub_70110C4(v98, attr, 1, 0, 0x20000000);
v100 = *(_DWORD *)(v98 + 12);
if ( dword_B1B5FBC <= v100 )
{
v102 = (unsigned int *)&byte_8;
v103 = 0;
do
{
LABEL_173:
while ( 1 )
{
v105 = __ldaxr(v102);
if ( v105 == v103 )
break;
__clrex();
v103 = *v102;
if ( (*v102 & 0x20000000) != 0 )
goto LABEL_163;
}
}
while ( __stlxr(v103 | 0x20000000, v102) );
}
else
{
v101 = *(_QWORD *)(qword_B1B5FA8 + 8LL * (v100 / 0x10000)) + 24LL * (int)(v100 - (v99 & 0xFFFF0000));
v104 = *(_DWORD *)(v101 + 8);
v102 = (unsigned int *)(v101 + 8);
v103 = v104;
if ( (v104 & 0x20000000) == 0 )
goto LABEL_173;
}
LABEL_163:
*(_BYTE *)(v98 + 1560) |= 1u;
}
}
++v96;
}
while ( (v96 & 0x80000000) == 0 && *(_DWORD *)(v95 + 320) > v96 );
}
v106 = *(_QWORD *)(a1 + 128);
v107 = *(int *)(v106 + 144);
if ( (_DWORD)v107 )
{
v108 = *(_QWORD *)(v106 + 136);
v109 = v108 + 8 * v107;
v110 = *(_QWORD *)(*(_QWORD *)v108 + 296LL);
if ( v110 )
goto LABEL_182;
while ( 1 )
{
v108 += 8;
if ( v108 == v109 )
break;
v110 = *(_QWORD *)(*(_QWORD *)v108 + 296LL);
if ( v110 )
{
LABEL_182:
v111 = *(_QWORD *)(v110 + 32);
if ( (*(_BYTE *)(v111 + 1560) & 1) == 0 )
{
*(_QWORD *)attr[0].__size = sub_964D250;
v158[0] = v151;
sub_70110C4(v111, attr, 1, 0, 0x20000000);
v113 = *(_DWORD *)(v111 + 12);
if ( dword_B1B5FBC <= v113 )
{
v115 = (unsigned int *)&byte_8;
v116 = 0;
do
{
LABEL_188:
while ( 1 )
{
v118 = __ldaxr(v115);
if ( v118 == v116 )
break;
__clrex();
v116 = *v115;
if ( (*v115 & 0x20000000) != 0 )
goto LABEL_179;
}
}
while ( __stlxr(v116 | 0x20000000, v115) );
}
else
{
v114 = *(_QWORD *)(qword_B1B5FA8 + 8LL * (v113 / 0x10000)) + 24LL * (int)(v113 - (v112 & 0xFFFF0000));
v117 = *(_DWORD *)(v114 + 8);
v115 = (unsigned int *)(v114 + 8);
v116 = v117;
if ( (v117 & 0x20000000) == 0 )
goto LABEL_188;
}
LABEL_179:
*(_BYTE *)(v111 + 1560) |= 1u;
}
}
}
}
*(_QWORD *)(a1 + 128) = 0;
sub_6EF6A40(0, 1);
if ( v5 )
sub_91AB884(v5);
qword_B32D8A8 = *(_QWORD *)(a1 + 136);
if ( (*(_DWORD *)(qword_B32D8A8 + 267) & 0x40) != 0 )
{
if ( !v147 )
{
LABEL_195:
if ( v146 == 3 )
goto LABEL_208;
goto LABEL_196;
}
}
else
{
sub_96342B8();
if ( !v147 )
goto LABEL_195;
}
sub_963B790(*(_QWORD *)(a1 + 136), a1);
if ( v146 == 3 )
goto LABEL_208;
LABEL_196:
if ( !*(_BYTE *)(a1 + 145) )
goto LABEL_208;
v119 = *(_QWORD *)(a1 + 136) + 352LL;
if ( (sub_8D6D5A4(v119) & 1) != 0 )
{
v120 = sub_8D6D814(v119);
if ( !v120 )
goto LABEL_206;
LABEL_205:
v121 = sub_90A82F4(*(_QWORD *)(*(_QWORD *)(a1 + 136) + 48LL), 1);
sub_8D49364(v120, *(_QWORD *)(v121 + 736));
goto LABEL_206;
}
if ( qword_B3299B8 )
{
v120 = sub_95998C4();
if ( v120 )
goto LABEL_205;
}
LABEL_206:
v122 = *(_QWORD *)(*(_QWORD *)(a1 + 136) + 88LL);
if ( v122 )
*(_BYTE *)(v122 + 580) = *(_BYTE *)(v122 + 580) & 0xFD | (2 * v145);
LABEL_208:
sub_6D99C24(attr, 1, L" SeamlessTravel FlushLevelStreaming ", 0, 2);

GUObjectArray

找到 GUObjectArray 并且附近有字符串 CloseDisregardForGC

在 ida 中搜索字符串定位到相同位置,可知 GUObjectArray 偏移为 B1B5F98

1
2
3
4
5
6
if ( byte_B1B5FA4 )
{
sub_6CF0A3C(&v327, "CloseDisregardForGC");
sub_6FE1340(&dword_B1B5F98);
v205 = ((__int64 (__fastcall *)(__int16 **))sub_6CF0A4C)(&v327);
}

GName

UE 4.23 及之后的版本,Epic 重构了名称存储系统,引入了 FNamePool,现在的 GName 本质上就是指向 FNamePool 实例的指针或引用,通过 FNamePool 的构造函数会初始化一些 Name,例如 None,ByteProperty,IntProperty,构造函数通过宏 #include "UObject/UnrealNames.inl" 加载了引擎预定义的硬编码名称

字符串搜索 ByteProperty,交叉引用定位到所在函数,该函数即为 FNamePool 构造函数

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
_int64 __fastcall sub_6D9F41C(__int64 a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

v207 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
v2 = a1 + 98368;
pthread_rwlock_init((pthread_rwlock_t *)a1, 0);
memset((void *)(a1 + 56), 0, 0x10008u);
v3 = a1 + 65600;
*(_QWORD *)(a1 + 64) = sub_6BC713C(0x20000, 2);
do
{
pthread_rwlock_init((pthread_rwlock_t *)v3, 0);
*(_OWORD *)(v3 + 56) = 0u;
*(_OWORD *)(v3 + 72) = 0u;
v3 += 128;
}
while ( v3 != v2 );
memset((void *)(a1 + 98368), 0, 0xAFCu);
v4 = 0x8000;
*(_QWORD *)(v2 + 11016) = 0x20000000000LL;
v5 = (_QWORD *)(a1 + 65672);
*(_QWORD *)(v2 + 11008) = 0;
*(_QWORD *)(v2 + 11088) = 0;
*(_OWORD *)(v2 + 11096) = xmmword_486CAB0;
*(_QWORD *)(v2 + 12136) = 0;
*(_DWORD *)(v2 + 12144) = 0;
do
{
*v5 = a1;
v6 = (void *)sub_6BBB7A0(1024, 4);
*(v5 - 1) = v6;
memset(v6, 0, 0x400u);
*((_DWORD *)v5 - 3) = 255;
v4 -= 128;
v5 += 16;
}
while ( v4 );
v7 = __strlen_chk("None", 5u);
*(_DWORD *)v2 = sub_6DA1220(a1, "None", v7);
v8 = __strlen_chk("ByteProperty", 0xDu);
*(_DWORD *)(v2 + 4) = sub_6DA1220(a1, "ByteProperty", v8);
v9 = __strlen_chk("IntProperty", 0xCu);
*(_DWORD *)(v2 + 8) = sub_6DA1220(a1, "IntProperty", v9);
v10 = __strlen_chk("BoolProperty", 0xDu);
*(_DWORD *)(v2 + 12) = sub_6DA1220(a1, "BoolProperty", v10);
v11 = __strlen_chk("FloatProperty", 0xEu);
*(_DWORD *)(v2 + 16) = sub_6DA1220(a1, "FloatProperty", v11);

在对这个函数进行交叉引用查找,传入的参数即为 GName

1
sub_6D9F41C((__int64)&unk_B171CC0);
1
2
3
4
5
6
.text:0000000006DA1C88                 LDRB            W8, [X22,#byte_B18CC80@PAGEOFF]
.text:0000000006DA1C8C TBNZ W8, #0, loc_6DA1CA4
.text:0000000006DA1C90 ADRL X0, unk_B171CC0
.text:0000000006DA1C98 BL sub_6D9F41C
.text:0000000006DA1C9C MOV W8, #1
.text:0000000006DA1CA0 STRB W8, [X22,#byte_B18CC80@PAGEOFF]

可知 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
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
Process name: com.tencent.ace.match2024, Pid: 30674
Base Address of libUE4.so Found At 79d20ce000
UWorld: 79dd3fb8a8 | World: 7915f3e830 | Name: FirstPersonExampleMap
Level: 79e4cdfd40 | Name: PersistentLevel
ActorList: 792bdabd80, ActorCount: 103

Id: 0, Addr: 7917be4300, Actor: WorldInfo
Id: 1, Addr: 7921cffd80, Actor: LightmassImportanceVolume
Id: 2, Addr: 791bbcfdc0, Actor: TemplateLabel
Id: 3, Addr: 79e4cdfa80, Actor: SkySphereBlueprint
Id: 4, Addr: 791d9ac100, Actor: AtmosphericFog
Id: 5, Addr: 791d9ab140, Actor: SphereReflectionCapture
Id: 6, Addr: 7915aed800, Actor: NetworkPlayerStart
Id: 7, Addr: 791d9abc80, Actor: LightSource
Id: 8, Addr: 7915f3d060, Actor: PostProcessVolume
Id: 9, Addr: 791d9ab380, Actor: SkyLight
Id: 10, Addr: 791d9af040, Actor: EditorCube8
Id: 11, Addr: 791d9aee00, Actor: EditorCube9
Id: 12, Addr: 791d9aa3c0, Actor: EditorCube10
Id: 13, Addr: 791d9aa180, Actor: EditorCube11
Id: 14, Addr: 791d9a9f40, Actor: EditorCube12
Id: 15, Addr: 791d9a9d00, Actor: EditorCube13
Id: 16, Addr: 791d9a9ac0, Actor: EditorCube14
Id: 17, Addr: 791d9a9880, Actor: EditorCube15
Id: 18, Addr: 791d9a9640, Actor: EditorCube16
Id: 19, Addr: 791d9a3340, Actor: EditorCube17
Id: 20, Addr: 791d9af940, Actor: EditorCube18
Id: 21, Addr: 791d9aebc0, Actor: Floor
Id: 22, Addr: 791bbcbc80, Actor: Wall1
Id: 23, Addr: 791bbcba40, Actor: Wall2
Id: 24, Addr: 791bbcb800, Actor: Wall3
Id: 25, Addr: 791bbcb5c0, Actor: Wall4
Id: 26, Addr: 791d9aacc0, Actor: BigWall
Id: 27, Addr: 791d9aaf00, Actor: BigWall2
Id: 28, Addr: 791bbcb380, Actor: Wall_400x400
Id: 29, Addr: 791bbcb140, Actor: Wall_400x401
Id: 30, Addr: 791bbcaf00, Actor: Wall_400x402
Id: 31, Addr: 791bbcacc0, Actor: Wall_400x403
Id: 32, Addr: 791bbcaa80, Actor: Wall_400x404
Id: 33, Addr: 791d9ab800, Actor: PointLight
Id: 34, Addr: 791d9ae980, Actor: Floor_400x400
Id: 35, Addr: 791d9ae740, Actor: Floor_400x401
Id: 36, Addr: 790f9137c0, Actor: TextRenderActor
Id: 37, Addr: 790f914540, Actor: TextRenderActor2
Id: 38, Addr: 790f914300, Actor: TextRenderActor3
Id: 39, Addr: 791d9ab5c0, Actor: 运行时虚拟纹理体积
Id: 40, Addr: 791bbc9880, Actor: Wall_Door_400x400
Id: 41, Addr: 791bbcce80, Actor: SM_Door
Id: 42, Addr: 790f913580, Actor: TriggerBox
Id: 43, Addr: 790f9140c0, Actor: TextRenderActor4
Id: 44, Addr: 791bbca840, Actor: Wall_400x405
Id: 45, Addr: 791bbca600, Actor: Wall_400x406
Id: 46, Addr: 791bbcd0c0, Actor: SM_MERGED_Shape_Pipe_Flag
Id: 47, Addr: 7915ee6980, Actor: Plane_Blueprint
Id: 48, Addr: 791d9aa600, Actor: Cube
Id: 49, Addr: 791d9aaa80, Actor: Cube2
Id: 50, Addr: 791d9aa840, Actor: Cube3
Id: 51, Addr: 791bbca3c0, Actor: Wall_400x407
Id: 52, Addr: 791bbca180, Actor: Wall_400x408
Id: 53, Addr: 790f913e80, Actor: TextRenderActor6
Id: 54, Addr: 791bbcfb80, Actor: TextRenderActor10
Id: 55, Addr: 791bbc9f40, Actor: Wall_400x409
Id: 56, Addr: 791bbc9d00, Actor: Wall_400x410
Id: 57, Addr: 791bbcf700, Actor: TextRenderActor12
Id: 58, Addr: 791bbcf4c0, Actor: TextRenderActor13
Id: 59, Addr: 791bbcf280, Actor: TextRenderActor14
Id: 60, Addr: 791bbcf040, Actor: TextRenderActor15
Id: 61, Addr: 791d9aba40, Actor: Actor
Id: 62, Addr: 791d9ae500, Actor: Shape_Pipe_Flag
Id: 63, Addr: 791d9ae080, Actor: Shape_Pipe_Flag
Id: 64, Addr: 791bbcee00, Actor: Shape_Pipe_Flag
Id: 65, Addr: 791bbcebc0, Actor: Shape_Pipe_Flag
Id: 66, Addr: 791bbce980, Actor: Shape_Pipe_Flag
Id: 67, Addr: 791bbce740, Actor: Shape_Pipe_Flag
Id: 68, Addr: 791bbce500, Actor: Shape_Pipe_Flag
Id: 69, Addr: 791bbce2c0, Actor: Shape_Pipe_Flag
Id: 70, Addr: 791bbccc40, Actor: Shape_Pipe_Flag
Id: 71, Addr: 791bbcc100, Actor: Shape_Pipe_Flag
Id: 72, Addr: 791bbcbec0, Actor: Shape_Pipe_Flag
Id: 73, Addr: 791bbcc580, Actor: Shape_Pipe_Flag
Id: 74, Addr: 791bbcc340, Actor: Shape_Pipe_Flag
Id: 75, Addr: 791bbcca00, Actor: Shape_Pipe_Flag
Id: 76, Addr: 791bbcc7c0, Actor: Shape_Pipe_Flag
Id: 77, Addr: 791bbcd300, Actor: Shape_Pipe_Flag
Id: 78, Addr: 791842b270, Actor: SM_MERGED_Shape_Pipe_Flag_1_Blueprint2
Id: 79, Addr: 791842ade0, Actor: SM_MERGED_Shape_Pipe_Flag_1_Blueprint3
Id: 80, Addr: 791842b700, Actor: SM_MERGED_Shape_Pipe_Flag_1_Blueprint
Id: 81, Addr: 791d9af700, Actor: EditorCube19
Id: 82, Addr: 791d9ae2c0, Actor: Shape_Pipe_Flag
Id: 83, Addr: 790f913c40, Actor: TextRenderActor8
Id: 84, Addr: 790f913a00, Actor: TextRenderActor9
Id: 85, Addr: 791bbcf940, Actor: TextRenderActor11
Id: 86, Addr: 790f9149c0, Actor: TextRenderActor16
Id: 87, Addr: 791bbc9ac0, Actor: Wall_400x411
Id: 88, Addr: 790f914780, Actor: TextRenderActor17
Id: 89, Addr: 7915aee980, Actor: DefaultPhysicsVolume
Id: 90, Addr: 79185ad600, Actor: FirstPersonGameMode_C
Id: 91, Addr: 7915d8c7c0, Actor: GameSession
Id: 92, Addr: 7918740d00, Actor: ParticleEventManager
Id: 93, Addr: 79185a0700, Actor: GameNetworkManager
Id: 94, Addr: 79184c2630, Actor: FirstPersonExampleMap_C
Id: 95, Addr: 7915f3e040, Actor: FirstPersonCharacter_C
Id: 96, Addr: 7915aeb780, Actor: GameStateBase
Id: 97, Addr: 790a434e10, Actor: AbstractNavData-Default
Id: 98, Addr: 790f9a7a20, Actor: PlayerController
Id: 99, Addr: 792bddeb00, Actor: PlayerState
Id: 100, Addr: 790f838020, Actor: PlayerCameraManager
Id: 101, Addr: 7917e86140, Actor: CameraActor
Id: 102, Addr: 7912eefc80, Actor: FirstPersonHUD_C

根据这个地址写 frida 脚本锁血

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
const playerAddressStr = "0x7915f3e040";
const HP_Offset = 0x510;
const playerAddress = ptr(playerAddressStr);
const hpPtr = playerAddress.add(HP_Offset);

function lockHPDirectly() {
try {
if (Process.findRangeByAddress(playerAddress)) {

const currentHP = hpPtr.readFloat();

hpPtr.writeFloat(999999.0);

if (currentHP < 900000) {
console.log(`[!] Detected player [${Target_Actor_Name}] at address: ${playerAddress}`);
console.log(`[!] HP changed from ${currentHP} to locked value`);
}
} else {
console.log("[-] Player address is not valid in the current process memory space.");
}
} catch (e) {
// console.log("[-] Error accessing player address: " + e.message);
}
}

console.log(`[*] Script started, target address: ${playerAddressStr}`);
setInterval(lockHPDirectly, 500);

但是这样的话每次游戏重新开始地址都会变化,需要重新获取地址,不够方便,所以下面就把获取地址的逻辑和锁血逻辑结合起来写在一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
UCLASS(customConstructor, config=Engine)
class ENGINE_API UWorld final : public UObject, public FNetworkNotify
{
GENERATED_UCLASS_BODY()

~UWorld();

#if WITH_EDITORONLY_DATA
/** List of all the layers referenced by the world's actors */
UPROPERTY()
TArray< class ULayer* > Layers;

// Group actors currently "active"
UPROPERTY(Transient)
TArray<AActor*> ActiveGroupActors;

/** Information for thumbnail rendering */
UPROPERTY(VisibleAnywhere, Instanced, Category=Thumbnail)
class UThumbnailInfo* ThumbnailInfo;
#endif // WITH_EDITORONLY_DATA

/** Persistent level containing the world info, default brush and actors spawned during gameplay among other things */
UPROPERTY(Transient)
class ULevel* PersistentLevel;

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 开始摆放,而 PersistentLevelUWorld 内部定义的第一个有效成员变量,故 PersistentLevel 偏移为 0x30

在 dump 出的 SDK 中也能验证到

1
2
Class: World.Object
Level* PersistentLevel;//[Offset: 0x30, Size: 0x8]
  • ActorList Offset(相对于 Level):0x98

UnrealEngine/Engine/Source/Runtime/Engine/Classes/Engine/Level.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UCLASS(MinimalAPI)
class ULevel : public UObject, public IInterface_AssetUserData
{
GENERATED_BODY()

public:

/** URL associated with this level. */
FURL URL;

/** Array of all actors in this level, used by FActorIteratorBase and derived classes */
TArray<AActor*> Actors;

/** Array of actors to be exposed to GC in this level. All other actors will be referenced through ULevelActorContainer */
TArray<AActor*> ActorsForGC;

/** Set before calling LoadPackage for a streaming level to ensure that OwningWorld is correct on the Level */
ENGINE_API static TMap<FName, TWeakObjectPtr<UWorld> > StreamedLevelsOwningWorld;

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 的解析,可以参考这两篇文章,已经写得足够详细

[原创]UE4.27SDK-Dump

ue4游戏逆向之GName内存解析(4.23版本及其以上)

执行锁血完整脚本

1
frida -U -f com.tencent.ace.match2024 -l ./lock_hp.js
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
const GWorld_Offset = 0xB32D8A8;
const GName_Offset = 0xB171CC0;
const HP_Offset = 0x510; // 生命值偏移
const Target_Actor_Name = "FirstPersonCharacter_C";

function lockHP() {
const mod = Process.findModuleByName("libUE4.so");
if (!mod) {
console.log("[-] Mod not found!");
return;
}
const libBase = mod.base;
// console.log("[+] BaseAddr: " + libBase);

// 定位 GWorld 和 GName
const GName = libBase.add(GName_Offset);
const GWorldPtr = libBase.add(GWorld_Offset).readPointer();

if (GWorldPtr.isNull()) {
console.log("[-] GWorld is null");
return;
}

// 遍历 Actor 列表寻找玩家
// 层级: GWorld -> PersistentLevel -> ActorList
const Level = GWorldPtr.add(0x30).readPointer();
const Actors = Level.add(0x98).readPointer();
const ActorsCount = Level.add(0xA0).readU32(); // 0x98+8 = 0xA0

let playerAddress = null;

for (let i = 0; i < ActorsCount; i++) {
const actor = Actors.add(i * 8).readPointer(); //64 位系统中,一个内存地址(指针)占 8 字节
if (actor.isNull()) continue;

// 获取 FNameIndex
const nameId = actor.add(0x18).readU32();
const name = getFNameFromID(GName, nameId);

if (name === Target_Actor_Name) {
playerAddress = actor;
// console.log(`[+] Find TargetPlayer: ${name} at ${playerAddress}`);
break;
}
}

// 执行锁血
if (playerAddress) {
const hpPtr = playerAddress.add(HP_Offset);
const currentHP = hpPtr.readFloat();

hpPtr.writeFloat(999999.0);
if (currentHP < 900000) {
console.log(`[!] Detected player [${Target_Actor_Name}] at address: ${playerAddress}`);
console.log(`[!] HP changed from ${currentHP} to locked value`);
}
}
}

function getFNameFromID(GNameAddr, index) {
try {
const Block = index >> 16;
const Offset = index & 65535;
const FNamePool = GNameAddr.add(0x30); // FNamePool 偏移
const NamePoolChunk = FNamePool.add(0x10 + Block * 8).readPointer();
const FNameEntry = NamePoolChunk.add(Offset * 2);

const FNameEntryHeader = FNameEntry.readU16();
const str_length = FNameEntryHeader >> 6;

if (str_length > 0 && str_length < 255) {
return FNameEntry.add(2).readUtf8String(str_length);
}
} catch (e) {
return null;
}
return null;
}
console.log("[*] Script started...");
setInterval(lockHP, 500); // 每 500 毫秒检查并锁定一次

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook_hit(){
const mod = Process.findModuleByName("libUE4.so");
if (!mod) {
console.log("[-] Mod not found!");
return;
}
let libBase = mod.base;
console.log("[*] libUE4.so base address: " + libBase);
let TargetOffset = 0x5e93094;
let TargetAddr = libBase.add(TargetOffset);
Memory.protect(TargetAddr, 0x1000, 'rwx');
TargetAddr.writeByteArray([0xC0, 0x03, 0x5F, 0xD6]); // ret
}

setImmediate(hook_hit,0);

后面了解到 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Interceptor.attach(TargetAddr, {
onEnter: function(args) {
// args[0] 即 this 指针
var actorPtr = args[0];

console.log(" FirstPersonCharacter 实例地址: " + actorPtr);

// 真实函数地址
var vtable = actorPtr.readPointer();
var realFunc = vtable.add(0x8B0).readPointer();
console.log("该实例对应的真实 Native 函数地址: " + realFunc);
console.log("Function Offset: " + realFunc.sub(libBase));

}
});

找到偏移地址对应的函数 sub_5E843D4

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
__int64 __fastcall sub_5E843D4(
__int64 a1,
__int64 a2,
__int64 a3,
__int64 a4,
char a5,
__int64 a6,
float a7,
float a8,
float a9,
float a10,
float a11,
float a12,
__int64 a13,
__int64 a14,
__int64 a15,
int a16)
{
__int128 v16; // q1
__int128 v17; // q2
__int128 v18; // q3
__int128 v19; // q0
__int128 v20; // q2
__int128 v21; // q1

*(_QWORD *)(a1 + 1496) = a2;
*(_QWORD *)(a1 + 1504) = a3;
*(_QWORD *)(a1 + 1512) = a4;
*(float *)(a1 + 1524) = a7;
*(float *)(a1 + 1528) = a8;
*(float *)(a1 + 1532) = a9;
*(float *)(a1 + 1536) = a10;
*(float *)(a1 + 1540) = a11;
*(float *)(a1 + 1544) = a12;
*(_BYTE *)(a1 + 1520) = a5 & 1;
*(_QWORD *)(a1 + 1548) = a15;
*(_DWORD *)(a1 + 1556) = a16;
*(_OWORD *)(a1 + 1560) = *(_OWORD *)a6;
v16 = *(_OWORD *)(a6 + 64);
v17 = *(_OWORD *)(a6 + 16);
v18 = *(_OWORD *)(a6 + 32);
*(_OWORD *)(a1 + 1608) = *(_OWORD *)(a6 + 48);
*(_OWORD *)(a1 + 1624) = v16;
*(_OWORD *)(a1 + 1576) = v17;
*(_OWORD *)(a1 + 1592) = v18;
v20 = *(_OWORD *)(a6 + 96);
v19 = *(_OWORD *)(a6 + 112);
v21 = *(_OWORD *)(a6 + 80);
*(_QWORD *)(a1 + 1688) = *(_QWORD *)(a6 + 128);
*(_OWORD *)(a1 + 1656) = v20;
*(_OWORD *)(a1 + 1672) = v19;
*(_OWORD *)(a1 + 1640) = v21;
return sub_5E840C8();
}

进入最后 return 的函数 sub_5E840C8

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
_DWORD *__fastcall sub_5E840C8(__int64 a1)
{
int v1; // w9
float v3; // s0
_DWORD *result; // x0
float v5; // s0
void *dest; // [xsp+8h] [xbp-28h] BYREF
__int64 v7; // [xsp+10h] [xbp-20h]
__int64 v8; // [xsp+18h] [xbp-18h]

v8 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v3 = *(float *)(a1 + 1296);
result = *(_DWORD **)(a1 + 1312);
v5 = v3 + -100.0;
*(float *)(a1 + 1296) = v5;
if ( result
&& (*(_BYTE *)(*(_QWORD *)(qword_B1B5FA8 + 8LL * (result[3] / 0x10000))
+ 24LL * (int)(result[3] - (v1 & 0xFFFF0000))
+ 11) & 0x20) == 0 )
{
result = (_DWORD *)(*(__int64 (__fastcall **)(_DWORD *))(*(_QWORD *)result + 1184LL))(result);
v5 = *(float *)(a1 + 1296);
}
if ( v5 <= 0.0 )
{
dest = 0LL;
v7 = 0LL;
sub_5E65368(&dest, 13LL);
LODWORD(v7) = v7 + 13;
if ( (int)v7 > SHIDWORD(v7) )
sub_5E653F0(&dest);
j__memcpy_0(dest, L"RestartLevel", 0x1AuLL);
sub_9092010(a1, &dest, 0LL);
result = dest;
if ( dest )
return (_DWORD *)sub_6BBC3B8();
}
return result;
}

-100.0 可以发现这里就是真正的扣血逻辑所在函数

1
2
3
.text:0000000005E840F0 08 59 B8 52                 MOV             W8, #0xC2C80000
.text:0000000005E840F4 01 01 27 1E FMOV S1, W8
.text:0000000005E840F8 00 28 21 1E FADD S0, S0, S1

可以直接 nop 掉扣血处的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function nop_hpsub(){
const mod = Process.findModuleByName("libUE4.so");
if (!mod) {
console.log("[-] Mod not found!");
return;
}
let libBase = mod.base;
console.log("[*] libUE4.so base address: " + libBase);
let TargetOffset = 0x5E840F8;
let TargetAddr = libBase.add(TargetOffset);
Memory.protect(TargetAddr, 0x1000, 'rwx');
TargetAddr.writeByteArray([0x1F, 0x20, 0x03, 0xD5]); // nop
}
setImmediate(nop_hpsub,0);

Method 3

修改位置坐标,但是只成功实现了瞬间的传送,很快又会被拉回原始位置

在 SDK 中找到获取玩家位置的偏移

1
2
Vector K2_GetActorLocation();// 0x965ddf8
bool K2_SetActorLocation(Vector NewLocation, bool bSweep, out HitResult SweepHitResult, bool bTeleport);// 0x965dc3c

在 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
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
function getActorLocation(actor_addr){
const base = getModuleBase();
if (!base) return;
const actorPtr = ptr(actor_addr);
let buf = Memory.alloc(0x100);
let f_addr = base.add(0x965ddf8);
let getLocationFunc = new NativeFunction(f_addr, 'void', ['pointer','pointer','pointer']);
getLocationFunc(actorPtr,buf,buf);
}

function setActorLocation(actor_addr,x,y,z){
const base = getModuleBase();
if (!base) return;
const actorPtr = ptr(actor_addr);
let f_addr = base.add(0x8C3181C);
let setLocationFunc = new NativeFunction(f_addr, 'bool', ['pointer','bool','pointer','bool','float','float','float']);
setLocationFunc(actorPtr,0,ptr(0),0,x,y,z);


}

function location_change(){
const p = lockHP();
if (!p) {
console.log("[-] playerAddress is null 未找到目标 Actor");
return;
}
getActorLocation(p);
setActorLocation(p,1000,500,500);
}

setImmediate(location_change,500);

逃出房间

最终成功出去

section1

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

猜测是透明度的原因

在 sdk 的三个不同类中看到了和透明度相关的函数

1
2
3
4
5
6
7
8
Class: SceneComponent.ActorComponent.Object
void SetVisibility(bool bNewVisibility, bool bPropagateToChildren);// 0x9910794

Class: Widget.Visual.Object
void SetVisibility(enum InVisibility);// 0x89170ec

Class: MovieSceneLevelVisibilitySection.MovieSceneSection.MovieSceneSignedObject.Object
void SetVisibility(enum InVisibility);// 0x877e000

其中 SceneComponent::SetVisibility 是用于控制游戏世界中 3D 物体是否被渲染的函数,它的参数 bNewVisibility 为 true 时,组件可见,参数 bPropagateToChildren 是一个层级传播参数,为 true 时不仅改变当前组件的可见性,还会递归地将此设置应用到所有挂载在它下面的子组件

Widget::SetVisibility 是用于控制 2D UI 元素 的显示、布局占用以及交互能力的函数

MovieSceneLevelVisibilitySection::SetVisibility 是用于在 过场动画时间轴中,控制整个关卡的加载或显示状态

所以选择 hook SceneComponent::SetVisibility 来实现透明度的修改,再把之前锁血的功能也结合在一起

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
const GWorld_Offset = 0xB32D8A8;
const GName_Offset = 0xB171CC0;

const HP_Offset = 0x510;
const Target_Actor_Name = "FirstPersonCharacter_C";

const SetVisibility_Offset = 0x8E619BC;
const GetActorLocation_Offset = 0x965ddf8; // Vector K2_GetActorLocation();// 0x965ddf8
const RootComponent_Offset = 0x130; // SceneComponent* RootComponent;//[Offset: 0x130, Size: 0x8]

let libBase = null;
function get_base_addr(){
const mod = Process.findModuleByName("libUE4.so");
if (!mod) {
console.log("[-] Mod not found!");
return;
}
const libBase = mod.base;
console.log("[*] libUE4.so base address: " + libBase);
return libBase;
}

let NativeFuncs = null;
function init_native_functions(){
if(NativeFuncs) return NativeFuncs;
const BaseAddr = get_base_addr();
if(!BaseAddr) return null;

try{
const SetVisibilityPtr = BaseAddr.add(SetVisibility_Offset);
const SetVisibility = new NativeFunction(SetVisibilityPtr,'void',['pointer','int','int']);

const GetActorLocationPtr = BaseAddr.add(GetActorLocation_Offset);
const GetActorLocation = new NativeFunction(GetActorLocationPtr,'void',['pointer','pointer','pointer']);

NativeFuncs = { SetVisibility, GetActorLocation };
return NativeFuncs;
}catch(e){
console.log("[-] Init functions failed: " + e);
return null;
}
}

function getFNameFromID(GNameAddr, index) {
try {
const Block = index >> 16;
const Offset = index & 65535;
const FNamePool = GNameAddr.add(0x30); // FNamePool 偏移
const NamePoolChunk = FNamePool.add(0x10 + Block * 8).readPointer();
const FNameEntry = NamePoolChunk.add(Offset * 2);

const FNameEntryHeader = FNameEntry.readU16();
const str_length = FNameEntryHeader >> 6;

if (str_length > 0 && str_length < 255) {
return FNameEntry.add(2).readUtf8String(str_length);
}
} catch (e) {
return null;
}
return null;
}

function main(){
const BaseAddr = get_base_addr();
const funcs = init_native_functions();
if(!BaseAddr || !funcs) return;


const GWorldPtr = BaseAddr.add(GWorld_Offset).readPointer();
const GNamePtr = BaseAddr.add(GName_Offset);

if(GWorldPtr.isNull()) return;

const PersistentLevel = GWorldPtr.add(0x30).readPointer();
const ActorArray = PersistentLevel.add(0x98).readPointer();
const ActorCount = PersistentLevel.add(0xA0).readU32();

console.log("[+] Found " + ActorCount + " actors.");

const locBuffer = Memory.alloc(0x20);

for(let i = 0; i < ActorCount; i++){
const actor = ActorArray.add(i * 8).readPointer();
if(actor.isNull()) continue;

try{
const nameId = actor.add(0x18).readU32();
const name = getFNameFromID(GNamePtr, nameId);

if (name === Target_Actor_Name) {
const hpPtr = actor.add(HP_Offset);
const currentHP = hpPtr.readFloat();
if (currentHP < 900000) {
hpPtr.writeFloat(999999.0);
console.log(`[+] Locked HP for ${name} (Addr: ${actor})`);
}
}

funcs.GetActorLocation(actor,locBuffer,locBuffer);
const z = locBuffer.add(8).readFloat();
if(z > 2000.0 && name !== Target_Actor_Name){
const rootComponent = actor.add(RootComponent_Offset).readPointer();
if(!rootComponent.isNull()){
// Component*, bNewVisibility=1 (True), bPropagateToChildren=1 (True)
funcs.SetVisibility(rootComponent,1,1);
const x = locBuffer.readFloat();
const y = locBuffer.add(4).readFloat();
console.log(`[+] Revealed hidden actor at (${x}, ${y}, ${z})`);
// console.log(`[!] Revealing Hidden Actor at Z=${z.toFixed(0)}`);
}
}

}catch(e){

}
}
console.log("[*] Reveal complete.");
}


console.log("[*] Script loaded. Will execute reveal in 3 seconds...");

setTimeout(function() {
console.log("[*] Starting main process...");
setInterval(main, 1000);
main();
}, 3000);

可知 section1 为 8939

section2

提示说让立方体变得不可穿透,先问 ai 了解到 SetCollisionEnabled

在 SDK 中找到该函数

1
void SetCollisionEnabled(byte NewType);// 0x98edb3c

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
2
3
4
5
void SetCollisionResponseToChannel(byte Channel, byte NewResponse);// 0x98eb640
void SetCollisionResponseToAllChannels(byte NewResponse);// 0x98eb590
void SetCollisionProfileName(FName InCollisionProfileName, bool bUpdateOverlaps);// 0x98eda38
void SetCollisionObjectType(byte Channel);// 0x98ed954
void SetCollisionEnabled(byte NewType);// 0x98edb3c

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
2
3
4
5
Class: TriggerBase.Actor.Object
ShapeComponent* CollisionComponent;//[Offset: 0x220, Size: 0x8]

--------------------------------
Class: TriggerBox.TriggerBase.Actor.Object
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
const ShapeComponent_Offset   = 0x220; //ShapeComponent* CollisionComponent;//[Offset: 0x220, Size: 0x8]

function set_Collision_VTable(ActorPtr){
try{
const ShapeComponentPtr = ActorPtr.add(ShapeComponent_Offset).readPointer();
if(ShapeComponentPtr.isNull()) return;

const VTable = ShapeComponentPtr.readPointer();
if(VTable.isNull()) return;

const FuncAddr_SetResponse = VTable.add(0x850).readPointer();
const SetCollisionResponse = new NativeFunction(FuncAddr_SetResponse, 'void', ['pointer', 'uint8']);

const FuncAddr_SetEnabled = VTable.add(0x660).readPointer();
const SetCollisionEnabled = new NativeFunction(FuncAddr_SetEnabled, 'void', ['pointer', 'uint8']);

SetCollisionResponse(ShapeComponentPtr, 2); // 对应 ECollisionResponse::ECR_Block:阻挡所有射线和物理碰撞

SetCollisionEnabled(ShapeComponentPtr, 3); // 对应 ECollisionEnabled::QueryAndPhysic:既挡路又检测射线

// console.log(`[+] Collision Enabled for Actor: ${ActorPtr} via VTable`);

}catch(e){
console.log(`[-] set_Collison_VTable error: ${e}`);
}
}

if (Collision_Target_Names.includes(name) || name.indexOf("Cube") !== -1) {
set_Collision_VTable(actor);
}

所以 section2 是 008

section3

在 Objects.txt 搜索 flag 看到了 getlastflag

1
2
3
4
5
[0x1f19]:
Name: getlastflag
Class: Function
ObjectPtr: 0x79275d4a00
ClassPtr: 0x794ba15880
1
2
Class: MyActor.Actor.Object
bool getlastflag();// 0x6a91fec

在 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
27
28
29
30
31
32
33
34
35
36
37
38
__int64 __fastcall sub_6A91FEC(__int64 a1, __int64 a2, _BYTE *a3)
{
__int64 v3; // x8
__int64 result; // x0

v3 = *(_QWORD *)(a2 + 32);
if ( v3 )
++v3;
*(_QWORD *)(a2 + 32) = v3;
result = ((__int64 (*)(void))loc_6A91A54)();
*a3 = result & 1;
return result;
}


__int64 sub_6A91A40()
{
char *v0; // x0
void *v1; // x0
void *v2; // x19
char *v3; // x0
__int64 (*v5)(void); // [xsp+8h] [xbp-8h]

sub_8C2D494();
v0 = (char *)malloc(0xBuLL);
strcpy(v0, "libplay.so");
v1 = dlopen(v0, 2);
if ( !v1 )
return 0LL;
v2 = v1;
v3 = (char *)malloc(0xEuLL);
strcpy(v3, "get_last_flag");
v5 = (__int64 (*)(void))dlsym(v2, v3);
if ( !dlerror() )
return 0LL;
strcpy((char *)malloc(0x21uLL), "input your flag in this variable");
return v5();
}

在 libplay.so 中找到 get_last_flag 函数

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
void __fastcall get_last_flag(__int64 a1)
{
__int64 v2; // x19
__int64 v3; // x0
__int64 v4; // kr00_8
__int128 v5; // [xsp+0h] [xbp-30h]
__int16 v6; // [xsp+10h] [xbp-20h]
__int64 v7; // [xsp+28h] [xbp-8h]

v7 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( !atomic_load(&dword_3E6C) )
{
LOBYTE(xmmword_3E50) = xmmword_3E50 ^ 0xD2;
BYTE1(xmmword_3E50) ^= 0x94u;
BYTE2(xmmword_3E50) ^= 0x5Au;
BYTE3(xmmword_3E50) ^= 0xC1u;
BYTE4(xmmword_3E50) ^= 0x35u;
BYTE5(xmmword_3E50) ^= 0x85u;
BYTE6(xmmword_3E50) ^= 0x71u;
BYTE7(xmmword_3E50) ^= 0xBCu;
BYTE8(xmmword_3E50) ^= 0x71u;
BYTE9(xmmword_3E50) ^= 0x55u;
BYTE10(xmmword_3E50) ^= 0x5Bu;
BYTE11(xmmword_3E50) ^= 0xE7u;
BYTE12(xmmword_3E50) ^= 0x84u;
BYTE13(xmmword_3E50) ^= 0xEAu;
BYTE14(xmmword_3E50) ^= 0xA3u;
HIBYTE(xmmword_3E50) ^= 0x72u;
LOBYTE(word_3E60) = word_3E60 ^ 0x71;
HIBYTE(word_3E60) ^= 0x61u;
byte_3E30 ^= 0xC8u;
byte_3E31 ^= 0x17u;
byte_3E32 ^= 0x81u;
byte_3E33 ^= 0xB1u;
byte_3E34 ^= 0xB7u;
byte_3E35 ^= 0x63u;
byte_3E36 ^= 0x7Bu;
byte_3E37 ^= 0x34u;
byte_3E38 ^= 0xEDu;
byte_3E39 ^= 0xF2u;
byte_3E3A ^= 0xB7u;
byte_3E3C ^= 0x47u;
byte_3E3B ^= 0x45u;
byte_3E3D ^= 0x1Cu;
byte_3E3E ^= 0xE3u;
byte_3E3F ^= 0xA2u;
byte_3E40 ^= 0x43u;
byte_3E41 ^= 0xEFu;
byte_3E42 ^= 0x97u;
byte_3E43 ^= 0x9Cu;
byte_3E44 ^= 0xF7u;
byte_3E45 ^= 0xA6u;
byte_3E46 ^= 0xC4u;
byte_3E47 ^= 0x76u;
}
atomic_store(1u, &dword_3E6C);
v5 = xmmword_3E50;
v6 = word_3E60;
v2 = sub_192C(a1);
v3 = sub_1970();
sub_19BC(v2, v3);
v4 = (__int64)*(&off_3DB0 + (sub_1A00() == 24));
__asm { BR X8 }
}

上面有字符串异或和间接跳转

用 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
50
00401458    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
22
00400e4c    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
93
00400eec    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
26
import 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}

参考文章:

2024 腾讯游戏安全大赛 mobile 初赛 wp

腾讯游戏安全2024初赛安卓复现

2024腾讯游戏安全竞赛安卓赛道初赛复现


腾讯游戏安全大赛 2024 安卓初赛题解
http://example.com/2026/01/19/TxGame2024_pre/
作者
Eleven
发布于
2026年1月19日
许可协议