腾讯游戏安全大赛 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
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 | |
可知 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
109Process 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
27const 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);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
24UCLASS(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
开始摆放,而 PersistentLevel 是 UWorld
内部定义的第一个有效成员变量,故 PersistentLevel 偏移为
0x30
在 dump 出的 SDK 中也能验证到 1
2Class: 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
18UCLASS(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 | |
Method 2
另一种思路是直接 hook 掉扣血的函数