腾讯游戏安全大赛 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 掉扣血的函数


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