2024腾讯游戏安全竞赛Andorid初赛
2024腾讯游戏安全竞赛Andorid初赛
需要附件请直接联系我
准备工作
寻找Gname
0xB171CC0
寻找Gworld
0xB32D8A8
寻找GUobject
0xB1B5F98
DumpSDK
1 | ./ue4dumper64 --strings --newue+ --gname 0xB171CC0 --package com.tencent.ace.match2024 --output /data/local/tmp |
出门
玩游戏发现多个问题,比如触碰墙壁马上嘶,碰门嘶,所以绕过方式有
把触碰墙壁门,扣血的逻辑nop
把血量hook为满血
瞬移
穿墙
Strings.txt 中搜索 /Game/ 路径,找到所有游戏自定义资产
1 | /Game/FirstPersonBP/Blueprints/FirstPersonCharacter ← 玩家角色 |
HP 和 HP_C 出现在 Audio 路径下但实际是个 UserWidget(UI控件),这是关键线索
在 sdku.txt 中搜索 Class: HP_C、Class: FirstPersonCharacter_C、Class: FirstPersonExampleMap_C,找到三个核心类:
1 | HP_C (血量UI) |
函数名是中文 更新生命值,直接暴露了功能。UE4 Blueprint 编译后会保留原始函数名。
1 | FirstPersonCharacter_C (玩家角色): |
看到 float 生命值在偏移 0x510,加上 ReceiveHit 事件处理函数 ,这是血量存储和伤害入口
1 | FirstPersonExampleMap_C (关卡脚本): |
这里有两个重要发现:
1. 关卡脚本也有生命值字段 → 血量可能在两处同步
2. Cube2/Cube_3/Cube3 绑了 ActorHit 事件 → 碰墙掉血的来源
3. SM_Door_69 绑了 TakeAnyDamage → 门需要被射击才能打开
1 | var moduleName = "libUE4.so"; |
hook血量
0x5e94adc(更新生命值)
原函数:
1 | // 原函数:解包碰撞参数,调用虚表处理伤害 |
new NativeCallback(function(self, frame) { … }, “uint64”, [“pointer”, “pointer”])
这是创建一个替代函数,签名必须和原函数一致
1 | Interceptor.attach(base.add(HP_UPDATE_HEALTH), { |
1 | function setHealth() { |
levelScriptPtr 是 FirstPersonExampleMap_C 的实例指针,也就是关卡脚本 Actor
撞墙扣血绕过
1 | float v6 = 0.0; |
这是个 Blueprint thunk,从帧中提取 float 参数,调用虚表函数更新 UI
ReceiveHit (0x5e93094)
1 | // 解包8个参数(MyComp, Other, OtherComp, bSelfMoved, HitLocation...) |
解包参数后分发到 ExecuteUbergraph。NOP 掉它就不会处理任何碰撞事件。
反编译 ExecuteUbergraph (0x6fcd294):
这是 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 | UObject 头部 (0x28 bytes): |
1 | // ============================================================ |
这里return 0返回未碰撞
门自动受损(自动开)
1 | // sdku.txt 第 31566 行 |
UE4 的 Actor 有几种碰撞/伤害委托,名字就是签名:
- ActorHitSignature → 物理碰撞
- ActorBeginOverlapSignature → 进入重叠区域
- TakeAnyDamageSignature → 受到伤害
门绑的是 TakeAnyDamage 而不是 ActorHit 或 BeginOverlap,说明它不是碰一下就触发,也不是走进去就触发,而是必须对它造成伤害才会触发事件处理。
在标准 UE4 FirstPerson 模板里,造成伤害的方式就是射击(发射 Projectile 命中目标 → ApplyDamage)。所以我判断是射击开门。
后来发现角色的 Fire 输入被删了,只能改为从 Frida 调用 ApplyDamage 来代替射击。
1 | setTimeout(function () { |
1 | // 调用 ApplyDamage 对门造成伤害,触发 TakeAnyDamage 事件 |
在 SDK dump 里搜 ApplyDamage
sdku.txt 第 10656 行
1 | static float ApplyDamage(Actor* DamagedActor, float BaseDamage, Controller* EventInstigator, |
thunk 最后一行把拆出来的参数传给了 sub_8FAC0EC, 参数是普通的寄存器传递(X0-X3 + S0),可以直接从 Frida 调用。所以 APPLY_DAMAGE_IMPL = 0x8FAC0EC。
穿墙
在地图绕了一圈发现有个场景进不去,只能穿墙,刚好这里也需要
1 | setTimeout(function () { |
1 | var noclipEnabled = false; |
传送
在后面是实现了
主动调用 teleport(-1850, 1300, 268) 方法
此部分完整代码
效果
1 | var moduleName = "libUE4.so"; |
Section1
但是意外把所有的关卡的字打出来了
获取 TextRenderActor 的 UClass
1
2levelScriptPtr + 0x3e0 → TextRenderActor17
*TextRenderActor17 + 0x10 → UClass* (TextRenderActor类的指针)所有 TextRenderActor 实例共享同一个 UClass 指针,用它当指纹识别。
遍历关卡的 Actor 列表
ULevel 内部存了 TArray<AActor*>,探测 Level+0x90 到 Level+0xB0,找到有效的 TArray,遍历每个 Actor,比对 UClass == TextRenderActor 的 UClass
对每个 TextRenderActor
取消隐藏,读 actor + 0x58 的 bit 5(bHidden),清空该组件
读文字,actor + 0x220 → TextRenderComponent → +0x448 → FText → 解析 ITextData 里的字符串
读坐标,actor + 0x130 → RootComponent → +0x11c → RelativeLocation (X,Y,Z)
但是仍然没有看见可见flag,找不到字段,调试一下
1 | // ==================== Debug Sky Actors ==================== |
这里做了一个飞天,需要在frida终端主动调用teleportToFlag()方法
1 | function forceRevealSky() { |
Section2
试过了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 | function section2_fix() { |
此处需要注释穿墙逻辑
1 | function scheduleAutoActions() { |
此外注释到穿墙逻辑后,飞到flag无法实现,enableNoclip() 把角色的 MovementMode 改成了飞行模式,并且很可能禁用了角色的碰撞响应。UE4 中 noclip,fly 模式下角色的碰撞胶囊体不参与物理碰撞检测,所以即使 cube 已经设为 Block,角色也会直接穿过去,不会触发 ReceiveHit , OnActorHit 事件。
Section3
找不到对象,写个扫描函数
1 | // 扫描 Section 3 区域所有 actor |
inspectActor 检查一个 actor 的详细信息:
- Class 指针 ,这个 actor 是什么类
- 位置 :在关卡中的坐标
- UFunctions :这个类有哪些函数(遍历 UClass.Children 链表)
- 类继承链:父类层级和 FName
- 内存 dump:0x220-0x300 的原始数据(自定义字段区域)
1 | [M2102K1AC::com.tencent.ace.match2024 ]-> inspectActor("0x7cd9ed9880") |
后来看了下其实就在
ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/
改了码表
UT1fc0gIYDArdz80Z0Xem46J
1 | enc_table = [0x31,0xbb,0x87,0x09,0xf8,0xe4,0xe7,0x90,0xf4,0x99,0xcc,0x69,0x5f,0x04,0x46,0x89, |
解密链路:
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 | var moduleName = "libUE4.so"; |

































