0%

TencentGameSecurity2026-Android

Balabala:

和2021-2022版本的加固很像,除了没有反调试基本思路是一样的。

展示:

了解目标、确定方向:

打开游戏,触发示例方块得到样例flag,目标是触发屋顶的方块,得到真正的flag,常规游玩、车是开不到那地方的。

所以第一个目标:传送车的坐标到触发块的坐标。

解包APK发现是Godot引擎制作的游戏,gdc脚本也被加密了,本身对godot不是很熟悉,不过既然是开源的就下载源码下来看看,同时上网搜索发现:Godot官方是支持用脚本加密的。

懒了: https://www.52pojie.cn/thread-2002779-1-1.html ,直接照抄思路dump密钥(脚本末尾)

得到ce4df8753b59a5a39ade58ac07ef947a3da39f2af75e3284d51217c04d49a061 ,

尝试使用社区工具gdre_tools来解密显然是不行的,下载源码后发现脚本解密逻辑中的AES_MODE和游戏逆向出来的处理方式不太一样,要改掉那部分逻辑又得重新下个godot引擎编译很麻烦,干脆直接复用游戏内的解密逻辑,然后再用gdre_tools读取gdc脚本:

游戏引擎(libgodot_android.so)解密魔改部分:

  • sub_3801410
    这是外层 FileAccessEncrypted::open_and_parse 。
  • 0x380170c
    检查魔数 1128612935 = 0x43454447 = “GDEC”。
  • 0x38019e4
    外层真正调用解密 wrapper 的位置。
  • sub_376EF68
    是一层很薄的 wrapper,直接转到 sub_197DE18。
  • sub_197C210
    AES key schedule 初始化。
  • sub_197DE18
    “魔改流模式”核心。

解密脚本:

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
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import struct
from pathlib import Path


DEFAULT_INPUT = Path("lib/arm64-v8a/libsec2026.so")
DEFAULT_OUTPUT = Path("lib/arm64-v8a/libsec2026.payload.bin")
DEFAULT_MAP_SIZE_OFFSET = 0x69A60
DEFAULT_COMP_SIZE_OFFSET = 0x69A64
DEFAULT_BLOB_OFFSET = 0x69A6C
DEFAULT_ENTRY_OFFSET = 0x10


def parse_int(value: str) -> int:
return int(value, 0)


def read_u32_le(data: bytes, offset: int) -> int:
return struct.unpack_from("<I", data, offset)[0]


def to_signed32(value: int) -> int:
value &= 0xFFFFFFFF
return value - 0x1_0000_0000 if value & 0x80000000 else value


class Bitstream:
def __init__(self, data: bytes) -> None:
self.data = data
self.offset = 0
self.word = 0x80000000

def read_byte(self) -> int:
if self.offset >= len(self.data):
raise EOFError("read past compressed blob")
value = self.data[self.offset]
self.offset += 1
return value

def next_bit(self) -> int:
carry = 1 if (self.word & 0x80000000) else 0
self.word = (self.word << 1) & 0xFFFFFFFF
if self.word == 0:
if self.offset + 4 > len(self.data):
raise EOFError("reload past compressed blob")
reloaded = read_u32_le(self.data, self.offset)
self.offset += 4
total = reloaded + reloaded + carry
carry = 1 if total > 0xFFFFFFFF else 0
self.word = total & 0xFFFFFFFF
return carry


def decode_varlen(bitstream: Bitstream) -> int:
value = 1
while True:
value = ((value << 1) + bitstream.next_bit()) & 0xFFFFFFFF
if bitstream.next_bit():
return value


def unpack_payload(blob: bytes) -> bytes:
bitstream = Bitstream(blob)
out = bytearray()
last_offset = 0xFFFFFFFF

while True:
if bitstream.next_bit():
out.append(bitstream.read_byte())
continue

code = decode_varlen(bitstream)
if code >= 3:
offset_low = bitstream.read_byte()
last_offset = (~(offset_low | ((code - 3) << 8))) & 0xFFFFFFFF
if last_offset == 0:
break

length = bitstream.next_bit()
length = ((length << 1) + bitstream.next_bit()) & 0xFFFFFFFF
if length == 0:
length = (decode_varlen(bitstream) + 2) & 0xFFFFFFFF

if ((last_offset + 0xD00) >> 32) == 0:
length = (length + 1) & 0xFFFFFFFF

src = len(out) + to_signed32(last_offset)
if src < 0:
raise ValueError(f"invalid back-reference: src={src} offset={last_offset:#x}")

copies = length + 1
for _ in range(copies):
out.append(out[src])
src += 1

if bitstream.offset != len(blob):
raise ValueError(
f"decompression ended at 0x{bitstream.offset:x}, expected 0x{len(blob):x}"
)
return bytes(out)


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Offline unpacker for the runtime-compressed payload in libsec2026.so."
)
parser.add_argument(
"input",
nargs="?",
type=Path,
default=DEFAULT_INPUT,
help=f"input shared object (default: {DEFAULT_INPUT})",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"where to write the unpacked payload (default: {DEFAULT_OUTPUT})",
)
parser.add_argument(
"--map-size-offset",
type=parse_int,
default=DEFAULT_MAP_SIZE_OFFSET,
help=f"offset of the mapped-size dword (default: {DEFAULT_MAP_SIZE_OFFSET:#x})",
)
parser.add_argument(
"--comp-size-offset",
type=parse_int,
default=DEFAULT_COMP_SIZE_OFFSET,
help=f"offset of the compressed-size dword (default: {DEFAULT_COMP_SIZE_OFFSET:#x})",
)
parser.add_argument(
"--blob-offset",
type=parse_int,
default=DEFAULT_BLOB_OFFSET,
help=f"offset of the compressed blob (default: {DEFAULT_BLOB_OFFSET:#x})",
)
parser.add_argument(
"--entry-offset",
type=parse_int,
default=DEFAULT_ENTRY_OFFSET,
help=f"runtime branch target inside the unpacked payload (default: {DEFAULT_ENTRY_OFFSET:#x})",
)
parser.add_argument(
"--pad-to-map-size",
action="store_true",
help="pad the output file to the loader's mmap size",
)
return parser


def main() -> int:
args = build_parser().parse_args()

image = args.input.read_bytes()
map_size = read_u32_le(image, args.map_size_offset)
comp_size = read_u32_le(image, args.comp_size_offset)
blob = image[args.blob_offset : args.blob_offset + comp_size]
if len(blob) != comp_size:
raise ValueError("compressed blob extends past end of file")

payload = unpack_payload(blob)
if len(payload) > map_size:
raise ValueError(
f"payload is larger than mapped size: 0x{len(payload):x} > 0x{map_size:x}"
)

written = payload.ljust(map_size, b"\x00") if args.pad_to_map_size else payload
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_bytes(written)

sha256 = hashlib.sha256(payload).hexdigest()
print(f"input: {args.input}")
print(f"output: {args.output}")
print(f"map size: 0x{map_size:x}")
print(f"blob size: 0x{comp_size:x}")
print(f"payload size: 0x{len(payload):x}")
print(f"entry offset: 0x{args.entry_offset:x}")
print(f"entry file: {args.output}@0x{args.entry_offset:x}")
print(f"sha256: {sha256}")
if args.pad_to_map_size and len(written) != len(payload):
print(f"padded size: 0x{len(written):x}")
return 0


if __name__ == "__main__":
raise SystemExit(main())

同时获取到了flag的获取逻辑:

image-20260412090242150

然后就是处理传送车的问题:反过来利用 Godot 自己的对象系统和场景系统,直接让游戏正常执行这段逻辑。

根据Unity开发经验猜测:Godot肯定也有某种find gameObject方法,找了一下 Godot 恰好给了这样做的条件。只要在 Frida 里拿到引擎内部的这些能力:

  • 获取全局 singleton;
  • 构造 VariantStringNameString
  • 在主线程上远程调用对象方法;

就可以像脚本层一样直接操纵场景节点。

Frida 远程调用 Godot 内部方法移车

这部分的实现对应脚本是frida_move_trigger_sec2026.js(太长放末尾了)。

核心思路远程调用引擎提供的接口

  1. libgodot_android.so 固定偏移解析出运行时接口。
  2. 自己构造 Godot Variant 参数。
  3. 通过 Engine.get_main_loop() 拿到 SceneTree
  4. 继续拿到 root,再用 find_child() 找到:
    • car
    • Trigger2
    • Label2
  5. 读取 Trigger2 的全局变换,直接把车和物理 body 移过去。

实在懒得搜坐标手动改,还是原生的办法通用性高一些。

如果只是改可见节点的位置,车不一定真的算“进入触发器”,因为场景里真正参与碰撞的是物理对象。也正因为这样,frida_move_trigger_sec2026.js 不是只改一次 transform 就结束了,而是同时做了两层同步:

  1. 对节点本身调用:
    • set_global_transform
    • force_update_transform
  2. PhysicsServer3D 调用:
    • body_set_state(transform)
    • body_set_state(linear_velocity, Vector3.ZERO)
    • body_set_state(angular_velocity, Vector3.ZERO)
    • body_set_state(sleeping, false)

libsec.so分析:

IDA打开可以分析的函数不多:

image-20260412061724909

主要是解压子程序,然后通过BR命令跳转到子程序去执行逻辑。

二环主要逻辑:

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
00001450        // 二环装载器:根据 arg1
00001450 // 的自相对头字段计算重定位基址与下一环入口;把
00001450 // rebase + *(u32 *)(arg1 + 0xc) 处的内嵌 UPX
00001450 // 风格容器复制到临时映射并解包;遍历 arg3 +
00001450 // 0x40 开始的 0x38
00001450 // 字节描述符,逐段映射、修补并设置权限;如遇可执行尾段则通过
00001450 // memfd 构造 16 字节 RX 跳板;最后在 0x1778
00001450 // 跳入重定位后的下一环入口。
00001458 int64_t x19
00001458 int64_t var_e0 = x19
0000147c float128 v15
0000147c sub_1004(arg1, x19, v15) // 设置描述符号
00001484 // 计算重定位基址:arg1
00001484 // 头字段是自相对偏移,因此 base = arg1 - *(u32
00001484 // *)arg1。
0000148c double v9 = float.d(arg1 - zx.q(*arg1))
00001490 int32_t x1 = (*(arg1 + 8)).d
00001494 int64_t var_10 = *arg1
000014a4 // 定位内嵌的下一环压缩块:packed_blob = rebase_base
000014a4 // + *(u32 *)(arg1 + 0xc)。
000014a4 char* x19_2 = v9 i+ zx.q(arg1[3])
000014b4 // 定位重定位后的下一环入口:next_entry =
000014b4 // rebase_base + *(u32 *)(arg1 + 4)。
000014b4 double v10 = float.d(v9 i+ zx.q(arg1[1]))
000014b8 uint64_t x0_6 = zx.q(arg1.d - x19_2.d)
000014dc // mmap
000014dc // 申请 RW
000014dc // 临时映射,用来承接下一环的压缩容器。
000014dc char* x0_7 = sub_74()
000014f0 // 拷贝缓冲区
000014f0 // 把内嵌压缩块复制到临时映射中。
000014f0 sub_f48(x0_7, x19_2, x0_6)
000014fc uint64_t var_20 = zx.q(*(x0_7 + 0x18))
00001500 void* var_18 = arg3
0000150c uint64_t var_30 = zx.q(*(x0_7 + 0x1c)) + 0xc
00001510 void* var_28 = &x0_7[0x18]
0000151c // 初始化 / 推进 scratch+0x18 处的 UPX
0000151c // 风格块流解析器。
0000151c sub_1150(&var_30, &var_20)
00001520 // 执行起始地址
00001520 // 描述符表起点:arg3 + 0x40;每项大小 0x38 字节。
00001520 int64_t* x19_3 = arg3 + 0x40
00001530 // desc_end = desc_table + (*(u16 *)(arg3 + 0x38) *
00001530 // 0x38),准备遍历全部段描述符。
00001530 void* x23_1 = &x19_3[zx.q(*(arg3 + 0x38)) * 7]
00001530
00001538 if (x19_3 u< x23_1)
0000153c int32_t x24_1 = 0
00001540 int64_t x21_1 = 0
00001540
00001750 do // 修补映射的主逻辑
00001550 // 读取 desc->type_flags;只有 (type_flags &
00001550 // 0x2ffffffff) == 1 的项会按可装载段处理。
00001564 if ((*x19_3 & 0x2ffffffff) == 1)
00001568 if (x21_1 == 0)
0000156c // 首个可装载段会建立统一的
0000156c // load_bias:load_bias = rebase_base -
0000156c // desc->vaddr。
00001574 x21_1 = v9 i- x19_3[2]
00001574
00001578 // 计算当前段的逻辑末尾 desc->vaddr +
00001578 // desc->filesz,并从块流中取出下一个 0xc
00001578 // 字节块头。
00001580 int32_t x20_2 = (x19_3[2]).d + (x19_3[4]).d
00001584 var_30 = 0xc
00001594 int32_t var_50
00001594 sub_10f0(&var_30, &var_50, 0xc)
000015a0 var_28 -= 0xc
000015a8 int32_t var_4c
000015a8 var_30 = zx.q(var_4c)
000015ac int32_t x0_21 = var_50
000015b0 uint64_t x1_9 = zx.q(x0_21)
000015b4 // chunk_len =
000015b4 // hdr[0];记录本次要回填到当前段的块长度。
000015b4 var_20 = x1_9
000015c0 // 计算当前块的落点:dst = load_bias +
000015c0 // (desc->vaddr + desc->filesz - chunk_len)。
000015c0 int64_t x1_11 = zx.q(x20_2) - x1_9 + x21_1
000015c0
000015dc if (x19_3[1] + x19_3[4] u> zx.q(x1) || x24_1 != 0)
000015ec int32_t var_64_1
000015ec
000015ec if ((*(x19_3 + 4) & 1) == 0)
00001600 // 若 desc->flags.bit0
00001600 // 未置位,则走匿名 RW
00001600 // 映射路径(sub_f6c)。
00001604 sub_f6c(x0_21, x1_11, arg1.d)
00001608 var_64_1 = 0
000015ec else
000015f0 // 若 desc->flags.bit0 置位,则走
000015f0 // file-backed /
000015f0 // 对齐映射路径(sub_1374)。
000015f8 var_64_1 = sub_1374(x0_21, x1_11, arg1)
000015f8
0000160c // 保存当前 chunk_len /
0000160c // dst,随后推进块流到下一个块。
0000160c uint32_t x0_23 = var_20.d
00001620 sub_1150(&var_30, &var_20)
00001620
00001628 // 只有可执行 / file-backed
00001628 // 类型的段才会继续进入下面的跳板与最终权限处理逻辑。
0000162c if ((*(x19_3 + 4) & 1) != 0)
00001644 int32_t var_40_1 = 0xd4000001
00001650 int32_t var_3c_1 = 0xa9417be2
0000165c int32_t var_38_1 = 0xa8c207e0
00001668 int32_t var_34_1 = 0xd61f03c0
00001668
0000166c // 特殊分支:只处理 masked
0000166c // type_flags == 0x100000001
0000166c // 的描述符(可执行尾段 /
0000166c // 跳板场景)。
0000167c if ((*x19_3 & 0x1ffffffff) == 0x100000001)
00001680 // 计算尾部 slack = desc->memsz -
00001680 // desc->filesz;若满足页对齐约束,则为尾段构造
00001680 // RX 跳板。
0000169c if ((neg.d(x1_11.d + (x19_3[5]).d - (x19_3[4]).d)
0000169c & not.d(arg1.d)) u> 0xf)
0000169c jump(0x16a0)
0000169c
000016dc int64_t __saved_fp
000016dc // 通过 memfd_create -> write(16-byte
000016dc // veneer) -> mmap(PROT_RX)
000016dc // 生成临时可执行跳板,结果保存在
000016dc // x26。
000016dc sub_16e4(x19_3, x1_11, x21_1, 0xffffffffffff, x23_1, x24_1,
000016dc 0xc, arg2, x1, &__saved_fp, v9, v10)
000016e0 undefined
000016e0
0000172c // 调用 sub_12c4
0000172c // 完成当前段的收尾:设权限、刷
0000172c // I-cache,并按需要释放中间映射。
00001740 sub_12c4(x0_23, x1_11, x19_3, var_64_1, x21_1)
00001740
00001744 x24_1 += 1
000015dc else
000015e0 x24_1 += 1
000015e0
00001748 // 移动到下一个 0x38
00001748 // 字节描述符,继续装载循环。
00001748 x19_3 = &x19_3[7]
00001750 while (x23_1 u> x19_3)
00001750
00001764 // 释放之前为压缩容器申请的 scratch 映射。
00001764 sub_1c(x0_7, x0_6)
00001778 // 正式跳入下一环入口:blr next_entry(zx.q(*arg2),
00001778 // *(arg2 + 8), *(arg2 + 0x10))。
00001778 v10(zx.q(*arg2), *(arg2 + 8), *(arg2 + 0x10))
0000179c return 0

选择在00001764 通过Frida脚本动态dump(脚本见末尾)下来继续分析:

三环:

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
00001c48    int64_t sub_1c48()

00001c48 // 三环入口的首次自修改例程:用 8
00001c48 // 字节常量循环改写 0xa25d2 开始的 0x21
00001c48 // 字节,先解锁后续跳板/代码片段。
00001c48 ff4300d1 sub sp, sp, #0x10
00001c4c cac98bd2 mov x10, #0x5e4e
00001c50 e9031f2a mov w9, wzr {_start}
00001c54 eae9b4f2 movk x10, #0xa74f, lsl #0x10
00001c58 1f2003d5 nop
00001c5c // 定位三环入口后要被原地改写的 0x21 字节区域。
00001c5c a84b5050 adr x8, 0xa25d2
00001c60 eadecdf2 movk x10, #0x6ef7, lsl #0x20
00001c64 6af4f2f2 movk x10, #0x97a3, lsl #0x30 {-0x685c910858b0a1b2}
00001c68 ea0700f9 str x10, [sp, #0x8 {var_8}] {-0x685c910858b0a1b2}

00001c6c 2a7d4093 sxtw x10, w9
00001c70 eb230091 add x11, sp, #0x8 {var_8}
00001c74 2b0940b3 bfxil x11, x9, #0, #0x3 {var_8}
00001c78 2d010012 and w13, w9, #0x1
00001c7c 0c696a38 ldrb w12, [x8, x10]
00001c80 6b014039 ldrb w11, [x11]
00001c84 6e010c0a and w14, w11, w12
00001c88 6b010c2a orr w11, w11, w12
00001c8c ec030e4b neg w12, w14
00001c90 ee03292a mvn w14, w9
00001c94 29010d0b add w9, w9, w13
00001c98 ce791f32 orr w14, w14, #0xfffffffe
00001c9c 6d010c0a and w13, w11, w12
00001ca0 29010e0b add w9, w9, w14
00001ca4 6b010c4a eor w11, w11, w12
00001ca8 29090011 add w9, w9, #0x2
00001cac 6b050d0b add w11, w11, w13, lsl #0x1
00001cb0 3f850071 cmp w9, #0x21
00001cb4 0b692a38 strb w11, [x8, x10]
00001cb8 abfdff54 b.lt 0x1c6c

00001cbc ff430091 add sp, sp, #0x10
00001cc0 c0035fd6 ret

因为三环入口 0x1c48 会先自修改代码,静态分析已经啃不动了,看静态毫无头绪,Hook要打上还得抓时机控制、不然直接崩。想着应该可以用Stalker去追执行流说不定方便点,但由于页权限切换、还有自修改代码等原因、反正我是一个个线程去试了,没扒下来。一番静态压根找不到Process的链路,转向动态,从native层和游戏层之间的沟通入手:

动态分析:

根据解包的配置知道,libsec的在游戏中被调用的入口点是

1
2
[configuration]
entry_symbol = "extension_init"

而Godot GDExtension的标准API是:

1
2
3
4
5
GDExtensionBool extension_init(
GDExtensionInterfaceGetProcAddress p_get_proc_address,
GDExtensionClassLibraryPtr p_library,
GDExtensionInitialization *r_initialization
);

在Godot游戏引擎要调用拓展的时候第一个传入的函数指针p_get_proc_address类似于引擎接口查询函数可以直接查到Godot 的注册接口地址classdb_register_extension_class_method 在注册拓展函数的时候,会交一个GDExtensionClassMethodInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct GDExtensionClassMethodInfo {
void *name; // StringName*
void *method_userdata; // 用户数据
void *call_func; // 标准调用入口
void *ptrcall_func; // ptrcall 入口
uint32_t method_flags; // 方法标志
uint32_t has_return_value; // 是否有返回值
uint8_t _pad0[0x0c]; // 中间未关心字段
uint32_t argument_count; // 参数个数
uint8_t _pad1[0x10]; // 中间未关心字段
uint32_t default_argument_count;
uint8_t _pad2[0x0c]; // 结构尾部
} GDExtensionClassMethodInfo;

call_func注册了拓展函数的入口点,注册时可以拿到三环关键函数的地址:

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
[sec2026_proc] direct classdb_register_extension_class_method class= method= call=libsec2026.so!0x63de4 ptrcall=libsec2026.so!0x63e3c lr=libsec2026.so!0x60bf0
[sec2026_proc] registered method #1 source=direct:libsec2026.so!0x60bf0 class=<unnamed_class> method=<unnamed_method_1> call=libsec2026.so!0x63de4 ptrcall=libsec2026.so!0x63e3c
[sec2026_proc] <unnamed_class>.<unnamed_method_1>@direct:libsec2026.so!0x60bf0.call_func@0x7237065de4 runtime bytes: ff 03 01 d1 fd 7b 02 a9 f3 1b 00 f9 fd 83 00 91 08 00 40 f9 f3 03 04 aa e4 03 05 aa 09 0d 40 f9
[sec2026_proc] <unnamed_class>.<unnamed_method_1>@direct:libsec2026.so!0x60bf0.call_func@0x7237065de4 runtime disassembly:
[sec2026_proc] 0x7237065de4 sub sp, sp, #0x40
[sec2026_proc] 0x7237065de8 stp x29, x30, [sp, #0x20]
[sec2026_proc] 0x7237065dec str x19, [sp, #0x30]
[sec2026_proc] 0x7237065df0 add x29, sp, #0x20
[sec2026_proc] 0x7237065df4 ldr x8, [x0]
[sec2026_proc] 0x7237065df8 mov x19, x4
[sec2026_proc] 0x7237065dfc mov x4, x5
[sec2026_proc] 0x7237065e00 ldr x9, [x8, #0x18]
[sec2026_proc] 0x7237065e04 add x8, sp, #8
[sec2026_proc] 0x7237065e08 blr x9
[sec2026_proc] <indirect call via [object Object]>
[sec2026_proc] 0x7237065e0c adrp x8, #0x72370ed000
[sec2026_proc] 0x7237065e10 add x1, sp, #8
[sec2026_proc] 0x7237065e14 mov x0, x19
[sec2026_proc] 0x7237065e18 ldr x8, [x8, #0xf48]
[sec2026_proc] 0x7237065e1c ldr x8, [x8]
[sec2026_proc] 0x7237065e20 blr x8
[sec2026_proc] <indirect call via [object Object]>
[sec2026_proc] 0x7237065e24 add x0, sp, #8
[sec2026_proc] 0x7237065e28 bl #0x7237068dc0
[sec2026_proc] 0x7237065e2c ldp x29, x30, [sp, #0x20]
[sec2026_proc] 0x7237065e30 ldr x19, [sp, #0x30]
[sec2026_proc] 0x7237065e34 add sp, sp, #0x40
[sec2026_proc] 0x7237065e38 ret

对照dump下来的逻辑:

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
00063de4    uint64_t __convention("apple-arm64-objc-fast-arc-0") sub_63de4(int64_t arg1, 
00063de4 void* arg2 @ x19, void* arg3 @ fp, int64_t arg4, int64_t arg5, int64_t arg6, int64_t arg7)

00063de4 // GameExtension.Process 的 call_func 入口。第一个参数 x0
00063de4 // 就是 method_userdata;运行时会先取 *(x0)
00063de4 // 作为分发表,再取该表的 +0x18
00063de4 // 项作为真实处理函数并间接调用,所以后续稳定落到
00063de4 // sub_4d78c 不是静态直连,而是 slot 分发结果。
00063de4 f40300aa mov x20, x0
00063de8 41fbffd0 adrp x1, 0xfffffffffffcd000
00063dec 21001e91 add x1, x1, #0x780 {-0x32880}
00063df0 a0630091 add x0, fp, #0x18
00063df4 e2031f2a mov w2, wzr {_start}
00063df8 52d9fe97 bl sub_1a340
00063dfc 424b8a52 mov w2, #0x525a
00063e00 a1630091 add x1, fp, #0x18
00063e04 e00314aa mov x0, x20
00063e08 a218bd72 movk w2, #0xe8c5, lsl #0x10 {0xe8c5525a}
00063e0c // 运行时这里是关键间接跳转。按实际执行到的代码看,call_func
00063e0c // 先用 x0(method_userdata) 取 *x0 作为分发表,再取 [*x0
00063e0c // + 0x18] 作为真实处理函数;静态里看到的
00063e0c // x21,本质上就是这个 slot 0x18 取出的函数指针。
00063e0c a0023fd6 blr x21
00063e10 f40300aa mov x20, x0
00063e14 a0630091 add x0, fp, #0x18
00063e18 9c3bff97 bl sub_32c88
00063e1c 000200f0 adrp x0, 0xa6000
00063e20 00402c91 add x0, x0, #0xb10 {0xa6b10}
00063e24 148400f8 str x20, [x0], #0x8 {0xa6b10} {0xa6b18}
00063e28 44690094 bl sub_7e338
00063e2c e00313aa mov x0, x19
00063e30 d2ffff17 b 0x63d78

可以发现自修改改了不少,动态分析就省去了那部分分析逻辑,而因为执行函数的时候是通过method_userdata下发分发的,调用时又加上了userdate做了一层偏移,触发flag获取的逻辑后,可以得到Object.Process在三环内的真正地址:

1
[21091116AC::com.tencent.ACE.gamesec2026.preliminary ]-> [sec2026_proc] <unnamed_class>.<unnamed_method_1>@direct:libsec2026.so!0x60bf0 first dispatch chain via call_func slot=0x18 table=0x72370e9d90 libsec2026.so!0x4d7a8 -> libsec2026.so!0x4ffd0

结合静态来看,开始套娃:

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
0004d78c    // call_func
0004d78c // 落下来的第一层调度器。这里先做一次懒初始化,再从全局
0004d78c // slot
0004d78c // 里取出真实处理函数,把参数继续分发到下一层。
0004d78c fd7bbda9 stp fp, lr, [sp, #-0x30]! {__saved_fp} {__saved_lr}
0004d790 f50b00f9 str x21, [sp, #0x10 {var_20}]
0004d794 f44f02a9 stp x20, x19, [sp, #0x20] {__saved_x20} {__saved_x19}
0004d798 fd030091 mov fp, sp {__saved_fp}
0004d79c c8020090 adrp x8, 0xa5000
0004d7a0 08c11991 add x8, x8, #0x670 {0xa5670}
0004d7a4 08fddf08 ldarb w8, [x8] {0xa5670}
0004d7a8 // 第一层稳定调度入口:从这里开始已经不是 Godot
0004d7a8 // 包装层,而是三环内部自己的分发逻辑。
0004d7a8 48020036 tbz w8, #0, 0x4d7f0

0004d7ac a9020090 adrp x9, 0xa1000
0004d7b0 c8020090 adrp x8, 0xa5000
0004d7b4 a3630091 add x3, fp, #0x18 {var_18}
0004d7b8 290d41f9 ldr x9, [x9, #0x218] {0xa1218}
0004d7bc 083543f9 ldr x8, [x8, #0x668] {0xa5668}
0004d7c0 010840f9 ldr x1, [x0, #0x10]
0004d7c4 e2031faa mov x2, xzr {_start}
0004d7c8 290140f9 ldr x9, [x9]
0004d7cc e00308aa mov x0, x8
0004d7d0 // 通过 **0xa1218
0004d7d0 // 把当前参数和上下文继续送入下一层公共分发出口。
0004d7d0 20013fd6 blr x9
0004d7d4 a8634039 ldrb w8, [fp, #0x18 {var_18}]
0004d7d8 1f0100f1 cmp x8, #0
0004d7dc e0079f1a cset w0, ne
0004d7e0 f44f42a9 ldp x20, x19, [sp, #0x20] {__saved_x20} {__saved_x19}
0004d7e4 f50b40f9 ldr x21, [sp, #0x10 {var_20}]
0004d7e8 fd7bc3a8 ldp fp, lr, [sp], #0x30 {__saved_fp} {__saved_lr}
0004d7ec c0035fd6 ret

0004d7f0 c8020090 adrp x8, 0xa5000
0004d7f4 08c11991 add x8, x8, #0x670
0004d7f8 f30300aa mov x19, x0
0004d7fc e00308aa mov x0, x8 {0xa5670}
0004d800 7dc20094 bl sub_7e1f4
0004d804 e803002a mov w8, w0
0004d808 e00313aa mov x0, x19
0004d80c // 检查初始化标志;未初始化时先走一次建表/取句柄流程。
0004d80c 08fdff34 cbz w8, 0x4d7ac

0004d810 a8020090 adrp x8, 0xa1000
0004d814 083141f9 ldr x8, [x8, #0x260] {0xa1260}
0004d818 // 从 **0xa1260
0004d818 // 取出一条运行时函数指针。这里只能静态看到
0004d818 // slot,看不到最终会跳到哪。
0004d818 150140f9 ldr x21, [x8]
0004d81c 4fd4fe97 bl sub_2958
0004d820 f40300aa mov x20, x0
0004d824 e1fbfff0 adrp x1, 0xfffffffffffcc000
0004d828 21683991 add x1, x1, #0xe5a {-0x331a6}
0004d82c a0630091 add x0, fp, #0x18 {var_18}
0004d830 e2031f2a mov w2, wzr {_start}
0004d834 c332ff97 bl sub_1a340
0004d838 22bc9452 mov w2, #0xa5e1
0004d83c a1630091 add x1, fp, #0x18 {var_18}
0004d840 e00314aa mov x0, x20 {0xa2820}
0004d844 4246a072 movk w2, #0x232, lsl #0x10 {0x232a5e1}
0004d848 // 通过 x21
0004d848 // 间接调用初始化函数,说明这一层已经进入 slot
0004d848 // 驱动的动态分发。
0004d848 a0023fd6 blr x21
0004d84c f40300aa mov x20, x0
0004d850 a0630091 add x0, fp, #0x18 {var_18}
0004d854 0d95ff97 bl sub_32c88
0004d858 c0020090 adrp x0, 0xa5000
0004d85c 00a01991 add x0, x0, #0x668 {0xa5668}
0004d860 // 缓存初始化得到的句柄/上下文,后续调用会直接复用,不再重复建表。
0004d860 148400f8 str x20, [x0], #0x8 {0xa5668} {0xa5670}
0004d864 b5c20094 bl sub_7e338
0004d868 e00313aa mov x0, x19
0004d86c d0ffff17 b 0x4d7ac

此时已经可以根据运行的参数,计算出之后的调度链(大致会执行哪些函数都看一遍、结果发现是套娃),通过binary ninja继续跟,直到套娃:0x4ffd0 类似的分发函数,按照类似的思路去做:

1
2
3
4
5
6
7
8
9
10
11
function maybeInstallProcessDynamicDispatchProbe(label, address) {
const p = ptr(address);
const ownerModule = Process.findModuleByAddress(p);
if (ownerModule === null || ownerModule.name !== MODULE_NAME) {
return;
}

const relativeOffset = ptrToNumber(p) - ptrToNumber(ownerModule.base);
if (!PROCESS_DYNAMIC_DISPATCH_TARGET_PROBE_OFFSETS.has(relativeOffset)) {
return;
}

重复这个操作下来两三次:0x4d7a8 -> 0x4ffd0 -> 0x4bd68 -> 0x4c8e4 -> 0x4e198 -> 0x4e548 -> 0x5bf18 -> 0x5b69c -> 0x5b5e0 -> 0x5b950。其中 0x5b818/0x5bcec 的 state dump 直接打出了 ChaCha20-like 常量、key、nonce,正式把整个处理逻辑给扒干净了,非常之搞笑。(脚本见末尾)

正好有运行时反汇编的操作,顺手就可以确定加密逻辑,并把flag推随机值的一起做了,验证之后是没问题的:(见样例源代码)

安全机制解析:

其实思路和2021还是2022的ACE保护差不多,都是游戏引擎侧对入口(global-meta.data、gdc)这种做加密,然后提高保护程序的分析难度,上混淆vmp、控制流平坦化什么的,这次赛题没有反调试、但是多环嵌套代码、自修改和动态跳转已经对Hook时机、方式有了一定的保护。同时三环的自修改写得也挺有意思,有外部分发、根据参数跳转执行的方式也对静态分析有了不小的挑战,同时动态分析时Frida的inlinehook是没法随意提前打上去拿各种信息的,stalker不知道为什么总是会崩。总之,很有意思!

解题优化思路:如果有动态trace多线程dump执行流的办法,其实这道题就变得非常简单了,下面的frida脚本其实已经有了雏形,可能以后再继续研究看看有没有更好的办法去自动化吧。

脚本集:

移车的Frida脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
/*
* Usage:
* frida -U -f com.tencent.ACE.gamesec2026.preliminary -l tools/frida_move_trigger_sec2026.js --no-pause
*
* Manual mode:
* Enter the game scene, then run `sec2026.trigger()` in the Frida REPL.
*/

(function () {
'use strict';

const MODULE_NAME = 'libsec2026.so';
const GODOT_MODULE_NAME = 'libgodot_android.so';
const TRACE_PREFIX = '[sec2026_move]';

const OFFSETS_GODOT = {
variantCall: 0x3c10f34,
variantDestroy: 0x3c10f18,
globalGetSingleton: 0x3c13630,
stringNameNewLatin1: 0x3c12d70,
stringNewUtf8: 0x3c123d8,
getVariantFromTypeConstructor: 0x3c11748,
getVariantToTypeConstructor: 0x3c119fc,
};

const VARIANT_TYPES = {
BOOL: 1,
INT: 2,
STRING: 4,
VECTOR3: 9,
OBJECT: 24,
};

const PHYSICS_CLASSES = ['VehicleBody3D', 'RigidBody3D', 'PhysicsBody3D'];

const BODY_STATE_TRANSFORM = 0;
const BODY_STATE_LINEAR_VELOCITY = 1;
const BODY_STATE_ANGULAR_VELOCITY = 2;
const BODY_STATE_SLEEPING = 3;

const CALL_ERROR_NAMES = {
0: 'OK',
1: 'INVALID_METHOD',
2: 'INVALID_ARGUMENT',
3: 'TOO_MANY_ARGUMENTS',
4: 'TOO_FEW_ARGUMENTS',
5: 'INSTANCE_IS_NULL',
6: 'METHOD_NOT_CONST',
};

const MAIN_THREAD_ID = Process.id;
const VARIANT_SIZE = 0x40;
const STRING_SIZE = 0x10;
const STRING_NAME_SIZE = 0x10;
const CALL_ERROR_SIZE = 0x10;
const GODOT_STRING_COW_PTR_OFFSET = 0x00;
const GODOT_STRING_SIZE_BACK_OFFSET = 0x10;
const MAX_LOG_STRING_CHARS = 512;

let api = null;
let apiReady = false;
let attemptCount = 0;
let sawSecModule = false;
let sawGodotModule = false;

const stringNameCache = new Map();
const stringObjectCache = new Map();
const stringVariantCache = new Map();
const boolVariantCache = new Map();
const intVariantCache = new Map();
let zeroVectorVariant = null;

function log(message) {
console.log(`${TRACE_PREFIX} ${message}`);
}

function hex(value) {
return ptr(value).toString();
}

function samePtr(a, b) {
return ptr(a).toString() === ptr(b).toString();
}

function u64ToNumber(value) {
if (typeof value === 'number') {
return value;
}
return parseInt(value.toString(), 10);
}

function getModule(name) {
if (typeof Process.findModuleByName === 'function') {
const found = Process.findModuleByName(name);
if (found !== null) {
return found;
}
}

if (typeof Process.getModuleByName === 'function') {
try {
return Process.getModuleByName(name);
} catch (_) {
}
}

return null;
}

function ensureRuntimeReady() {
const sec = getModule(MODULE_NAME);
const godot = getModule(GODOT_MODULE_NAME);

if (sec !== null && !sawSecModule) {
sawSecModule = true;
log(`module ready: ${MODULE_NAME} base=${sec.base} size=0x${sec.size.toString(16)}`);
}

if (godot !== null && !sawGodotModule) {
sawGodotModule = true;
log(`module ready: ${GODOT_MODULE_NAME} base=${godot.base} size=0x${godot.size.toString(16)}`);
}

if (api === null && sec !== null && godot !== null) {
resolveApiViaOffsets(godot);
}

return {
secLoaded: sec !== null,
godotLoaded: godot !== null,
};
}

function resolveApiViaOffsets(godotModule) {
if (api !== null) {
return true;
}

const base = ptr(godotModule.base);
const resolved = {
globalGetSingleton: new NativeFunction(base.add(OFFSETS_GODOT.globalGetSingleton), 'pointer', ['pointer']),
stringNameNewLatin1: new NativeFunction(base.add(OFFSETS_GODOT.stringNameNewLatin1), 'void', ['pointer', 'pointer', 'uchar']),
stringNewUtf8: new NativeFunction(base.add(OFFSETS_GODOT.stringNewUtf8), 'void', ['pointer', 'pointer']),
variantCall: new NativeFunction(base.add(OFFSETS_GODOT.variantCall), 'void', ['pointer', 'pointer', 'pointer', 'int64', 'pointer', 'pointer']),
variantDestroy: new NativeFunction(base.add(OFFSETS_GODOT.variantDestroy), 'void', ['pointer']),
getVariantFromTypeConstructor: new NativeFunction(base.add(OFFSETS_GODOT.getVariantFromTypeConstructor), 'pointer', ['int']),
getVariantToTypeConstructor: new NativeFunction(base.add(OFFSETS_GODOT.getVariantToTypeConstructor), 'pointer', ['int']),
};

resolved.variantFromBool = new NativeFunction(
ptr(resolved.getVariantFromTypeConstructor(VARIANT_TYPES.BOOL)),
'void',
['pointer', 'pointer']
);
resolved.variantFromInt = new NativeFunction(
ptr(resolved.getVariantFromTypeConstructor(VARIANT_TYPES.INT)),
'void',
['pointer', 'pointer']
);
resolved.variantFromString = new NativeFunction(
ptr(resolved.getVariantFromTypeConstructor(VARIANT_TYPES.STRING)),
'void',
['pointer', 'pointer']
);
resolved.variantFromVector3 = new NativeFunction(
ptr(resolved.getVariantFromTypeConstructor(VARIANT_TYPES.VECTOR3)),
'void',
['pointer', 'pointer']
);
resolved.variantFromObject = new NativeFunction(
ptr(resolved.getVariantFromTypeConstructor(VARIANT_TYPES.OBJECT)),
'void',
['pointer', 'pointer']
);

resolved.variantToBool = new NativeFunction(
ptr(resolved.getVariantToTypeConstructor(VARIANT_TYPES.BOOL)),
'void',
['pointer', 'pointer']
);
resolved.variantToInt = new NativeFunction(
ptr(resolved.getVariantToTypeConstructor(VARIANT_TYPES.INT)),
'void',
['pointer', 'pointer']
);
resolved.variantToString = new NativeFunction(
ptr(resolved.getVariantToTypeConstructor(VARIANT_TYPES.STRING)),
'void',
['pointer', 'pointer']
);
resolved.variantToVector3 = new NativeFunction(
ptr(resolved.getVariantToTypeConstructor(VARIANT_TYPES.VECTOR3)),
'void',
['pointer', 'pointer']
);
resolved.variantToObject = new NativeFunction(
ptr(resolved.getVariantToTypeConstructor(VARIANT_TYPES.OBJECT)),
'void',
['pointer', 'pointer']
);

api = resolved;
apiReady = true;
log(`resolved GDExtension api via offsets (base=${godotModule.base})`);
return true;
}

function allocVariant() {
return Memory.alloc(VARIANT_SIZE);
}

function allocStringObject() {
return Memory.alloc(STRING_SIZE);
}

function allocStringNameObject() {
return Memory.alloc(STRING_NAME_SIZE);
}

function destroyVariant(variantPtr) {
if (api === null) {
return;
}

const p = ptr(variantPtr);
if (p.isNull()) {
return;
}

try {
api.variantDestroy(p);
} catch (_) {
}
}

function getStringName(name) {
if (stringNameCache.has(name)) {
return stringNameCache.get(name);
}

const obj = allocStringNameObject();
api.stringNameNewLatin1(obj, Memory.allocUtf8String(name), 0);
stringNameCache.set(name, obj);
return obj;
}

function getStringObject(text) {
if (stringObjectCache.has(text)) {
return stringObjectCache.get(text);
}

const obj = allocStringObject();
api.stringNewUtf8(obj, Memory.allocUtf8String(text));
stringObjectCache.set(text, obj);
return obj;
}

function makeVariantFromString(text) {
if (stringVariantCache.has(text)) {
return stringVariantCache.get(text);
}

const variant = allocVariant();
api.variantFromString(variant, getStringObject(text));
stringVariantCache.set(text, variant);
return variant;
}

function makeVariantFromBool(value) {
const key = value ? 'true' : 'false';
if (boolVariantCache.has(key)) {
return boolVariantCache.get(key);
}

const storage = Memory.alloc(1);
storage.writeU8(value ? 1 : 0);
const variant = allocVariant();
api.variantFromBool(variant, storage);
boolVariantCache.set(key, variant);
return variant;
}

function makeVariantFromInt(value) {
const key = `${value}`;
if (intVariantCache.has(key)) {
return intVariantCache.get(key);
}

const storage = Memory.alloc(8);
storage.writeS64(value);
const variant = allocVariant();
api.variantFromInt(variant, storage);
intVariantCache.set(key, variant);
return variant;
}

function makeVariantFromVector3(x, y, z) {
const storage = Memory.alloc(12);
storage.writeFloat(x);
storage.add(4).writeFloat(y);
storage.add(8).writeFloat(z);
const variant = allocVariant();
api.variantFromVector3(variant, storage);
return variant;
}

function getZeroVectorVariant() {
if (zeroVectorVariant !== null) {
return zeroVectorVariant;
}

zeroVectorVariant = makeVariantFromVector3(0.0, 0.0, 0.0);
return zeroVectorVariant;
}

function makeVariantFromObject(objectPtr) {
const storage = Memory.alloc(Process.pointerSize);
storage.writePointer(ptr(objectPtr));
const variant = allocVariant();
api.variantFromObject(variant, storage);
return variant;
}

function callMethod(selfVariant, methodName, args) {
const ret = allocVariant();
const error = Memory.alloc(CALL_ERROR_SIZE);
error.writeU32(0);
error.add(4).writeS32(0);
error.add(8).writeS32(0);

let argv = NULL;
const argc = args !== undefined ? args.length : 0;
if (argc > 0) {
argv = Memory.alloc(argc * Process.pointerSize);
for (let i = 0; i < argc; i += 1) {
argv.add(i * Process.pointerSize).writePointer(ptr(args[i]));
}
}

api.variantCall(selfVariant, getStringName(methodName), argv, argc, ret, error);
return {
ret,
error: error.readU32(),
argument: error.add(4).readS32(),
expected: error.add(8).readS32(),
};
}

function logCallError(prefix, result) {
if (result.error === 0) {
return;
}

const name = CALL_ERROR_NAMES[result.error] || `UNKNOWN_${result.error}`;
log(`${prefix} failed: ${name} arg=${result.argument} expected=${result.expected}`);
}

function variantToObject(variantPtr) {
const out = Memory.alloc(Process.pointerSize);
out.writePointer(NULL);
api.variantToObject(out, variantPtr);
return out.readPointer();
}

function variantToBool(variantPtr) {
const out = Memory.alloc(1);
out.writeU8(0);
api.variantToBool(out, variantPtr);
return out.readU8() !== 0;
}

function variantToInt(variantPtr) {
const out = Memory.alloc(8);
out.writeS64(0);
api.variantToInt(out, variantPtr);
return Number(out.readS64());
}

function readUtf32String(dataPtr, maxChars) {
const chars = [];
for (let i = 0; i < maxChars; i += 1) {
const cp = dataPtr.add(i * 4).readU32();
if (cp === 0) {
break;
}

if (cp <= 0xffff) {
chars.push(String.fromCharCode(cp));
} else {
const n = cp - 0x10000;
chars.push(String.fromCharCode(0xd800 + (n >> 10), 0xdc00 + (n & 0x3ff)));
}
}
return chars.join('');
}

function readGodotString(stringObjectPtr) {
const obj = ptr(stringObjectPtr);
if (obj.isNull()) {
return '';
}

const dataPtr = obj.add(GODOT_STRING_COW_PTR_OFFSET).readPointer();
if (dataPtr.isNull()) {
return '';
}

const rawSize = u64ToNumber(dataPtr.sub(GODOT_STRING_SIZE_BACK_OFFSET).readU64());
if (rawSize <= 0 || rawSize > MAX_LOG_STRING_CHARS + 1) {
return '';
}

return readUtf32String(dataPtr, rawSize - 1);
}

function variantToString(variantPtr) {
const out = allocStringObject();
api.variantToString(out, variantPtr);
return readGodotString(out);
}

function variantToVector3(variantPtr) {
const out = Memory.alloc(12);
out.writeFloat(0);
out.add(4).writeFloat(0);
out.add(8).writeFloat(0);
api.variantToVector3(out, variantPtr);
return {
x: out.readFloat(),
y: out.add(4).readFloat(),
z: out.add(8).readFloat(),
};
}

function formatVec3(vec) {
return `${vec.x.toFixed(3)},${vec.y.toFixed(3)},${vec.z.toFixed(3)}`;
}

function callBoolMethod(selfVariant, methodName, args) {
const result = callMethod(selfVariant, methodName, args || []);
if (result.error !== 0) {
logCallError(methodName, result);
destroyVariant(result.ret);
return null;
}

const value = variantToBool(result.ret);
destroyVariant(result.ret);
return value;
}

function findNamedChild(rootVariant, name) {
const result = callMethod(rootVariant, 'find_child', [
makeVariantFromString(name),
makeVariantFromBool(true),
makeVariantFromBool(false),
]);
if (result.error !== 0) {
logCallError(`find_child("${name}")`, result);
destroyVariant(result.ret);
return null;
}

const objectPtr = variantToObject(result.ret);
if (ptr(objectPtr).isNull()) {
destroyVariant(result.ret);
return null;
}

return {
variant: result.ret,
object: objectPtr,
};
}

function tryReadLabelText(rootVariant) {
const label = findNamedChild(rootVariant, 'Label2');
if (label === null) {
return null;
}

try {
const result = callMethod(label.variant, 'get_text', []);
if (result.error !== 0) {
logCallError('Label2.get_text', result);
destroyVariant(result.ret);
return null;
}

const text = variantToString(result.ret);
destroyVariant(result.ret);
return text;
} finally {
destroyVariant(label.variant);
}
}

function matchNodeClass(nodeVariant, classes) {
for (let i = 0; i < classes.length; i += 1) {
const className = classes[i];
const isMatch = callBoolMethod(nodeVariant, 'is_class', [makeVariantFromString(className)]);
if (isMatch === true) {
return className;
}
}

return null;
}

function getChildCount(nodeVariant, label) {
const result = callMethod(nodeVariant, 'get_child_count', []);
if (result.error !== 0) {
logCallError(`${label}.get_child_count`, result);
destroyVariant(result.ret);
return -1;
}

const count = variantToInt(result.ret);
destroyVariant(result.ret);
return count;
}

function getChildNode(nodeVariant, index, label) {
const result = callMethod(nodeVariant, 'get_child', [makeVariantFromInt(index)]);
if (result.error !== 0) {
logCallError(`${label}.get_child(${index})`, result);
destroyVariant(result.ret);
return null;
}

const objectPtr = variantToObject(result.ret);
if (ptr(objectPtr).isNull()) {
destroyVariant(result.ret);
return null;
}

return {
variant: result.ret,
object: objectPtr,
};
}

function findDescendantByClasses(nodeVariant, classes, label, depth, maxDepth) {
if (depth >= maxDepth) {
return null;
}

const childCount = getChildCount(nodeVariant, label);
if (childCount <= 0) {
return null;
}

for (let index = 0; index < childCount; index += 1) {
const child = getChildNode(nodeVariant, index, label);
if (child === null) {
continue;
}

const matchedClass = matchNodeClass(child.variant, classes);
if (matchedClass !== null) {
return {
variant: child.variant,
object: child.object,
matchedClass,
borrowed: false,
};
}

const nested = findDescendantByClasses(child.variant, classes, `${label}/${index}`, depth + 1, maxDepth);
destroyVariant(child.variant);
if (nested !== null) {
return nested;
}
}

return null;
}

function selectPhysicsTarget(carNode) {
const rootClass = matchNodeClass(carNode.variant, PHYSICS_CLASSES);
if (rootClass !== null) {
return {
variant: carNode.variant,
object: carNode.object,
matchedClass: rootClass,
borrowed: true,
};
}

const found = findDescendantByClasses(carNode.variant, PHYSICS_CLASSES, 'car', 0, 8);
if (found !== null) {
return found;
}

return {
variant: carNode.variant,
object: carNode.object,
matchedClass: 'car_root',
borrowed: true,
};
}

function applyTransform(nodeVariant, label, xformVariant) {
const result = callMethod(nodeVariant, 'set_global_transform', [xformVariant]);
if (result.error !== 0) {
logCallError(`${label}.set_global_transform`, result);
}
destroyVariant(result.ret);

const forceResult = callMethod(nodeVariant, 'force_update_transform', []);
if (forceResult.error !== 0) {
logCallError(`${label}.force_update_transform`, forceResult);
}
destroyVariant(forceResult.ret);
}

function tryOverlapsBody(triggerVariant, bodyObjectPtr, label) {
const bodyVariant = makeVariantFromObject(bodyObjectPtr);
try {
const value = callBoolMethod(triggerVariant, 'overlaps_body', [bodyVariant]);
log(`${label} overlaps_body = ${value}`);
return value;
} finally {
destroyVariant(bodyVariant);
}
}

function tryApplyPhysicsTeleport(physicsVariant, xformVariant) {
const physicsServerObj = api.globalGetSingleton(getStringName('PhysicsServer3D'));
if (ptr(physicsServerObj).isNull()) {
log('global_get_singleton("PhysicsServer3D") returned null');
return false;
}

const physicsServerVariant = makeVariantFromObject(physicsServerObj);
let ridResult = null;
try {
ridResult = callMethod(physicsVariant, 'get_rid', []);
if (ridResult.error !== 0) {
logCallError('physics target.get_rid', ridResult);
return false;
}

let result = callMethod(physicsServerVariant, 'body_set_state', [
ridResult.ret,
makeVariantFromInt(BODY_STATE_TRANSFORM),
xformVariant,
]);
if (result.error !== 0) {
logCallError('PhysicsServer3D.body_set_state(transform)', result);
}
destroyVariant(result.ret);

result = callMethod(physicsServerVariant, 'body_set_state', [
ridResult.ret,
makeVariantFromInt(BODY_STATE_LINEAR_VELOCITY),
getZeroVectorVariant(),
]);
if (result.error !== 0) {
logCallError('PhysicsServer3D.body_set_state(linear_velocity)', result);
}
destroyVariant(result.ret);

result = callMethod(physicsServerVariant, 'body_set_state', [
ridResult.ret,
makeVariantFromInt(BODY_STATE_ANGULAR_VELOCITY),
getZeroVectorVariant(),
]);
if (result.error !== 0) {
logCallError('PhysicsServer3D.body_set_state(angular_velocity)', result);
}
destroyVariant(result.ret);

result = callMethod(physicsServerVariant, 'body_set_state', [
ridResult.ret,
makeVariantFromInt(BODY_STATE_SLEEPING),
makeVariantFromBool(false),
]);
if (result.error !== 0) {
logCallError('PhysicsServer3D.body_set_state(sleeping)', result);
}
destroyVariant(result.ret);

return true;
} finally {
destroyVariant(physicsServerVariant);
if (ridResult !== null) {
destroyVariant(ridResult.ret);
}
}
}

function tryMoveCarToTrigger() {
if (api === null) {
return false;
}

const engineObj = api.globalGetSingleton(getStringName('Engine'));
if (ptr(engineObj).isNull()) {
log('global_get_singleton("Engine") returned null');
return false;
}

const engineVariant = makeVariantFromObject(engineObj);
let sceneTreeVariant = null;
let rootVariant = null;
let car = null;
let trigger = null;
let physicsTarget = null;
let xformResult = null;

try {
const mainLoopResult = callMethod(engineVariant, 'get_main_loop', []);
if (mainLoopResult.error !== 0) {
logCallError('Engine.get_main_loop', mainLoopResult);
destroyVariant(mainLoopResult.ret);
return false;
}
sceneTreeVariant = mainLoopResult.ret;

const rootResult = callMethod(sceneTreeVariant, 'get_root', []);
if (rootResult.error !== 0) {
logCallError('SceneTree.get_root', rootResult);
destroyVariant(rootResult.ret);
return false;
}
rootVariant = rootResult.ret;

car = findNamedChild(rootVariant, 'car');
trigger = findNamedChild(rootVariant, 'Trigger2');
if (car === null || trigger === null) {
log(`scene not ready: car=${car !== null} trigger2=${trigger !== null}`);
return false;
}

physicsTarget = selectPhysicsTarget(car);
log(
`found nodes: car=${hex(car.object)} trigger2=${hex(trigger.object)} ` +
`physics=${hex(physicsTarget.object)} class=${physicsTarget.matchedClass}`
);

const beforePosResult = callMethod(physicsTarget.variant, 'get_global_position', []);
if (beforePosResult.error === 0) {
log(`physics target before = ${formatVec3(variantToVector3(beforePosResult.ret))}`);
} else {
logCallError('physics target.get_global_position', beforePosResult);
}
destroyVariant(beforePosResult.ret);

tryOverlapsBody(trigger.variant, physicsTarget.object, 'before move');

const monitorResult = callMethod(trigger.variant, 'set_monitoring', [makeVariantFromBool(true)]);
if (monitorResult.error !== 0) {
logCallError('Trigger2.set_monitoring(true)', monitorResult);
}
destroyVariant(monitorResult.ret);

xformResult = callMethod(trigger.variant, 'get_global_transform', []);
if (xformResult.error !== 0) {
logCallError('Trigger2.get_global_transform', xformResult);
destroyVariant(xformResult.ret);
xformResult = null;
return false;
}

const triggerPosResult = callMethod(trigger.variant, 'get_global_position', []);
if (triggerPosResult.error === 0) {
log(`trigger2 position = ${formatVec3(variantToVector3(triggerPosResult.ret))}`);
} else {
logCallError('Trigger2.get_global_position', triggerPosResult);
}
destroyVariant(triggerPosResult.ret);

applyTransform(car.variant, 'car', xformResult.ret);
if (!samePtr(physicsTarget.object, car.object)) {
applyTransform(physicsTarget.variant, 'physics target', xformResult.ret);
}
tryApplyPhysicsTeleport(physicsTarget.variant, xformResult.ret);

const afterPosResult = callMethod(physicsTarget.variant, 'get_global_position', []);
if (afterPosResult.error === 0) {
log(`physics target after = ${formatVec3(variantToVector3(afterPosResult.ret))}`);
} else {
logCallError('physics target.get_global_position', afterPosResult);
}
destroyVariant(afterPosResult.ret);

tryOverlapsBody(trigger.variant, physicsTarget.object, 'after move');

const overlapping = callBoolMethod(trigger.variant, 'has_overlapping_bodies', []);
const labelText = tryReadLabelText(rootVariant);
log(
`attempt=${attemptCount.toString().padStart(2, '0')} ` +
`overlapping=${overlapping} label=${JSON.stringify(labelText)}`
);

return overlapping === true ||
(typeof labelText === 'string' && labelText.indexOf('flag{sec2026_PART1_') !== -1);
} finally {
destroyVariant(engineVariant);
if (car !== null) {
destroyVariant(car.variant);
}
if (trigger !== null) {
destroyVariant(trigger.variant);
}
if (physicsTarget !== null && physicsTarget.borrowed === false) {
destroyVariant(physicsTarget.variant);
}
if (rootVariant !== null) {
destroyVariant(rootVariant);
}
if (sceneTreeVariant !== null) {
destroyVariant(sceneTreeVariant);
}
if (xformResult !== null) {
destroyVariant(xformResult.ret);
}
}
}

function executeOnMainThread(work) {
if (typeof Process.runOnThread === 'function') {
try {
return Process.runOnThread(MAIN_THREAD_ID, work);
} catch (_) {
}
}

return work();
}

function getStatus() {
const runtime = ensureRuntimeReady();
return {
apiReady,
secLoaded: runtime.secLoaded,
godotLoaded: runtime.godotLoaded,
attemptCount,
};
}

function manualTrigger() {
const runtime = ensureRuntimeReady();
if (!runtime.secLoaded || !runtime.godotLoaded || api === null) {
const status = getStatus();
log(`manual trigger ignored: runtime not ready ${JSON.stringify(status)}`);
return {
ok: false,
reason: 'runtime_not_ready',
status,
};
}

attemptCount += 1;
const currentAttempt = attemptCount;
log(`manual trigger begin attempt=${currentAttempt}`);

let ok = false;
let error = null;
try {
ok = !!executeOnMainThread(() => tryMoveCarToTrigger());
} catch (e) {
error = `${e}`;
log(`manual trigger exception attempt=${currentAttempt}: ${error}`);
}

const status = getStatus();
log(`manual trigger end attempt=${currentAttempt} ok=${ok}`);
return {
ok,
error,
status,
};
}

setImmediate(() => {
log(`waiting for ${MODULE_NAME}`);
log('manual mode enabled; enter the game scene and run `sec2026.trigger()`');
ensureRuntimeReady();
});

globalThis.sec2026 = {
status() {
const status = getStatus();
log(`status ${JSON.stringify(status)}`);
return status;
},
trigger() {
return manualTrigger();
},
};
})();

动态dump反汇编分析的脚本:

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
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
/*
* Usage:
* frida -U -f com.tencent.ACE.gamesec2026.preliminary \
* -l tools/frida_move_trigger_sec2026.js \
* -l tools/frida_trace_process_libc_sec2026.js
*
* Goal:
* Keep the tracing path stable:
* - resolve GameExtension.Process dynamically from Godot registration
* - hook Process call_func / ptrcall_func
* - only while Process is active, observe safe libc boundaries
*
* Deliberately not used here:
* - libsec2026.so internal text hooks
* - stage2 / stage3 direct hooks
* - Stalker
*/

(function () {
'use strict';

const PACKAGE_NAME = 'com.tencent.ACE.gamesec2026.preliminary';
const MODULE_NAME = 'libsec2026.so';
const GODOT_MODULE_NAME = 'libgodot_android.so';
const TRACE_PREFIX = '[sec2026_proc]';

const OFFSETS_GODOT = {
classdbRegisterExtensionClassMethod: 0x3c07e9c,
getVariantToTypeConstructor: 0x3c119fc,
};

const OFFSETS_SEC = {
extensionInit: 0x56d50,
};

const TARGET = {
className: 'GameExtension',
methodName: 'Process',
};

const INTERESTING_INTERFACE_NAMES = new Set([
'classdb_register_extension_class',
'classdb_register_extension_class2',
'classdb_register_extension_class3',
'classdb_register_extension_class4',
'classdb_register_extension_class5',
'classdb_register_extension_class_method',
]);

const GDEXT_INIT = Process.pointerSize === 8 ? {
minimumInitializationLevel: 0x00,
userdata: 0x08,
initialize: 0x10,
deinitialize: 0x18,
size: 0x20,
} : {
minimumInitializationLevel: 0x00,
userdata: 0x04,
initialize: 0x08,
deinitialize: 0x0c,
size: 0x10,
};

const METHOD_INFO = Process.pointerSize === 8 ? {
name: 0x00,
methodUserdata: 0x08,
callFunc: 0x10,
ptrcallFunc: 0x18,
methodFlags: 0x20,
hasReturnValue: 0x24,
argumentCount: 0x34,
defaultArgumentCount: 0x48,
size: 0x58,
} : {
name: 0x00,
methodUserdata: 0x04,
callFunc: 0x08,
ptrcallFunc: 0x0c,
methodFlags: 0x10,
hasReturnValue: 0x14,
argumentCount: 0x20,
defaultArgumentCount: 0x2c,
size: 0x34,
};

const STRING_NAME_DATA_OFFSET = 0x08;
const GODOT_STRING_COW_PTR_OFFSET = 0x00;
const GODOT_STRING_SIZE_BACK_OFFSET = 0x10;
const GODOT_PACKED_ARRAY_DATA_OFFSET = 0x00;
const MAX_NAME_CHARS = 256;
const MAX_PROCESS_VARIANT_BYTES = 0x100;
const MAX_PROCESS_VARIANT_STRING_CHARS = 0x200;
const MAP_FAILED = ptr('0xffffffffffffffff');
const ENTRY_SCAN_MAX_INSNS = 48;
const DISPATCH_TARGET_SCAN_MAX_INSNS = 160;
const DISPATCH_THUNK_SCAN_MAX_INSNS = 24;
const DISPATCH_CHAIN_MAX_DEPTH = 3;
const MAX_INTERNAL_TARGET_HOOKS_PER_METHOD = 12;
const ENABLE_INTERNAL_CALL_HOOKS = false;
const ENABLE_DISPATCH_TARGET_HOOKS = false;
const ENABLE_DISPATCH_TARGET_ANALYSIS = true;
const ENABLE_LIBC_BOUNDARY_HOOKS = false;
const ENABLE_CALL_FUNC_HOOK = true;
const ENABLE_PTRCALL_FUNC_HOOK = false;
const ENABLE_FIRST_BACKTRACE = false;
const ENABLE_VERBOSE_PROCESS_INVOCATION_LOGS = false;
const ENABLE_PROCESS_RUNTIME_SLOT_SNAPSHOT = false;
const ENABLE_PROCESS_LEAF_SLOT_ANALYSIS = false;
const ENABLE_PROCESS_LEAF_CALL_ANALYSIS = true;
const PROCESS_LEAF_CALL_ANALYSIS_RECURSION_DEPTH = 1;
const PROCESS_DYNAMIC_DISPATCH_LEAF_ANALYSIS_RECURSION_DEPTH = 2;
const PROCESS_LEAF_CALL_ANALYSIS_RECURSE_OFFSETS = new Set([
0x4bd68,
0x5b950,
0x5bcec,
]);
const PROCESS_DYNAMIC_DISPATCH_TARGET_PROBE_OFFSETS = new Set([
0x4c8e4,
]);
const PROCESS_RUNTIME_TARGET_DISASM_INSNS = 96;
const PROCESS_RUNTIME_TARGET_DISASM_OVERRIDES = new Map([
[0x5bcec, 192],
]);
const ENABLE_PROCESS_STATE_DUMP = true;
const ENABLE_PROCESS_VARIANT_IO_TRACE = true;
const PROCESS_STATE_DUMP_TARGET_OFFSETS = new Map([
[0x5b818, 'init'],
[0x5bcec, 'refill'],
]);
const PROCESS_STATE_DUMP_CONTEXT_WORD_COUNT = 16;
const PROCESS_STATE_DUMP_CURSOR_OFFSET = 0x80;
const PROCESS_RUNTIME_SLOT_OFFSETS = {
invokePtrPtr: 0xa1218,
createPtrPtr: 0xa1260,
handle: 0xa5908,
initFlag: 0xa5910,
};
const VARIANT_TYPES = {
STRING: 4,
PACKED_BYTE_ARRAY: 29,
};

let loaderHooksInstalled = false;
let moduleObserverInstalled = false;
let godotHookInstalled = false;
let libcHooksInstalled = false;
let extensionInitHooked = false;
let processResolved = false;
let secModuleSeen = false;
let godotModuleSeen = false;
let processSeq = 0;
let cachedGetProcAddress = null;
let registeredMethodCount = 0;
let godotVariantApi = null;

const hookKeys = new Set();
const getProcHooks = new Set();
const interfaceHooks = new Set();
const initCallbackHooks = new Set();
const stringNameCache = new Map();
const activeInvocationsByThread = new Map();
const activeInvocationsById = new Map();
const analyzedMethodEntries = new Set();
const internalCallHookKeys = new Set();
const firstBacktraceLabels = new Set();
const dispatchTargetHookKeys = new Set();
const loggedDispatchAnalysisKeys = new Set();
const loggedRuntimeDisassemblyKeys = new Set();
const loggedRuntimeSlotStages = new Set();
const loggedProcessLeafAnalysisKeys = new Set();
const loggedProcessLeafCallAnalysisKeys = new Set();
const loggedProcessDynamicDispatchProbeKeys = new Set();
const processStateDumpHookKeys = new Set();
const loggedProcessStateDumpStages = new Set();
const loggedProcessVariantStages = new Set();

function log(message) {
console.log(`${TRACE_PREFIX} ${message}`);
}

function hex(value) {
return ptr(value).toString();
}

function ptrToNumber(value) {
return parseInt(ptr(value).toString(), 16);
}

function wordToNumber(value) {
if (typeof value === 'number') {
return value;
}

const text = value.toString();
if (text.indexOf('0x') === 0 || text.indexOf('-0x') === 0) {
return parseInt(text, 16);
}
return parseInt(text, 10);
}

function wordToInt(value) {
try {
return ptr(value).toInt32();
} catch (_) {
const n = wordToNumber(value) >>> 0;
return n > 0x7fffffff ? n - 0x100000000 : n;
}
}

function getModule(name) {
if (typeof Process.findModuleByName === 'function') {
const found = Process.findModuleByName(name);
if (found !== null) {
return found;
}
}

if (typeof Process.getModuleByName === 'function') {
try {
return Process.getModuleByName(name);
} catch (_) {
}
}

return null;
}

function findGlobalExport(name) {
if (typeof Module.findExportByName === 'function') {
try {
const address = Module.findExportByName(null, name);
if (address !== null) {
return address;
}
} catch (_) {
}
}

const candidates = ['libc.so', 'libdl.so', 'linker64', 'linker'];
for (let i = 0; i < candidates.length; i += 1) {
const mod = getModule(candidates[i]);
if (mod === null) {
continue;
}

try {
return mod.getExportByName(name);
} catch (_) {
}
}

return null;
}

function safeReadUtf8(address, maxLength) {
const p = ptr(address);
if (p.isNull()) {
return null;
}
try {
if (typeof maxLength === 'number') {
return p.readUtf8String(maxLength);
}
return p.readUtf8String();
} catch (_) {
return null;
}
}

function codePointToString(codePoint) {
if (codePoint <= 0xffff) {
return String.fromCharCode(codePoint);
}
const cp = codePoint - 0x10000;
return String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
}

function readUtf32String(dataPtr, maxChars) {
const chars = [];
const limit = Math.min(maxChars, MAX_NAME_CHARS);
for (let i = 0; i < limit; i += 1) {
const cp = dataPtr.add(i * 4).readU32();
if (cp === 0) {
break;
}
chars.push(cp > 0x10ffff ? '?' : codePointToString(cp));
}
return chars.join('');
}

function readGodotString(stringObjectPtr) {
const obj = ptr(stringObjectPtr);
if (obj.isNull()) {
return '';
}

const dataPtr = obj.add(GODOT_STRING_COW_PTR_OFFSET).readPointer();
if (dataPtr.isNull()) {
return '';
}

const rawSize = wordToNumber(dataPtr.sub(GODOT_STRING_SIZE_BACK_OFFSET).readU64());
if (rawSize <= 0 || rawSize > MAX_NAME_CHARS + 1) {
return '';
}

return readUtf32String(dataPtr, rawSize - 1);
}

function readGodotStringLimited(stringObjectPtr, maxChars) {
const obj = ptr(stringObjectPtr);
if (obj.isNull()) {
return '';
}

let dataPtr;
try {
dataPtr = obj.add(GODOT_STRING_COW_PTR_OFFSET).readPointer();
} catch (_) {
return '';
}

if (dataPtr.isNull()) {
return '';
}

let rawSize;
try {
rawSize = wordToNumber(dataPtr.sub(GODOT_STRING_SIZE_BACK_OFFSET).readU64());
} catch (_) {
return '';
}

if (rawSize <= 0) {
return '';
}

const limit = Math.min(rawSize - 1, maxChars);
return readUtf32String(dataPtr, limit);
}

function readStringName(stringNamePtr) {
const sn = ptr(stringNamePtr);
if (sn.isNull()) {
return '<null>';
}

try {
const dataPtr = sn.readPointer();
if (dataPtr.isNull()) {
return '';
}

const cacheKey = dataPtr.toString();
if (stringNameCache.has(cacheKey)) {
return stringNameCache.get(cacheKey);
}

const name = readGodotString(dataPtr.add(STRING_NAME_DATA_OFFSET));
stringNameCache.set(cacheKey, name);
return name;
} catch (e) {
return `<decode failed: ${e}>`;
}
}

function isUsefulName(name) {
return typeof name === 'string' &&
name.length !== 0 &&
name !== '<null>' &&
name.indexOf('<decode failed') !== 0;
}

function readLooseName(namePtr) {
const raw = ptr(namePtr);
const decoded = readStringName(raw);
if (isUsefulName(decoded)) {
return decoded;
}

const direct = safeReadUtf8(raw, 128);
if (typeof direct === 'string' && direct.length !== 0) {
return direct;
}

try {
const indirect = raw.readPointer();
const indirectText = safeReadUtf8(indirect, 128);
if (typeof indirectText === 'string' && indirectText.length !== 0) {
return indirectText;
}
} catch (_) {
}

return '';
}

function safeReadPointer(address) {
try {
return ptr(address).readPointer();
} catch (_) {
return null;
}
}

function safeReadU64(address) {
try {
return ptr(address).readU64();
} catch (_) {
return null;
}
}

function safeReadU32(address) {
try {
return ptr(address).readU32();
} catch (_) {
return null;
}
}

function resolveGodotVariantApi() {
if (godotVariantApi !== null) {
return godotVariantApi;
}

const godot = getModule(GODOT_MODULE_NAME);
if (godot === null) {
return null;
}

try {
const base = ptr(godot.base);
const api = {
getVariantToTypeConstructor: new NativeFunction(base.add(OFFSETS_GODOT.getVariantToTypeConstructor), 'pointer', ['int']),
};

api.variantToString = new NativeFunction(
ptr(api.getVariantToTypeConstructor(VARIANT_TYPES.STRING)),
'void',
['pointer', 'pointer']
);
api.variantToPackedByteArray = new NativeFunction(
ptr(api.getVariantToTypeConstructor(VARIANT_TYPES.PACKED_BYTE_ARRAY)),
'void',
['pointer', 'pointer']
);

godotVariantApi = api;
log(`resolved Godot variant converters (base=${hex(base)})`);
return api;
} catch (e) {
log(`failed to resolve Godot variant converters: ${e}`);
return null;
}
}

function previewBytesAscii(address, size) {
const p = ptr(address);
try {
let text = '';
for (let i = 0; i < size; i += 1) {
const value = p.add(i).readU8();
text += value >= 0x20 && value <= 0x7e ? String.fromCharCode(value) : '.';
}
return text;
} catch (e) {
return `<read failed: ${e}>`;
}
}

function readPackedByteArrayObject(arrayObjectPtr, maxBytes) {
const obj = ptr(arrayObjectPtr);
if (obj.isNull()) {
return null;
}

const candidateOffsets = [0, Process.pointerSize];
for (let i = 0; i < candidateOffsets.length; i += 1) {
const dataPtr = safeReadPointer(obj.add(candidateOffsets[i]));
if (dataPtr === null || ptr(dataPtr).isNull()) {
continue;
}

const sizeCandidates = [
safeReadU64(ptr(dataPtr).sub(0x10)),
safeReadU64(ptr(dataPtr).sub(0x08)),
];

for (let j = 0; j < sizeCandidates.length; j += 1) {
const rawSize = sizeCandidates[j];
if (rawSize === null) {
continue;
}

const size = wordToNumber(rawSize);
if (size < 0 || size > maxBytes) {
continue;
}

return {
dataPtr: ptr(dataPtr),
size,
hex: readBytesHex(dataPtr, size),
ascii: previewBytesAscii(dataPtr, size),
};
}
}

return null;
}

function variantToProcessString(variantPtr) {
const api = resolveGodotVariantApi();
if (api === null) {
return null;
}

try {
const out = Memory.alloc(0x10);
api.variantToString(out, ptr(variantPtr));
return readGodotStringLimited(out, MAX_PROCESS_VARIANT_STRING_CHARS);
} catch (e) {
return `<variantToString failed: ${e}>`;
}
}

function variantToPackedByteArray(variantPtr) {
const api = resolveGodotVariantApi();
if (api === null) {
return null;
}

try {
const out = Memory.alloc(0x10);
api.variantToPackedByteArray(out, ptr(variantPtr));
return readPackedByteArrayObject(out, MAX_PROCESS_VARIANT_BYTES);
} catch (e) {
return {
error: `${e}`,
};
}
}

function logProcessInputVariantOnce(label, argsPointer, argc) {
if (!ENABLE_PROCESS_VARIANT_IO_TRACE) {
return;
}

const key = `${label}@input`;
if (loggedProcessVariantStages.has(key)) {
return;
}

if (argc < 1) {
return;
}

const argv = ptr(argsPointer);
if (argv.isNull()) {
return;
}

const variantPtr = safeReadPointer(argv);
if (variantPtr === null || ptr(variantPtr).isNull()) {
return;
}

const packed = variantToPackedByteArray(variantPtr);
if (packed !== null && packed.error === undefined) {
log(
`${label} first arg0 PackedByteArray len=${packed.size} ` +
`hex=${packed.hex} ascii=${packed.ascii}`
);
loggedProcessVariantStages.add(key);
return;
}

const asString = variantToProcessString(variantPtr);
if (typeof asString === 'string' && asString.length !== 0) {
log(`${label} first arg0 fallback string=${JSON.stringify(asString)}`);
loggedProcessVariantStages.add(key);
return;
}

if (packed !== null && packed.error !== undefined) {
log(`${label} first arg0 decode failed: ${packed.error}`);
loggedProcessVariantStages.add(key);
}
}

function logProcessReturnVariantOnce(label, retVariantPtr) {
if (!ENABLE_PROCESS_VARIANT_IO_TRACE) {
return;
}

const key = `${label}@return`;
if (loggedProcessVariantStages.has(key)) {
return;
}

const text = variantToProcessString(retVariantPtr);
if (typeof text !== 'string') {
return;
}

log(`${label} first return string=${JSON.stringify(text)}`);
loggedProcessVariantStages.add(key);
}

function formatHex32(value) {
if (value === null) {
return '<unreadable>';
}

const text = (value >>> 0).toString(16);
return `0x${'00000000'.slice(text.length)}${text}`;
}

function readU32Words(baseAddress, count) {
const base = ptr(baseAddress);
const words = [];
for (let i = 0; i < count; i += 1) {
words.push(safeReadU32(base.add(i * 4)));
}
return words;
}

function countReadableWords(words) {
let readable = 0;
for (let i = 0; i < words.length; i += 1) {
if (words[i] !== null) {
readable += 1;
}
}
return readable;
}

function formatU32WordRange(words, start, end) {
const parts = [];
for (let i = start; i < end; i += 1) {
parts.push(formatHex32(words[i]));
}
return `[${parts.join(' ')}]`;
}

function dumpProcessStateWords(stage, targetAddress, ctxAddress) {
const ctx = ptr(ctxAddress);
if (ctx.isNull()) {
return false;
}

const words = readU32Words(ctx, PROCESS_STATE_DUMP_CONTEXT_WORD_COUNT);
const cursor = safeReadU32(ctx.add(PROCESS_STATE_DUMP_CURSOR_OFFSET));
if (countReadableWords(words) === 0 && cursor === null) {
return false;
}

const stageLabel = stage === 'init'
? 'init@0x5b818.leave'
: 'preblock@0x5bcec.enter';

log(
`state dump ${stageLabel} target=${formatAddress(targetAddress)} ` +
`ctx=${hex(ctx)} cursor=${formatHex32(cursor)}`
);
log(` constants=${formatU32WordRange(words, 0, 4)}`);
log(` key=${formatU32WordRange(words, 4, 12)}`);
log(
` counter=${formatU32WordRange(words, 12, 13)} ` +
`nonce=${formatU32WordRange(words, 13, 16)}`
);
return true;
}

function formatAddress(address) {
const p = ptr(address);
const mod = Process.findModuleByAddress(p);
if (mod !== null) {
return `${mod.name}!0x${(ptrToNumber(p) - ptrToNumber(mod.base)).toString(16)}`;
}
return hex(p);
}

function formatProt(prot) {
const value = prot & 0xff;
if (value === 0) {
return '---';
}
return `${(value & 1) !== 0 ? 'r' : '-'}${(value & 2) !== 0 ? 'w' : '-'}${(value & 4) !== 0 ? 'x' : '-'}`;
}

function isMapFailed(value) {
return ptr(value).toString() === MAP_FAILED.toString();
}

function installHook(address, label, callbacks) {
const p = ptr(address);
const key = `${label}@${p}`;
if (hookKeys.has(key)) {
return true;
}

try {
Interceptor.attach(p, callbacks);
hookKeys.add(key);
log(`installed ${label} hook at ${hex(p)}`);
return true;
} catch (e) {
log(`failed to hook ${label} at ${hex(p)}: ${e}`);
return false;
}
}

function parseMethodInfo(infoPtr) {
const base = ptr(infoPtr);
if (base.isNull()) {
return null;
}

try {
return {
name: readStringName(base.add(METHOD_INFO.name).readPointer()),
methodUserdata: base.add(METHOD_INFO.methodUserdata).readPointer(),
callFunc: base.add(METHOD_INFO.callFunc).readPointer(),
ptrcallFunc: base.add(METHOD_INFO.ptrcallFunc).readPointer(),
methodFlags: base.add(METHOD_INFO.methodFlags).readU32(),
hasReturnValue: base.add(METHOD_INFO.hasReturnValue).readU32(),
argumentCount: base.add(METHOD_INFO.argumentCount).readU32(),
defaultArgumentCount: base.add(METHOD_INFO.defaultArgumentCount).readU32(),
};
} catch (e) {
log(`parseMethodInfo failed at ${hex(base)}: ${e}`);
return null;
}
}

function getInvocationByThreadId(threadId) {
return activeInvocationsByThread.get(threadId);
}

function getNewestActiveInvocation() {
let newest = null;
activeInvocationsByThread.forEach((invocation) => {
if (newest === null || invocation.id > newest.id) {
newest = invocation;
}
});
return newest;
}

function pushInvocationHitById(invocationId, line) {
if (invocationId === 0) {
return false;
}

const invocation = activeInvocationsById.get(invocationId);
if (invocation === undefined) {
return false;
}

invocation.hits.push(line);
if (ENABLE_VERBOSE_PROCESS_INVOCATION_LOGS) {
log(`Process#${invocation.id} ${line}`);
}
return true;
}

function summarizeInvocation(invocation, retval) {
if (!ENABLE_VERBOSE_PROCESS_INVOCATION_LOGS) {
return;
}
if (invocation.hits.length === 0) {
log(`Process#${invocation.id} leave via ${invocation.kind} retval=${hex(retval)} with no recorded hits`);
return;
}

log(`Process#${invocation.id} summary (${invocation.hits.length} hits):`);
for (let i = 0; i < invocation.hits.length; i += 1) {
log(` ${invocation.hits[i]}`);
}
log(`Process#${invocation.id} leave via ${invocation.kind} retval=${hex(retval)}`);
}

function logBacktraceOnce(label, context) {
if (!ENABLE_FIRST_BACKTRACE) {
return;
}
if (firstBacktraceLabels.has(label)) {
return;
}
firstBacktraceLabels.add(label);

try {
const frames = Thread.backtrace(context, Backtracer.ACCURATE)
.slice(0, 10)
.map((address) => formatAddress(address));
log(`${label} first backtrace:`);
for (let i = 0; i < frames.length; i += 1) {
log(` bt#${i} ${frames[i]}`);
}
} catch (e) {
log(`${label} first backtrace failed: ${e}`);
}
}

function readBytesHex(address, size) {
const p = ptr(address);
try {
let text = '';
for (let i = 0; i < size; i += 1) {
const value = p.add(i).readU8();
const part = value < 0x10 ? `0${value.toString(16)}` : value.toString(16);
text += i === 0 ? part : ` ${part}`;
}
return text;
} catch (e) {
return `<read failed: ${e}>`;
}
}

function parseDirectCallTarget(instruction) {
const text = `${instruction}`;
const match = /0x[0-9a-fA-F]+/.exec(text);
if (match === null) {
return null;
}

try {
return ptr(match[0]);
} catch (_) {
return null;
}
}

function ptrToBigInt(value) {
return BigInt(ptr(value).toString());
}

function bigIntToPtr(value) {
return ptr(`0x${BigInt.asUintN(64, value).toString(16)}`);
}

function parseImmediateToken(token) {
let text = `${token}`.trim();
if (text.startsWith('#')) {
text = text.slice(1);
}
if (text.startsWith('-0x')) {
return -BigInt(`0x${text.slice(3)}`);
}
if (text.startsWith('0x')) {
return BigInt(text);
}
return BigInt(text);
}

function getTrackedRegister(registers, name) {
if (name === 'sp') {
return registers.sp;
}
const key = name.startsWith('w') ? `x${name.slice(1)}` : name;
const value = registers[key];
if (value === undefined) {
return null;
}
return name.startsWith('w') ? BigInt.asUintN(32, value) : BigInt.asUintN(64, value);
}

function setTrackedRegister(registers, name, value) {
const normalized = BigInt.asUintN(64, value);
if (name === 'sp') {
registers.sp = normalized;
return;
}
const key = name.startsWith('w') ? `x${name.slice(1)}` : name;
registers[key] = name.startsWith('w') ? BigInt.asUintN(32, normalized) : normalized;
}

function getTrackedStack(stack, address) {
const key = BigInt.asUintN(64, address).toString();
return stack.has(key) ? stack.get(key) : null;
}

function setTrackedStack(stack, address, value) {
const key = BigInt.asUintN(64, address).toString();
stack.set(key, BigInt.asUintN(64, value));
}

function trySimulateThunkInstruction(text, registers, stack) {
let match = /^mov (x\d+|w\d+|sp), (x\d+|w\d+|sp)$/.exec(text);
if (match !== null) {
const source = getTrackedRegister(registers, match[2]);
if (source === null) {
return false;
}
setTrackedRegister(registers, match[1], source);
return true;
}

match = /^adr (x\d+), #(0x[0-9a-fA-F]+)$/.exec(text);
if (match !== null) {
setTrackedRegister(registers, match[1], BigInt(match[2]));
return true;
}

match = /^mvn (w\d+), (w\d+)$/.exec(text);
if (match !== null) {
const source = getTrackedRegister(registers, match[2]);
if (source === null) {
return false;
}
setTrackedRegister(registers, match[1], BigInt.asUintN(32, ~BigInt.asUintN(32, source)));
return true;
}

match = /^(and|orr) (x\d+|w\d+), (x\d+|w\d+), #(.+)$/.exec(text);
if (match !== null) {
const source = getTrackedRegister(registers, match[3]);
if (source === null) {
return false;
}
const immediate = parseImmediateToken(match[4]);
const value = match[1] === 'and'
? BigInt.asUintN(64, source & immediate)
: BigInt.asUintN(64, source | immediate);
setTrackedRegister(registers, match[2], value);
return true;
}

match = /^add (x\d+|w\d+|sp), (x\d+|w\d+|sp), (x\d+|w\d+)$/.exec(text);
if (match !== null) {
const left = getTrackedRegister(registers, match[2]);
const right = getTrackedRegister(registers, match[3]);
if (left === null || right === null) {
return false;
}
setTrackedRegister(registers, match[1], left + right);
return true;
}

match = /^add (x\d+|w\d+|sp), (x\d+|w\d+|sp), #(.+)$/.exec(text);
if (match !== null) {
const left = getTrackedRegister(registers, match[2]);
if (left === null) {
return false;
}
setTrackedRegister(registers, match[1], left + parseImmediateToken(match[3]));
return true;
}

match = /^sub (sp), (sp), #(.+)$/.exec(text);
if (match !== null) {
const left = getTrackedRegister(registers, match[2]);
if (left === null) {
return false;
}
setTrackedRegister(registers, match[1], left - parseImmediateToken(match[3]));
return true;
}

match = /^str (x\d+|w\d+), \[sp, #(.+)\]!$/.exec(text);
if (match !== null) {
const value = getTrackedRegister(registers, match[1]);
const sp = getTrackedRegister(registers, 'sp');
if (value === null || sp === null) {
return false;
}
const nextSp = sp + parseImmediateToken(match[2]);
setTrackedRegister(registers, 'sp', nextSp);
setTrackedStack(stack, nextSp, value);
return true;
}

match = /^str (x\d+|w\d+), \[sp(?:, #(.+))?\]$/.exec(text);
if (match !== null) {
const value = getTrackedRegister(registers, match[1]);
const sp = getTrackedRegister(registers, 'sp');
if (value === null || sp === null) {
return false;
}
const offset = match[2] !== undefined ? parseImmediateToken(match[2]) : 0n;
setTrackedStack(stack, sp + offset, value);
return true;
}

match = /^ldr (x\d+|w\d+), \[sp(?:, #(.+))?\]$/.exec(text);
if (match !== null) {
const sp = getTrackedRegister(registers, 'sp');
if (sp === null) {
return false;
}
const offset = match[2] !== undefined ? parseImmediateToken(match[2]) : 0n;
const value = getTrackedStack(stack, sp + offset);
if (value === null) {
return false;
}
setTrackedRegister(registers, match[1], value);
return true;
}

return text === 'nop';
}

function resolveThunkBranchTarget(address, maxInstructions) {
const registers = { sp: 0n };
const stack = new Map();
let current = ptr(address);

for (let i = 0; i < maxInstructions; i += 1) {
let instruction;
try {
instruction = Instruction.parse(current);
} catch (_) {
return null;
}

const text = `${instruction}`.trim().replace(/\s+/g, ' ');
if (instruction.mnemonic === 'b') {
return parseDirectCallTarget(instruction);
}

if (instruction.mnemonic === 'br') {
const match = /^br (x\d+)$/.exec(text);
if (match === null) {
return null;
}
const target = getTrackedRegister(registers, match[1]);
return target === null ? null : bigIntToPtr(target);
}

if (instruction.mnemonic === 'ret' || instruction.mnemonic === 'blr') {
return null;
}

if (!trySimulateThunkInstruction(text, registers, stack)) {
return null;
}

current = instruction.next;
}

return null;
}

function resolveKnownDispatchThunkTarget(address, maxInstructions) {
let current = ptr(address);
let adrRegister = null;
let adrTarget = null;
let branchRegister = null;
let stackLoadRegister = null;
let sawStrSpPreIndex = false;

for (let i = 0; i < maxInstructions; i += 1) {
let instruction;
try {
instruction = Instruction.parse(current);
} catch (_) {
return null;
}

const text = `${instruction}`.trim().replace(/\s+/g, ' ');
let match = /^adr (x\d+), #(0x[0-9a-fA-F]+)$/.exec(text);
if (match !== null) {
adrRegister = match[1];
adrTarget = ptr(match[2]);
} else if (adrRegister !== null && text === `str ${adrRegister}, [sp, #-0x10]!`) {
sawStrSpPreIndex = true;
} else {
match = /^ldr (x\d+), \[sp, #8\]$/.exec(text);
if (match !== null) {
stackLoadRegister = match[1];
} else {
match = /^br (x\d+)$/.exec(text);
if (match !== null) {
branchRegister = match[1];
break;
}
}
}

if (instruction.mnemonic === 'ret') {
break;
}

current = instruction.next;
}

if (
adrTarget === null ||
!sawStrSpPreIndex ||
stackLoadRegister === null ||
branchRegister === null ||
stackLoadRegister !== branchRegister
) {
return null;
}

return ptr(adrTarget).add(0x20);
}

function resolveKnownIndirectSlotThunkTarget(address, maxInstructions) {
let current = ptr(address);
let pageBase = null;
let slotAddress = null;
let sawBranch = false;

for (let i = 0; i < maxInstructions; i += 1) {
let instruction;
try {
instruction = Instruction.parse(current);
} catch (_) {
return null;
}

const text = `${instruction}`.trim().replace(/\s+/g, ' ');
let match = /^adrp x8, #(0x[0-9a-fA-F]+)$/.exec(text);
if (match !== null) {
pageBase = ptr(match[1]);
current = instruction.next;
continue;
}

match = /^ldr x8, \[x8, #(.+)\]$/.exec(text);
if (match !== null && pageBase !== null && slotAddress === null) {
slotAddress = ptr(pageBase).add(wordToNumber(parseImmediateToken(match[1]).toString()));
current = instruction.next;
continue;
}

if (/^br x1$/.test(text) || /^br x8$/.test(text)) {
sawBranch = true;
break;
}

current = instruction.next;
}

if (!sawBranch || slotAddress === null) {
return null;
}

const first = safeReadPointerAtAddress(slotAddress);
if (first === null || ptr(first).isNull()) {
return null;
}

const second = safeReadPointerAtAddress(first);
if (second === null || ptr(second).isNull()) {
return null;
}

return second;
}

function resolveDispatchChain(address) {
const chain = [ptr(address)];
let current = ptr(address);

for (let depth = 0; depth < DISPATCH_CHAIN_MAX_DEPTH; depth += 1) {
let next = resolveThunkBranchTarget(current, DISPATCH_THUNK_SCAN_MAX_INSNS);
if (next === null) {
next = resolveKnownDispatchThunkTarget(current, DISPATCH_THUNK_SCAN_MAX_INSNS);
}
if (next === null) {
next = resolveKnownIndirectSlotThunkTarget(current, DISPATCH_THUNK_SCAN_MAX_INSNS);
}
if (next === null || next.toString() === current.toString()) {
break;
}

const currentModule = Process.findModuleByAddress(current);
const nextModule = Process.findModuleByAddress(next);
if (currentModule === null || nextModule === null) {
break;
}

if (currentModule.name !== nextModule.name) {
chain.push(next);
break;
}

chain.push(next);
current = next;
}

return chain;
}

function logRuntimeDisassemblyOnce(label, address, maxInstructions) {
const p = ptr(address);
const key = `${label}@${p}`;
if (loggedRuntimeDisassemblyKeys.has(key)) {
return;
}
loggedRuntimeDisassemblyKeys.add(key);

let instructionLimit = maxInstructions;
const ownerModule = Process.findModuleByAddress(p);
if (ownerModule !== null && ownerModule.name === MODULE_NAME) {
const relativeOffset = ptrToNumber(p) - ptrToNumber(ownerModule.base);
const override = PROCESS_RUNTIME_TARGET_DISASM_OVERRIDES.get(relativeOffset);
if (override !== undefined) {
instructionLimit = override;
}
}

const lines = [];
let current = p;
const visited = new Set();
for (let i = 0; i < instructionLimit; i += 1) {
const currentKey = current.toString();
if (visited.has(currentKey)) {
lines.push(`${hex(current)} <visited>`);
break;
}
visited.add(currentKey);

let instruction;
try {
instruction = Instruction.parse(current);
} catch (e) {
lines.push(`${hex(current)} <parse failed: ${e}>`);
break;
}

lines.push(`${hex(current)} ${instruction}`);
if (instruction.mnemonic === 'ret' || instruction.mnemonic === 'br') {
break;
}

if (instruction.mnemonic === 'b') {
const target = parseDirectCallTarget(instruction);
if (target !== null) {
const targetModule = Process.findModuleByAddress(target);
if (ownerModule !== null && targetModule !== null && ownerModule.name === targetModule.name) {
current = target;
continue;
}
}
break;
}

current = instruction.next;
}

log(`${label} runtime bytes: ${readBytesHex(p, 32)}`);
log(`${label} runtime disassembly:`);
for (let i = 0; i < lines.length; i += 1) {
log(` ${lines[i]}`);
}
}

function maybeInstallProcessDynamicDispatchProbe(label, address) {
const p = ptr(address);
const ownerModule = Process.findModuleByAddress(p);
if (ownerModule === null || ownerModule.name !== MODULE_NAME) {
return;
}

const relativeOffset = ptrToNumber(p) - ptrToNumber(ownerModule.base);
if (!PROCESS_DYNAMIC_DISPATCH_TARGET_PROBE_OFFSETS.has(relativeOffset)) {
return;
}

const hookLabel = `${label}.dynamic_dispatch_probe`;
const hookKey = `${hookLabel}@${p}`;
if (hookKeys.has(hookKey)) {
return;
}

let listener = null;
try {
listener = Interceptor.attach(p, {
onEnter(args) {
if (loggedProcessDynamicDispatchProbeKeys.has(hookKey)) {
return;
}
loggedProcessDynamicDispatchProbeKeys.add(hookKey);

const arg0 = ptr(args[0]);
const arg1 = ptr(args[1]);
const arg2Value = wordToInt(args[2]);
const arg3 = ptr(args[3]);
const dispatchBase = arg0.add(arg2Value >> 1);

let table = null;
let slotAddress = null;
let callTarget = null;
let mode = 'direct';

if ((arg2Value & 1) !== 0) {
mode = 'indirect';
table = safeReadPointerAtAddress(dispatchBase);
if (table !== null && !ptr(table).isNull()) {
slotAddress = ptr(table).add(wordToInt(arg1));
callTarget = safeReadPointerAtAddress(slotAddress);
}
} else {
callTarget = arg1;
}

const parts = [
`${label} runtime dispatch probe`,
`mode=${mode}`,
`arg0=${hex(arg0)}`,
`arg1=${hex(arg1)}`,
`arg2=0x${(arg2Value >>> 0).toString(16)}`,
`arg3=${hex(arg3)}`,
`dispatch_base=${hex(dispatchBase)}`,
];

if (table !== null) {
parts.push(`table=${formatPointerOrNull(table)}`);
}
if (slotAddress !== null) {
parts.push(`slot=${hex(slotAddress)}`);
}
parts.push(`target=${callTarget === null ? '<unresolved>' : formatAddress(callTarget)}`);
log(parts.join(' '));

if (callTarget !== null && !ptr(callTarget).isNull()) {
const chain = resolveDispatchChain(callTarget);
log(`${label} runtime dispatch chain ${chain.map((item) => formatAddress(item)).join(' -> ')}`);
logRuntimeDisassemblyOnce(
`${label}.runtime_dispatch.target@${formatAddress(chain[chain.length - 1])}`,
chain[chain.length - 1],
PROCESS_RUNTIME_TARGET_DISASM_INSNS
);
analyzeProcessLeafCallsOnce(
`${label}.runtime_dispatch.target@${formatAddress(chain[chain.length - 1])}`,
chain[chain.length - 1],
PROCESS_DYNAMIC_DISPATCH_LEAF_ANALYSIS_RECURSION_DEPTH
);
}

if (listener !== null) {
try {
listener.detach();
} catch (_) {
}
}
},
});
hookKeys.add(hookKey);
log(`installed ${hookLabel} hook at ${hex(p)}`);
} catch (e) {
log(`failed to hook ${hookLabel} at ${hex(p)}: ${e}`);
}
}

function analyzeDispatchTargetOnce(kind, dispatchTable, slotOffset, dispatchTarget, label) {
const key = `${kind}@${ptr(dispatchTarget)}`;
if (loggedDispatchAnalysisKeys.has(key)) {
return;
}
loggedDispatchAnalysisKeys.add(key);

const chain = resolveDispatchChain(dispatchTarget);
log(
`${label} first dispatch chain via ${kind} slot=0x${slotOffset.toString(16)} table=${hex(dispatchTable)} ` +
`${chain.map((item) => formatAddress(item)).join(' -> ')}`
);

logRuntimeDisassemblyOnce(
`${label}.dispatch_target.leaf@${formatAddress(chain[chain.length - 1])}`,
chain[chain.length - 1],
DISPATCH_TARGET_SCAN_MAX_INSNS
);

analyzeProcessLeafRuntimeSlotsOnce(label, chain[chain.length - 1]);
analyzeProcessLeafCallsOnce(label, chain[chain.length - 1]);
}

function safeReadU8(address) {
try {
return ptr(address).readU8();
} catch (_) {
return null;
}
}

function readProcessRuntimeFunctionSlot(module, offset) {
const slotAddress = ptr(module.base).add(offset);
const slotPointer = safeReadPointer(slotAddress);
const target = slotPointer !== null && !slotPointer.isNull()
? safeReadPointer(slotPointer)
: null;
return {
slotAddress,
slotPointer,
target,
};
}

function formatPointerOrNull(value) {
if (value === null) {
return '<unreadable>';
}
return ptr(value).isNull() ? '0x0' : hex(value);
}

function formatTargetOrNull(value) {
if (value === null) {
return '<unreadable>';
}
return ptr(value).isNull() ? '0x0' : formatAddress(value);
}

function snapshotProcessRuntimeSlotsOnce(label, stage) {
if (!ENABLE_PROCESS_RUNTIME_SLOT_SNAPSHOT) {
return;
}

const key = `${label}@${stage}`;
if (loggedRuntimeSlotStages.has(key)) {
return;
}
loggedRuntimeSlotStages.add(key);

const sec = getModule(MODULE_NAME);
if (sec === null) {
return;
}

const base = ptr(sec.base);
const initFlagAddress = base.add(PROCESS_RUNTIME_SLOT_OFFSETS.initFlag);
const handleAddress = base.add(PROCESS_RUNTIME_SLOT_OFFSETS.handle);
const initFlag = safeReadU8(initFlagAddress);
const handle = safeReadPointer(handleAddress);
const invoke = readProcessRuntimeFunctionSlot(sec, PROCESS_RUNTIME_SLOT_OFFSETS.invokePtrPtr);
const create = readProcessRuntimeFunctionSlot(sec, PROCESS_RUNTIME_SLOT_OFFSETS.createPtrPtr);

log(
`${label} runtime slots ${stage}: ` +
`init_flag@${hex(initFlagAddress)}=${initFlag === null ? '<unreadable>' : `0x${initFlag.toString(16)}`} ` +
`handle@${hex(handleAddress)}=${formatPointerOrNull(handle)} ` +
`slot_0xa1218=${hex(invoke.slotAddress)} -> ${formatPointerOrNull(invoke.slotPointer)} -> ${formatTargetOrNull(invoke.target)} ` +
`slot_0xa1260=${hex(create.slotAddress)} -> ${formatPointerOrNull(create.slotPointer)} -> ${formatTargetOrNull(create.target)}`
);

if (invoke.target !== null && !ptr(invoke.target).isNull()) {
logRuntimeDisassemblyOnce(
`${label}.slot_0xa1218.target@${formatAddress(invoke.target)}`,
invoke.target,
PROCESS_RUNTIME_TARGET_DISASM_INSNS
);
}

if (create.target !== null && !ptr(create.target).isNull()) {
logRuntimeDisassemblyOnce(
`${label}.slot_0xa1260.target@${formatAddress(create.target)}`,
create.target,
PROCESS_RUNTIME_TARGET_DISASM_INSNS
);
}
}

function safeReadPointerAtAddress(address) {
try {
return ptr(address).readPointer();
} catch (_) {
return null;
}
}

function analyzeProcessLeafRuntimeSlotsOnce(label, leafAddress) {
if (!ENABLE_PROCESS_LEAF_SLOT_ANALYSIS) {
return;
}

const p = ptr(leafAddress);
const key = `${label}@${p}`;
if (loggedProcessLeafAnalysisKeys.has(key)) {
return;
}
loggedProcessLeafAnalysisKeys.add(key);

const pageRegisters = new Map();
const absoluteRegisters = new Map();
let current = p;
let initFlagAddress = null;
let handleAddress = null;
let invokeSlotPtrPtrAddress = null;
let createSlotPtrPtrAddress = null;

for (let i = 0; i < PROCESS_RUNTIME_TARGET_DISASM_INSNS; i += 1) {
let instruction;
try {
instruction = Instruction.parse(current);
} catch (_) {
break;
}

const text = `${instruction}`.trim().replace(/\s+/g, ' ');
let match = /^adrp (x\d+), #(0x[0-9a-fA-F]+)$/.exec(text);
if (match !== null) {
pageRegisters.set(match[1], ptr(match[2]));
absoluteRegisters.delete(match[1]);
current = instruction.next;
continue;
}

match = /^add (x\d+), (x\d+), #(.+)$/.exec(text);
if (match !== null) {
const base = absoluteRegisters.has(match[2])
? absoluteRegisters.get(match[2])
: pageRegisters.get(match[2]);
if (base !== undefined) {
absoluteRegisters.set(match[1], ptr(base).add(wordToNumber(parseImmediateToken(match[3]).toString())));
}
current = instruction.next;
continue;
}

match = /^ldarb (w\d+), \[(x\d+)\]$/.exec(text);
if (match !== null && absoluteRegisters.has(match[2])) {
initFlagAddress = absoluteRegisters.get(match[2]);
current = instruction.next;
continue;
}

match = /^ldr (x\d+), \[(x\d+), #(.+)\]$/.exec(text);
if (match !== null) {
const page = pageRegisters.get(match[2]);
if (page !== undefined) {
const slotAddress = ptr(page).add(wordToNumber(parseImmediateToken(match[3]).toString()));
const loaded = safeReadPointerAtAddress(slotAddress);
absoluteRegisters.set(match[1], loaded !== null ? loaded : ptr('0'));

const offsetText = match[3].trim();
if (offsetText === '0x908') {
handleAddress = slotAddress;
} else if (offsetText === '0x218') {
invokeSlotPtrPtrAddress = slotAddress;
} else if (offsetText === '0x260') {
createSlotPtrPtrAddress = slotAddress;
}
}

current = instruction.next;
continue;
}

match = /^ldr (x\d+), \[(x\d+)\]$/.exec(text);
if (match !== null && absoluteRegisters.has(match[2])) {
const loaded = safeReadPointerAtAddress(absoluteRegisters.get(match[2]));
absoluteRegisters.set(match[1], loaded !== null ? loaded : ptr('0'));
current = instruction.next;
continue;
}

current = instruction.next;
if (instruction.mnemonic === 'ret') {
break;
}
}

const initFlagValue = initFlagAddress !== null ? safeReadU8(initFlagAddress) : null;
const handleValue = handleAddress !== null ? safeReadPointerAtAddress(handleAddress) : null;
const invokeSlotPointer = invokeSlotPtrPtrAddress !== null ? safeReadPointerAtAddress(invokeSlotPtrPtrAddress) : null;
const createSlotPointer = createSlotPtrPtrAddress !== null ? safeReadPointerAtAddress(createSlotPtrPtrAddress) : null;
const invokeTarget = invokeSlotPointer !== null && !ptr(invokeSlotPointer).isNull()
? safeReadPointerAtAddress(invokeSlotPointer)
: null;
const createTarget = createSlotPointer !== null && !ptr(createSlotPointer).isNull()
? safeReadPointerAtAddress(createSlotPointer)
: null;

log(
`${label} leaf runtime slots: ` +
`init_flag=${initFlagAddress === null ? '<unresolved>' : `${hex(initFlagAddress)}=${initFlagValue === null ? '<unreadable>' : `0x${initFlagValue.toString(16)}`}`} ` +
`handle=${handleAddress === null ? '<unresolved>' : `${hex(handleAddress)}=${formatPointerOrNull(handleValue)}`} ` +
`invoke_slot=${invokeSlotPtrPtrAddress === null ? '<unresolved>' : `${hex(invokeSlotPtrPtrAddress)} -> ${formatPointerOrNull(invokeSlotPointer)} -> ${formatTargetOrNull(invokeTarget)}`} ` +
`create_slot=${createSlotPtrPtrAddress === null ? '<unresolved>' : `${hex(createSlotPtrPtrAddress)} -> ${formatPointerOrNull(createSlotPointer)} -> ${formatTargetOrNull(createTarget)}`}`
);

if (invokeTarget !== null && !ptr(invokeTarget).isNull()) {
logRuntimeDisassemblyOnce(
`${label}.invoke_target@${formatAddress(invokeTarget)}`,
invokeTarget,
PROCESS_RUNTIME_TARGET_DISASM_INSNS
);
}

if (createTarget !== null && !ptr(createTarget).isNull()) {
logRuntimeDisassemblyOnce(
`${label}.create_target@${formatAddress(createTarget)}`,
createTarget,
PROCESS_RUNTIME_TARGET_DISASM_INSNS
);
}
}

function analyzeProcessLeafCallsOnce(label, leafAddress, remainingDepth) {
if (!ENABLE_PROCESS_LEAF_CALL_ANALYSIS) {
return;
}

if (remainingDepth === undefined) {
remainingDepth = PROCESS_LEAF_CALL_ANALYSIS_RECURSION_DEPTH;
}

const p = ptr(leafAddress);
const key = `${p}@depth=${remainingDepth}`;
if (loggedProcessLeafCallAnalysisKeys.has(key)) {
return;
}
loggedProcessLeafCallAnalysisKeys.add(key);

const ownerModule = Process.findModuleByAddress(p);
const pageRegisters = new Map();
const absoluteRegisters = new Map();
const registerSources = new Map();
let current = p;
let callIndex = 0;

function maybeAnalyzeNestedLeafCall(callLabel, finalTarget) {
if (remainingDepth <= 0) {
return;
}

const target = ptr(finalTarget);
if (target.isNull() || target.toString() === p.toString()) {
return;
}

if (ownerModule === null) {
return;
}

const targetModule = Process.findModuleByAddress(target);
if (targetModule === null || targetModule.name !== ownerModule.name) {
return;
}

const relativeOffset = ptrToNumber(target.sub(targetModule.base));
if (
PROCESS_LEAF_CALL_ANALYSIS_RECURSE_OFFSETS.size !== 0 &&
!PROCESS_LEAF_CALL_ANALYSIS_RECURSE_OFFSETS.has(relativeOffset)
) {
return;
}

analyzeProcessLeafCallsOnce(callLabel, target, remainingDepth - 1);
}

function logResolvedLeafCall(callKind, firstTarget, slotAddress) {
const chain = resolveDispatchChain(firstTarget);
const finalTarget = chain[chain.length - 1];
const chainText = chain.map((item) => formatAddress(item)).join(' -> ');
if (slotAddress !== null) {
log(`${label} leaf call#${callIndex} ${callKind} slot=${hex(slotAddress)} chain=${chainText}`);
} else {
log(`${label} leaf call#${callIndex} ${callKind} chain=${chainText}`);
}

logRuntimeDisassemblyOnce(
`${label}.leaf_call_${callIndex}.target@${formatAddress(finalTarget)}`,
finalTarget,
PROCESS_RUNTIME_TARGET_DISASM_INSNS
);

maybeInstallProcessStateDumpHook(finalTarget);

maybeInstallProcessDynamicDispatchProbe(
`${label}.leaf_call_${callIndex}.target@${formatAddress(finalTarget)}`,
finalTarget
);

maybeAnalyzeNestedLeafCall(
`${label}.leaf_call_${callIndex}.target@${formatAddress(finalTarget)}`,
finalTarget
);
}

for (let i = 0; i < PROCESS_RUNTIME_TARGET_DISASM_INSNS; i += 1) {
let instruction;
try {
instruction = Instruction.parse(current);
} catch (_) {
break;
}

const text = `${instruction}`.trim().replace(/\s+/g, ' ');
let match = /^adrp (x\d+), #(0x[0-9a-fA-F]+)$/.exec(text);
if (match !== null) {
pageRegisters.set(match[1], ptr(match[2]));
absoluteRegisters.delete(match[1]);
registerSources.delete(match[1]);
current = instruction.next;
continue;
}

match = /^adr (x\d+), #(0x[0-9a-fA-F]+)$/.exec(text);
if (match !== null) {
const target = ptr(match[2]);
absoluteRegisters.set(match[1], target);
registerSources.set(match[1], {
kind: 'direct',
target,
});
current = instruction.next;
continue;
}

match = /^mov (x\d+), (x\d+)$/.exec(text);
if (match !== null) {
if (absoluteRegisters.has(match[2])) {
absoluteRegisters.set(match[1], absoluteRegisters.get(match[2]));
} else {
absoluteRegisters.delete(match[1]);
}
if (registerSources.has(match[2])) {
registerSources.set(match[1], registerSources.get(match[2]));
} else {
registerSources.delete(match[1]);
}
current = instruction.next;
continue;
}

match = /^add (x\d+), (x\d+), #(.+)$/.exec(text);
if (match !== null) {
const base = absoluteRegisters.has(match[2])
? absoluteRegisters.get(match[2])
: pageRegisters.get(match[2]);
if (base !== undefined) {
const target = ptr(base).add(wordToNumber(parseImmediateToken(match[3]).toString()));
absoluteRegisters.set(match[1], target);
registerSources.set(match[1], {
kind: 'direct',
target,
});
}
current = instruction.next;
continue;
}

match = /^ldr (x\d+), \[(x\d+), #(.+)\]$/.exec(text);
if (match !== null) {
const base = absoluteRegisters.has(match[2])
? absoluteRegisters.get(match[2])
: pageRegisters.get(match[2]);
if (base !== undefined) {
const slotAddress = ptr(base).add(wordToNumber(parseImmediateToken(match[3]).toString()));
const loaded = safeReadPointerAtAddress(slotAddress);
if (loaded !== null) {
absoluteRegisters.set(match[1], loaded);
registerSources.set(match[1], {
kind: 'indirect',
slotAddress,
target: loaded,
});
}
}
current = instruction.next;
continue;
}

match = /^ldr (x\d+), \[(x\d+)\]$/.exec(text);
if (match !== null && absoluteRegisters.has(match[2])) {
const slotAddress = absoluteRegisters.get(match[2]);
const loaded = safeReadPointerAtAddress(slotAddress);
if (loaded !== null) {
absoluteRegisters.set(match[1], loaded);
registerSources.set(match[1], {
kind: 'indirect',
slotAddress,
target: loaded,
});
}
current = instruction.next;
continue;
}

match = /^blr (x\d+)$/.exec(text);
if (match !== null) {
callIndex += 1;
const source = registerSources.get(match[1]);
if (source !== undefined) {
if (source.kind === 'indirect') {
if (source.target !== null && !ptr(source.target).isNull()) {
logResolvedLeafCall('indirect', source.target, source.slotAddress);
}
} else {
logResolvedLeafCall('direct', source.target, null);
}
} else if (absoluteRegisters.has(match[1])) {
const target = absoluteRegisters.get(match[1]);
logResolvedLeafCall('direct', target, null);
}
current = instruction.next;
continue;
}

if (instruction.mnemonic === 'bl') {
const target = parseDirectCallTarget(instruction);
if (target !== null) {
callIndex += 1;
logResolvedLeafCall('direct', target, null);
}
current = instruction.next;
continue;
}

current = instruction.next;
if (instruction.mnemonic === 'ret') {
break;
}
}
}

function maybeInstallProcessStateDumpHook(target) {
if (!ENABLE_PROCESS_STATE_DUMP) {
return;
}

const p = ptr(target);
if (p.isNull()) {
return;
}

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

const relativeOffset = ptrToNumber(p.sub(mod.base));
const stage = PROCESS_STATE_DUMP_TARGET_OFFSETS.get(relativeOffset);
if (stage === undefined || loggedProcessStateDumpStages.has(stage)) {
return;
}

const key = `${stage}@${p}`;
if (processStateDumpHookKeys.has(key)) {
return;
}

const callbacks = stage === 'init'
? {
onEnter(args) {
this.ctxAddress = args[0];
},
onLeave() {
if (loggedProcessStateDumpStages.has(stage)) {
return;
}
if (this.ctxAddress === undefined) {
return;
}
if (dumpProcessStateWords(stage, p, this.ctxAddress)) {
loggedProcessStateDumpStages.add(stage);
}
},
}
: {
onEnter(args) {
if (loggedProcessStateDumpStages.has(stage)) {
return;
}
if (dumpProcessStateWords(stage, p, args[0])) {
loggedProcessStateDumpStages.add(stage);
}
},
};

const ok = installHook(p, `process_state_dump.${stage}`, callbacks);
if (ok) {
processStateDumpHookKeys.add(key);
}
}

function installInternalCallHook(ownerLabel, target, sourceAddress) {
const p = ptr(target);
if (p.isNull()) {
return;
}

const key = `${ownerLabel}@${p}`;
if (internalCallHookKeys.has(key)) {
return;
}

const ok = installHook(p, `${ownerLabel}.internal`, {
onEnter() {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

pushInvocationHitById(
invocation.id,
`internal caller=${ownerLabel} target=${formatAddress(p)} from=${formatAddress(sourceAddress)} ` +
`x0=${hex(this.context.x0)} x1=${hex(this.context.x1)} x2=${hex(this.context.x2)}`
);
},
});

if (ok) {
internalCallHookKeys.add(key);
}
}

function installDispatchTargetHook(label, kind, target) {
const p = ptr(target);
if (p.isNull()) {
return;
}

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

const key = `${label}.${kind}.dispatch@${p}`;
if (dispatchTargetHookKeys.has(key)) {
return;
}

const ok = installHook(p, `${label}.${kind}.dispatch`, {
onEnter() {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

logBacktraceOnce(`${label}.${kind}.dispatch@${p}`, this.context);
pushInvocationHitById(
invocation.id,
`dispatch ${kind} target=${formatAddress(p)} x0=${hex(this.context.x0)} x1=${hex(this.context.x1)} ` +
`x2=${hex(this.context.x2)} x3=${hex(this.context.x3)}`
);
},
onLeave(retval) {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

pushInvocationHitById(
invocation.id,
`dispatch ${kind} leave target=${formatAddress(p)} retval=${hex(retval)}`
);
},
});

if (ok) {
dispatchTargetHookKeys.add(key);
}
}

function analyzeMethodEntry(kind, address, label, maxInstructions) {
const p = ptr(address);
if (p.isNull()) {
return;
}

const analysisKey = `${label}.${kind}@${p}`;
if (analyzedMethodEntries.has(analysisKey)) {
return;
}
analyzedMethodEntries.add(analysisKey);

const ownerModule = Process.findModuleByAddress(p);
const lines = [];
const internalTargets = [];
const scanLimit = typeof maxInstructions === 'number' ? maxInstructions : ENTRY_SCAN_MAX_INSNS;
let current = p;

for (let i = 0; i < scanLimit; i += 1) {
let instruction;
try {
instruction = Instruction.parse(current);
} catch (e) {
lines.push(`${hex(current)} <parse failed: ${e}>`);
break;
}

const rendered = `${hex(current)} ${instruction}`;
lines.push(rendered);

if (instruction.mnemonic === 'bl') {
const target = parseDirectCallTarget(instruction);
if (target !== null && ownerModule !== null) {
const targetModule = Process.findModuleByAddress(target);
if (targetModule !== null && targetModule.name === ownerModule.name) {
internalTargets.push({
from: current,
target,
});
}
}
}

if (instruction.mnemonic === 'blr') {
let operandText = '?';
try {
if (instruction.operands !== undefined && instruction.operands.length > 0) {
operandText = `${instruction.operands[0]}`;
}
} catch (_) {
}
lines.push(` <indirect call via ${operandText}>`);
}

current = instruction.next;
if (instruction.mnemonic === 'ret' || instruction.mnemonic === 'br' || instruction.mnemonic === 'b') {
break;
}
}

log(`${analysisKey} runtime bytes: ${readBytesHex(p, 32)}`);
log(`${analysisKey} runtime disassembly:`);
for (let i = 0; i < lines.length; i += 1) {
log(` ${lines[i]}`);
}

if (internalTargets.length === 0) {
log(`${analysisKey} direct internal bl targets: none in first ${scanLimit} instructions`);
return;
}

const limit = Math.min(internalTargets.length, MAX_INTERNAL_TARGET_HOOKS_PER_METHOD);
for (let i = 0; i < limit; i += 1) {
const item = internalTargets[i];
log(`${analysisKey} direct internal bl#${i + 1}: ${formatAddress(item.from)} -> ${formatAddress(item.target)}`);
if (ENABLE_INTERNAL_CALL_HOOKS) {
installInternalCallHook(label, item.target, item.from);
}
}

if (!ENABLE_INTERNAL_CALL_HOOKS) {
log(`${analysisKey} internal target auto-hooking disabled for stability`);
}
}

function installProcessCallbackHook(kind, address, meta) {
const p = ptr(address);
if (p.isNull()) {
return;
}

const label = meta.label;
installHook(p, `${label}.${kind}`, {
onEnter(args) {
const tid = Process.getCurrentThreadId();
const invocation = {
id: ++processSeq,
tid,
kind,
hits: [],
};

activeInvocationsByThread.set(tid, invocation);
activeInvocationsById.set(invocation.id, invocation);

logBacktraceOnce(label, this.context);

const dispatchTable = safeReadPointer(args[0]);
if (dispatchTable !== null && !dispatchTable.isNull()) {
const slotOffset = kind === 'call_func' ? 0x18 : 0x20;
const dispatchTarget = safeReadPointer(dispatchTable.add(slotOffset));
if (dispatchTarget !== null && !dispatchTarget.isNull()) {
if (ENABLE_DISPATCH_TARGET_ANALYSIS) {
analyzeDispatchTargetOnce(kind, dispatchTable, slotOffset, dispatchTarget, label);
}
if (ENABLE_DISPATCH_TARGET_HOOKS) {
installDispatchTargetHook(label, kind, dispatchTarget);
}
}
}

if (kind === 'call_func') {
snapshotProcessRuntimeSlotsOnce(label, 'first-enter');
this.processReturnVariant = args[4];
logProcessInputVariantOnce(label, args[2], wordToNumber(args[3]));
}

if (ENABLE_VERBOSE_PROCESS_INVOCATION_LOGS && kind === 'call_func') {
const argc = wordToNumber(args[3]);
log(
`${label} Process#${invocation.id} enter via ${kind} tid=${tid} ` +
`userdata=${hex(args[0])} instance=${hex(args[1])} args=${hex(args[2])} argc=${argc} ` +
`ret=${hex(args[4])} err=${hex(args[5])} lr=${formatAddress(this.returnAddress)}`
);
} else if (ENABLE_VERBOSE_PROCESS_INVOCATION_LOGS) {
log(
`${label} Process#${invocation.id} enter via ${kind} tid=${tid} ` +
`userdata=${hex(args[0])} instance=${hex(args[1])} args=${hex(args[2])} ret=${hex(args[3])} ` +
`lr=${formatAddress(this.returnAddress)}`
);
}
},
onLeave(retval) {
const tid = Process.getCurrentThreadId();
const invocation = activeInvocationsByThread.get(tid);
if (invocation === undefined) {
return;
}

if (invocation.kind === 'call_func') {
snapshotProcessRuntimeSlotsOnce(label, 'first-leave');
if (this.processReturnVariant !== undefined) {
logProcessReturnVariantOnce(label, this.processReturnVariant);
}
}

activeInvocationsByThread.delete(tid);
summarizeInvocation(invocation, retval);
activeInvocationsById.delete(invocation.id);
},
});
}

function installRegisteredMethodCallbacks(className, methodName, info, sourceLabel) {
const displayClassName = isUsefulName(className) ? className : '<unnamed_class>';
const displayMethodName = isUsefulName(methodName) ? methodName : `<unnamed_method_${registeredMethodCount + 1}>`;
const label = `${displayClassName}.${displayMethodName}@${sourceLabel}`;

registeredMethodCount += 1;
log(
`registered method #${registeredMethodCount} source=${sourceLabel} class=${displayClassName} method=${displayMethodName} ` +
`call=${formatAddress(info.callFunc)} ptrcall=${formatAddress(info.ptrcallFunc)}`
);

if (ENABLE_CALL_FUNC_HOOK) {
analyzeMethodEntry('call_func', info.callFunc, label);
installProcessCallbackHook('call_func', info.callFunc, { label });
}

if (ENABLE_PTRCALL_FUNC_HOOK) {
analyzeMethodEntry('ptrcall_func', info.ptrcallFunc, label);
installProcessCallbackHook('ptrcall_func', info.ptrcallFunc, { label });
} else {
log(`${label} ptrcall_func hook disabled for crash isolation target=${formatAddress(info.ptrcallFunc)}`);
}

if (displayClassName === TARGET.className && displayMethodName === TARGET.methodName) {
processResolved = true;
}
}

function formatInitializationLevel(level) {
switch (level >>> 0) {
case 0:
return 'CORE';
case 1:
return 'SERVERS';
case 2:
return 'SCENE';
case 3:
return 'EDITOR';
default:
return `UNKNOWN(${level})`;
}
}

function readInitializationInfo(initStruct) {
const p = ptr(initStruct);
if (p.isNull()) {
return null;
}

try {
return {
minimumInitializationLevel: p.add(GDEXT_INIT.minimumInitializationLevel).readU32(),
userdata: p.add(GDEXT_INIT.userdata).readPointer(),
initialize: p.add(GDEXT_INIT.initialize).readPointer(),
deinitialize: p.add(GDEXT_INIT.deinitialize).readPointer(),
};
} catch (e) {
log(`readInitializationInfo failed at ${hex(p)}: ${e}`);
return null;
}
}

function installInitLifecycleHook(kind, address) {
const p = ptr(address);
if (p.isNull()) {
return;
}

const key = `${kind}@${p}`;
if (initCallbackHooks.has(key)) {
return;
}

const ok = installHook(p, `gdextension.${kind}`, {
onEnter(args) {
const userdata = ptr(args[0]);
const level = wordToInt(args[1]);
log(
`${kind} enter userdata=${hex(userdata)} level=${formatInitializationLevel(level)} ` +
`lr=${formatAddress(this.returnAddress)}`
);

if (kind === 'initialize' && cachedGetProcAddress !== null) {
probeInterestingInterfaces(cachedGetProcAddress, `${kind}:${formatInitializationLevel(level)}`);
}
},
onLeave(retval) {
if (kind === 'initialize') {
log(`${kind} leave retval=${hex(retval)}`);
} else {
log(`${kind} leave`);
}
},
});

if (ok) {
initCallbackHooks.add(key);
}
}

function probeInterestingInterfaces(getProcAddress, reason) {
const p = ptr(getProcAddress);
if (p.isNull()) {
return;
}

let getProc = null;
try {
getProc = new NativeFunction(p, 'pointer', ['pointer']);
} catch (e) {
log(`failed to wrap get_proc_address at ${hex(p)}: ${e}`);
return;
}

INTERESTING_INTERFACE_NAMES.forEach((name) => {
try {
const nameBuf = Memory.allocUtf8String(name);
const result = ptr(getProc(nameBuf));
log(`probe get_proc_address("${name}") via ${reason} -> ${formatAddress(result)}`);
if (!result.isNull()) {
maybeInstallInterfaceHook(name, result);
}
} catch (e) {
log(`probe get_proc_address("${name}") via ${reason} failed: ${e}`);
}
});
}

function installMethodRegistrationHook(name, address) {
const p = ptr(address);
const key = `${name}@${p}`;
if (interfaceHooks.has(key)) {
return;
}

const ok = installHook(p, name, {
onEnter(args) {
const className = readLooseName(args[1]);
const info = parseMethodInfo(args[2]);
if (info === null) {
return;
}
const methodName = isUsefulName(info.name) ? info.name : readLooseName(ptr(args[2]).add(METHOD_INFO.name).readPointer());

log(
`${name} class=${className} method=${methodName} flags=0x${info.methodFlags.toString(16)} ` +
`argc=${info.argumentCount} defaults=${info.defaultArgumentCount} has_ret=${info.hasReturnValue} ` +
`userdata=${hex(info.methodUserdata)} call=${formatAddress(info.callFunc)} ptrcall=${formatAddress(info.ptrcallFunc)} ` +
`lr=${formatAddress(this.returnAddress)}`
);

installRegisteredMethodCallbacks(className, methodName, info, `${name}:${formatAddress(this.returnAddress)}`);

if (className === TARGET.className && methodName === TARGET.methodName) {
processResolved = true;
log(`target method resolved via interface: ${className}.${methodName}`);
}
},
});

if (ok) {
interfaceHooks.add(key);
}
}

function installClassRegistrationHook(name, address) {
const p = ptr(address);
const key = `${name}@${p}`;
if (interfaceHooks.has(key)) {
return;
}

const ok = installHook(p, name, {
onEnter(args) {
const className = readLooseName(args[1]);
const parentName = readLooseName(args[2]);
const creationInfo = ptr(args[3]);
log(
`${name} class=${className} parent=${parentName} library=${hex(args[0])} ` +
`creation_info=${hex(creationInfo)} lr=${formatAddress(this.returnAddress)}`
);
},
});

if (ok) {
interfaceHooks.add(key);
}
}

function maybeInstallInterfaceHook(name, address) {
if (!INTERESTING_INTERFACE_NAMES.has(name)) {
return;
}

if (name.indexOf('classdb_register_extension_class_method') === 0) {
installMethodRegistrationHook(name, address);
return;
}

if (name.indexOf('classdb_register_extension_class') === 0) {
installClassRegistrationHook(name, address);
}
}

function installGetProcHook(address) {
const p = ptr(address);
const key = p.toString();
if (getProcHooks.has(key)) {
return;
}

const ok = installHook(p, 'get_proc_address', {
onEnter(args) {
this.requestedName = safeReadUtf8(args[0], 256);
},
onLeave(retval) {
if (typeof this.requestedName !== 'string') {
return;
}

const result = ptr(retval);
if (INTERESTING_INTERFACE_NAMES.has(this.requestedName)) {
log(`get_proc_address("${this.requestedName}") -> ${formatAddress(result)}`);
}

if (!result.isNull()) {
maybeInstallInterfaceHook(this.requestedName, result);
}
},
});

if (ok) {
getProcHooks.add(key);
}
}

function findExtensionInitAddress(module) {
return ptr(module.base).add(OFFSETS_SEC.extensionInit);
}

function installExtensionInitHook(module) {
if (extensionInitHooked) {
return;
}

const target = findExtensionInitAddress(module);
extensionInitHooked = installHook(target, 'extension_init', {
onEnter(args) {
const getProcAddress = ptr(args[0]);
const library = ptr(args[1]);
const initStruct = ptr(args[2]);
this.getProcAddress = getProcAddress;
this.initStruct = initStruct;

log(
`extension_init enter get_proc=${formatAddress(getProcAddress)} ` +
`library=${hex(library)} init=${hex(initStruct)} lr=${formatAddress(this.returnAddress)}`
);

cachedGetProcAddress = getProcAddress;
installGetProcHook(getProcAddress);
},
onLeave(retval) {
log(`extension_init leave retval=${hex(retval)}`);

if (wordToInt(retval) === 0) {
return;
}

probeInterestingInterfaces(this.getProcAddress, 'extension_init.leave');

const info = readInitializationInfo(this.initStruct);
if (info === null) {
return;
}

log(
`init struct min_level=${formatInitializationLevel(info.minimumInitializationLevel)} ` +
`userdata=${hex(info.userdata)} initialize=${formatAddress(info.initialize)} ` +
`deinitialize=${formatAddress(info.deinitialize)}`
);

installInitLifecycleHook('initialize', info.initialize);
installInitLifecycleHook('deinitialize', info.deinitialize);
},
});
}

function installDirectGodotHook(module) {
if (godotHookInstalled) {
return;
}

const target = ptr(module.base).add(OFFSETS_GODOT.classdbRegisterExtensionClassMethod);
godotHookInstalled = installHook(target, 'classdb_register_extension_class_method', {
onEnter(args) {
const className = readLooseName(args[1]);
const info = parseMethodInfo(args[2]);
if (info === null) {
return;
}
const methodName = isUsefulName(info.name) ? info.name : readLooseName(ptr(args[2]).add(METHOD_INFO.name).readPointer());

log(
`direct classdb_register_extension_class_method class=${className} method=${methodName} ` +
`call=${formatAddress(info.callFunc)} ptrcall=${formatAddress(info.ptrcallFunc)} ` +
`lr=${formatAddress(this.returnAddress)}`
);

installRegisteredMethodCallbacks(className, methodName, info, `direct:${formatAddress(this.returnAddress)}`);

if (className === TARGET.className && methodName === TARGET.methodName) {
processResolved = true;
log(
`resolved ${className}.${methodName} ` +
`userdata=${hex(info.methodUserdata)} call=${formatAddress(info.callFunc)} ` +
`ptrcall=${formatAddress(info.ptrcallFunc)} argc=${info.argumentCount} ` +
`flags=0x${info.methodFlags.toString(16)} has_ret=${info.hasReturnValue}`
);
}
},
});
}

function installObservedLibcHook(name, handlersFactory) {
const address = findGlobalExport(name);
if (address === null) {
return;
}

installHook(address, name, handlersFactory());
}

function installLibcHooks() {
if (libcHooksInstalled) {
return;
}
libcHooksInstalled = true;

const mmapHandlersFactory = () => ({
onEnter(args) {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

this.invocationId = invocation.id;
this.addr = ptr(args[0]);
this.length = wordToNumber(args[1]);
this.prot = wordToNumber(args[2]);
this.flags = wordToNumber(args[3]);
this.fd = wordToInt(args[4]);
this.offset = wordToNumber(args[5]);
this.caller = formatAddress(this.returnAddress);
this.tid = tid;
},
onLeave(retval) {
if (this.invocationId === undefined) {
return;
}

const result = isMapFailed(retval) ? 'MAP_FAILED' : hex(retval);
pushInvocationHitById(
this.invocationId,
`tid=${this.tid} ${this.hookLabel} caller=${this.caller} addr=${hex(this.addr)} len=0x${this.length.toString(16)} ` +
`prot=${formatProt(this.prot)} flags=0x${this.flags.toString(16)} fd=${this.fd} off=0x${this.offset.toString(16)} => ${result}`
);
},
});

installObservedLibcHook('memfd_create', () => ({
onEnter(args) {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

this.invocationId = invocation.id;
this.name = safeReadUtf8(args[0], 256);
this.flags = wordToNumber(args[1]);
this.caller = formatAddress(this.returnAddress);
this.tid = tid;
},
onLeave(retval) {
if (this.invocationId === undefined) {
return;
}

const name = this.name !== null ? `"${this.name}"` : '<null>';
pushInvocationHitById(
this.invocationId,
`tid=${this.tid} memfd_create caller=${this.caller} name=${name} flags=0x${this.flags.toString(16)} => fd=${wordToInt(retval)}`
);
},
}));

installObservedLibcHook('mmap', () => {
const handlers = mmapHandlersFactory();
const originalOnEnter = handlers.onEnter;
handlers.onEnter = function (args) {
this.hookLabel = 'mmap';
originalOnEnter.call(this, args);
};
return handlers;
});

installObservedLibcHook('mmap64', () => {
const handlers = mmapHandlersFactory();
const originalOnEnter = handlers.onEnter;
handlers.onEnter = function (args) {
this.hookLabel = 'mmap64';
originalOnEnter.call(this, args);
};
return handlers;
});

installObservedLibcHook('mprotect', () => ({
onEnter(args) {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

this.invocationId = invocation.id;
this.addr = ptr(args[0]);
this.length = wordToNumber(args[1]);
this.prot = wordToNumber(args[2]);
this.caller = formatAddress(this.returnAddress);
this.tid = tid;
},
onLeave(retval) {
if (this.invocationId === undefined) {
return;
}

pushInvocationHitById(
this.invocationId,
`tid=${this.tid} mprotect caller=${this.caller} addr=${hex(this.addr)} len=0x${this.length.toString(16)} ` +
`prot=${formatProt(this.prot)} => ${wordToInt(retval)}`
);
},
}));

installObservedLibcHook('munmap', () => ({
onEnter(args) {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

this.invocationId = invocation.id;
this.addr = ptr(args[0]);
this.length = wordToNumber(args[1]);
this.caller = formatAddress(this.returnAddress);
this.tid = tid;
},
onLeave(retval) {
if (this.invocationId === undefined) {
return;
}

pushInvocationHitById(
this.invocationId,
`tid=${this.tid} munmap caller=${this.caller} addr=${hex(this.addr)} len=0x${this.length.toString(16)} => ${wordToInt(retval)}`
);
},
}));

installObservedLibcHook('pthread_create', () => ({
onEnter(args) {
const tid = Process.getCurrentThreadId();
const invocation = getInvocationByThreadId(tid);
if (invocation === undefined) {
return;
}

this.invocationId = invocation.id;
this.threadPtr = ptr(args[0]);
this.startRoutine = ptr(args[2]);
this.startArg = ptr(args[3]);
this.caller = formatAddress(this.returnAddress);
this.tid = tid;
},
onLeave(retval) {
if (this.invocationId === undefined) {
return;
}

pushInvocationHitById(
this.invocationId,
`tid=${this.tid} pthread_create caller=${this.caller} start=${formatAddress(this.startRoutine)} ` +
`arg=${hex(this.startArg)} thread_ptr=${hex(this.threadPtr)} => ${wordToInt(retval)}`
);
},
}));
}

function maybeInstallForLoadedModules(reason) {
const sec = getModule(MODULE_NAME);
if (sec !== null && !secModuleSeen) {
secModuleSeen = true;
log(`module ready: ${MODULE_NAME} base=${sec.base} size=0x${sec.size.toString(16)} via ${reason}`);
}
if (sec !== null) {
installExtensionInitHook(sec);
}

const godot = getModule(GODOT_MODULE_NAME);
if (godot !== null) {
if (!godotModuleSeen) {
godotModuleSeen = true;
log(`module ready: ${GODOT_MODULE_NAME} base=${godot.base} size=0x${godot.size.toString(16)} via ${reason}`);
}
installDirectGodotHook(godot);
}
}

function installLoaderHook(name) {
const address = findGlobalExport(name);
if (address === null) {
return;
}

installHook(address, name, {
onEnter(args) {
this.path = safeReadUtf8(args[0], 512);
},
onLeave(retval) {
if (ptr(retval).isNull()) {
return;
}

if (typeof this.path === 'string') {
if (this.path.indexOf(MODULE_NAME) !== -1 || this.path.indexOf(GODOT_MODULE_NAME) !== -1) {
log(`${name} returned handle=${hex(retval)} path=${this.path}`);
}
}

maybeInstallForLoadedModules(name);
},
});
}

function startLoaderHooks() {
if (loaderHooksInstalled) {
return;
}
loaderHooksInstalled = true;

maybeInstallForLoadedModules('startup');
if (godotHookInstalled && secModuleSeen) {
return;
}

installLoaderHook('android_dlopen_ext');
installLoaderHook('__loader_android_dlopen_ext');
installLoaderHook('dlopen');
installLoaderHook('__loader_dlopen');
}

function installModuleObserver() {
if (moduleObserverInstalled) {
return;
}
moduleObserverInstalled = true;

if (typeof Process.attachModuleObserver !== 'function') {
return;
}

Process.attachModuleObserver({
onAdded(module) {
if (module.name === MODULE_NAME || module.name === GODOT_MODULE_NAME) {
maybeInstallForLoadedModules('moduleObserver');
}
},
});
}

function getStatus() {
return {
loaderHooksInstalled,
directMethodHookInstalled: godotHookInstalled,
libcBoundaryHooksEnabled: ENABLE_LIBC_BOUNDARY_HOOKS,
callFuncHookEnabled: ENABLE_CALL_FUNC_HOOK,
ptrcallFuncHookEnabled: ENABLE_PTRCALL_FUNC_HOOK,
firstBacktraceEnabled: ENABLE_FIRST_BACKTRACE,
dispatchTargetAnalysisEnabled: ENABLE_DISPATCH_TARGET_ANALYSIS,
verboseProcessInvocationLogsEnabled: ENABLE_VERBOSE_PROCESS_INVOCATION_LOGS,
processRuntimeSlotSnapshotEnabled: ENABLE_PROCESS_RUNTIME_SLOT_SNAPSHOT,
processLeafSlotAnalysisEnabled: ENABLE_PROCESS_LEAF_SLOT_ANALYSIS,
processLeafCallAnalysisEnabled: ENABLE_PROCESS_LEAF_CALL_ANALYSIS,
libcHooksInstalled,
extensionInitHooked,
processCallbackResolved: processResolved,
registeredMethodCount,
secLoaded: secModuleSeen,
godotLoaded: godotModuleSeen,
activeInvocations: activeInvocationsByThread.size,
};
}

setImmediate(() => {
log(`package=${PACKAGE_NAME}`);
log(`waiting for ${GODOT_MODULE_NAME}`);
if (ENABLE_LIBC_BOUNDARY_HOOKS) {
log('libsec internal hooks disabled; observing only Godot registration + libc boundaries');
installLibcHooks();
} else {
log('libsec internal hooks disabled; libc boundary hooks disabled for crash isolation');
}
startLoaderHooks();
installModuleObserver();
});

globalThis.sec2026proc = {
status() {
const status = getStatus();
log(`status ${JSON.stringify(status)}`);
return status;
},
};
})();

用例:

1
frida -U -f com.tencent.ACE.gamesec2026.preliminary -l .\frida_trace_process_libc_sec2026.js -l frida_move_trigger_sec2026.js

进游戏后远程调用一次 sec2026.trigger() 移车就可以拿到关键的反汇编执行流了。

最后一个动态dump二进制的脚本,其实非常鸡肋,实在是懒的用静态工具去解:

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
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
/*
* Usage:
* frida -U -f com.tencent.ACE.gamesec2026.preliminary -l tools/frida_dump_sec2026.js --no-pause
*
* Dumps are written inside the app cache dir:
* /data/user/0/com.tencent.ACE.gamesec2026.preliminary/cache/
* or /data/data/com.tencent.ACE.gamesec2026.preliminary/cache/
*
* What it captures:
* 1. First-stage runtime mapping of the decompressed stage2 payload
* 2. Second-stage loader activity (mmap/mprotect/munmap)
* 3. All stage2-created mappings right before control transfers onward
* 4. The 0x10 executable trampoline if one is built
*/

(function () {
'use strict';

const PACKAGE_NAME = 'com.tencent.ACE.gamesec2026.preliminary';
const MODULE_NAME = 'libsec2026.so';
const GODOT_MODULE_NAME = 'libgodot_android.so';

const FIRST_STAGE = {
entry: 0x69870,
stage2MapResult: 0x69950,
stage2Branch: 0x6996c,
stage2Size: 0x17a0,
};

const STAGE2 = {
entryThunk: 0x10,
postMapReturn: 0x108,
sysMunmapAligned: 0x1c,
sysMprotectAligned: 0x38,
sysMmap: 0x74,
flushIcache: 0x80,
mapNextImage: 0x1450,
buildExecTrampoline: 0x16e4,
callbackBlr: 0x1778,
};

const CANDIDATES = {
extensionInitOffset: 0x56d50,
};

const STAGE3 = {
maxSliceDumpSize: 0x40000,
};

const GODOT_KEY_HOOK = {
enabled: true,
openAndParse: 0x3801410,
keySize: 32,
};

const GODOT_FILE_ACCESS = {
seek: 0x130,
getPosition: 0x140,
getLength: 0x148,
getBuffer: 0x198,
};

const GODOT_STREAM_CAPTURE = {
previewSize: 0x40,
sparsePckHeaderPrefix: 'e8a8f75bff6a5984acf5774657f14b536c17000000000000ece4afe803248e2a',
sparsePckHeaderShort: 'e8a8f75bff6a5984acf5774657f14b53',
};

const GODOT_FAE_FIELDS = {
plainLen: 0x180,
dataPtr: 0x190,
};

const MAX_DUMP_SIZE = 0x2000000;

let moduleBase = null;
let stage2Base = null;
let stage2HooksInstalled = false;
let stage2RuntimeDumped = false;
let dumpSeq = 0;
let sessionSeq = 0;
let dumpDir = null;
let logFilePath = null;
let moduleObserver = null;
let ioFns = null;
let disableExportHooks = true;
let godotKeyHookInstalled = false;
const dumpedGodotKeys = new Set();
const dumpedGodotOpenParseContexts = new Set();
const dumpedGodotSparseKeys = new Set();
const godotFileAccessCache = new Map();

const hooks = [];
const activeSessions = new Map();

function log(message) {
const line = `[sec2026] ${message}`;
console.log(line);

if (logFilePath !== null) {
try {
const f = new File(logFilePath, 'a');
f.write(`${line}\n`);
f.flush();
f.close();
} catch (_) {
}
}
}

function hex(value) {
return ptr(value).toString();
}

function toU32(value) {
return ptr(value).toUInt32();
}

function toS32(value) {
return ptr(value).toInt32();
}

function fileSafe(name) {
return name.replace(/[^0-9A-Za-z_.-]/g, '_');
}

function ptrInModule(address, module) {
const p = ptr(address);
const start = ptr(module.base);
const end = start.add(module.size);
return p.compare(start) >= 0 && p.compare(end) < 0;
}

function chooseDumpDir() {
return dumpDir !== null ? dumpDir : `/data/user/0/${PACKAGE_NAME}/cache`;
}

function writeBytes(path, bytes) {
const f = new File(path, 'wb');
f.write(bytes);
f.flush();
f.close();
}

function writeText(path, text) {
const f = new File(path, 'w');
f.write(text);
f.flush();
f.close();
}

function getIoFns() {
if (ioFns !== null) {
return ioFns;
}

const libc = Process.findModuleByName('libc.so');
if (libc === null) {
throw new Error('libc.so not found');
}

ioFns = {
open: new NativeFunction(libc.getExportByName('open'), 'int', ['pointer', 'int', 'int']),
write: new NativeFunction(libc.getExportByName('write'), 'int', ['int', 'pointer', 'int']),
close: new NativeFunction(libc.getExportByName('close'), 'int', ['int']),
};
return ioFns;
}

function writeMemoryRangeToFile(path, address, size) {
const { open, write, close } = getIoFns();
const pathBuf = Memory.allocUtf8String(path);
const O_WRONLY = 1;
const O_CREAT = 0x40;
const O_TRUNC = 0x200;
const fd = open(pathBuf, O_WRONLY | O_CREAT | O_TRUNC, 0o600);
if (fd < 0) {
throw new Error(`open failed for ${path}`);
}

try {
let offset = 0;
while (offset < size) {
const remaining = size - offset;
const chunk = remaining > 0x400000 ? 0x400000 : remaining;
const written = write(fd, ptr(address).add(offset), chunk);
if (written <= 0) {
throw new Error(`write failed at offset 0x${offset.toString(16)}`);
}
offset += written;
}
} finally {
close(fd);
}
}

function dumpMemory(name, address, size) {
const base = ptr(address);
const dumpPath = `${chooseDumpDir()}/${fileSafe(name)}`;
writeMemoryRangeToFile(dumpPath, base, size);
log(`dumped ${size} bytes from ${hex(base)} -> ${dumpPath}`);
return dumpPath;
}

function dumpIfReasonable(name, address, size) {
if (size === 0) {
log(`skip empty dump ${name}`);
return null;
}
if (size > MAX_DUMP_SIZE) {
log(`skip oversized dump ${name}: 0x${size.toString(16)}`);
return null;
}
try {
return dumpMemory(name, address, size);
} catch (e) {
log(`dump failed for ${name} at ${hex(address)} size=0x${size.toString(16)}: ${e}`);
return null;
}
}

function bytesToHexString(bytes) {
const parts = [];
for (let i = 0; i < bytes.length; i++) {
parts.push(bytes[i].toString(16).padStart(2, '0'));
}
return parts.join('');
}

function u64ToNumber(value) {
if (typeof value === 'number') {
return value;
}
return parseInt(value.toString(), 10);
}

function makeU64(value) {
if (typeof uint64 === 'function') {
return uint64(value);
}
return value;
}

function getGodotFileAccessOps(fileObj) {
const vtable = fileObj.readPointer();
if (vtable.isNull()) {
return null;
}

const cacheKey = vtable.toString();
if (godotFileAccessCache.has(cacheKey)) {
return godotFileAccessCache.get(cacheKey);
}

const ops = {
seek: new NativeFunction(vtable.add(GODOT_FILE_ACCESS.seek).readPointer(), 'void', ['pointer', 'uint64']),
getPosition: new NativeFunction(vtable.add(GODOT_FILE_ACCESS.getPosition).readPointer(), 'uint64', ['pointer']),
getLength: new NativeFunction(vtable.add(GODOT_FILE_ACCESS.getLength).readPointer(), 'uint64', ['pointer']),
getBuffer: new NativeFunction(vtable.add(GODOT_FILE_ACCESS.getBuffer).readPointer(), 'uint64', ['pointer', 'pointer', 'uint64']),
};
godotFileAccessCache.set(cacheKey, ops);
return ops;
}

function previewGodotBaseStream(args) {
const fileRef = ptr(args[1]);
if (fileRef.isNull()) {
return null;
}

const fileObj = fileRef.readPointer();
if (fileObj.isNull()) {
return null;
}

const ops = getGodotFileAccessOps(fileObj);
if (ops === null) {
return null;
}

const position = u64ToNumber(ops.getPosition(fileObj));
const length = u64ToNumber(ops.getLength(fileObj));
const remaining = Math.max(0, length - position);
const want = Math.min(GODOT_STREAM_CAPTURE.previewSize, remaining);
const bytes = [];

if (want > 0) {
const buffer = Memory.alloc(want);
try {
ops.seek(fileObj, makeU64(position));
const read = u64ToNumber(ops.getBuffer(fileObj, buffer, makeU64(want)));
for (let i = 0; i < read; i++) {
bytes.push(buffer.add(i).readU8());
}
} finally {
try {
ops.seek(fileObj, makeU64(position));
} catch (_) {
}
}
}

return {
fileObj,
position,
length,
previewHex: bytesToHexString(bytes),
previewSize: bytes.length,
};
}

function formatReturnAddress(address) {
try {
return DebugSymbol.fromAddress(address).toString();
} catch (e) {
return `symbol lookup failed: ${e}`;
}
}

function inspectGodotOpenAndParse(base, args, context) {
if (!GODOT_KEY_HOOK.enabled) {
return null;
}

try {
const keyVec = ptr(args[2]);
if (keyVec.isNull()) {
return null;
}

const keyPtr = keyVec.add(8).readPointer();
if (keyPtr.isNull()) {
return null;
}

const keyBytes = [];
for (let i = 0; i < GODOT_KEY_HOOK.keySize; i++) {
keyBytes.push(keyPtr.add(i).readU8());
}
const keyHex = bytesToHexString(keyBytes);
const stream = previewGodotBaseStream(args);

if (!dumpedGodotKeys.has(keyHex)) {
dumpedGodotKeys.add(keyHex);
log(`godot key seen = ${keyHex}`);
dumpIfReasonable(
`godot_script_encryption_key_${String(dumpedGodotKeys.size).padStart(2, '0')}.bin`,
keyPtr,
GODOT_KEY_HOOK.keySize
);
}

if (stream !== null) {
const contextKey = `${keyHex}|0x${stream.position.toString(16)}|${stream.previewHex.slice(0, 64)}`;
if (!dumpedGodotOpenParseContexts.has(contextKey)) {
dumpedGodotOpenParseContexts.add(contextKey);
log(
`godot open_and_parse key=${keyHex} pos=0x${stream.position.toString(16)} len=0x${stream.length.toString(16)} ` +
`head=${stream.previewHex.slice(0, 96)}`
);
}
}

const sparseMatch = stream !== null &&
(stream.previewHex.startsWith(GODOT_STREAM_CAPTURE.sparsePckHeaderPrefix) ||
stream.previewHex.startsWith(GODOT_STREAM_CAPTURE.sparsePckHeaderShort));

if (sparseMatch) {
const sparseKey = `${keyHex}|0x${stream.position.toString(16)}`;
if (!dumpedGodotSparseKeys.has(sparseKey)) {
dumpedGodotSparseKeys.add(sparseKey);
log(
`godot sparsepck candidate key=${keyHex} pos=0x${stream.position.toString(16)} len=0x${stream.length.toString(16)} ` +
`head=${stream.previewHex}`
);
log(`godot sparsepck caller: ${formatReturnAddress(context.lr)}`);
dumpIfReasonable(
`godot_sparsepck_key_${String(dumpedGodotSparseKeys.size).padStart(2, '0')}.bin`,
keyPtr,
GODOT_KEY_HOOK.keySize
);
}
}

return {
faeObj: ptr(args[0]),
keyHex,
keyPtr,
stream,
sparseMatch,
};
} catch (e) {
log(`godot script key capture failed: ${e}`);
return null;
}
}

function isSyscallError(retval) {
const value = ptr(retval);
return value.compare(ptr('0xfffffffffffff000')) >= 0;
}

function recordSessionLog(session, line) {
session.logs.push(line);
log(line);
}

function ensureSessionDumpState(session) {
if (session.dumpedMapKeys === undefined) {
session.dumpedMapKeys = {};
}
}

function mapRecordKey(record) {
return `${record.result}_${record.length}_${record.prot}_${record.flags}_${record.fd}`;
}

function dumpMapRecord(session, record, reason) {
if (!record.success) {
return null;
}

ensureSessionDumpState(session);
const key = mapRecordKey(record);
if (session.dumpedMapKeys[key] === true) {
return null;
}

session.dumpedMapKeys[key] = true;
const name = `sec2026_session_${String(session.id).padStart(2, '0')}_${reason}_${hex(record.result)}_size_0x${record.length.toString(16)}.bin`;
return dumpIfReasonable(name, record.result, record.length);
}

function maybeDumpInterestingMap(session, record, reason) {
if (!record.success) {
return null;
}

const isLarge = record.length >= 0x10000;
const isFileBacked = record.fd >= 0;
const isExecutable = (record.prot & 0x4) !== 0;
if (!(isLarge || isFileBacked || isExecutable)) {
return null;
}

return dumpMapRecord(session, record, reason);
}

function findMapContaining(session, address) {
const target = ptr(address);
for (const record of session.mmaps) {
if (!record.success) {
continue;
}
const start = ptr(record.result);
const end = start.add(record.length);
if (target.compare(start) >= 0 && target.compare(end) < 0) {
return record;
}
}
return null;
}

function findLargestMap(session) {
let best = null;
for (const record of session.mmaps) {
if (!record.success) {
continue;
}
if (best === null || record.length > best.length) {
best = record;
}
}
return best;
}

function rangeContains(record, address) {
const p = ptr(address);
const start = ptr(record.result);
const end = start.add(record.length);
return p.compare(start) >= 0 && p.compare(end) < 0;
}

function ptrToNumber(value) {
return parseInt(ptr(value).toString(), 16);
}

function findMapOverlapping(session, address, size) {
const start = ptrToNumber(address);
const end = start + size;

for (const record of session.mmaps) {
if (!record.success) {
continue;
}

const recordStart = ptrToNumber(record.result);
const recordEnd = recordStart + record.length;
if (start < recordEnd && recordStart < end) {
return record;
}
}

return null;
}

function computeRecordOverlap(record, address, size) {
const recordStart = ptrToNumber(record.result);
const recordEnd = recordStart + record.length;
const start = ptrToNumber(address);
const end = start + size;
const overlapStart = Math.max(recordStart, start);
const overlapEnd = Math.min(recordEnd, end);

if (overlapEnd <= overlapStart) {
return null;
}

return {
start: ptr(`0x${overlapStart.toString(16)}`),
size: overlapEnd - overlapStart,
offset: overlapStart - recordStart,
};
}

function dumpOverlappingSlice(session, record, address, size, reason) {
const overlap = computeRecordOverlap(record, address, size);
if (overlap === null) {
return null;
}

const clampedSize = Math.min(overlap.size, STAGE3.maxSliceDumpSize);
return dumpIfReasonable(
`sec2026_session_${String(session.id).padStart(2, '0')}_${reason}_off_0x${overlap.offset.toString(16)}_${hex(overlap.start)}_size_0x${clampedSize.toString(16)}.bin`,
overlap.start,
clampedSize
);
}

function maybeDumpExecTransition(session, record, address, size, reason) {
if (record === null) {
return null;
}

const name = `${reason}_${String(++dumpSeq).padStart(2, '0')}`;
return dumpOverlappingSlice(session, record, address, size, name);
}

function installHook(address, callbacks) {
try {
hooks.push(Interceptor.attach(address, callbacks));
return true;
} catch (e) {
log(`failed to hook ${hex(address)}: ${e}`);
return false;
}
}

function dumpSessionArtifacts(session) {
const baseName = `sec2026_session_${String(session.id).padStart(2, '0')}`;
const meta = {
id: session.id,
threadId: session.threadId,
stage2Base: hex(session.stage2Base),
headerArg: hex(session.headerArg),
godotArg: hex(session.godotArg),
scratchArg: hex(session.scratchArg),
retval: session.retval ? hex(session.retval) : null,
mmaps: session.mmaps,
mprotects: session.mprotects,
munmaps: session.munmaps,
callbackTargets: session.callbackTargets,
logs: session.logs,
};

for (let i = 0; i < session.mmaps.length; i += 1) {
const map = session.mmaps[i];
if (!map.success) {
continue;
}
const name = `${baseName}_map_${String(i).padStart(2, '0')}_${hex(map.result)}_size_0x${map.length.toString(16)}.bin`;
dumpIfReasonable(name, map.result, map.length);
}

if (session.retval && !session.retval.isNull()) {
const trampName = `${baseName}_ret_trampoline_${hex(session.retval)}.bin`;
dumpIfReasonable(trampName, session.retval, 0x10);
}

const metaPath = `${chooseDumpDir()}/${baseName}.json`;
writeText(metaPath, JSON.stringify(meta, null, 2));
log(`wrote metadata -> ${metaPath}`);
}

function installStage2Hooks(base) {
if (stage2HooksInstalled) {
return;
}

stage2HooksInstalled = true;
log(`installing stage2 hooks at ${hex(base)}`);

installHook(base.add(STAGE2.mapNextImage), {
onEnter(args) {
const session = {
id: ++sessionSeq,
threadId: this.threadId,
stage2Base: base,
headerArg: ptr(args[0]),
godotArg: ptr(args[1]),
scratchArg: ptr(args[2]),
mmaps: [],
mprotects: [],
munmaps: [],
callbackTargets: [],
stage3Record: null,
stage3RunCount: 0,
logs: [],
retval: null,
};
activeSessions.set(this.threadId, session);
this.session = session;
recordSessionLog(
session,
`stage2_map_next_image enter tid=${this.threadId} arg0=${hex(args[0])} arg1=${hex(args[1])} arg2=${hex(args[2])}`
);
},
onLeave(retval) {
const session = this.session || activeSessions.get(this.threadId);
if (!session) {
return;
}
session.retval = ptr(retval);
recordSessionLog(session, `stage2_map_next_image leave retval=${hex(retval)}`);
try {
dumpSessionArtifacts(session);
} catch (e) {
log(`session ${session.id} dump failed: ${e}`);
}
activeSessions.delete(this.threadId);
},
});

installHook(base.add(STAGE2.sysMmap), {
onEnter() {
this.argsRecord = {
threadId: this.threadId,
addr: hex(this.context.x0),
length: toU32(this.context.x1),
prot: toU32(this.context.x2),
flags: toU32(this.context.x3),
fd: toS32(this.context.x4),
offset: toU32(this.context.x5),
};
},
onLeave(retval) {
const session = activeSessions.get(this.threadId);
if (!session) {
return;
}
const record = Object.assign({}, this.argsRecord, {
result: hex(retval),
success: !isSyscallError(retval),
});
session.mmaps.push(record);
recordSessionLog(
session,
`sys_mmap len=0x${record.length.toString(16)} prot=0x${record.prot.toString(16)} flags=0x${record.flags.toString(16)} fd=${record.fd} -> ${record.result}`
);
if (record.length >= 0x10000 && record.fd >= 0) {
dumpMapRecord(session, record, 'file_backed_map');
} else if ((record.prot & 0x4) !== 0 && record.length >= 0x1000) {
dumpMapRecord(session, record, 'exec_map');
} else {
maybeDumpInterestingMap(session, record, 'mmap');
}
},
});

installHook(base.add(STAGE2.sysMprotectAligned), {
onEnter() {
const session = activeSessions.get(this.threadId);
if (!session) {
return;
}
const record = {
addr: hex(this.context.x0),
length: toU32(this.context.x1),
prot: toU32(this.context.x2),
};
session.mprotects.push(record);
recordSessionLog(
session,
`sys_mprotect_aligned addr=${record.addr} len=0x${record.length.toString(16)} prot=0x${record.prot.toString(16)}`
);

const overlapping = findMapOverlapping(session, this.context.x0, record.length);
if (overlapping !== null && (record.prot & 0x4) !== 0) {
maybeDumpExecTransition(session, overlapping, this.context.x0, record.length, 'stage_exec_mprotect');
}
},
});

installHook(base.add(STAGE2.flushIcache), {
onEnter() {
const session = activeSessions.get(this.threadId);
if (!session) {
return;
}

const addr = ptr(this.context.x0);
const length = toU32(this.context.x1);
recordSessionLog(session, `flush_icache addr=${hex(addr)} len=0x${length.toString(16)}`);

const overlapping = findMapOverlapping(session, addr, length);
if (overlapping !== null) {
maybeDumpExecTransition(session, overlapping, addr, length, 'stage_exec_flush');
}
},
});

installHook(base.add(STAGE2.sysMunmapAligned), {
onEnter() {
const session = activeSessions.get(this.threadId);
if (!session) {
return;
}
const record = {
addr: hex(this.context.x0),
length: toU32(this.context.x1),
};
session.munmaps.push(record);
recordSessionLog(
session,
`sys_munmap_aligned addr=${record.addr} len=0x${record.length.toString(16)}`
);
},
});

installHook(base.add(STAGE2.buildExecTrampoline), {
onLeave(retval) {
if (isSyscallError(retval) || ptr(retval).isNull()) {
return;
}
dumpIfReasonable(`sec2026_trampoline_${String(++dumpSeq).padStart(2, '0')}_${hex(retval)}.bin`, retval, 0x10);
},
});

installHook(base.add(STAGE2.postMapReturn), {
onEnter() {
const session = activeSessions.get(this.threadId);
const line = `stage2 post-map return x0=${hex(this.context.x0)} x2=${hex(this.context.x2)}`;
if (session) {
recordSessionLog(session, line);
const largest = findLargestMap(session);
if (largest !== null) {
dumpMapRecord(session, largest, 'post_map_largest');
}
} else {
log(line);
}
},
});

installHook(base.add(STAGE2.callbackBlr), {
onEnter() {
const session = activeSessions.get(this.threadId);
const target = hex(this.context.x3);
const line = `stage2 callback target=${target} x0=${hex(this.context.x0)} x1=${hex(this.context.x1)} x2=${hex(this.context.x2)}`;
if (session) {
session.callbackTargets.push({
target,
x0: hex(this.context.x0),
x1: hex(this.context.x1),
x2: hex(this.context.x2),
});
recordSessionLog(session, line);
const containing = findMapContaining(session, this.context.x3);
if (containing !== null) {
session.stage3Record = containing;
dumpMapRecord(session, containing, 'callback_target_map');
}
} else {
log(line);
}
},
});
}

function installFirstStageHooks(base) {
if (moduleBase !== null) {
return;
}

moduleBase = base;
log(`hooking ${MODULE_NAME} at ${hex(base)}`);

installHook(base.add(FIRST_STAGE.entry), {
onEnter() {
log(`first-stage entry hit x0=${hex(this.context.x0)} x1=${hex(this.context.x1)} x2=${hex(this.context.x2)}`);
},
});

installHook(base.add(FIRST_STAGE.stage2MapResult), {
onEnter() {
const mapped = ptr(this.context.x0);
if (mapped.isNull()) {
return;
}
if (stage2Base !== null && stage2Base.equals(mapped)) {
return;
}
stage2Base = mapped;
log(`stage2 runtime base = ${hex(stage2Base)}`);

if (!stage2RuntimeDumped) {
stage2RuntimeDumped = true;
dumpIfReasonable(
`sec2026_stage2_runtime_${String(++dumpSeq).padStart(2, '0')}_${hex(stage2Base)}.bin`,
stage2Base,
FIRST_STAGE.stage2Size
);
}

installStage2Hooks(stage2Base);
},
});

installHook(base.add(FIRST_STAGE.stage2Branch), {
onEnter() {
log(`first-stage branch x14=${hex(this.context.x14)}`);
},
});
}

function installGodotKeyHook(base) {
if (!GODOT_KEY_HOOK.enabled || godotKeyHookInstalled) {
return;
}

const target = base.add(GODOT_KEY_HOOK.openAndParse);
const ok = installHook(target, {
onEnter(args) {
this.godotOpenParseInfo = inspectGodotOpenAndParse(base, args, this.context);
},
onLeave(retval) {
const info = this.godotOpenParseInfo;
if (!info || !info.sparseMatch) {
return;
}

log(
`godot sparsepck open_and_parse retval=${toS32(retval)} key=${info.keyHex} ` +
`pos=0x${info.stream.position.toString(16)}`
);

if (toS32(retval) !== 0) {
return;
}

try {
const plainLen = u64ToNumber(info.faeObj.add(GODOT_FAE_FIELDS.plainLen).readU64());
const dataPtr = info.faeObj.add(GODOT_FAE_FIELDS.dataPtr).readPointer();
if (dataPtr.isNull() || plainLen <= 0) {
log(`godot sparsepck success but buffer unavailable plainLen=0x${plainLen.toString(16)} dataPtr=${hex(dataPtr)}`);
return;
}

dumpIfReasonable(
`godot_sparsepck_plain_${String(dumpedGodotSparseKeys.size).padStart(2, '0')}_${hex(dataPtr)}.bin`,
dataPtr,
plainLen
);
} catch (e) {
log(`godot sparsepck plain dump failed: ${e}`);
}
},
});

if (ok) {
godotKeyHookInstalled = true;
log(`hooking ${GODOT_MODULE_NAME} open_and_parse candidate at ${hex(target)}`);
}
}

function findModuleBase(name) {
if (typeof Process.findModuleByName === 'function') {
const mod = Process.findModuleByName(name);
if (mod !== null) {
return mod.base;
}
}

if (typeof Module.findBaseAddress === 'function') {
return Module.findBaseAddress(name);
}

if (typeof Process.getModuleByName === 'function') {
try {
return Process.getModuleByName(name).base;
} catch (_) {
return null;
}
}

return null;
}

function installExportHooks(module) {
if (disableExportHooks) {
log('export hooks disabled for stability');
return;
}

if (typeof module.enumerateExports !== 'function') {
log('module.enumerateExports() unavailable; skipping export hooks');
return;
}

let exports;
try {
exports = module.enumerateExports();
} catch (e) {
log(`enumerateExports failed: ${e}`);
return;
}

const interesting = exports.filter((entry) => {
const name = (entry.name || '').toLowerCase();
return name.includes('init') || name.includes('flag') || name.includes('godot');
});

if (interesting.length === 0) {
log('no interesting exports found');
return;
}

for (const entry of interesting) {
log(`export ${entry.name} @ ${entry.address}`);
}

const extensionInit = interesting.find((entry) => entry.name === 'extension_init')
|| interesting.find((entry) => entry.name.toLowerCase().includes('init'));

if (!extensionInit) {
return;
}

log(`hooking export ${extensionInit.name} at ${extensionInit.address}`);
installHook(ptr(extensionInit.address), {
onEnter(args) {
this.module = module;
this.threadId = Process.getCurrentThreadId();
this.calls = {};
log(
`export ${extensionInit.name} hit tid=${this.threadId} x0=${hex(args[0])} x1=${hex(args[1])} x2=${hex(args[2])} x3=${hex(args[3])}`
);

try {
Stalker.follow(this.threadId, {
events: {
call: true,
},
onCallSummary: (summary) => {
Object.keys(summary).forEach((key) => {
this.calls[key] = (this.calls[key] || 0) + summary[key];
});
},
});
} catch (e) {
log(`Stalker.follow failed: ${e}`);
}
},
onLeave(retval) {
try {
Stalker.unfollow(this.threadId);
Stalker.garbageCollect();
} catch (_) {
}

const entries = Object.entries(this.calls)
.map(([address, count]) => ({ address: ptr(address), count }))
.filter((item) => ptrInModule(item.address, this.module))
.sort((a, b) => b.count - a.count)
.slice(0, 40);

if (entries.length === 0) {
log(`export ${extensionInit.name} leave retval=${hex(retval)} with no in-module call summary`);
return;
}

const lines = entries.map((item) => `${hex(item.address)} count=${item.count}`);
log(`export ${extensionInit.name} leave retval=${hex(retval)} in-module calls:\n${lines.join('\n')}`);
},
});
}

function installSymbolHooks(module) {
if (disableExportHooks) {
log('symbol hooks disabled for stability');
return;
}

if (typeof module.enumerateSymbols !== 'function') {
log('module.enumerateSymbols() unavailable; skipping symbol hooks');
return;
}

let symbols;
try {
symbols = module.enumerateSymbols();
} catch (e) {
log(`enumerateSymbols failed: ${e}`);
return;
}

const interesting = symbols.filter((entry) => {
const name = (entry.name || '').toLowerCase();
return name.includes('extension_init') || name.includes('init') || name.includes('flag');
}).slice(0, 20);

for (const entry of interesting) {
log(`symbol ${entry.name} @ ${entry.address}`);
}
}

function installOffsetProbe(base, offset, name) {
if (disableExportHooks) {
log(`${name} probe disabled for stability`);
return;
}

try {
const ok = installHook(base.add(offset), {
onEnter(args) {
log(`${name} probe hit @ ${hex(base.add(offset))} x0=${hex(args[0])} x1=${hex(args[1])} x2=${hex(args[2])} x3=${hex(args[3])}`);
},
});
if (ok) {
log(`installed ${name} probe at ${hex(base.add(offset))}`);
} else {
log(`did not install ${name} probe at ${hex(base.add(offset))}`);
}
} catch (e) {
log(`failed to install ${name} probe at +0x${offset.toString(16)}: ${e}`);
}
}

function startModuleObserver() {
if (typeof Process.attachModuleObserver !== 'function') {
log('Process.attachModuleObserver unavailable; falling back to polling');
const timer = setInterval(() => {
let ready = true;

const base = findModuleBase(MODULE_NAME);
if (base !== null) {
installFirstStageHooks(base);
} else {
ready = false;
}

const godotBase = findModuleBase(GODOT_MODULE_NAME);
if (GODOT_KEY_HOOK.enabled && godotBase !== null) {
installGodotKeyHook(godotBase);
} else if (GODOT_KEY_HOOK.enabled) {
ready = false;
}

if (ready) {
clearInterval(timer);
}
}, 100);
return;
}

moduleObserver = Process.attachModuleObserver({
onAdded(module) {
if (module.name === GODOT_MODULE_NAME) {
installGodotKeyHook(module.base);
}

if (module.name !== MODULE_NAME) {
return;
}

log(`module observer saw ${module.name} base=${module.base} size=0x${module.size.toString(16)}`);
installFirstStageHooks(module.base);
installExportHooks(module);
installSymbolHooks(module);
installOffsetProbe(module.base, CANDIDATES.extensionInitOffset, 'extension_init_candidate');
},
onRemoved(module) {
if (module.name === MODULE_NAME) {
log(`module removed ${module.name}`);
}
},
});
}

setImmediate(() => {
dumpDir = chooseDumpDir();
logFilePath = `${chooseDumpDir()}/sec2026_trace.log`;
log(`dump dir = ${chooseDumpDir()}`);
log(`log file = ${logFilePath}`);
log('waiting for libsec2026.so');
if (GODOT_KEY_HOOK.enabled) {
log(`waiting for ${GODOT_MODULE_NAME} key hook`);
}
startModuleObserver();
});
})();