2024腾讯游戏安全竞赛Andorid初赛

需要附件请直接联系我

准备工作

image-20260401145311334

寻找Gname

image-20260401103220796

0xB171CC0

寻找Gworld

image-20260401110155687

0xB32D8A8

寻找GUobject

image-20260401110308956

0xB1B5F98

DumpSDK

1
2
3
4
./ue4dumper64 --strings --newue+ --gname 0xB171CC0 --package com.tencent.ace.match2024 --output /data/local/tmp
./ue4dumper64 --objs --newue+ --gname 0xB171CC0 --guobj 0xB1B5F98 --package com.tencent.ace.match2024 --output /data/local/tmp
./ue4dumper64 --sdku --newue+ --gname 0xB171CC0 --guobj 0xB1B5F98 --package com.tencent.ace.match2024 --output /data/local/tmp --verbose
./ue4dumper64 --sdkw --newue+ --gname 0xB171CC0 --gworld 0xB32D8A8 --package com.tencent.ace.match2024 --output /data/local/tmp

出门

玩游戏发现多个问题,比如触碰墙壁马上嘶,碰门嘶,所以绕过方式有

  1. 把触碰墙壁门,扣血的逻辑nop

  2. 把血量hook为满血

  3. 瞬移

  4. 穿墙

Strings.txt 中搜索 /Game/ 路径,找到所有游戏自定义资产

1
2
3
4
5
/Game/FirstPersonBP/Blueprints/FirstPersonCharacter     ← 玩家角色                                                               
/Game/FirstPersonBP/Blueprints/FirstPersonHUD ← HUD
/Game/FirstPersonBP/Blueprints/FirstPersonProjectile ← 投射物(子弹)
/Game/FirstPersonBP/Blueprints/FirstPersonGameMode ← 游戏模式
/Game/FirstPerson/Audio/HP ← HP 相关

HP 和 HP_C 出现在 Audio 路径下但实际是个 UserWidget(UI控件),这是关键线索

在 sdku.txt 中搜索 Class: HP_C、Class: FirstPersonCharacter_C、Class: FirstPersonExampleMap_C,找到三个核心类:

1
2
3
4
HP_C (血量UI)
Class: HP_C.UserWidget.Widget.Visual.Object
ProgressBar* ProgressBar; // 0x268 — 血条进度条
void 更新生命值(float xxx); // 0x5e94adc — 关键函数

image-20260401160108636

函数名是中文 更新生命值,直接暴露了功能。UE4 Blueprint 编译后会保留原始函数名。

1
2
3
4
5
FirstPersonCharacter_C (玩家角色):
Class: FirstPersonCharacter_C.Character.Pawn.Actor.Object
float 生命值; // 0x510 — 角色血量字段
HP_C* 生命值UI; // 0x520 — 引用HP_C控件
void ReceiveHit(); // 0x5e93094 — 碰撞事件

看到 float 生命值在偏移 0x510,加上 ReceiveHit 事件处理函数 ,这是血量存储和伤害入口

1
2
3
4
5
6
7
8
9
FirstPersonExampleMap_C (关卡脚本):
Class: FirstPersonExampleMap_C.LevelScriptActor.Actor.Object
float 生命值; // 0x230 — 关卡也存了一份血量!
// 绑定事件(墙壁碰撞):
BndEvt__Cube2_ActorHitSignature // Cube2 碰撞
BndEvt__Cube_3_ActorHitSignature // Cube_3 碰撞
BndEvt__Cube3_ActorHitSignature // Cube3 碰撞
// 大门事件:
BndEvt__SM_Door_69_TakeAnyDamageSignature // 大门受损事件

这里有两个重要发现:
1. 关卡脚本也有生命值字段 → 血量可能在两处同步
2. Cube2/Cube_3/Cube3 绑了 ActorHit 事件 → 碰墙掉血的来源
3. SM_Door_69 绑了 TakeAnyDamage → 门需要被射击才能打开

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
var moduleName = "libUE4.so";
var base = null;

// ==================== Offsets ====================
var CHAR_HEALTH_OFFSET = 0x510; // FirstPersonCharacter_C.生命值 (float)
var CHAR_HP_UI_OFFSET = 0x520; // FirstPersonCharacter_C.生命值UI (HP_C*)
var LEVEL_HEALTH_OFFSET = 0x230; // FirstPersonExampleMap_C.生命值 (float)
var UOBJECT_OUTER_OFFSET = 0x20; // UObject.OuterPrivate
var LEVEL_SCRIPT_OFFSET = 0xe8; // ULevel.LevelScriptActor
var CONTROLLER_OFFSET = 0x258; // Pawn.Controller
var DOOR_OFFSET = 0x410; // FirstPersonExampleMap_C -> SM_Door_69*
var CAPSULE_OFFSET = 0x290; // Character.CapsuleComponent
var MOV_COMP_OFFSET = 0x288; // Character.CharacterMovementComponent
// vtable offsets (byte offset, not index)
var VT_SET_COLLISION_ENABLED = 1632; // PrimitiveComponent vtable offset for SetCollisionEnabled
var VT_SET_MOVEMENT_MODE = 1496; // CharacterMovementComponent vtable offset for SetMovementMode

// ==================== Function Addresses ====================
var CHAR_BEGIN_PLAY = 0x5e92c58; // FirstPersonCharacter_C::ReceiveBeginPlay
var CHAR_RECEIVE_HIT = 0x5e93094; // FirstPersonCharacter_C::ReceiveHit
var HP_UPDATE_HEALTH = 0x5e94adc; // HP_C::更新生命值
var APPLY_DAMAGE_IMPL = 0x8FAC0EC; // UGameplayStatics::ApplyDamage (actual C++ impl)
var SET_COLLISION_IMPL = 0x8C21320; // AActor::SetActorEnableCollision (actual C++ impl)

// ==================== Config ====================
var MAX_HEALTH = 99999.0;

// ==================== Global State ====================
var charPtr = null;
var levelScriptPtr = null;
var doorPtr = null;
var controllerPtr = null;
var healthTimer = null;

hook血量

0x5e94adc(更新生命值)

image-20260401184749627

原函数:

1
2
3
4
5
// 原函数:解包碰撞参数,调用虚表处理伤害
int64 ReceiveHit(void* self, void* frame) {
// 从 frame 中解包 8 个参数...
// 调用 vtable[278](self, MyComp, Other, ...) → 执行扣血逻辑
}

new NativeCallback(function(self, frame) { … }, “uint64”, [“pointer”, “pointer”])

这是创建一个替代函数,签名必须和原函数一致

1
2
3
4
5
Interceptor.attach(base.add(HP_UPDATE_HEALTH), {
onEnter: function (args) {
setHealth();
}
});
1
2
3
4
5
6
7
8
9
10
function setHealth() {
try {
if (charPtr && !charPtr.isNull()) {
charPtr.add(CHAR_HEALTH_OFFSET).writeFloat(MAX_HEALTH);
}
if (levelScriptPtr && !levelScriptPtr.isNull()) {
levelScriptPtr.add(LEVEL_HEALTH_OFFSET).writeFloat(MAX_HEALTH);
}
} catch (e) {}
}

levelScriptPtr 是 FirstPersonExampleMap_C 的实例指针,也就是关卡脚本 Actor

撞墙扣血绕过

1
2
3
4
5
float v6 = 0.0;
// 从Blueprint帧中读取float参数到v6
sub_6FCBF2C(a2, a2[3], &v6);
// 通过虚表调用实际UI更新
return vtable[1184/8](a1, v6); // a1=HP_C*, v6=血量值

这是个 Blueprint thunk,从帧中提取 float 参数,调用虚表函数更新 UI

ReceiveHit (0x5e93094)

1
2
3
// 解包8个参数(MyComp, Other, OtherComp, bSelfMoved, HitLocation...)
// 最后调用虚表函数处理碰撞逻辑
return vtable[2224/8](a1, params...);

解包参数后分发到 ExecuteUbergraph。NOP 掉它就不会处理任何碰撞事件。

反编译 ExecuteUbergraph (0x6fcd294):

image-20260401185124006

这是 UE4 Blueprint 虚拟机解释器,执行字节码指令。关卡脚本的所有事件(Cube碰撞、门事件)都通过这个 VM 执行,无法直接反编译出具体逻辑

路径1: 角色碰墙 → Character::ReceiveHit (0x5e93094) → ExecuteUbergraph → 减血

路径2: 墙壁被碰 → LevelScript::BndEvt__Cube*_ActorHit → Blueprint VM → 减血

路径1可以通过 NOP ReceiveHit 阻断,但路径2走的是 Blueprint VM,没法单独 hook。

要运行时找到对象实例,利用 UE4 的对象布局:

1
2
3
4
5
UObject 头部 (0x28 bytes):
0x00: vtable*
0x10: UClass*
0x18: FName
0x20: OuterPrivate* ← Actor 的 Outer 指向 ULevel
1
2
3
4
5
6
7
8
9
10
11
12
// ============================================================
// Hook 2: NOP ReceiveHit — 屏蔽角色碰撞伤害事件
// ============================================================
try {
Interceptor.replace(base.add(CHAR_RECEIVE_HIT), new NativeCallback(function (self, frame) {
setHealth();
return 0;
}, "uint64", ["pointer", "pointer"]));
console.log("[+] Character ReceiveHit NOP'd at: " + base.add(CHAR_RECEIVE_HIT));
} catch (e) {
console.log("[-] Failed to hook ReceiveHit: " + e);
}

image-20260401202455375

这里return 0返回未碰撞

门自动受损(自动开)

image-20260401202909468

1
2
3
4
5
6
7
8
// sdku.txt 第 31566 行
void BndEvt__FirstPersonExampleMap_SM_Door_69_K2Node_ActorBoundEvent_0_TakeAnyDamageSignature__DelegateSignature(
Actor* DamagedActor,
float Damage,
const DamageType* DamageType,
Controller* InstigatedBy,
Actor* DamageCauser
);

image-20260401202941085

UE4 的 Actor 有几种碰撞/伤害委托,名字就是签名:

  • ActorHitSignature → 物理碰撞
  • ActorBeginOverlapSignature → 进入重叠区域
  • TakeAnyDamageSignature → 受到伤害

门绑的是 TakeAnyDamage 而不是 ActorHit 或 BeginOverlap,说明它不是碰一下就触发,也不是走进去就触发,而是必须对它造成伤害才会触发事件处理。

在标准 UE4 FirstPerson 模板里,造成伤害的方式就是射击(发射 Projectile 命中目标 → ApplyDamage)。所以我判断是射击开门。

后来发现角色的 Fire 输入被删了,只能改为从 Frida 调用 ApplyDamage 来代替射击。

1
2
3
4
setTimeout(function () {
console.log("[*] Auto-attempting door damage...");
damageDoor(9999.0);
}, 2000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 调用 ApplyDamage 对门造成伤害,触发 TakeAnyDamage 事件
function damageDoor(damage) {
if (!resolveDoorPointers()) return;
damage = damage || 100.0;

// sub_8FAC0EC(DamagedActor, EventInstigator, DamageCauser, DamageTypeClass, BaseDamage)
// ARM64 ABI: X0=actor, X1=controller, X2=character, X3=damageType(null), S0=damage
var applyDamage = new NativeFunction(
base.add(APPLY_DAMAGE_IMPL),
"pointer",
["pointer", "pointer", "pointer", "pointer", "float"]
);
var result = applyDamage(doorPtr, controllerPtr, charPtr, ptr(0), damage);
console.log("[+] ApplyDamage(" + damage + ") on door -> result: " + result);
}

在 SDK dump 里搜 ApplyDamage

sdku.txt 第 10656 行

1
2
static float ApplyDamage(Actor* DamagedActor, float BaseDamage, Controller* EventInstigator,
Actor* DamageCauser, class DamageType DamageTypeClass);// 0x974e21c

image-20260401203443333

thunk 最后一行把拆出来的参数传给了 sub_8FAC0EC, 参数是普通的寄存器传递(X0-X3 + S0),可以直接从 Frida 调用。所以 APPLY_DAMAGE_IMPL = 0x8FAC0EC。

穿墙

在地图绕了一圈发现有个场景进不去,只能穿墙,刚好这里也需要

1
2
3
4
setTimeout(function () {
console.log("[*] Auto-enabling noclip...");
enableNoclip();
}, 1000);
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
var noclipEnabled = false;
var MOVEMENT_MODE_OFFSET = 0x168; // CharacterMovementComponent.MovementMode (byte)

function enableNoclip() {
if (!charPtr) {
console.log("[-] Character not found yet");
return;
}

// 1. 直接写内存:MovementMode = 5 (Flying)
var movComp = charPtr.add(MOV_COMP_OFFSET).readPointer();
var oldMode = movComp.add(MOVEMENT_MODE_OFFSET).readU8();
movComp.add(MOVEMENT_MODE_OFFSET).writeU8(5);
console.log("[+] MovementMode: " + oldMode + " -> 5 (Flying)");

// 把Walking的刹车减速度复制到Flying,否则松手后角色不会停
var brakingWalk = movComp.add(0x1b4).readFloat(); // BrakingDecelerationWalking
var brakingFly = movComp.add(0x1c0).readFloat(); // BrakingDecelerationFlying
if (brakingFly < brakingWalk) {
movComp.add(0x1c0).writeFloat(brakingWalk);
console.log("[+] BrakingDecelerationFlying: " + brakingFly + " -> " + brakingWalk);
}

// 2. 用已验证的C++函数关闭角色碰撞
// sub_8C21320(Actor*, bool) — SetActorEnableCollision
var setActorCollision = new NativeFunction(
base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]
);
setActorCollision(charPtr, 0); // false = 关闭碰撞
console.log("[+] Actor collision disabled");

noclipEnabled = true;
console.log("[+] Noclip ON");
}

image-20260401203910696

传送

在后面是实现了

主动调用 teleport(-1850, 1300, 268) 方法

此部分完整代码

效果

image-20260401204134548

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
var moduleName = "libUE4.so";
var base = null;

// ==================== Offsets ====================
var CHAR_HEALTH_OFFSET = 0x510; // FirstPersonCharacter_C.生命值 (float)
var CHAR_HP_UI_OFFSET = 0x520; // FirstPersonCharacter_C.生命值UI (HP_C*)
var LEVEL_HEALTH_OFFSET = 0x230; // FirstPersonExampleMap_C.生命值 (float)
var UOBJECT_OUTER_OFFSET = 0x20; // UObject.OuterPrivate
var LEVEL_SCRIPT_OFFSET = 0xe8; // ULevel.LevelScriptActor
var CONTROLLER_OFFSET = 0x258; // Pawn.Controller
var DOOR_OFFSET = 0x410; // FirstPersonExampleMap_C -> SM_Door_69*
var CAPSULE_OFFSET = 0x290; // Character.CapsuleComponent
var MOV_COMP_OFFSET = 0x288; // Character.CharacterMovementComponent
// vtable offsets (byte offset, not index)
var VT_SET_COLLISION_ENABLED = 1632; // PrimitiveComponent vtable offset for SetCollisionEnabled
var VT_SET_MOVEMENT_MODE = 1496; // CharacterMovementComponent vtable offset for SetMovementMode

// ==================== Function Addresses ====================
var CHAR_BEGIN_PLAY = 0x5e92c58; // FirstPersonCharacter_C::ReceiveBeginPlay
var CHAR_RECEIVE_HIT = 0x5e93094; // FirstPersonCharacter_C::ReceiveHit
var HP_UPDATE_HEALTH = 0x5e94adc; // HP_C::更新生命值
var APPLY_DAMAGE_IMPL = 0x8FAC0EC; // UGameplayStatics::ApplyDamage (actual C++ impl)
var SET_COLLISION_IMPL = 0x8C21320; // AActor::SetActorEnableCollision (actual C++ impl)

// ==================== Config ====================
var MAX_HEALTH = 99999.0;

// ==================== Global State ====================
var charPtr = null;
var levelScriptPtr = null;
var doorPtr = null;
var controllerPtr = null;
var healthTimer = null;

// ==================== Utility Functions ====================

function findModule() {
base = Module.findBaseAddress(moduleName);
if (base) {
console.log("[+] " + moduleName + " found at: " + base);
return true;
}
var modules = Process.enumerateModules();
for (var i = 0; i < modules.length; i++) {
var m = modules[i];
if (m.name.indexOf("UE4") !== -1 || m.name.indexOf("Unreal") !== -1) {
base = m.base;
moduleName = m.name;
console.log("[+] Found UE4 module: " + m.name + " at " + m.base);
return true;
}
}
console.log("[-] UE4 module not found!");
return false;
}

function setHealth() {
try {
if (charPtr && !charPtr.isNull()) {
charPtr.add(CHAR_HEALTH_OFFSET).writeFloat(MAX_HEALTH);
}
if (levelScriptPtr && !levelScriptPtr.isNull()) {
levelScriptPtr.add(LEVEL_HEALTH_OFFSET).writeFloat(MAX_HEALTH);
}
} catch (e) {}
}

function startHealthTimer() {
if (healthTimer !== null) return;
healthTimer = setInterval(setHealth, 50);
console.log("[+] Health restoration timer started (50ms interval)");
}

// ==================== Door Functions ====================

// 读取门和控制器指针
function resolveDoorPointers() {
if (!levelScriptPtr || !charPtr) {
console.log("[-] Need character and level script pointers first");
return false;
}
doorPtr = levelScriptPtr.add(DOOR_OFFSET).readPointer();
controllerPtr = charPtr.add(CONTROLLER_OFFSET).readPointer();

console.log("[*] Door (SM_Door_69): " + doorPtr);
console.log("[*] Controller: " + controllerPtr);

if (!doorPtr || doorPtr.isNull()) {
console.log("[-] Door pointer is null (level script refs not initialized yet)");
return false;
}
return true;
}

// 调用 ApplyDamage 对门造成伤害,触发 TakeAnyDamage 事件
function damageDoor(damage) {
if (!resolveDoorPointers()) return;
damage = damage || 100.0;

// sub_8FAC0EC(DamagedActor, EventInstigator, DamageCauser, DamageTypeClass, BaseDamage)
// ARM64 ABI: X0=actor, X1=controller, X2=character, X3=damageType(null), S0=damage
var applyDamage = new NativeFunction(
base.add(APPLY_DAMAGE_IMPL),
"pointer",
["pointer", "pointer", "pointer", "pointer", "float"]
);
var result = applyDamage(doorPtr, controllerPtr, charPtr, ptr(0), damage);
console.log("[+] ApplyDamage(" + damage + ") on door -> result: " + result);
}

// 直接关闭门的碰撞,穿过去
function disableDoorCollision() {
if (!resolveDoorPointers()) return;

// sub_8C21320(Actor*, bool bEnable)
var setCollision = new NativeFunction(
base.add(SET_COLLISION_IMPL),
"void",
["pointer", "int"]
);
setCollision(doorPtr, 0);
console.log("[+] Door collision disabled");
}

// ==================== Noclip Functions ====================

var noclipEnabled = false;
var MOVEMENT_MODE_OFFSET = 0x168; // CharacterMovementComponent.MovementMode (byte)

function enableNoclip() {
if (!charPtr) {
console.log("[-] Character not found yet");
return;
}

// 1. 直接写内存:MovementMode = 5 (Flying)
var movComp = charPtr.add(MOV_COMP_OFFSET).readPointer();
var oldMode = movComp.add(MOVEMENT_MODE_OFFSET).readU8();
movComp.add(MOVEMENT_MODE_OFFSET).writeU8(5);
console.log("[+] MovementMode: " + oldMode + " -> 5 (Flying)");

// 把Walking的刹车减速度复制到Flying,否则松手后角色不会停
var brakingWalk = movComp.add(0x1b4).readFloat(); // BrakingDecelerationWalking
var brakingFly = movComp.add(0x1c0).readFloat(); // BrakingDecelerationFlying
if (brakingFly < brakingWalk) {
movComp.add(0x1c0).writeFloat(brakingWalk);
console.log("[+] BrakingDecelerationFlying: " + brakingFly + " -> " + brakingWalk);
}

// 2. 用已验证的C++函数关闭角色碰撞
// sub_8C21320(Actor*, bool) — SetActorEnableCollision
var setActorCollision = new NativeFunction(
base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]
);
setActorCollision(charPtr, 0); // false = 关闭碰撞
console.log("[+] Actor collision disabled");

noclipEnabled = true;
console.log("[+] Noclip ON");
}

function disableNoclip() {
if (!charPtr) return;

// 恢复 Walking
var movComp = charPtr.add(MOV_COMP_OFFSET).readPointer();
movComp.add(MOVEMENT_MODE_OFFSET).writeU8(1);

// 恢复碰撞
var setActorCollision = new NativeFunction(
base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]
);
setActorCollision(charPtr, 1); // true = 开启碰撞

noclipEnabled = false;
console.log("[+] Noclip OFF");
}

function toggleNoclip() {
if (noclipEnabled) disableNoclip();
else enableNoclip();
}

// ==================== Install Hooks ====================

function installHooks() {
if (!findModule()) return;

// ============================================================
// Hook 1: Character ReceiveBeginPlay — 获取角色实例 + 导航到关卡脚本
// ============================================================
Interceptor.attach(base.add(CHAR_BEGIN_PLAY), {
onEnter: function (args) {
charPtr = args[0];
console.log("[+] === Character Found ===");
console.log("[+] Character instance: " + charPtr);

var health = charPtr.add(CHAR_HEALTH_OFFSET).readFloat();
console.log("[+] Character health (offset 0x510): " + health);

// Navigate: Character -> Outer (Level) -> LevelScriptActor
try {
var outer = charPtr.add(UOBJECT_OUTER_OFFSET).readPointer();
levelScriptPtr = outer.add(LEVEL_SCRIPT_OFFSET).readPointer();
console.log("[+] LevelScript: " + levelScriptPtr);

var levelHealth = levelScriptPtr.add(LEVEL_HEALTH_OFFSET).readFloat();
console.log("[+] Level health (offset 0x230): " + levelHealth);
} catch (e) {
console.log("[-] Failed to find LevelScript: " + e);
}

// 启动无限血量
setHealth();
startHealthTimer();
console.log("[+] === Infinite Health Active ===");

// 延迟1秒后自动开启穿墙
setTimeout(function () {
console.log("[*] Auto-enabling noclip...");
enableNoclip();
}, 1000);

// 延迟2秒后自动尝试对门造成伤害(等关卡脚本初始化完成)
setTimeout(function () {
console.log("[*] Auto-attempting door damage...");
damageDoor(9999.0);
}, 2000);

// 延迟3秒后如果门还在,再关闭碰撞作为备选
setTimeout(function () {
try {
var door = levelScriptPtr.add(DOOR_OFFSET).readPointer();
if (door && !door.isNull()) {
console.log("[*] Door still exists, disabling collision as fallback...");
disableDoorCollision();
}
} catch (e) {}
}, 3000);
}
});

// ============================================================
// Hook 2: NOP ReceiveHit — 屏蔽角色碰撞伤害事件
// ============================================================
try {
Interceptor.replace(base.add(CHAR_RECEIVE_HIT), new NativeCallback(function (self, frame) {
setHealth();
return 0;
}, "uint64", ["pointer", "pointer"]));
console.log("[+] Character ReceiveHit NOP'd at: " + base.add(CHAR_RECEIVE_HIT));
} catch (e) {
console.log("[-] Failed to hook ReceiveHit: " + e);
}

// ============================================================
// Hook 3: HP_C::更新生命值 — UI更新前写满血量
// ============================================================
Interceptor.attach(base.add(HP_UPDATE_HEALTH), {
onEnter: function (args) {
setHealth();
}
});
console.log("[+] HP_C::更新生命值 hooked at: " + base.add(HP_UPDATE_HEALTH));

console.log("[+] All hooks installed! Waiting for ReceiveBeginPlay...");
console.log("[*] Manual commands:");
console.log("[*] toggleNoclip() — toggle noclip (fly through walls)");
console.log("[*] enableNoclip() — enable noclip");
console.log("[*] disableNoclip() — disable noclip");
console.log("[*] damageDoor(100) — apply damage to door");
console.log("[*] disableDoorCollision() — disable door collision");
}

// ==================== Entry Point ====================

if (Module.findBaseAddress(moduleName) === null) {
console.log("[*] Waiting for " + moduleName + " to load...");
var interval = setInterval(function () {
if (Module.findBaseAddress(moduleName) !== null) {
clearInterval(interval);
installHooks();
}
}, 500);
} else {
installHooks();
}

Section1

image-20260401204157594

image-20260401204212857

但是意外把所有的关卡的字打出来了

image-20260401205140571

  1. 获取 TextRenderActor 的 UClass

    1
    2
    levelScriptPtr + 0x3e0 → TextRenderActor17
    *TextRenderActor17 + 0x10 → UClass* (TextRenderActor类的指针)

    所有 TextRenderActor 实例共享同一个 UClass 指针,用它当指纹识别。

    image-20260401205322470

  2. 遍历关卡的 Actor 列表

    ULevel 内部存了 TArray<AActor*>,探测 Level+0x90 到 Level+0xB0,找到有效的 TArray,遍历每个 Actor,比对 UClass == TextRenderActor 的 UClass

  3. 对每个 TextRenderActor

    取消隐藏,读 actor + 0x58 的 bit 5(bHidden),清空该组件

    读文字,actor + 0x220 → TextRenderComponent → +0x448 → FText → 解析 ITextData 里的字符串

    读坐标,actor + 0x130 → RootComponent → +0x11c → RelativeLocation (X,Y,Z)

但是仍然没有看见可见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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// ==================== Debug Sky Actors ====================

// 对比一个可见的天空 Actor(FLAG{)和一个不可见的 pipe Actor
function debugSkyActors() {
if (!charPtr) return;
var level = charPtr.add(UOBJECT_OUTER_OFFSET).readPointer();

for (var probe = 0x90; probe <= 0xB0; probe += 8) {
try {
var data = level.add(probe).readPointer();
var num = level.add(probe + 8).readU32();
if (num < 10 || num > 10000 || data.isNull()) continue;

var visible = null; // FLAG{ TextRenderActor (可见)
var invisible = null; // pipe Actor (不可见, Y在-1800~-2400范围)
var textClass = null;

var ta17 = levelScriptPtr.add(TEXT_RENDER_ACTOR17_OFFSET).readPointer();
if (ta17 && !ta17.isNull()) textClass = ta17.add(0x10).readPointer();

for (var i = 0; i < num; i++) {
var actor = data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
if (z <= 2000) continue;

var cls = actor.add(0x10).readPointer();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();

if (textClass && cls.equals(textClass) && !visible) {
visible = actor;
} else if (y < -1700 && y > -2400 && !invisible) {
invisible = actor; // 管道flag区域的Actor
}
if (visible && invisible) break;
}

function dumpActor(label, actor) {
if (!actor) { console.log(label + ": not found"); return; }
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
var comp220 = actor.add(0x220).readPointer(); // Mesh/TextRender component

console.log(label + " actor=" + actor + " cls=" + actor.add(0x10).readPointer());

// Actor flags
console.log(" Actor+0x58: 0x" + actor.add(0x58).readU8().toString(16));

// Root component flags
if (root && !root.isNull()) {
var loc = readActorLocation(actor);
var scaleX = root.add(0x134).readFloat();
var scaleY = root.add(0x138).readFloat();
var scaleZ = root.add(0x13c).readFloat();
console.log(" Root: " + loc + " scale=(" + scaleX + "," + scaleY + "," + scaleZ + ")");
console.log(" Root+0x14c(vis): 0x" + root.add(0x14c).readU8().toString(16));
console.log(" Root+0x14d(hig): 0x" + root.add(0x14d).readU8().toString(16));
}

// Component at 0x220
if (comp220 && !comp220.isNull()) {
var sX = comp220.add(0x134).readFloat();
var sY = comp220.add(0x138).readFloat();
var sZ = comp220.add(0x13c).readFloat();
console.log(" Comp0x220: scale=(" + sX + "," + sY + "," + sZ + ")");
console.log(" Comp0x220+0x14c: 0x" + comp220.add(0x14c).readU8().toString(16));
console.log(" Comp0x220+0x14d: 0x" + comp220.add(0x14d).readU8().toString(16));
// PrimitiveComponent 渲染标志
console.log(" Comp0x220+0x210: 0x" + comp220.add(0x210).readU8().toString(16));
console.log(" Comp0x220+0x211: 0x" + comp220.add(0x211).readU8().toString(16));
console.log(" Comp0x220+0x212: 0x" + comp220.add(0x212).readU8().toString(16));
}
}

dumpActor("[VISIBLE FLAG{]", visible);
dumpActor("[INVISIBLE PIPE]", invisible);
break;
} catch (e) { console.log("err: " + e); }
}
}

image-20260401212329961

image-20260401212424635

这里做了一个飞天,需要在frida终端主动调用teleportToFlag()方法

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
function forceRevealSky() {
if (!charPtr) { console.log("[-] No character"); return; }
var level = charPtr.add(UOBJECT_OUTER_OFFSET).readPointer();

// sub_8E619BC(SceneComponent*, bVisible, propagateMode)
// propagateMode: 2=propagate to children, 1=self only
var setVis = new NativeFunction(
base.add(SET_VISIBILITY_IMPL), "void", ["pointer", "int", "int"]
);

for (var probe = 0x90; probe <= 0xB0; probe += 8) {
try {
var data = level.add(probe).readPointer();
var num = level.add(probe + 8).readU32();
if (num < 10 || num > 10000 || data.isNull()) continue;

var count = 0;
for (var i = 0; i < num; i++) {
try {
var actor = data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
if (z <= 2000) continue;

// 对 RootComponent 调用 SetVisibility(true, propagate)
setVis(root, 1, 2);

// 对 offset 0x220 的组件也调用
try {
var comp = actor.add(0x220).readPointer();
if (comp && !comp.isNull()) {
setVis(comp, 1, 2);
}
} catch (e) {}

count++;
} catch (e) {}
}
console.log("[+] SetVisibility(true) on " + count + " sky actors");
break;
} catch (e) {}
}
}

// ==================== Teleport ====================

// 使用 AActor::TeleportTo 的实际 C++ 函数
// sub_8C1E398(actor, locX, locY, locZ, rotPitch, rotYaw, rotRoll)
function teleportActor(actor, x, y, z) {
var fn = new NativeFunction(
base.add(TELEPORT_IMPL), "int",
["pointer", "float", "float", "float", "float", "float", "float"]
);
return fn(actor, x, y, z, 0, 0, 0);
}


function teleport(x, y, z) {
if (!charPtr) { console.log("[-] No character"); return; }
teleportActor(charPtr, x, y, z);
console.log("[+] Teleported to (" + x + ", " + y + ", " + z + ")");
}

function teleportToFlag() {
teleport(-375, -2000, 3800);
}

image-20260401212257483

Section2

image-20260401214624923

试过了disableNoclip(),所有的墙体和cube无法穿透,但是那个hit me的立方体仍旧可穿透

题目提示让立方体变得不可穿透,说明 Plane_Blueprint 这个 cube 默认碰撞是关闭的,需要开启碰撞才能触发 ReceiveHit 事件显示 flag

  • 从 Objects.txt 找到 Plane_Blueprint 实例
  • 写 findPlaneBlueprint() 遍历所有 actor,通过 class 指针与普通 StaticMeshActor 不同 + 0x230 处有有效 FlagActor1 来定位

从 SDK 找到 Plane_Blueprint_C 类,在 sdku.txt 中找到 Plane_Blueprint_C 类定义,它继承自 StaticMeshActor,多了:

  • 偏移 0x230 = FlagActor1(碰撞后要显示的 flag 文本)
  • ReceiveHit 虚函数(碰撞事件处理)

说明这是那个cube

Plane_Blueprint:通常指这个蓝图资源本身

Plane_Blueprint_C:指这个蓝图编译后生成的运行时类

从 SDK 找到三个关键函数地址:SetCollisionEnabled、SetCollisionResponseToAllChannels、SetNotifyRigidBodyCollision,得到它们的 vtable 偏移:0x660、0x850、0x658

写 findPlaneBlueprint() 遍历关卡所有 actor:

  • 先拿 door 的 class 指针作为标准 StaticMeshActor类的参考
  • 跳过 class 相同的(普通 StaticMeshActor)
  • 对 class 不同的,检查偏移 0x230 是否有有效的 FlagActor1 指针
  • 有就是 Plane_Blueprint_C

对找到的 actor 调用 SetActorEnableCollision(true) + SetCollisionEnabled(QueryAndPhysics) ,SetCollisionResponseToAllChannels(Block),SetNotifyRigidBodyCollision(true),让它从可穿透变成实体,玩家碰到就触发 ReceiveHit

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
function section2_fix() {
if (!base || !levelScriptPtr) { console.log("[-] No base/levelScript"); return; }
console.log("[*] Section 2: making cubes solid...");

// 1. Plane_Blueprint — 可穿透的特殊立方体
var pb = findPlaneBlueprint();
if (pb) {
enableCollisionOnActor(pb, "Plane_Blueprint");
var flagActor = pb.add(0x230).readPointer();
if (flagActor && !flagActor.isNull()) {
console.log(" [*] FlagActor1: " + flagActor + " at " + readActorLocation(flagActor));
}
} else {
console.log(" [-] Plane_Blueprint not found");
}

// 2. LevelScript 里引用的立方体: Cube_3 (0x400), Cube3 (0x408)
var cubeOffsets = [0x400, 0x408];
var cubeNames = ["Cube_3", "Cube3"];
for (var i = 0; i < cubeOffsets.length; i++) {
try {
var cube = levelScriptPtr.add(cubeOffsets[i]).readPointer();
if (cube && !cube.isNull()) {
enableCollisionOnActor(cube, cubeNames[i]);
}
} catch (e) {}
}

// 3. 遍历 Section 2 区域的其他立方体
var actors = getLevelActors();
if (actors) {
var extra = 0;
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var x = root.add(REL_LOCATION_OFFSET).readFloat();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
if (x < -1000 && x > -2500 && y > 1000 && y < 2500 && z < 500 && z > 100) {
enableCollisionOnComp(root, "area_cube_" + i);
extra++;
}
} catch (e) {}
}
if (extra > 0) console.log(" [+] Also fixed " + extra + " nearby actors");
}
console.log("[+] Section 2 done");
}

此处需要注释穿墙逻辑

1
2
3
4
5
6
7
function scheduleAutoActions() {
// setTimeout(function () { enableNoclip(); }, 1000);
setTimeout(function () { forceRevealSky(); revealAllText(); }, 2000);
setTimeout(function () { damageDoor(9999.0); }, 3000);
setTimeout(function () { disableDoorCollision(); }, 4000);
setTimeout(function () { section2_fix(); }, 5000);
}

此外注释到穿墙逻辑后,飞到flag无法实现,enableNoclip() 把角色的 MovementMode 改成了飞行模式,并且很可能禁用了角色的碰撞响应。UE4 中 noclip,fly 模式下角色的碰撞胶囊体不参与物理碰撞检测,所以即使 cube 已经设为 Block,角色也会直接穿过去,不会触发 ReceiveHit , OnActorHit 事件。

image-20260402091850488

image-20260402091925251

Section3

image-20260402103221335

找不到对象,写个扫描函数

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
// 扫描 Section 3 区域所有 actor
function scanSection3() {
var actors = getLevelActors();
if (!actors) { console.log("[-] No actors"); return; }
console.log("[*] Scanning Section 3 area...");
// Section 3 大致在 (-701, 1876) 附近,扩大范围搜索
var found = 0;
var classes = {};
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var x = root.add(REL_LOCATION_OFFSET).readFloat();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
// Section 3 区域: x ~ -700, y ~ 1200-2500
if (x > -1200 && x < -200 && y > 800 && y < 2500 && z > 100 && z < 800) {
var cls = actor.add(0x10).readPointer();
var classKey = cls.toString();
if (!classes[classKey]) classes[classKey] = 0;
classes[classKey]++;
console.log(" [" + found + "] " + actor + " cls=" + cls +
" (" + x.toFixed(0) + "," + y.toFixed(0) + "," + z.toFixed(0) + ")");
found++;
}
} catch (e) {}
}
console.log("[*] Found " + found + " actors in Section 3");
console.log("[*] Unique classes:");
for (var c in classes) console.log(" " + c + " x" + classes[c]);
}

// 检查指定 actor 的详细信息
function inspectActor(addrStr) {
var actor = ptr(addrStr);
console.log("[*] Inspecting actor: " + actor);
var cls = actor.add(0x10).readPointer();
console.log(" Class: " + cls);
console.log(" Location: " + readActorLocation(actor));

// 遍历 class 的 UFunction 链
console.log(" UFunctions (children at 0x48):");
try {
var child = cls.add(0x48).readPointer();
var idx = 0;
while (child && !child.isNull() && idx < 30) {
// 读 native func ptr at 0xd8
var nativePtr = ptr("0");
try { nativePtr = child.add(0xd8).readPointer(); } catch(e){}
var offset = nativePtr.isNull() ? "null" : nativePtr.sub(base).toString(16);
console.log(" [" + idx + "] UFunc=" + child + " native=0x" + offset);
child = child.add(0x28).readPointer();
idx++;
}
} catch (e) { console.log(" Error: " + e); }

// 遍历 class 的 SuperStruct 链找类名
console.log(" Class hierarchy (SuperStruct at 0x30):");
try {
var c = cls;
for (var depth = 0; depth < 10 && c && !c.isNull(); depth++) {
var nameIdx = c.add(0x18).readU32();
var nameNum = c.add(0x1c).readU32();
console.log(" [" + depth + "] " + c + " FName=(" + nameIdx + "," + nameNum + ")");
c = c.add(0x30).readPointer();
}
} catch (e) {}

// Dump 前 0x300 字节看自定义字段
console.log(" Memory dump (0x220-0x300):");
try {
var hex = [];
for (var off = 0x220; off < 0x300; off += 8) {
var val = actor.add(off).readPointer();
hex.push("0x" + off.toString(16) + "=" + val);
}
console.log(" " + hex.join("\n "));
} catch (e) {}
}

inspectActor 检查一个 actor 的详细信息:

  1. Class 指针 ,这个 actor 是什么类
  2. 位置 :在关卡中的坐标
  3. UFunctions :这个类有哪些函数(遍历 UClass.Children 链表)
  4. 类继承链:父类层级和 FName
  5. 内存 dump:0x220-0x300 的原始数据(自定义字段区域)
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
[M2102K1AC::com.tencent.ace.match2024 ]-> inspectActor("0x7cd9ed9880")
[*] Inspecting actor: 0x7cd9ed9880
Class: 0x7d04fa6c80
Location: (-874,1563,350)
UFunctions (children at 0x48):
[0] UFunc=0x7cdaea66e0 native=0x6a91fec
Class hierarchy (SuperStruct at 0x30):
[0] 0x7d04fa6c80 FName=(370245,0)
[1] 0x7d169f49e0 FName=(0,0)
[2] 0x7d04fa7e30 FName=(0,0)
Memory dump (0x220-0x300):
0x220=0x93e87baa64c0fa24
0x228=0x1b03fe1f58f0921d
0x230=0xa140dd4ab8311d17
0x238=0x54aeac8af82931f7
0x240=0x7d12aae8b0
0x248=0x3c0200280008
0x250=0x7d04582400
0x258=0xba185
0x260=0x7cdcd90040
0x268=0x7d12a4ae50
0x270=0xc0000
0x278=0x0
0x280=0x0
0x288=0x0
0x290=0x0
0x298=0x20089680d1a02
0x2a0=0x0
0x2a8=0x0
0x2b0=0x0
0x2b8=0x0
0x2c0=0x0
0x2c8=0x0
0x2d0=0x0
0x2d8=0x3f800000
0x2e0=0x0
0x2e8=0x0
0x2f0=0x0
0x2f8=0x0
[M2102K1AC::com.tencent.ace.match2024 ]-> teleportToSection2()
[M2102K1AC::com.tencent.ace.match2024 ]->

后来看了下其实就在

image-20260402103337287

image-20260402103428386

image-20260402110023864

image-20260402110132079

ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/

改了码表

UT1fc0gIYDArdz80Z0Xem46J

image-20260402110705726

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
enc_table = [0x31,0xbb,0x87,0x09,0xf8,0xe4,0xe7,0x90,0xf4,0x99,0xcc,0x69,0x5f,0x04,0x46,0x89,
0x75,0x5c,0xf0,0xcc,0xbd,0x2e,0xa3,0x68,0x0f,0xd6,0xdc,0x4e,0x7a,0x4d,0x63,0xd0,
0x60,0x24,0x2d,0x75,0x3c,0x16,0xfc,0x41,0x1d,0x6e,0xdf,0xa4,0x0d,0xd3,0xa6,0x9d,
0xb9,0x58,0x88,0xb2,0xbb,0x8d,0x9f,0x25,0x1b,0x11,0xb0,0x41,0x2f,0xcd,0x10,0xb6,0x84]
key_table = [0x70,0xF8,0xC2,0x39,0xBA,0xA0,0xA1,0xD7,0xBC,0xD0,0x86,0x22,0x13,0x49,0x08,0xC6,
0x25,0x0D,0xA2,0x9F,0xE9,0x7B,0xF5,0x3F,0x57,0x8F,0x86,0x2F,0x18,0x2E,0x07,0xB5,
0x06,0x43,0x45,0x1C,0x56,0x7D,0x90,0x2C,0x73,0x01,0xAF,0xD5,0x7F,0xA0,0xD2,0xE8,
0xCF,0x2F,0xF0,0xCB,0xC1,0xBC,0xAD,0x16,0x2F,0x24,0x86,0x76,0x17,0xF4,0x3B,0x99,0x84]
b64_table = ''.join(chr(e ^ k) for e, k in zip(enc_table, key_table))
print(f"Base64 表: {b64_table[:64]}")

enc_expected = [0x9d,0x43,0xb0,0xd7,0xd4,0x53,0x1c,0x7d,0xb4,0xb6,0xf6,0x37,
0x23,0x66,0xdb,0x92,0x19,0xdf,0xcf,0xf9,0x9a,0x92,0xf2,0x3c]
key_expected = [0xC8,0x17,0x81,0xB1,0xB7,0x63,0x7B,0x34,0xED,0xF2,0xB7,0x45,
0x47,0x1C,0xE3,0xA2,0x43,0xEF,0x97,0x9C,0xF7,0xA6,0xC4,0x76]
b64_encoded = ''.join(chr(e ^ k) for e, k in zip(enc_expected, key_expected))
print(f"Base64 编码值: {b64_encoded}")

enc_key = [0xd8,0x98,0x54,0xc1,0x64,0x93,0x56,0x84,0x38,0x4f,0x60,0xbb,0xa9,0xa4,0xcc,0x88,0x8d,0x9f]
key_key = [0xD2,0x94,0x5A,0xC1,0x35,0x85,0x71,0xBC,0x71,0x55,0x5B,0xE7,0x84,0xEA,0xA3,0x72,0x71,0x61]
xor_key = [e ^ k for e, k in zip(enc_key, key_key)]
print(f"XOR 密钥: {[hex(b) for b in xor_key]}")

decode_map = {c: i for i, c in enumerate(b64_table[:64])}
indices = [decode_map[c] for c in b64_encoded]
raw = []
for i in range(0, len(indices), 4):
g = indices[i:i+4]
raw.append((g[0] << 2) | (g[1] >> 4))
raw.append(((g[1] & 0xF) << 4) | (g[2] >> 2))
raw.append(((g[2] & 0x3) << 6) | g[3])
print(f"Base64 解码: {[hex(b) for b in raw]}")

flag = ''.join(chr(r ^ k) for r, k in zip(raw, xor_key))
print(f"\nSection 3 flag: {flag}")

解密链路:

1. 自定义 Base64 表解密  
2. Base64 编码后的期望值解密→ UT1fc0gIYDArdz80Z0Xem46J                                                                                                                                                                                                       
3. XOR 密钥解密,18 字节 key                                                                                                                                                                                                                                 
4. 自定义 Base64 解码期望值 ,18 字节密文                                                                                                                                                                                                                                          
5. 密文 XOR 密钥, _Anti_Cheat_Expert           

xmmword_3E50(16字节)+ word_3E60(2字节)= 18 字节,正好等于 24 个 Base64 字符解码后的原始数据长度(24 × 3/4 = 18),所以判断它是 XOR 密钥

就是24字节先xor0xC8u 0x17…..然后换标base64解码然后xor xmmword_3E50 xor 0xD2 0x94 0x5A….

完整flag:FLAG{8939008_Anti_Cheat_Expert}

完整frida代码

如何使用请拷打AI

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
var moduleName = "libUE4.so";
var base = null;

// ==================== Offsets ====================
var CHAR_HEALTH_OFFSET = 0x510;
var LEVEL_HEALTH_OFFSET = 0x230;
var UOBJECT_OUTER_OFFSET = 0x20;
var LEVEL_SCRIPT_OFFSET = 0xe8;
var CONTROLLER_OFFSET = 0x258;
var DOOR_OFFSET = 0x410;
var MOV_COMP_OFFSET = 0x288;
var ROOT_COMP_OFFSET = 0x130;
var REL_LOCATION_OFFSET = 0x11c;
var MOVEMENT_MODE_OFFSET = 0x168;
var SHAPE_COMP_OFFSET = 0x220;
var TEXT_RENDER_ACTOR17_OFFSET = 0x3e0;
var TEXT_RENDER_COMP_OFFSET = 0x220;
var FTEXT_OFFSET = 0x448;

// ==================== Function Addresses ====================
var CHAR_BEGIN_PLAY = 0x5e92c58;
var CHAR_RECEIVE_HIT = 0x5e93094;
var HP_UPDATE_HEALTH = 0x5e94adc;
var APPLY_DAMAGE_IMPL = 0x8FAC0EC;
var SET_COLLISION_IMPL = 0x8C21320;
var TELEPORT_IMPL = 0x8C1E398;
var SET_VISIBILITY_IMPL = 0x8E619BC;
var FNAME_INIT = 0x6DA3798;

// ==================== Global State ====================
var charPtr = null;
var levelScriptPtr = null;
var healthTimer = null;
var noclipEnabled = false;
var MAX_HEALTH = 99999.0;

// ==================== Utility ====================

function findModule() {
base = Module.findBaseAddress(moduleName);
if (base) { console.log("[+] " + moduleName + " at: " + base); return true; }
var modules = Process.enumerateModules();
for (var i = 0; i < modules.length; i++) {
if (modules[i].name.indexOf("UE4") !== -1) {
base = modules[i].base; moduleName = modules[i].name;
console.log("[+] " + moduleName + " at: " + base); return true;
}
}
return false;
}

function setHealth() {
try {
if (charPtr) charPtr.add(CHAR_HEALTH_OFFSET).writeFloat(MAX_HEALTH);
if (levelScriptPtr) levelScriptPtr.add(LEVEL_HEALTH_OFFSET).writeFloat(MAX_HEALTH);
} catch (e) {}
}

function readActorLocation(actor) {
try {
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
var x = root.add(REL_LOCATION_OFFSET).readFloat();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
return "(" + x.toFixed(0) + "," + y.toFixed(0) + "," + z.toFixed(0) + ")";
} catch (e) { return "(err)"; }
}

function teleportActor(actor, x, y, z) {
var fn = new NativeFunction(base.add(TELEPORT_IMPL), "int",
["pointer", "float", "float", "float", "float", "float", "float"]);
return fn(actor, x, y, z, 0, 0, 0);
}

function teleport(x, y, z) {
if (!charPtr) return;
teleportActor(charPtr, x, y, z);
console.log("[+] Teleported to (" + x + "," + y + "," + z + ")");
}

function readFText(ftextAddr) {
try {
var textData = ftextAddr.readPointer();
if (!textData || textData.isNull()) return "(null)";
var offsets = [0x08, 0x28, 0x30, 0x38];
for (var i = 0; i < offsets.length; i++) {
try {
var strPtr = textData.add(offsets[i]).readPointer();
var strNum = textData.add(offsets[i] + 8).readU32();
if (strNum > 0 && strNum < 256 && !strPtr.isNull()) {
var text = strPtr.readUtf16String(strNum - 1);
if (text && text.length > 0 && text.charCodeAt(0) > 31) return text;
}
} catch (e) {}
}
return "(unreadable)";
} catch (e) { return "(err)"; }
}

// 获取关卡 Actor 列表
function getLevelActors() {
var level = charPtr.add(UOBJECT_OUTER_OFFSET).readPointer();
for (var probe = 0x90; probe <= 0xB0; probe += 8) {
try {
var data = level.add(probe).readPointer();
var num = level.add(probe + 8).readU32();
if (num > 10 && num < 10000 && !data.isNull()) return { data: data, num: num };
} catch (e) {}
}
return null;
}

// ==================== Hook 1: 锁血 ====================

function startHealthTimer() {
if (healthTimer) return;
healthTimer = setInterval(setHealth, 50);
console.log("[+] Health timer started");
}

// ==================== Hook 4: 穿墙 ====================

function enableNoclip() {
if (!charPtr) return;
var movComp = charPtr.add(MOV_COMP_OFFSET).readPointer();
movComp.add(MOVEMENT_MODE_OFFSET).writeU8(5); // Flying
var brakingWalk = movComp.add(0x1b4).readFloat();
movComp.add(0x1c0).writeFloat(brakingWalk);
var setActorColl = new NativeFunction(base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]);
setActorColl(charPtr, 0);
noclipEnabled = true;
console.log("[+] Noclip ON");
}

function disableNoclip() {
if (!charPtr) return;
var movComp = charPtr.add(MOV_COMP_OFFSET).readPointer();
movComp.add(MOVEMENT_MODE_OFFSET).writeU8(1); // Walking
var setActorColl = new NativeFunction(base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]);
setActorColl(charPtr, 1);
noclipEnabled = false;
console.log("[+] Noclip OFF");
}

function toggleNoclip() { if (noclipEnabled) disableNoclip(); else enableNoclip(); }

// ==================== 门 ====================

function damageDoor(damage) {
if (!levelScriptPtr || !charPtr) return;
var doorPtr = levelScriptPtr.add(DOOR_OFFSET).readPointer();
var controllerPtr = charPtr.add(CONTROLLER_OFFSET).readPointer();
if (!doorPtr || doorPtr.isNull()) { console.log("[-] Door null"); return; }
var fn = new NativeFunction(base.add(APPLY_DAMAGE_IMPL), "pointer",
["pointer", "pointer", "pointer", "pointer", "float"]);
fn(doorPtr, controllerPtr, charPtr, ptr(0), damage || 9999.0);
console.log("[+] Door damaged");
}

function disableDoorCollision() {
if (!levelScriptPtr) return;
var doorPtr = levelScriptPtr.add(DOOR_OFFSET).readPointer();
if (!doorPtr || doorPtr.isNull()) return;
var fn = new NativeFunction(base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]);
fn(doorPtr, 0);
console.log("[+] Door collision OFF");
}

// ==================== Section 1: 天空 flag 显示 ====================

function forceRevealSky() {
if (!charPtr) return;
var actors = getLevelActors();
if (!actors) return;
var setVis = new NativeFunction(base.add(SET_VISIBILITY_IMPL), "void", ["pointer", "int", "int"]);
var count = 0;
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
if (z <= 2000) continue;
setVis(root, 1, 2);
try { var comp = actor.add(0x220).readPointer();
if (comp && !comp.isNull()) setVis(comp, 1, 2); } catch (e) {}
count++;
} catch (e) {}
}
console.log("[+] SetVisibility(true) on " + count + " sky actors");
}

function revealAllText() {
if (!levelScriptPtr || !charPtr) return;
var ta17 = levelScriptPtr.add(TEXT_RENDER_ACTOR17_OFFSET).readPointer();
if (!ta17 || ta17.isNull()) return;
var textClass = ta17.add(0x10).readPointer();
var actors = getLevelActors();
if (!actors) return;
var results = [];
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
if (!actor.add(0x10).readPointer().equals(textClass)) continue;
var comp = actor.add(TEXT_RENDER_COMP_OFFSET).readPointer();
var text = (comp && !comp.isNull()) ? readFText(comp.add(FTEXT_OFFSET)) : "?";
results.push(" " + readActorLocation(actor) + " " + text);
} catch (e) {}
}
console.log("[+] " + results.length + " TextRenderActors:");
for (var r = 0; r < results.length; r++) console.log(results[r]);
}

function teleportToFlag() { teleport(-375, -2000, 3800); }

// ==================== Section 2: 让立方体不可穿透 ====================
// Plane_Blueprint_C (extends StaticMeshActor):
// offset 0x220 = StaticMeshComponent (碰撞组件)
// offset 0x230 = FlagActor1 (flag 文本 actor)
// LevelScript offsets:
// 0x400 = Cube_3, 0x408 = Cube3
// vtable offsets on PrimitiveComponent:
// 0x660 = SetCollisionEnabled
// 0x658 = SetNotifyRigidBodyCollision
// 0x850 = SetCollisionResponseToAllChannels

// 从已知组件 vtable 提取真正的 C++ 实现函数地址
var _collFuncs = null;
function getCollisionFuncs() {
if (_collFuncs) return _collFuncs;
// 用 door (已知 StaticMeshActor) 的组件获取正确的 vtable
var doorPtr = levelScriptPtr.add(DOOR_OFFSET).readPointer();
var doorComp = doorPtr.add(0x220).readPointer();
var vt = doorComp.readPointer();
_collFuncs = {
setCollEnabled: new NativeFunction(vt.add(0x660).readPointer(), "void", ["pointer", "int"]),
setRespAll: new NativeFunction(vt.add(0x850).readPointer(), "void", ["pointer", "int"]),
setNotify: new NativeFunction(vt.add(0x658).readPointer(), "void", ["pointer", "int"])
};
console.log(" [*] Collision funcs from door vtable: OK");
return _collFuncs;
}

function enableCollisionOnComp(comp, label) {
if (!comp || comp.isNull()) return false;
var f = getCollisionFuncs();
try { f.setCollEnabled(comp, 3); }
catch (e) { console.log(" [!] " + label + " setCollEnabled crashed: " + e.message); }
try { f.setRespAll(comp, 2); }
catch (e) { console.log(" [!] " + label + " setRespAll crashed: " + e.message); }
try { f.setNotify(comp, 1); }
catch (e) { console.log(" [!] " + label + " setNotify crashed: " + e.message); }
return true;
}

function enableCollisionOnActor(actor, label) {
var setActorColl = new NativeFunction(base.add(SET_COLLISION_IMPL), "void", ["pointer", "int"]);
setActorColl(actor, 1);
var comp = actor.add(ROOT_COMP_OFFSET).readPointer();
if (comp && !comp.isNull()) {
enableCollisionOnComp(comp, label);
console.log(" [+] " + label + " collision → Block " + readActorLocation(actor));
return true;
}
return false;
}

// 找到 Plane_Blueprint actor (类不同于普通 StaticMeshActor)
function findPlaneBlueprint() {
var actors = getLevelActors();
if (!actors) return null;
// 获取标准 StaticMeshActor 类指针 (从 levelScript 存的 door)
var doorPtr = levelScriptPtr.add(DOOR_OFFSET).readPointer();
var smActorClass = doorPtr.add(0x10).readPointer();
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var cls = actor.add(0x10).readPointer();
if (cls.equals(smActorClass)) continue; // 跳过普通 StaticMeshActor
// 检查 offset 0x220 是否有组件 (StaticMeshActor 子类)
var comp = actor.add(0x220).readPointer();
if (!comp || comp.isNull()) continue;
// 检查 offset 0x230 是否有 FlagActor1 指针
var flagActor = actor.add(0x230).readPointer();
if (!flagActor || flagActor.isNull()) continue;
// 验证 FlagActor1 看起来像 UObject (offset 0x10 = class ptr)
var flagCls = flagActor.add(0x10).readPointer();
if (flagCls && !flagCls.isNull()) {
return actor;
}
} catch (e) {}
}
return null;
}

function section2_fix() {
if (!base || !levelScriptPtr) { console.log("[-] No base/levelScript"); return; }
console.log("[*] Section 2: making cubes solid...");

// 1. Plane_Blueprint — 可穿透的特殊立方体
var pb = findPlaneBlueprint();
if (pb) {
enableCollisionOnActor(pb, "Plane_Blueprint");
var flagActor = pb.add(0x230).readPointer();
if (flagActor && !flagActor.isNull()) {
console.log(" [*] FlagActor1: " + flagActor + " at " + readActorLocation(flagActor));
}
} else {
console.log(" [-] Plane_Blueprint not found");
}

// 2. LevelScript 里引用的立方体: Cube_3 (0x400), Cube3 (0x408)
var cubeOffsets = [0x400, 0x408];
var cubeNames = ["Cube_3", "Cube3"];
for (var i = 0; i < cubeOffsets.length; i++) {
try {
var cube = levelScriptPtr.add(cubeOffsets[i]).readPointer();
if (cube && !cube.isNull()) {
enableCollisionOnActor(cube, cubeNames[i]);
}
} catch (e) {}
}

// 3. 遍历 Section 2 区域的其他立方体
var actors = getLevelActors();
if (actors) {
var extra = 0;
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var x = root.add(REL_LOCATION_OFFSET).readFloat();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
if (x < -1000 && x > -2500 && y > 1000 && y < 2500 && z < 500 && z > 100) {
enableCollisionOnComp(root, "area_cube_" + i);
extra++;
}
} catch (e) {}
}
if (extra > 0) console.log(" [+] Also fixed " + extra + " nearby actors");
}
console.log("[+] Section 2 done");
}

function teleportToSection2() { teleport(-1431, 1876, 300); }

// 直接模拟碰撞 Plane_Blueprint (备用方案)
function hitPlaneBlueprint() {
var pb = findPlaneBlueprint();
if (!pb) { console.log("[-] Plane_Blueprint not found"); return; }
// 把角色传送到 Plane_Blueprint 位置触发碰撞
var root = pb.add(ROOT_COMP_OFFSET).readPointer();
var x = root.add(REL_LOCATION_OFFSET).readFloat();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
console.log("[*] Teleporting to Plane_Blueprint at (" + x.toFixed(0) + "," + y.toFixed(0) + "," + z.toFixed(0) + ")");
teleport(x, y, z);
}


// ==================== Section 3: 找到解谜函数 ====================
// 黄色小球不是 SM_MERGED_Shape_Pipe_Flag_1_Blueprint_C
// 真正的黄色小球通过 scanSection3() 找到,class=unique,位置在 Section 3 区域
// 解谜函数: dlopen("libplay.so") → dlsym("get_last_flag") → 调用

// 直接调用 libplay.so 的 get_last_flag
function callGetLastFlag() {
var lib = Module.findBaseAddress("libplay.so");
if (!lib) {
// 尝试加载
var dlopen = new NativeFunction(Module.findExportByName(null, "dlopen"), "pointer", ["pointer", "int"]);
var soName = Memory.allocUtf8String("libplay.so");
var handle = dlopen(soName, 2);
console.log("[*] dlopen libplay.so → " + handle);
lib = Module.findBaseAddress("libplay.so");
}
if (!lib) { console.log("[-] libplay.so not loaded"); return; }
console.log("[*] libplay.so base: " + lib);

var getLastFlag = Module.findExportByName("libplay.so", "get_last_flag");
if (!getLastFlag) { console.log("[-] get_last_flag not found"); return; }
console.log("[*] get_last_flag: " + getLastFlag);

// Hook get_last_flag 的返回值
Interceptor.attach(getLastFlag, {
onEnter: function(args) {
console.log("[*] get_last_flag called, a1=" + args[0]);
this.a1 = args[0];
},
onLeave: function(retval) {
console.log("[*] get_last_flag returned: " + retval);
// 尝试读返回值作为字符串
try { console.log(" as string: " + retval.readUtf8String()); } catch(e){}
try { console.log(" as cstring: " + retval.readCString()); } catch(e){}
}
});

// 也 dump 解密后的数据
var data3E30 = lib.add(0x3E30);
var data3E50 = lib.add(0x3E50);
var data3DE0 = lib.add(0x3DE0);
console.log("[*] Data at 3E30 (24 bytes):");
try {
var bytes = [];
for (var i = 0; i < 24; i++) bytes.push(("0" + data3E30.add(i).readU8().toString(16)).slice(-2));
console.log(" hex: " + bytes.join(" "));
console.log(" str: " + data3E30.readCString(24));
} catch(e) { console.log(" " + e); }
console.log("[*] Data at 3E50 (18 bytes):");
try {
var bytes = [];
for (var i = 0; i < 18; i++) bytes.push(("0" + data3E50.add(i).readU8().toString(16)).slice(-2));
console.log(" hex: " + bytes.join(" "));
console.log(" str: " + data3E50.readCString(18));
} catch(e) { console.log(" " + e); }
console.log("[*] Data at 3DE0 (65 bytes - Base64 table):");
try {
console.log(" str: " + data3DE0.readCString(65));
} catch(e) { console.log(" " + e); }

// 尝试直接调用
try {
var func = new NativeFunction(getLastFlag, "pointer", ["pointer"]);
var result = func(ptr("0"));
console.log("[*] Direct call result: " + result);
try { console.log(" str: " + result.readCString()); } catch(e){}
} catch(e) {
console.log("[-] Direct call failed: " + e);
}
}

// 扫描 Section 3 区域所有 actor
function scanSection3() {
var actors = getLevelActors();
if (!actors) { console.log("[-] No actors"); return; }
console.log("[*] Scanning Section 3 area...");
// Section 3 大致在 (-701, 1876) 附近,扩大范围搜索
var found = 0;
var classes = {};
for (var i = 0; i < actors.num; i++) {
try {
var actor = actors.data.add(i * 8).readPointer();
if (actor.isNull()) continue;
var root = actor.add(ROOT_COMP_OFFSET).readPointer();
if (!root || root.isNull()) continue;
var x = root.add(REL_LOCATION_OFFSET).readFloat();
var y = root.add(REL_LOCATION_OFFSET + 4).readFloat();
var z = root.add(REL_LOCATION_OFFSET + 8).readFloat();
// Section 3 区域: x ~ -700, y ~ 1200-2500
if (x > -1200 && x < -200 && y > 800 && y < 2500 && z > 100 && z < 800) {
var cls = actor.add(0x10).readPointer();
var classKey = cls.toString();
if (!classes[classKey]) classes[classKey] = 0;
classes[classKey]++;
console.log(" [" + found + "] " + actor + " cls=" + cls +
" (" + x.toFixed(0) + "," + y.toFixed(0) + "," + z.toFixed(0) + ")");
found++;
}
} catch (e) {}
}
console.log("[*] Found " + found + " actors in Section 3");
console.log("[*] Unique classes:");
for (var c in classes) console.log(" " + c + " x" + classes[c]);
}

// 检查指定 actor 的详细信息
function inspectActor(addrStr) {
var actor = ptr(addrStr);
console.log("[*] Inspecting actor: " + actor);
var cls = actor.add(0x10).readPointer();
console.log(" Class: " + cls);
console.log(" Location: " + readActorLocation(actor));

// 遍历 class 的 UFunction 链
console.log(" UFunctions (children at 0x48):");
try {
var child = cls.add(0x48).readPointer();
var idx = 0;
while (child && !child.isNull() && idx < 30) {
// 读 native func ptr at 0xd8
var nativePtr = ptr("0");
try { nativePtr = child.add(0xd8).readPointer(); } catch(e){}
var offset = nativePtr.isNull() ? "null" : nativePtr.sub(base).toString(16);
console.log(" [" + idx + "] UFunc=" + child + " native=0x" + offset);
child = child.add(0x28).readPointer();
idx++;
}
} catch (e) { console.log(" Error: " + e); }

// 遍历 class 的 SuperStruct 链找类名
console.log(" Class hierarchy (SuperStruct at 0x30):");
try {
var c = cls;
for (var depth = 0; depth < 10 && c && !c.isNull(); depth++) {
var nameIdx = c.add(0x18).readU32();
var nameNum = c.add(0x1c).readU32();
console.log(" [" + depth + "] " + c + " FName=(" + nameIdx + "," + nameNum + ")");
c = c.add(0x30).readPointer();
}
} catch (e) {}

// Dump 前 0x300 字节看自定义字段
console.log(" Memory dump (0x220-0x300):");
try {
var hex = [];
for (var off = 0x220; off < 0x300; off += 8) {
var val = actor.add(off).readPointer();
hex.push("0x" + off.toString(16) + "=" + val);
}
console.log(" " + hex.join("\n "));
} catch (e) {}
}

// ==================== Auto Schedule ====================

function scheduleAutoActions() {
// setTimeout(function () { enableNoclip(); }, 1000);
setTimeout(function () { forceRevealSky(); revealAllText(); }, 2000);
setTimeout(function () { damageDoor(9999.0); }, 3000);
setTimeout(function () { disableDoorCollision(); }, 4000);
setTimeout(function () { section2_fix(); }, 5000);
}

// ==================== Install Hooks ====================

function installHooks() {
if (!findModule()) return;

// Hook 1: ReceiveBeginPlay — 获取指针 + 锁血
Interceptor.attach(base.add(CHAR_BEGIN_PLAY), {
onEnter: function (args) {
charPtr = args[0];
console.log("[+] Character: " + charPtr);
try {
var outer = charPtr.add(UOBJECT_OUTER_OFFSET).readPointer();
levelScriptPtr = outer.add(LEVEL_SCRIPT_OFFSET).readPointer();
console.log("[+] LevelScript: " + levelScriptPtr);
} catch (e) {}
setHealth();
startHealthTimer();
scheduleAutoActions();
}
});

// Hook 2: NOP ReceiveHit — 屏蔽碰撞伤害
try {
Interceptor.replace(base.add(CHAR_RECEIVE_HIT), new NativeCallback(function () {
setHealth(); return 0;
}, "uint64", ["pointer", "pointer"]));
} catch (e) {}

// Hook 3: 更新生命值 — UI更新前补血
Interceptor.attach(base.add(HP_UPDATE_HEALTH), { onEnter: function () { setHealth(); } });

console.log("[+] Hooks installed");
console.log("[*] Commands: toggleNoclip() | revealAllText() | forceRevealSky()");
console.log("[*] teleportToFlag() | damageDoor() | section2_fix()");
console.log("[*] teleportToSection2() | hitPlaneBlueprint()");
console.log("[*] scanSection3() | inspectActor(addr) | callGetLastFlag()");
}

// ==================== Entry ====================

if (Module.findBaseAddress(moduleName) === null) {
var interval = setInterval(function () {
if (Module.findBaseAddress(moduleName) !== null) {
clearInterval(interval); installHooks();
}
}, 500);
} else {
installHooks();
}