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

发现了穿墙透视,子弹射速有问题,可能移动速度也有问题

image-20260328151652587

准备工作

寻找Gname

搜索ByteProperty

GName 对应的就是全局变量 wlock_,地址 0xADF07C0。

注意,这题里的 UE4 不是老式一个 GNames 数组指针的形态FNamePool,NamePoolData。也就是说ByteProperty 等硬编码名字,是被 sub_6A21FA4 调 sub_6A23DA8 塞进这个池里的

image-20260328151718629

寻找GUObject

image-20260328153449855

dword_AE34A98 GUObjectArray

寻找Gworld

image-20260328154357283

qword_AFAC398,我这里的字符串识别错误了

image-20260328154502501

DumpSDK

1
2
3
4
./ue4dumper64 --strings --newue+ --gname 0xADF07C0 --package com.ACE2025.Game --output /data/local/tmp
./ue4dumper64 --objs --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game --output /data/local/tmp
./ue4dumper64 --sdku --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game --output /data/local/tmp --verbose
./ue4dumper64 --sdkw --newue+ --gname 0xADF07C0 --gworld 0xAFAC398 --package com.ACE2025.Game --o
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
./ue4dumper64 --package com.ACE2025.Game --actors --gname 0xADF07C0 --gworld 0xAFAC398 --output /data/local/tmp --newue+
Process name: com.ACE2025.Game, Pid: 3499
Base Address of libUE4.so Found At 7c14840000
UWorld: 7c1f7ec398 | World: 7bd99ce040 | Name: FirstPersonExampleMap
Level: 7be00a7c00 | Name: PersistentLevel
ActorList: 7bd9688960, ActorCount: 47

Id: 0, Addr: 7bdc0e5aa0, Actor: WorldInfo
Id: 1, Addr: 7be0079700, Actor: LightmassImportanceVolume
Id: 2, Addr: 7be1edc580, Actor: EditorCube8
Id: 3, Addr: 7be1edc340, Actor: EditorCube9
Id: 4, Addr: 7bdc7701c0, Actor: EditorCube10
Id: 5, Addr: 7be1ede080, Actor: EditorCube11
Id: 6, Addr: 7be1eddc00, Actor: EditorCube12
Id: 7, Addr: 7be1edd9c0, Actor: EditorCube13
Id: 8, Addr: 7be1edd780, Actor: EditorCube14
Id: 9, Addr: 7be1edd540, Actor: EditorCube15
Id: 10, Addr: 7be1edd300, Actor: EditorCube16
Id: 11, Addr: 7be1edd0c0, Actor: EditorCube17
Id: 12, Addr: 7be1edce80, Actor: EditorCube18
Id: 13, Addr: 7be1edcc40, Actor: EditorCube19
Id: 14, Addr: 7be1edca00, Actor: EditorCube20
Id: 15, Addr: 7be1edc7c0, Actor: EditorCube21
Id: 16, Addr: 7be1edb5c0, Actor: TemplateLabel
Id: 17, Addr: 7bd99be480, Actor: SkySphereBlueprint
Id: 18, Addr: 7bdc771180, Actor: AtmosphericFog
Id: 19, Addr: 7bdc770880, Actor: SphereReflectionCapture
Id: 20, Addr: 7be1edc100, Actor: Floor
Id: 21, Addr: 7be1edbec0, Actor: Wall1
Id: 22, Addr: 7be1edbc80, Actor: Wall2
Id: 23, Addr: 7be1edba40, Actor: Wall3
Id: 24, Addr: 7be1edb800, Actor: Wall4
Id: 25, Addr: 7bdc770640, Actor: BigWall
Id: 26, Addr: 7bdc770400, Actor: BigWall2
Id: 27, Addr: 7be0078f80, Actor: NetworkPlayerStart
Id: 28, Addr: 7bdc770d00, Actor: LightSource
Id: 29, Addr: 7bd99cd060, Actor: PostProcessVolume
Id: 30, Addr: 7bdc770ac0, Actor: SkyLight
Id: 31, Addr: 7be007a380, Actor: DefaultPhysicsVolume
Id: 32, Addr: 7be00a5b00, Actor: MyProjectGameMode
Id: 33, Addr: 7bdc772380, Actor: GameSession
Id: 34, Addr: 7bdc773580, Actor: ParticleEventManager
Id: 35, Addr: 7bdcaea600, Actor: GameNetworkManager
Id: 36, Addr: 7c0aedc800, Actor: AIController
Id: 37, Addr: 7be1edacc0, Actor: FirstPersonExampleMap_C
Id: 38, Addr: 7bd8b58d40, Actor: FirstPersonCharacter_C
Id: 39, Addr: 7bdcafcae0, Actor: ThirdPersonCharacter
Id: 40, Addr: 7be0075380, Actor: GameStateBase
Id: 41, Addr: 7bdb92c020, Actor: AbstractNavData-Default
Id: 42, Addr: 7bd8b5f340, Actor: PlayerController
Id: 43, Addr: 7c0af0dd00, Actor: PlayerState
Id: 44, Addr: 7bdfe95580, Actor: PlayerCameraManager
Id: 45, Addr: 7bd99c90e0, Actor: CameraActor
Id: 46, Addr: 7c0af0d280, Actor: MyProjectHUD

异常点1-3: 速度 加速度 后座

init_array自动执行了一些东西用 sub_27F0,sub_27F0 创建线程,线程入口是 sub_1B9C

image-20260329154041928

image-20260329154147813

sub_1B9C 先解密出这些字符串并读 /proc/self/maps,libUE4.so

可以解密出一些字符串

image-20260329160526187

sub_30A0(dst, src) 是一个一次性异或解密器,unk_890 里放的是两段拼在一起的密文数据,前 0x1A 字节,也就是 26 字节,当作 key,后 10 字节是被加密的目标字符串,逐字节执行dst[i] = src[26 + i] ^ src[i % 26];,总共解出 10 个字节,写到 unk_B658

此外

  • sub_29A4(dst, unk_7C1):解出 “/proc/self/maps\0”
  • sub_2B6C(dst, unk_7FB):解出 “/proc/%d/maps\0”
  • sub_2D1C(dst, unk_833):解出 “r\0”
  • sub_2ED8(dst, unk_865):解出 “-\0”
  • sub_30A0(dst, unk_890):解出 “libUE4.so\0”

然后 sub_B80(-1, “libUE4.so”) 取 libUE4.so 基址,sub_B80 的直线逻辑是这样的:

  • 0x1260 比较第一个参数 a1 是否小于 0。
  • 如果 a1 < 0,走 0x1294 -> 0x12A0,调用 sub_29A4 解出 “/proc/self/maps”。
  • 如果 a1 >= 0,走 0x1314 -> 0x1324,调用 sub_2B6C 解出 “/proc/%d/maps”。
  • sub_19E8 在 0x1B44 实际调用的是 __vsnprintf_chk,所以它就是个格式化字符串封装。
  • 负数参数时,格式化出 /proc/self/maps,非负参数时,格式化出 /proc//maps

/proc/self/maps:看当前这个进程自己的 maps。

/proc/1234/maps:看 PID 为 1234 的那个进程的 maps。

然后它继续做:

  • 0x1374 -> 0x1380:sub_2D1C 解出 “r”
  • 0x1390 -> 0x139C:fopen(filename, “r”)
  • 0x16B0 -> 0x16C0:fgets(line, …, fp),循环读每一行
  • 0x1784 -> 0x1790:strstr(line, module_name),检查这一行里有没有目标模块名
  • 0x17E4 -> 0x17F0:sub_2ED8 解出 “-“
  • 0x1800 -> 0x1810:strtok(line, “-“)
  • 0x1858 -> 0x1868:strtoul(token, NULL, 16)
  • 0x193C -> 0x1944:fclose(fp)
  • 0x19A8:返回解析出来的那个数

它打开 maps 文件,逐行读,找包含libUE4.so的那一行,然后把这一行按 - 切开,取前半段十六进制地址,再转成整数返回。Linux/Android 的 maps 行长这样:7b12340000-7b1f000000 r-xp … /data/app/…/libUE4.so

拿到 libUE4.so 基址后,继续

  • 0x224C:ue4_base + 0xAFAC398,后面一串是对象遍历
  • 0x2588 -> 0x2598:检查 candidate->vtable - ue4_base == 0xA63BE28
  • 0xA63BE28 是之前在 libUE4.so 里确认过的 AMyProjectCharacter vtable,所以这里是在筛主角对象

找到后,写这几处:

  • 0x2608 / 0x2628:Actor + 0x538 = 0.0f
  • 0x2644:取 Actor + 0x288,也就是 CharacterMovement
  • 0x26A0 / 0x26DC:CharacterMovement + 0x1A0 = 1e9f
  • 0x2708:CharacterMovement + 0x18C = 1e9f
  • 0x2714:写标志,避免重复执行

它遍历 UE4 运行时对象,筛选条件是:

  • *(qword *)Actor - UE4Base == 0xA63BE28

  • 0xA63BE28 正好就是我们之前在 libUE4.so 里看到的 AMyProjectCharacter vtable

    它改的是“运行时内存里的角色对象”,不是改磁盘文件。

当前确认到的 3 处实际 gameplay 写入是:

  1. 把 AMyProjectCharacter::RecoilAccumulationRate 写成 0.0f
    • sub_1B9C 里0x2608 先算出 selected_actor + 0x538
    • 0x2628 执行写入 0,这个偏移在 SDK 里对应 RecoilAccumulationRate,见 SDKw.txt:8338
  2. 把 CharacterMovement::MaxAcceleration 写成 1e9f
    • 0x2644 先取 selected_actor + 0x288,这是 CharacterMovement 指针,见 SDKw.txt:6448
    • 0x26A0 再算 CharacterMovement + 0x1A0
    • 0x26DC 写入常量 0x4E6E6B28
    • 这个偏移在 SDK 里对应 MaxAcceleration,见 SDKw.txt:6561
  3. 把 CharacterMovement::MaxWalkSpeed 写成 1e9f
    • 0x26E0 后继续拿着同一个 CharacterMovement
    • 0x2708 写 CharacterMovement + 0x18C
    • 这个偏移在 SDK 里对应 MaxWalkSpeed,见 SDKw.txt:6556

先在 libGame.so 里找写了哪里例如 0x2708 是

1
2
MOV W8, #0x4E6E6B28
STR W8, [X9,#0x18C]

这表示往某个对象偏移 0x18C 的位置写一个 32 位值 0x4E6E6B28。

这题里为什么能断言是 CharacterMovementComponent + 0x1A0,看 sub_1B9C 这一段

image-20260329165634147

image-20260329165700609

1
2
3
4
5
6
7
8
9
10
263c  LDR X8, [X19,#0x38]
2640 LDR X8, [X8]
2644 LDR X8, [X8,#0x288]
264c STR X8, [X10]
...
2698 LDR X8, [X19,#0x48]
269c LDR X8, [X8]
26a0 ADD X8, X8, #0x1A0
26dc STR W8, [X10]
2708 STR W8, [X9,#0x18C]

这段的含义是:

1. 先拿到当前选中的角色对象 actor,为什么是actor?
2. 从 actor + 0x288 取出一个指针
3. 再对这个取出来的指针加 0x1A0
4. 最后写值

先在libUE4.so找到MyProjectCharacter的类注册函数sub_67125B8 里直接出现了类名字符串

image-20260329171422214

这说明 sub_67125B8 是在注册/获取 MyProjectCharacter 这个类。这个注册函数把 sub_67125A8 当成构造入口传进去

sub_67125B8 里有这句:

image-20260329171533564

也就是说,sub_67125A8 被塞进了 MyProjectCharacter 的类注册描述里,而 sub_67125A8 本身只有两条指令

1
2
67125a8  LDR X0, [X0]
67125ac B sub_670E1B4

这就是一个标准 constructor thunk。它把对象指针取出来,然后直接跳到 sub_670E1B4。 所以到这里已经能下结论sub_670E1B4 == MyProjectCharacter 的实际构造函数.在这个构造函数开头,确实把 0xA63BE28 写进了对象首地址

sub_670E1B4 开头这几条最关键:

1
2
3
4
670e1f4  ADRP X8, #off_AC71358@PAGE
670e1f8 LDR X8, [X8,#off_AC71358@PAGEOFF] ; unk_A63BE18
670e208 ADD X10, X8, #0x10
670e210 STR X10, [X19]

意思是

1
2
3
X8  = 0xA63BE18
X10 = X8 + 0x10 = 0xA63BE28
*(uint64_t *)this = 0xA63BE28

而在构造函数里把值写到对象首地址 (qword)this”就是典型的 设置 primary vptr。

所以这里不是像 vtable,而是:

1
2
3
MyProjectCharacter::ctor(this) {
*(uint64_t *)this = 0xA63BE28;
}

这就已经把 0xA63BE28 == MyProjectCharacter 的主 vtable 地址点 证死了。

为什么不是 0xA63BE18,而是 0xA63BE28,这是 C++ ABI 的正常现象。我把 0xA63BE18 开始的字节也读出来了:

1
2
3
4
5
0xA63BE18: 00 00 00 00 00 00 00 00
0xA63BE20: 00 00 00 00 00 00 00 00
0xA63BE28: 60 b5 b2 05 00 00 00 00
0xA63BE30: 48 00 71 06 00 00 00 00
...

也就是说0xA63BE18 到 0xA63BE27 前 16 字节不是函数指针,真正函数指针表从 0xA63BE28 开始

这正好对应编译器在构造函数里做的 +0x10。所以

  • 0xA63BE18 是 vtable group 的起点附近
  • 0xA63BE28 是对象里实际保存的 address point
  • libGame.so 读对象首 qword 读到的也正是这个地址点

这也解释了为什么直接 xrefs_to 0xA63BE28 可能是空的代码引用的是 0xA63BE18,然后运行时再 +0x10。

因为 SDK 已经把 Character 类布局写出来了,这里明确写着Class: Character.Pawn.Actor.Object

CharacterMovementComponent* CharacterMovement; // [Offset: 0x288, Size: 0x8]

而当前对象又不是普通 Actor,它已经被前面的 vtable 判断缩到 AMyProjectCharacter 了。MyProjectCharacter 继承自 Character,见

SDKw.txt:8319,所以actor 是 MyProjectCharacter*,actor + 0x288 继承自 Character 的 CharacterMovement 指针

所以,按效果这个 so 目前明确做了三件事:

  • 去掉后坐力累计
  • 把最大加速度拉到极端值
  • 把最大移动速度拉到极端值

image-20260329204348105

image-20260329204421509

image-20260329204500346

异常点4: 自改朝向

AMyProjectCharacter

按输入绑定 -> 开火函数 -> 字段偏移 -> 行为验证 -> 排除误判这条链推。

  1. 先校对类布局。

    先用导出的 SDK 对齐 AMyProjectCharacter 的字段偏移,确认 FP_MuzzleLocation=0x4c8、ProjectileClass=0x510、FireSound=0x518、FireAnimation=0x520、RecoilPitch/Yaw/Recovery=0x52c~0x538。

  2. 用字符串和 xref 找到角色构造和输入绑定。

    MuzzleLocation 的 xref 落到 sub_670E1B4,这就是 AMyProjectCharacter 构造。然后我对 Fire、MoveForward、MoveRight、TurnRate、LookUpRate 做 xref,全部汇到 sub_670EB7C。这个函数连续绑定 action/axis,很明显就是 SetupPlayerInputComponent。

    这是 UE 角色类里一个很常见的函数,作用就是把按键绑定到函数,把摇杆/鼠标轴绑定到函数比如:

    • 按空格 -> Jump
    • 按开火 -> OnFire
    • W/S -> MoveForward
    • A/D -> MoveRight

    输入绑定表

    • “Jump” 绑定到跳跃
    • “Fire” 绑定到 sub_670F110
    • “ResetVR” 绑定到一个函数
    • “MoveForward” 绑定到 sub_670FA44
    • “MoveRight” 绑定到 sub_670FAAC
    • “TurnRate” 绑定到 sub_670FB14
    • “LookUpRate” 绑定到 sub_670FB60
  3. 在输入绑定里确定开火函数。sub_670EB7C 里 Fire直接绑定到了 sub_670F110,所以 sub_670F110 就是开火回调。

  4. 反编译 sub_670F110,把它拆成几段。前半段先更新 recoil 相关临时量。中间一段关键:

    调 sub_95EBDEC(),这个函数反编译后明确返回 AStaticMeshActor::StaticClass(),然后用 world + 这个 class 枚举场景里的 StaticMeshActor

    image-20260331164449245

    sub_95EBDEC() 先返回一个类

    sub_8C30D58(World, Class, OutArray) 再用这个类去找场景对象

    recoil 就是后坐力,这里能看出来有这几个字段:

    • RecoilPitch
    • RecoilYaw
    • RecoilRecoverySpeed
    • RecoilAccumulationRate

    FP = First Person Muzzle = 枪口 所以 FP_MuzzleLocation 就“第一人称枪口位置

    SceneComponent* FP_MuzzleLocation;//[Offset: 0x4c8, Size: 0x8]

    遍历时取对象名,和 E 做比较,命中后取目标位置与 FP_MuzzleLocation 位置做差,归一化,转成旋转,再通过 Pawn.Controller 指针调一个写旋转的方法

  5. 它是在改朝向

    这里访问的 a1+0x258 按 SDK 正好是 Pawn.Controller,后面那个调用只吃一个 Rotator 形态的数据,调用形态和 SetControlRotation 非常吻合。

    同时我专门去查了 ClientSetLocation、K2_SetActorLocation、K2_SetActorLocationAndRotation 的 xref,没有发现这条 0x670F110 开火链直接调用它们。

    所以这不是把角色坐标改了,而是把控制朝向硬拉到了某个目标上。表现出来就会像开火时自动偏到某个地方。

    AStaticMeshActor::StaticClass() 是 UE 的类系统概念。AStaticMeshActor 是静态网格体 actor”这个类,比如墙.地板,方块 场景装饰物

  6. 为什么说目标是静态网格锚点,不是敌人。因为 sub_95EBDEC() 明确是 StaticMeshActor::StaticClass(),所以这段自瞄逻辑本质上是在遍历场景静态网格 -> 按名字筛一个锚点 -> 把视角/开火方向拉过去。

  7. 一个容易混淆的点。sub_671096C 会把 recoil 的两个累积值缓慢插值回 0,这只是后坐力恢复,不是自瞄。

异常点5:开火随机

子弹乱飞

  1. sub_670F110 是开火函数

    在 0x670F644 它先调了 sub_670FBAC,拿发射旋转;

    在 0x670F654 又调了 sub_670FCEC,拿发射位置;

    然后 0x670F6B8 / 0x670F7A4 调 sub_8D2ED80 去 SpawnActor。

    image-20260331190758151

  2. 真正的异常点在 sub_670FBAC,这函数里有两次明确的 rand()

    image-20260331190625183

​ 第二次随机值会被算成:((rand() & 0xFFFFFF) / 16777000.0) * 60.0 - 30.0 也就是大约 [-30, +30] 度的随机偏移。

  1. 这个随机偏移最后被直接加进发射旋转。在 sub_670FBAC 末尾有三条关键指令:

    0x670FCBC FADD S8, S8, S1

    0x670FCC0 FADD S9, S9, S0

    0x670FCC4 FADD S10, S10, S12

    这就是把前面算出来的随机量混到最终返回的 rotator 里。

4. sub_670FCEC 只是算枪口位置,不是随机源。它会用 FP_MuzzleLocation 和 GunOffset 算 spawn location,没看到 rand()。

似乎子弹发射位置也有问题是从背后射出来的

OnFire 在 libUE4+0x670F644 会调用 sub_670FBAC。这一步会算一份带随机散布的 Rotation,里面有 rand(),所以子弹方向会乱飘。

丢弃原函数算出来的发射角,在真正 SpawnActor 前,直接覆盖它要用的旋转参数:

  • 旋转覆盖函数
  • 从 character + 0x258 取 Controller
  • 再从 controller + 0x288 读 ControlRotation

把这 3 个 float 直接写回 rotPtr

OnFire 到 SpawnActor 时,寄存器约定是:

  • x2 = 发射位置
  • x3 = 发射旋转

SpawnActor 是 UE 里生成一个 Actor 实例的函数,开火时它就是拿来生成子弹对象的

muzzle 就是枪口,FP_MuzzleLocation,第一人称,VR = VR 模式,MuzzleLocation = 枪口位置组件

原来的 x2 是 sub_670FCEC 算出来的,它会基于:

  • FP_MuzzleLocation @ 0x4C8
  • VR_MuzzleLocation @ 0x4D8
  • GunOffset @ 0x500

计算,现在要把位置也接管了:

  • 直接从 FP_MuzzleLocation 或 VR_MuzzleLocation 这个 SceneComponent 上读世界坐标
  • 读的是组件的 0x1D0/0x1D4/0x1D8 三个 float
  • 然后直接写回 locPtr,也就是 x2

合并入口如下,它会同时做两件事:

1
2
3
4
function overwriteProjectileSpawn(character, locPtr, rotPtr, useVrMuzzle) {
overwriteSpawnLocationFromMuzzle(character, locPtr, useVrMuzzle);
overwriteSpawnRotationFromController(character, rotPtr);
}
  • overwriteSpawnLocationFromMuzzle(…)
  • overwriteSpawnRotationFromController(…)

异常点6: 透视 (为解决)

参考了比较多的博客还是没解决

最终脚本

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
const LIB_GAME = 'libGame.so';
const LIB_UE4 = 'libUE4.so';

const WORKER_THREAD_RVA = 0x1B9C;
const BAD_STORE_RVAS = [
0x2628, // STR W8, [X9] -> RecoilAccumulationRate = 0.0f
0x26DC, // STR W8, [X10] -> MaxAcceleration = 1.0e9f
0x2708 // STR W8, [X9,#0x18C] -> MaxWalkSpeed = 1.0e9f
];

const RVA_GUOBJECT_WRAPPER = ptr('0xAFAC398');
const RVA_SET_MATERIAL = ptr('0x9574EB8');
const RVA_SET_RENDER_CUSTOM_DEPTH = ptr('0x9570AAC');
const RVA_SET_CUSTOM_STENCIL_VALUE = ptr('0x9570A04');
const RVA_SET_CUSTOM_STENCIL_MASK = ptr('0x957095C');

const RVA_ONFIRE_AUTOAIM_SETCONTROLROT_CALL = ptr('0x670F3F8');
const RVA_ONFIRE_RANDOM_SPREAD_CALL = ptr('0x670F644');
const RVA_ONFIRE_SPAWN_CALL_NON_VR = ptr('0x670F6B8');
const RVA_ONFIRE_SPAWN_CALL_VR = ptr('0x670F7A4');

const VT_MYPROJECT_CHARACTER = ptr('0xA63BE28');
const VT_TP_THIRDPERSON = ptr('0xA63A9A8');

const IDX_M_UE4MAN_CHESTLOGO = 0x3721;
const IDX_M_MALE_BODY = 0x3722;
const IDX_M_UE4MAN_BODY = 0x3688;
const IDX_POSTPROCESS_VOLUME = 0x3c18;

// Offsets
const OFF_CHARACTER_MESH = 0x280;
const OFF_MESH1P = 0x4B8;
const OFF_FP_GUN = 0x4C0;
const OFF_FP_MUZZLE_LOCATION = 0x4C8;
const OFF_VR_GUN = 0x4D0;
const OFF_VR_MUZZLE_LOCATION = 0x4D8;
const OFF_PAWN_CONTROLLER = 0x258;
const OFF_CONTROLLER_CONTROL_ROTATION = 0x288;
const OFF_SCENE_COMPONENT_LOCATION = 0x1D0;

const OFF_RENDER_FLAGS = 0x216;
const OFF_STENCIL_MASK = 0x21C;
const OFF_STENCIL_VALUE = 0x220;

const OFF_PPV_SETTINGS = 0x260;
const OFF_PPV_BLEND_WEIGHT = 0x7C8;
const OFF_PPV_ENABLED = 0x7CC;
const OFF_SETTINGS_WEIGHTED_BLENDABLES = 0x548; // inside Settings
const OFF_PPV_WEIGHTED_BLENDABLES = OFF_PPV_SETTINGS + OFF_SETTINGS_WEIGHTED_BLENDABLES; // 0x7A8

let ue4Base = null;
let installed = false;
let tickTimer = null;
let storesPatched = false;
let pthreadHookInstalled = false;

let fnSetMaterial = null;
let fnSetRenderCustomDepth = null;
let fnSetCustomStencilValue = null;
let fnSetCustomStencilMask = null;

let aimFixPatched = false;
let bulletSpreadFixInstalled = false;

const trackedMeshes = new Map();

const dummyThreadStart = new NativeCallback(function (arg) {
console.log('[+] blocked libGame worker thread entry');
return ptr(0);
}, 'pointer', ['pointer']);

function isValidPtr(p) {
return p !== null && !p.isNull() && p.compare(ptr('0x10000')) > 0;
}

function safeReadPointer(p) {
try {
const v = p.readPointer();
return isValidPtr(v) ? v : ptr(0);
} catch (e) {
return ptr(0);
}
}

function getObjectArrayInfo() {
try {
const wrapper = safeReadPointer(ue4Base.add(RVA_GUOBJECT_WRAPPER));
if (!isValidPtr(wrapper)) return null;

const objContainer = safeReadPointer(wrapper.add(0x30));
if (!isValidPtr(objContainer)) return null;

const objects = safeReadPointer(objContainer.add(0x98));
if (!isValidPtr(objects)) return null;

const count = objContainer.add(0xA0).readU32();
if (count === 0 || count > 200000) return null;

return { objects, count };
} catch (e) {
return null;
}
}

function getObjectByIndex(index) {
const info = getObjectArrayInfo();
if (info === null) return ptr(0);
if (index < 0 || index >= info.count) return ptr(0);
return safeReadPointer(info.objects.add(index * Process.pointerSize));
}

function overwriteSpawnRotationFromController(character, rotPtr) {
if (!isValidPtr(character) || !isValidPtr(rotPtr)) return false;

const controller = safeReadPointer(character.add(OFF_PAWN_CONTROLLER));
if (!isValidPtr(controller)) return false;

try {
const pitch = controller.add(OFF_CONTROLLER_CONTROL_ROTATION).readFloat();
const yaw = controller.add(OFF_CONTROLLER_CONTROL_ROTATION + 4).readFloat();
const roll = controller.add(OFF_CONTROLLER_CONTROL_ROTATION + 8).readFloat();

rotPtr.writeFloat(pitch);
rotPtr.add(4).writeFloat(yaw);
rotPtr.add(8).writeFloat(roll);
return true;
} catch (e) {
return false;
}
}

function overwriteSpawnLocationFromMuzzle(character, locPtr, useVrMuzzle) {
if (!isValidPtr(character) || !isValidPtr(locPtr)) return false;

const muzzleOffset = useVrMuzzle ? OFF_VR_MUZZLE_LOCATION : OFF_FP_MUZZLE_LOCATION;
const muzzle = safeReadPointer(character.add(muzzleOffset));
if (!isValidPtr(muzzle)) return false;

try {
const x = muzzle.add(OFF_SCENE_COMPONENT_LOCATION).readFloat();
const y = muzzle.add(OFF_SCENE_COMPONENT_LOCATION + 4).readFloat();
const z = muzzle.add(OFF_SCENE_COMPONENT_LOCATION + 8).readFloat();

locPtr.writeFloat(x);
locPtr.add(4).writeFloat(y);
locPtr.add(8).writeFloat(z);
return true;
} catch (e) {
return false;
}
}

function overwriteProjectileSpawn(character, locPtr, rotPtr, useVrMuzzle) {
overwriteSpawnLocationFromMuzzle(character, locPtr, useVrMuzzle);
overwriteSpawnRotationFromController(character, rotPtr);
}

function nop4(addr) {
Memory.patchCode(addr, 4, function (code) {
const writer = new Arm64Writer(code, { pc: addr });
writer.putNop();
writer.flush();
});
}

function patchBadStores() {
const base = Module.findBaseAddress(LIB_GAME);
if (base === null) {
return false;
}

if (storesPatched) {
return true;
}

BAD_STORE_RVAS.forEach(function (rva) {
const addr = base.add(rva);
nop4(addr);
console.log('[+] NOP ' + LIB_GAME + '+0x' + rva.toString(16) + ' @ ' + addr);
});

storesPatched = true;
return true;
}

function hookPthreadCreate() {
if (pthreadHookInstalled) {
return true;
}

const pthreadCreate = Module.findExportByName(null, 'pthread_create');
if (pthreadCreate === null) {
console.log('[-] pthread_create not found');
return false;
}

Interceptor.attach(pthreadCreate, {
onEnter(args) {
const startRoutine = args[2];
if (!isValidPtr(startRoutine)) return;

const mod = Process.findModuleByAddress(startRoutine);
if (mod === null || mod.name !== LIB_GAME) {
return;
}

const rva = startRoutine.sub(mod.base).toUInt32();
if (rva === WORKER_THREAD_RVA) {
console.log('[+] intercept pthread_create for ' + LIB_GAME + '+0x' + rva.toString(16));
args[2] = dummyThreadStart;
}
}
});

pthreadHookInstalled = true;
console.log('[+] installed ' + LIB_GAME + ' pthread_create hook');
return true;
}

function patchAutoAimFix() {
if (aimFixPatched) return true;
if (ue4Base === null) return false;
if (Process.arch !== 'arm64') {
console.log('[-] auto-aim patch skipped: unsupported arch ' + Process.arch);
return false;
}

try {
const site = ue4Base.add(RVA_ONFIRE_AUTOAIM_SETCONTROLROT_CALL);
nop4(site);

aimFixPatched = true;
console.log('[+] patched fire auto-aim control-rotation call');
return true;
} catch (e) {
console.log('[-] patchAutoAimFix failed: ' + e);
return false;
}
}

function installBulletSpreadFix() {
if (bulletSpreadFixInstalled) {
return true;
}
if (ue4Base === null) {
return false;
}

try {
nop4(ue4Base.add(RVA_ONFIRE_RANDOM_SPREAD_CALL));

Interceptor.attach(ue4Base.add(RVA_ONFIRE_SPAWN_CALL_NON_VR), {
onEnter(args) {
const character = this.context.x19;
const locPtr = this.context.x2;
const rotPtr = this.context.x3;
overwriteProjectileSpawn(character, locPtr, rotPtr, false);
}
});

Interceptor.attach(ue4Base.add(RVA_ONFIRE_SPAWN_CALL_VR), {
onEnter(args) {
const character = this.context.x19;
const locPtr = this.context.x2;
const rotPtr = this.context.x3;
overwriteProjectileSpawn(character, locPtr, rotPtr, true);
}
});

bulletSpreadFixInstalled = true;
console.log('[+] installed bullet spread fix hooks');
return true;
} catch (e) {
console.log('[-] installBulletSpreadFix failed: ' + e);
return false;
}
}

function install() {
ue4Base = Module.findBaseAddress(LIB_UE4);
if (ue4Base === null) return false;
if (installed) return true;

fnSetMaterial = new NativeFunction(ue4Base.add(RVA_SET_MATERIAL), 'pointer', ['pointer', 'int', 'pointer']);
fnSetRenderCustomDepth = new NativeFunction(ue4Base.add(RVA_SET_RENDER_CUSTOM_DEPTH), 'void', ['pointer', 'bool']);
fnSetCustomStencilValue = new NativeFunction(ue4Base.add(RVA_SET_CUSTOM_STENCIL_VALUE), 'void', ['pointer', 'int']);
fnSetCustomStencilMask = new NativeFunction(ue4Base.add(RVA_SET_CUSTOM_STENCIL_MASK), 'void', ['pointer', 'int']);

patchAutoAimFix();
installBulletSpreadFix();

Interceptor.attach(ue4Base.add(RVA_SET_MATERIAL), {
onEnter(args) {
const mesh = args[0];
if (!isValidPtr(mesh)) return;

const key = mesh.toString();
if (!trackedMeshes.has(key)) return;

const profile = trackedMeshes.get(key);
const slot = args[1].toInt32();

const mats = getNormalMaterials();
if (profile === 'tp') {
if (slot === 0 && isValidPtr(mats.tpBody)) args[2] = mats.tpBody;
if (slot === 1 && isValidPtr(mats.tpLogo)) args[2] = mats.tpLogo;
} else if (profile === 'fp') {
if (slot === 0 && isValidPtr(mats.fpBody)) args[2] = mats.fpBody;
}
}
});

installed = true;
console.log('[+] installed pp/material/autoaim fix hooks');
return true;
}

function getNormalMaterials() {
return {
tpLogo: getObjectByIndex(IDX_M_UE4MAN_CHESTLOGO),
tpBody: getObjectByIndex(IDX_M_MALE_BODY),
fpBody: getObjectByIndex(IDX_M_UE4MAN_BODY)
};
}

function clearCustomDepth(mesh) {
if (!isValidPtr(mesh)) return;

try {
const flags = mesh.add(OFF_RENDER_FLAGS).readU8();
mesh.add(OFF_RENDER_FLAGS).writeU8(flags & (~0x40));
} catch (e) {}

try { mesh.add(OFF_STENCIL_MASK).writeU8(0); } catch (e) {}
try { mesh.add(OFF_STENCIL_VALUE).writeS32(0); } catch (e) {}

try { fnSetRenderCustomDepth(mesh, false); } catch (e) {}
try { fnSetCustomStencilValue(mesh, 0); } catch (e) {}
try { fnSetCustomStencilMask(mesh, 0); } catch (e) {}
}

function forceThirdPersonMaterials(mesh, mats) {
if (!isValidPtr(mesh)) return false;
trackedMeshes.set(mesh.toString(), 'tp');

clearCustomDepth(mesh);

try {
if (isValidPtr(mats.tpBody)) fnSetMaterial(mesh, 0, mats.tpBody);
if (isValidPtr(mats.tpLogo)) fnSetMaterial(mesh, 1, mats.tpLogo);
return true;
} catch (e) {
return false;
}
}

function forceFirstPersonMaterials(mesh, mats) {
if (!isValidPtr(mesh)) return false;
trackedMeshes.set(mesh.toString(), 'fp');

clearCustomDepth(mesh);

try {
if (isValidPtr(mats.fpBody)) fnSetMaterial(mesh, 0, mats.fpBody);
return true;
} catch (e) {
return false;
}
}

function getVtableRva(obj) {
try {
const vt = obj.readPointer();
if (!isValidPtr(vt)) return null;
return vt.sub(ue4Base);
} catch (e) {
return null;
}
}

function repairCharacters() {
const info = getObjectArrayInfo();
if (info === null) return 0;

const mats = getNormalMaterials();
let changed = 0;

for (let i = 0; i < info.count; i++) {
const obj = safeReadPointer(info.objects.add(i * Process.pointerSize));
if (!isValidPtr(obj)) continue;

const vtRva = getVtableRva(obj);
if (vtRva === null) continue;

if (vtRva.compare(VT_TP_THIRDPERSON) === 0) {
const mesh = safeReadPointer(obj.add(OFF_CHARACTER_MESH));
if (forceThirdPersonMaterials(mesh, mats)) changed++;
continue;
}

if (vtRva.compare(VT_MYPROJECT_CHARACTER) === 0) {
const mesh = safeReadPointer(obj.add(OFF_CHARACTER_MESH));
const mesh1p = safeReadPointer(obj.add(OFF_MESH1P));
const fpGun = safeReadPointer(obj.add(OFF_FP_GUN));
const vrGun = safeReadPointer(obj.add(OFF_VR_GUN));

if (forceThirdPersonMaterials(mesh, mats)) changed++;
if (forceFirstPersonMaterials(mesh1p, mats)) changed++;
clearCustomDepth(fpGun);
clearCustomDepth(vrGun);
}
}

return changed;
}

function disablePostProcessVolume() {
const ppv = getObjectByIndex(IDX_POSTPROCESS_VOLUME);
if (!isValidPtr(ppv)) return false;

try {
ppv.add(OFF_PPV_BLEND_WEIGHT).writeFloat(0.0);
} catch (e) {}

try {
const b = ppv.add(OFF_PPV_ENABLED).readU8();
ppv.add(OFF_PPV_ENABLED).writeU8(b & (~1));
} catch (e) {}

try {
const wb = ppv.add(OFF_PPV_WEIGHTED_BLENDABLES);
wb.writePointer(ptr(0));
wb.add(8).writeU32(0);
wb.add(12).writeU32(0);
} catch (e) {}

return true;
}

function tick() {
if (!install()) return;

const n = repairCharacters();
const ppvOk = disablePostProcessVolume();

if (n > 0 || ppvOk) {
console.log('[+] repaired meshes=' + n + ', ppv=' + (ppvOk ? 'off' : 'missing'));
}
}

function main() {
try {
hookPthreadCreate();
} catch (e) {
console.log('[!] pthread hook error: ' + e);
}

const gamePatchTimer = setInterval(function () {
try {
if (patchBadStores()) {
clearInterval(gamePatchTimer);
}
} catch (e) {
console.log('[!] patch error: ' + e);
}
}, 50);

tick();
tickTimer = setInterval(tick, 1000);
}

setImmediate(main);