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

image-20260402151658599

image-20260402151725529

image-20260402151809356

dump

dump so我使用的是gc修改器可以dump

具体操作参考https://blog.csdn.net/qq_49619863/article/details/131155604

当然用frida也行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var target_so = "libil2cpp.so";
var path = "/data/data/com.com.sec2023.rocketmouse.mouse/";

function dump_so(so_name) {
var libso = Process.getModuleByName(so_name);
console.log("[name]:", libso.name);
console.log("[base]:", libso.base);
console.log("[size]:", ptr(libso.size));
console.log("[path]:", libso.path);
var file_path = path + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) {
Memory.protect(ptr(libso.base), libso.size, 'r--');
var libso_buffer = ptr(libso.base).readByteArray(libso.size);
file_handle.write(libso_buffer);
file_handle.flush();
file_handle.close();
console.log("[dump]:", file_path);
}
}

// dump_so(target_so);

il2cpp.so明显混淆加密

image-20260402163814957

需要dumpso 文件,使用常规方法il2cppdumper 无法成功

image-20260402164513300

1
7d23c25000-7d24ff1000

方案一: sofixer (手修elf)

用sofixer还是不能直接用il2cppdumper

问题1:运行时重定位未还原

ELF 动态链接器加载 SO 时,会根据 .rela.dyn 中的重定位表修改数据段中的指针。dump 出来的文件保留的是加载后的绝对地址。SoFixer 本应修复这个问题,但没有生效。

修复遍历 .rela.dyn(108,725 条)和 .rela.plt(512 条)中所有 R_AARCH64_RELATIVE 类型的重定位条目,对每个目标地址的值减去基地址 0x7d23c25000

1
2
3
val = read(r_offset)
if val >= BASE_ADDR:
write(r_offset, val - BASE_ADDR)

共修复了 108865个指针

问题2:Section Headers 损坏

SoFixer 重建的 section headers 质量很差:

  • .rela.plt 类型标记为 REL 而不是 RELA
  • section 名字合并成了 .text&ARM.extab
  • 缺少 .rodata、.got、.data.rel.ro、il2cpp 等关键 section

Il2CppDumper 需要正确的section信息来定位数据。

修复:从原始 libil2cpp.so 中移植完整的 27 个 section headers,并调整 sh_offset

原始文件: sh_offset = VA - 0x1000 (因为 LOAD 段的 p_offset ≠ p_vaddr)

内存dump: sh_offset = VA (因为 dump 中 文件偏移 == 虚拟地址),所以对每个带 SHF_ALLOC 标志的 section,设置 sh_offset = sh_addr。

问题 3:Il2CppType.data 被运行时替换(CLASS/VALUETYPE)

这是最核心的问题。IL2CPP 引擎在初始化时,会通过代码(不是 RELA 重定位)把每个 Il2CppType 结构体的 data 字段从编译期的整数索引替换为运行时的堆指针:

1
2
3
4
5
6
7
struct Il2CppType {
union {
TypeDefinitionIndex klassIndex; // 编译期: 小整数, 如 0x7fc
Il2CppClass* klass; // 运行时: 堆指针, 如 0x7d04b64d28
} data;
uint32_t attrs:16, type:8, ...;
};

Il2CppDumper需要data 是编译期的索引值,但 dump 出来的是运行时指针,导致数组越界崩溃。

问题 4:不只是 CLASS/VALUETYPE

分析发现几乎所有类型的 data 都被替换了

image-20260402204100779

仅从 metadata 反推只能修复 CLASS/VALUETYPE,无法覆盖全部。

最终方案从原始 SO 直接复制

Il2CppType 结构体位于 .data 段(VA 0x11020000x119A370),不在被加密的 il2cpp段(VA0x4634740xC4E42C)中。

这意味着原始 libil2cpp.so 中的 Il2CppType.data 字段是明文可读的正确值

原始文件中 VA 0x1139780 的 Il2CppType:data = 0x82 (genericParameterIndex, 正确)

1
2
3
4
type = 0x1e (MVAR)
dump 文件中同一位置:
data = 0x7d04b05498 (运行时堆指针, 错误!)
type = 0x1e (MVAR)

只需做一个地址转换(原始文件 file_offset = VA - 0x1000),就能从原始 SO 读取所有 10,472 个类型条目的正确 data 值,覆盖写入 dump 文件。共恢复了 6,980 个被篡改的字段。

中和 RELA 条目

Il2CppDumper 启动时会 Applying relocations——读取 RELA 表并重新应用。其中 3,492 条 R_AARCH64_RELATIVE 的目标地址恰好是 Il2CppType.data 字段,会把我们恢复的值再次覆盖为 RELA 中的 addend(一个 VA 指针)。

修复将这 3,492 条 RELA 的 r_info 设为 0(R_AARCH64_NONE),使其无效化。

总结流程

1
2
3
4
5
6
7
8
9
10
11
12
内存 dump (.bin)

├─ SoFixer → il2cpp.so (headers 修复不完整)

└─ fix_final.py 四步修复:
① 重定位还原: 108,865 个指针 - BASE_ADDR
② Il2CppType.data 恢复: 从原始 SO 复制 6,980 个字段
③ RELA 中和: 禁用 3,492 条会覆盖类型数据的重定位
④ Section headers 移植: 从原始 SO 复制 27 个正确的 section


il2cpp_final.so ──→ Il2CppDumper 成功

恢复代码:

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
import struct

DUMP_SO = "il2cpp.so" # SoFixer output
ORIG_SO = "libil2cpp.so" # Original (il2cpp section encrypted, but .data is not)
META_FILE = "global-metadata.dat"
OUTPUT_FILE = "il2cpp_final.so"
BASE_ADDR = 0x7d23c25000

R_AARCH64_RELATIVE = 0x403
R_AARCH64_GLOB_DAT = 0x401
R_AARCH64_JUMP_SLOT = 0x402
R_AARCH64_ABS64 = 0x101

def main():
print("[*] Reading files...")
with open(DUMP_SO, 'rb') as f:
so = bytearray(f.read())
with open(ORIG_SO, 'rb') as f:
orig = f.read()

so_size = len(so)
orig_size = len(orig)

def u64(d, off):
return struct.unpack_from('<Q', d, off)[0]
def w64(d, off, v):
struct.pack_into('<Q', d, off, v)
def u32(d, off):
return struct.unpack_from('<I', d, off)[0]

# Build VA -> file offset mapping for original SO
orig_phoff = u64(orig, 32)
orig_phentsize = struct.unpack_from('<H', orig, 54)[0]
orig_phnum = struct.unpack_from('<H', orig, 56)[0]

orig_segments = []
for i in range(orig_phnum):
ph = orig_phoff + i * orig_phentsize
p_type = u32(orig, ph)
p_offset = u64(orig, ph + 8)
p_vaddr = u64(orig, ph + 16)
p_filesz = u64(orig, ph + 32)
if p_type == 1 and p_filesz > 0: # PT_LOAD
orig_segments.append((p_vaddr, p_offset, p_filesz))
print(f" LOAD: VA 0x{p_vaddr:x}, offset 0x{p_offset:x}, filesz 0x{p_filesz:x}, delta=0x{p_vaddr - p_offset:x}")

def va_to_orig_offset(va):
for seg_va, seg_off, seg_filesz in orig_segments:
if seg_va <= va < seg_va + seg_filesz:
return va - seg_va + seg_off
return None

# ============= STEP 1: Fix relocations =============
print("\n[*] Step 1: Fixing relocations...")
dyn_offset = 0x10e0290
rela_offset = rela_size = jmprel_offset = pltrelsz = None
i = 0
while True:
tag = u64(so, dyn_offset + i * 16)
val = u64(so, dyn_offset + i * 16 + 8)
if tag == 0: break
if tag == 7: rela_offset = val
elif tag == 8: rela_size = val
elif tag == 23: jmprel_offset = val
elif tag == 2: pltrelsz = val
i += 1

fixed_relocs = 0
def fix_rela_section(offset, size):
nonlocal fixed_relocs
for j in range(size // 24):
ent = offset + j * 24
if ent + 24 > so_size: break
r_offset = u64(so, ent)
r_info = u64(so, ent + 8)
r_type = r_info & 0xffffffff
if r_type in (R_AARCH64_RELATIVE, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT, R_AARCH64_ABS64):
if r_offset < so_size:
val = u64(so, r_offset)
if val >= BASE_ADDR and val < BASE_ADDR + so_size:
w64(so, r_offset, val - BASE_ADDR)
fixed_relocs += 1

fix_rela_section(rela_offset, rela_size)
fix_rela_section(jmprel_offset, pltrelsz)

# Fix init/fini arrays
for arr_off, arr_size in [(0x13cb6b0, 0xc8), (0x10624a8, 0x10)]:
for j in range(0, arr_size, 8):
off = arr_off + j
if off + 8 <= so_size:
val = u64(so, off)
if val >= BASE_ADDR and val < BASE_ADDR + so_size:
w64(so, off, val - BASE_ADDR)
fixed_relocs += 1

print(f" Fixed {fixed_relocs} relocated pointers")

# ============= STEP 2: Restore ALL Il2CppType.data from original =============
print("\n[*] Step 2: Restoring Il2CppType.data from original SO...")

types_ptr = 0x10ba9f8
types_count = 10472

restored = 0
failed = 0

for i in range(types_count):
type_va = u64(so, types_ptr + i * 8)
if type_va == 0 or type_va >= so_size - 12:
continue

# Find original data for this Il2CppType
orig_off = va_to_orig_offset(type_va)
if orig_off is None or orig_off + 12 > orig_size:
failed += 1
continue

# Read original data field (first 8 bytes of Il2CppType)
orig_data = u64(orig, orig_off)
curr_data = u64(so, type_va)

# Also verify type enum matches
orig_type_enum = orig[orig_off + 10]
dump_type_enum = so[type_va + 10]

if orig_type_enum != dump_type_enum:
# Type enum mismatch - something is wrong, skip
failed += 1
if failed <= 5:
print(f" [!] types[{i}] @ VA 0x{type_va:x}: type mismatch orig=0x{orig_type_enum:02x} dump=0x{dump_type_enum:02x}")
continue

if curr_data != orig_data:
w64(so, type_va, orig_data)
restored += 1

print(f" Restored {restored} Il2CppType.data fields")
if failed > 0:
print(f" Failed/skipped: {failed}")

# ============= STEP 3: Neutralize RELA targeting Il2CppType.data =============
print("\n[*] Step 3: Neutralizing RELA entries targeting Il2CppType.data...")

type_addrs = set()
for i in range(types_count):
type_va = u64(so, types_ptr + i * 8)
if type_va > 0 and type_va < so_size - 12:
type_addrs.add(type_va)

neutralized = 0
for j in range(rela_size // 24):
ent = rela_offset + j * 24
if ent + 24 > so_size: break
r_offset = u64(so, ent)
r_info = u64(so, ent + 8)
r_type = r_info & 0xffffffff
if r_type == R_AARCH64_RELATIVE and r_offset in type_addrs:
w64(so, ent, 0)
w64(so, ent + 8, 0)
w64(so, ent + 16, 0)
neutralized += 1

print(f" Neutralized {neutralized} RELA entries")

# ============= STEP 4: Transplant section headers =============
print("\n[*] Step 4: Transplanting section headers...")

orig_shoff = u64(orig, 40)
orig_shentsize = struct.unpack_from('<H', orig, 58)[0]
orig_shnum = struct.unpack_from('<H', orig, 60)[0]
orig_shstrndx = struct.unpack_from('<H', orig, 62)[0]

orig_sections = []
for j in range(orig_shnum):
sh = bytearray(orig[orig_shoff + j * orig_shentsize : orig_shoff + (j+1) * orig_shentsize])
orig_sections.append(sh)

shstrtab_sh = orig_sections[orig_shstrndx]
shstrtab_offset = u64(shstrtab_sh, 24)
shstrtab_size = u64(shstrtab_sh, 32)
shstrtab_data = orig[shstrtab_offset : shstrtab_offset + shstrtab_size]

for j in range(orig_shnum):
sh = orig_sections[j]
sh_type = u32(sh, 4)
sh_flags = u64(sh, 8)
sh_addr = u64(sh, 16)
if sh_type != 0 and (sh_flags & 0x2):
w64(sh, 24, sh_addr)

while len(so) % 8 != 0:
so.append(0)
new_shstrtab_off = len(so)
so.extend(shstrtab_data)
w64(orig_sections[orig_shstrndx], 24, new_shstrtab_off)

for j in range(orig_shnum):
sh = orig_sections[j]
sh_type = u32(sh, 4)
sh_flags = u64(sh, 8)
if sh_type == 1 and not (sh_flags & 0x2):
old_off = struct.unpack_from('<Q', orig, orig_shoff + j * orig_shentsize + 24)[0]
old_sz = struct.unpack_from('<Q', orig, orig_shoff + j * orig_shentsize + 32)[0]
if old_off + old_sz <= orig_size:
while len(so) % 8 != 0:
so.append(0)
new_off = len(so)
so.extend(orig[old_off : old_off + old_sz])
w64(orig_sections[j], 24, new_off)

while len(so) % 16 != 0:
so.append(0)
new_shoff = len(so)
for sh in orig_sections:
so.extend(sh)

struct.pack_into('<Q', so, 40, new_shoff)
struct.pack_into('<H', so, 58, orig_shentsize)
struct.pack_into('<H', so, 60, orig_shnum)
struct.pack_into('<H', so, 62, orig_shstrndx)

# ============= DONE =============
with open(OUTPUT_FILE, 'wb') as f:
f.write(so)

# Verification
print(f"\n[*] Verification:")
type_names = {
0x11: 'VALUETYPE', 0x12: 'CLASS', 0x13: 'VAR', 0x1e: 'MVAR',
0x0f: 'PTR', 0x1d: 'SZARRAY', 0x15: 'GENERICINST',
}
bad_total = 0
for i in range(types_count):
tp = u64(so, types_ptr + i * 8)
if tp == 0 or tp >= len(so) - 12: continue
te = so[tp + 10]
data = u64(so, tp)

if te in (0x11, 0x12): # CLASS/VALUETYPE - should be small index
if data >= 3555:
bad_total += 1
elif te in (0x13, 0x1e): # VAR/MVAR - should be small index
if data >= 10000:
bad_total += 1
elif te in (0x0f, 0x1d): # PTR/SZARRAY - should be valid pointer
if data >= so_size or data == 0:
bad_total += 1

print(f" Bad type entries remaining: {bad_total}")
print(f"\n[*] Written to {OUTPUT_FILE}")
print(f"[*] Run: Il2CppDumper.exe {OUTPUT_FILE} {META_FILE}")

if __name__ == '__main__':
main()

image-20260402213033368

  1. 手动还原重定位——遍历 10 万条 RELA,逐个减基地址
  2. 手动恢复 Il2CppType.data——运行时被引擎替换成堆指针,从原始 SO 逆向复原 6980 个字段
  3. 手动中和 RELA 条目——防止 Il2CppDumper 重复应用重定位覆盖修复
  4. 手动移植 section headers——从原始 SO 搬 27 个 section header 并修正偏移

其实还真有大神纯手修elf

[原创]2023腾讯游戏安全竞赛初赛题解(安卓)

方案二: 对比分析

其实方案一的最后也意识到这个问题了,也做了类似的处理

因此用010editor对apk中的so和dump出来的so进行比对,补上尾部的重定位表

[原创] 腾讯游戏安全技术竞赛2023 安卓客户端初赛WriteUp-Android安全-看雪安全社区|专业技术交流与安全研究论坛

image-20260402221003012

直接把主要difference这段复制到libil2cpp.so(原来的)

image-20260402223328625

打开可以识别了

后面还有一些difference不用管,因为 IDA 和 Il2CppDumper 一样,加载 ELF 时会自动应用重定位表。

原始文件里的数据段存的是编译期的原始值,同时 .rela.dyn 里记录了地址 0x10624b8 处,需要加上加载基地址 (R_AARCH64_RELATIVE)

  • 动态链接器(安卓运行时)读 RELA,把值改成基地址 + 偏移 ,dump 出来看到的样子
  • IDA 加载时也读 RELA,自己完成重定位 ,显示正确的引用
  • Il2CppDumper 也一样

所以原始文件里数据段的看起来不对是正常的,是 ELF 共享库的标准工作方式。

方案三: 根本不用fix

腾讯2023SecAndroid初赛题目 | LLeaves Blog

邮电小误导了

绕了好久其实只用对dump出来的文件直接给输入基址他就自己会找CodeRegistration和MetadataRegistration

image-20260403204031857

CodeRegistration存储所有编译后的 C++ 代码的注册信息:

1
2
3
4
5
6
7
8
9
10
11
typedef struct Il2CppCodeRegistration {
uint32_t methodPointersCount;
Il2CppMethodPointer* methodPointers; // 每个 C# 方法对应的函数指针
uint32_t reversePInvokeWrapperCount;
Il2CppMethodPointer* reversePInvokeWrappers; // P/Invoke 反向调用包装
uint32_t genericMethodPointersCount;
Il2CppMethodPointer* genericMethodPointers; // 泛型方法的函数指针
uint32_t invokerPointersCount;
Il2CppMethodPointer* invokerPointers; // 方法调用器
// ...
}

Il2CppCodeRegistration;

简单说:C# 方法名 → 对应的 native 函数地址 的映射表。

1
2
3
4
5
6
7
8
9
10
11
12
13
MetadataRegistration
存储所有类型系统元数据的注册信息:
typedef struct Il2CppMetadataRegistration {
int32_t genericClassesCount;
Il2CppGenericClass** genericClasses; // 泛型类实例
int32_t genericInstsCount;
Il2CppGenericInst** genericInsts; // 泛型实例化参数
int32_t genericMethodTableCount;
Il2CppGenericMethodFunctionsDefinitions* genericMethodTable;
int32_t typesCount;
Il2CppType** types; // 所有类型信息(就是之前修的 Il2CppType)
// ...
}

Il2CppMetadataRegistration;

简单说:所有类型、泛型、类的元数据表。

Il2CppDumper 为什么需要它们

  • global-metadata.dat 知道方法叫什么名字,但不知道编译后的地址在哪
  • CodeRegistration 知道每个方法编译后的地址,但不知道叫什么名字
  • MetadataRegistration 提供类型系统信息,把两者关联起来

三者结合,Il2CppDumper 就能输出 void PlayerController.Update() { }

恢复符号

分别使用两个文件进行导入:运行Il2CppDumper提供的脚本ida_with_struct_py3.py,运行后分别选择script.json与il2cpp.h,耐心等待解析完毕

运行Il2CppDumper提供的脚本ida_py3.py,运行后选择script.json

frida检测对抗手段

frida有检测 可以直接用魔改版的项目,然后换个端口即可,hluda github可以搜到

1
2
3
./hs -l 0.0.0.0:9999
adb forward tcp:9999 tcp:9999(需要另外开一个终端),前面的是电脑端口
frida -H 127.0.0.1:9999 -f xxx -l r0tracer.js

实际上用frida dump出内容后仍然被kill了

flag获取

从csharp代码入手

MouseController.cs :主控制器,含金币收集、碰撞、生命等核心逻辑

image-20260404144602265

image-20260404144805227

SmallKeyboard.cs:键盘输入组件,有个可疑的 KeyboardNum 静态字段

Oo0.cs / oO0OoOOo.cs做了混淆

TssSdtInt.cs:腾讯安全盾(TSS)的防篡改数据类型

MouseController 中有两个关键字段,Coins (类型 TssSdtInt),分数,使用反作弊保护的整数

LblWeaks (类型 Text) — 名字可疑的UI文本,不像是显示弱点,像是显示flag

C# dump出来的代码只有方法签名和RVA地址,没有方法体

7d23d01234就是 VA,RVA = VA - 模块基址

首先看 CollectCoin (RVA 0x4652E4),因为收集金币,加分

image-20260404145425572

image-20260404145720723

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CollectCoin(this, coinCollider) {                          
this->Coins = TssSdtInt::op_Increment(this->Coins); // Coins++
Destroy(coinCollider->gameObject); // 销毁金币对象
PlayClipAtPoint(this->CoinCollectSound); // 播放音效
LblCoins->set_text(Coins.ToString()); // 更新UI显示
// 分数检查
if (TssSdtInt::op_Implicit(this->Coins) < 1000)
return; // 不够1000分,直接返回
// 达到1000分,生成flag
string deviceId = SystemInfo.get_deviceUniqueIdentifier();
string sub = deviceId.Substring(0, 6);
string flag = String.Concat(StringLiteral_4153, sub);
this->LblWeaks->set_text(flag); // 显示flag
}

到这里flag生成逻辑就清楚了:flag = 某个前缀+ 设备ID前6位

从 stringliteral.json 查到索引4153的值是 secP160k1。在IDA反汇编中看到该地址的注释标记为 sec2023_。这个不管,fridahook完就知道是哪个了

image-20260404150717335

不论哪个值,flag的结构是确定的:固定前缀 + deviceUniqueIdentifier[0:6]

image-20260404150000454

0x4653D0 的 B.LT ,NOP掉它,不管分数多少都会走到flag生成路径

一些辅助逻辑

image-20260404150110370

大概是

1
2
3
4
5
6
7
8
void HitByLaser(this, laserCollider) {                          
Lives = TssSdtInt::op_Decrement(Lives); // 生命-1
if (Lives <= 0) {
totalFailedNum++; // 全局失败计数+1
IsDead = true;
PnlRestart->SetActive(true); // 显示重启面板
}
}

可以hook掉来保证无敌

MouseController::Start (RVA 0x464598):

1
2
3
4
5
6
7
8
9
10
 void Start() {
if (SmallKeyboard.KeyboardNum == -1) {
IsInvincible = true; // 内置的无敌模式
ForwardMovSpd = 10.0;
}
if (totalFailedNum >= 3) {
Lives = min(totalFailedNum - 1, 3); // 失败多次后减少初始生命
LblWeaks->set_text(StringLiteral_2720);
}
}

image-20260404150359833

发现隐藏的调试入口:KeyboardNum == -1 会开启无敌

Frida脚本做了这几件事:

  1. NOP 0x4653D0 的 B.LT — 核心bypass,让每次收集金币都走flag路径
  2. Replace HitByLaser 为空函数,无敌,防止还没捡到金币就死了
  3. Hook String.Concat + 检查返回地址在CollectCoin范围内,精准捕获flag字符串并打印
    4. Hook TssSdtInt::op_Implicit ,监控实际分数 ,这样只要进游戏碰到第一个金币,flag就会直接打印到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
//   MouseController::CollectCoin  = 0x4652E4
// Score check: CMP W0, #0x3E8 = 0x4653CC
// Score branch: B.LT (skip flag) = 0x4653D0
// TssSdtInt::op_Implicit = 0x464794
// String.Concat = 0x831950
// MouseController::HitByLaser = 0x465444
var libBase = null;

function waitForLibrary(callback) {
var interval = setInterval(function () {
libBase = Module.findBaseAddress("libil2cpp.so");
if (libBase) {
clearInterval(interval);
console.log("[+] libil2cpp.so loaded at: " + libBase);
callback();
}
}, 500);
}

function readIl2cppString(ptr) {
if (ptr.isNull()) return "<null>";
try {
var len = ptr.add(0x10).readInt();
if (len <= 0 || len > 1024) return "<invalid len:" + len + ">";
return ptr.add(0x14).readUtf16String(len);
} catch (e) {
return "<read error>";
}
}

function hookAll() {
// ============================================================
// 1. Patch: NOP the B.LT at RVA 0x4653D0
// This skips the "coins < 1000" check so flag is generated
// on every coin collection.
// ============================================================
var patchAddr = libBase.add(0x4653D0);
Memory.patchCode(patchAddr, 4, function (code) {
var writer = new Arm64Writer(code, { pc: patchAddr });
writer.putNop(); // replace B.LT with NOP
writer.flush();
});
console.log("[+] Patched score check B.LT -> NOP at " + patchAddr);

// ============================================================
// 2. Hook CollectCoin to log coin collection events
// ============================================================
var collectCoin = libBase.add(0x4652E4);
Interceptor.attach(collectCoin, {
onEnter: function (args) {
this.thisPtr = args[0];
console.log("[*] CollectCoin called!");
},
onLeave: function (retval) {
// After CollectCoin, try to read LblWeaks (offset 0x90) text
try {
var lblWeaks = this.thisPtr.add(0x90).readPointer();
if (!lblWeaks.isNull()) {
console.log("[*] LblWeaks ptr: " + lblWeaks);
}
} catch (e) {}
}
});
console.log("[+] Hooked MouseController::CollectCoin at " + collectCoin);

// ============================================================
// 3. Hook String.Concat to capture the flag string
// Flag = StringLiteral_4153 + deviceUniqueIdentifier[0:6]
// We filter by checking if it's called from CollectCoin
// ============================================================
var strConcat = libBase.add(0x831950); // System.String.Concat(str0, str1)
Interceptor.attach(strConcat, {
onEnter: function (args) {
this.retAddr = this.returnAddress;
// Check if called from within CollectCoin (RVA range 0x4652E4 - 0x465444)
var retRVA = this.retAddr.sub(libBase).toInt32();
if (retRVA >= 0x4652E4 && retRVA <= 0x465444) {
this.isFlag = true;
var str0 = readIl2cppString(args[0]);
var str1 = readIl2cppString(args[1]);
console.log("[*] Flag Concat: str0=\"" + str0 + "\" + str1=\"" + str1 + "\"");
}
},
onLeave: function (retval) {
if (this.isFlag) {
var flag = readIl2cppString(retval);
console.log("\n========================================");
console.log("[FLAG] " + flag);
console.log("========================================\n");
}
}
});
console.log("[+] Hooked String.Concat to capture flag at " + strConcat);

// ============================================================
// 4. (Optional) Make player invincible by hooking HitByLaser
// This prevents dying before collecting enough coins.
// ============================================================
var hitByLaser = libBase.add(0x465444);
Interceptor.replace(hitByLaser, new NativeCallback(function (thisPtr, laserCollider, method) {
// Do nothing - player is now invincible
console.log("[*] HitByLaser blocked - player is invincible!");
}, 'void', ['pointer', 'pointer', 'pointer']));
console.log("[+] Hooked HitByLaser (invincible mode) at " + hitByLaser);

// ============================================================
// 5. (Optional) Hook TssSdtInt::op_Implicit to monitor score
// ============================================================
var opImplicit = libBase.add(0x464794);
var opImplicitOrig = new NativeFunction(opImplicit, 'int', ['pointer', 'pointer']);
Interceptor.attach(opImplicit, {
onLeave: function (retval) {
var retRVA = this.returnAddress.sub(libBase).toInt32();
// 0x4653CC is the call site checking coins >= 1000
if (retRVA >= 0x4653CC && retRVA <= 0x4653D0) {
console.log("[*] Score check: coins = " + retval.toInt32());
}
}
});
console.log("[+] Hooked TssSdtInt::op_Implicit to monitor score");

console.log("\n[+] All hooks installed! Collect any coin to trigger flag.");
console.log("[+] Player is invincible - you won't die from lasers.");
console.log("[+] Flag will be printed when String.Concat is called.\n");
}

// ============================================================
// Entry point: wait for libil2cpp.so to load, then hook
// ============================================================
console.log("[*] Waiting for libil2cpp.so to load...");
waitForLibrary(hookAll);

image-20260404151126811

image-20260404151058914

libil2cpp.so加密分析

要分析注册机首先从输入key开始分析,c#中发现了SmallKeyboard,显然是处理输入0x465880

先看反编译 iI1Ii

image-20260404162752568

分析时发现其中有三个分支,大概是

  • KeyType < 2 与之前的输入拼接

  • KeyType == 2 按下OK提交

  • KeyType == 3 按下Del删除前一个输入

因此需要着重分析第二个OK的分支。

1
2
3
4
5
if (KeyType == 2) {  // EnterKey
ulong i1I = Convert.ToUInt64(this->iIIIi); // 输入转uint64
SmallKeyboard__iI1Ii_4610736(this, i1I); // 验证函数
SmallKeyboard__oO0oOo0(this); // 重新生成token
}

去混淆

追踪iI1Ii_4610736发现被保护了

image-20260404162914136

image-20260404173346496

这是 B(无条件跳转),不是 BL(带链接的调用)。对IDA来说,B 意味着控制流转移到目标,不会返回这里,所以 IDA 认为 0x465AB4 之后的代码不属于任何正常控制流

而 0x2A8 处又是一个 B 到 sec2023 的 .init_proc:

0x2A8: B loc_13B8DC8 ; 跳到 sec2023 unpacker

0x2AC: (data…) ; 加密/packed的函数数据

IDA 跟着这两个B走,最终把整条链路都归入 .init_proc 的函数范围。所以当我查询 0x465AB4 所属函数时

image-20260404162938079

这是腾讯安全壳的unpacker

但是在上一层的下面发现了initproc

image-20260404163111812

image-20260404163129680

妖媚看汇编 ,要么进行处理,下面的指令应该是个正常函数的内容 却被归入了init_proc,所以反编译器不能正确处理,最简单的是直接nop掉源头

image-20260404173720363

再UCP重建函数即可

image-20260404173741542

VM分析

去除后大致逻辑如下:

image-20260404194708080

image-20260404195013295

image-20260404195026928

密钥和字节码怎么来的?就是从global-metadata加载的

7ED62A9C940924585528F182C4782BE139EB7954F975F7145C28719071F7BC8E 这个长名字就是 IL2CPP 的 PrivateImplementationDetails 命名规则 — 它是数据内容的SHA256哈希值

所以反过来,可以在 global-metadata.dat 的数据块里暴力搜索匹配的SHA256

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
import struct
import hashlib

with open('global-metadata.dat', 'rb') as f:
metadata = f.read()

# 解析 metadata 头
sig, version = struct.unpack('<Ii', metadata[:8])
print(f"Signature: 0x{sig:08X}, Version: {version}")

# 头部是一系列 (offset, size) 对,fieldAndParameterDefaultValueData 是第8个表(index=8)
header_entries = struct.unpack('<64i', metadata[8:8 + 64 * 4])
data_blob_offset = header_entries[8 * 2] # index 8 的 offset
data_blob_size = header_entries[8 * 2 + 1] # index 8 的 size
print(f"fieldAndParameterDefaultValueData: offset=0x{data_blob_offset:X}, size={data_blob_size}")

blob = metadata[data_blob_offset:data_blob_offset + data_blob_size]

# ========== 搜索 TEA 密钥 (uint[4] = 16字节) ==========
tea_hash = '7ed62a9c940924585528f182c4782be139eb7954f975f7145c28719071f7bc8e'

print(f"\n搜索 TEA 密钥")
for offset in range(len(blob) - 16 + 1):
chunk = blob[offset:offset + 16]
if hashlib.sha256(chunk).hexdigest() == tea_hash:
key = struct.unpack('<4I', chunk)
print(f"TEA key = [{', '.join(hex(v) for v in key)}]")
break
else:
print("未找到!")

# ========== 搜索 VM 字节码 (ushort[199] = 398字节) ==========
vm_hash = '98c5dbcef5d5d82c07c7b79290aed7b4b64180255f87038f6ad0459937eed610'

print(f"\n搜索 VM 字节码")
for offset in range(len(blob) - 398 + 1):
chunk = blob[offset:offset + 398]
if hashlib.sha256(chunk).hexdigest() == vm_hash:
bytecode = struct.unpack('<199H', chunk)
print(bytecode)
break
else:
print("未找到!")

image-20260404195611990

从反编译代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
OO0OoOOo_Oo0___ctor(v13, array, 0, OOoOO0, method);
// this ↑ ↑ ↑
// 字节码 0 寄存器数组
四个参数。对照 C# dump 中的构造函数签名:

// Oo0.cs
public Oo0(ushort[] OoOOO00, int oOOO0O0O, uint[] OOoOO0)
// ↑字节码数组 ↑初始PC值 ↑数据/寄存器数组

再对照构造函数的反编译(RVA 0x4660E8):
void Oo0___ctor(Oo0 *this, ushort[] OoOOO00, int oOOO0O0O, uint[] OOoOO0) {
Object___ctor(this);
this->O000O000000o(); // 初始化opcode→handler字典
this->OoOOO00 = OoOOO00; // 保存字节码
this->OOoOO0 = OOoOO0; // 保存寄存器数组
this->Ooooo = new uint[1000]; // 创建栈(1000大小)
this->oOOO0O0O = oOOO0O0O; // 保存初始PC
this->oooO0oOo = -1; // SP初始化为-1(空栈)
}

所以第二个参数 0 就是 startPC=0,意味着从字节码的第一条指令开始执行

image-20260404200758698

反编译oOOoO0o0(RVA 0x46AD44),这是 VM dispatch 主循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Oo0__oOOoO0o0(Oo0 *this) {
while (1) {
ushort opcode = this->OoOOO00[this->oOOO0Oo0]; // 读当前指令

// HALT 检查
if (opcode == oO0OoOOo.oO0Oo0oO) // == 0x12
break;

this->oOOO0Oo0++; // IP++

// 查字典找handler并调用
Action handler = this->oOOO0O00[opcode];
handler();
}
}

那22个handler是在 O000O000000o(RVA 0x46A55C)里注册的:

1
2
3
4
5
6
7
8
void Oo0__O000O000000o(Oo0 *this) {
dict = new Dictionary<int, Action>();
dict.Add(oO0OoOOo.oO0OO0O0, new Action(this.ooOOO0OO)); // ADD
dict.Add(oO0OoOOo.Oo0o0OO, new Action(this.oooOoo)); // SUB
dict.Add(oO0OoOOo.OO0OO0, new Action(this.OOOOOO0)); // MUL
// ... 共22个
this->oOOO0O00 = dict;
}

然后逐个反编译这22个handler函数,例如 PUSH_IMM(RVA 0x46B578):

1
2
3
4
5
6
void Oo0__O00O00000o(Oo0 *this) {
ushort operand = this->OoOOO00[this->oOOO0Oo0]; // 读操作数
this->oOOO0Oo0++; // IP++
this->oooO0oOo++; // SP++
this->Ooooo[this->oooO0oOo] = operand; // push到栈
}

image-20260404202520610

比如我们随便找一个

1
2
3
4
5
6
7
8
// 1. 读 opcode 值
oO0Oo00o = OO0OoOOo_oO0OoOOo_TypeInfo->static_fields->oO0Oo00o;

// 2. 创建 Action,绑定到 Method_OO0OoOOo_Oo0_O00O00000o__
System_Action___ctor(value_30, this, Method_OO0OoOOo_Oo0_O00O00000o__, 0);
// 这个就是handler的MethodInfo引用
// 3. 注册到字典
dict.Add(oO0Oo00o, value_8);
1
Method_OO0OoOOo_Oo0_O00O0000O0o__

就是OO0OoOOo+Oo0+O00O0000O0o

可以这样OO0OoOOo.Oo0$$O00O0000O0o

image-20260404202638736

image-20260404202702904

先把 IL2CPP 的字段名还原成有意义的名字:

1
2
3
4
5
void Oo0__PUSH_IMM(Oo0 *this) {
// this->fields.oOOO0Oo0 = IP (指令指针)
// this->fields.OoOOO00 = bytecode[] (字节码数组)
// this->fields.Ooooo = stack[] (栈)
// this->fields.oooO0oOo = SP (栈指针)

这些字段名的对应关系来自 C# dump 和构造函数分析:

1
2
3
4
[FieldOffset(Offset = "0x10")] private ushort[] OoOOO00;   // 字节码
[FieldOffset(Offset = "0x20")] private uint[] Ooooo; // 栈
[FieldOffset(Offset = "0x28")] private int oooO0oOo; // SP
[FieldOffset(Offset = "0x2C")] private int oOOO0Oo0; // IP

但是里面的部分逻辑还是被混淆了以 ooooOO0O(0x46B0BC)为例:

image-20260404202855120

反编译所有22个handler。每个handler都用了MBA混淆,例如 AND 被表示为:result = (v6 + (v6 ^ v8) + (v8 & ~v6) + (v8 & ~v6) + 1) & v8;用单比特真值表验证每个handler的真实操作。

我们如何去混淆[分享]Ollvm 指令替换混淆还原神器:GAMBA 使用指南-Android安全-看雪安全社区|专业技术交流与安全研究论坛

简单学习ollvm混淆&polyre例题解析 | Matriy’s blog

可以使用GAMBA

image-20260404203139229

简化玩就是个与操作,这样我们可以得到所有的逻辑

Oo0 是一个栈式虚拟机,包含

image-20260404170050457

22个操作码总结如下:

image-20260404170021875

魔改TEA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  v25 = *(array_1 + 24);      // 读 key 数组长度(用于边界检查)
v26 = -1091584273; // sum = 0xBEEFBEEF
v27 = -1650623010; // key_sum = 0x9D9D7DDE
n64 = 64; // 64轮
do {
// 更新左半 W21
v20 += (v26 - key[v26 & 3]) ^ (((v21 << 7) ^ (v21 >> 8)) + v21);
// sum - key[sum&3] 混合右半的值
--n64;
v26 -= 559038737; // sum += 0xDEADBEEF (等价于 -= 因为无符号溢出)
// 更新右半 W22
v21 += (v27 + key[(v27>>13) & 3]) ^ (((v20 << 8) ^ (v20 >> 7)) - v20);v
// ks + key[(ks>>13)&3] 混合左半的值
v27 -= 559038737; // key_sum += 0xDEADBEEF
} while (n64);

三层逆运算的叠加:

  1. 逆TEA:64轮反向,先撤销W22更新再撤销W21更新
  2. 逆VM重组:每字节 ^ shift 撤销XOR混合
  3. 逆字节变换:SUB↔ADD互逆,XOR自逆

总逻辑为用户输入数字(uint64) → 拆分为两个uint32 → VM字节变换 → TEA加密 → 与随机ID比较

VM对两个uint32分别做了字节拆分,逐字节变换 ,重新组装,我直接体现到代码里了,直接让AI搓一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import struct, hashlib

# ============================================================
# 1. 从 global-metadata.dat 提取字节码
# ============================================================
def load_bytecode():
with open('global-metadata.dat', 'rb') as f:
metadata = f.read()
header = struct.unpack('<64i', metadata[8:8+64*4])
blob_off, blob_size = header[16], header[17]
blob = metadata[blob_off:blob_off+blob_size]

vm_hash = '98c5dbcef5d5d82c07c7b79290aed7b4b64180255f87038f6ad0459937eed610'
for off in range(len(blob) - 398 + 1):
chunk = blob[off:off+398]
if hashlib.sha256(chunk).hexdigest() == vm_hash:
return list(struct.unpack('<199H', chunk))
raise Exception("bytecode not found")

# ============================================================
# 2. 反汇编器:字节码 → 可读指令
# ============================================================
OPCODES = {
0x01: ('ADD', 0), 0x02: ('SUB', 0), 0x03: ('MUL', 0),
0x04: ('CMP_LT', 0), 0x05: ('CMP_EQ', 0), 0x06: ('JMP', 1),
0x07: ('JNZ', 1), 0x08: ('JZ', 1), 0x09: ('PUSH', 1),
0x0A: ('LOAD_S', 0), 0x0B: ('LOAD_I', 1), 0x0C: ('STORE_S', 0),
0x0D: ('STORE_I', 1), 0x0E: ('PRINT', 0), 0x0F: ('POP', 0),
0x12: ('HALT', 0), 0x13: ('SHR', 0), 0x14: ('SHL', 0),
0x15: ('AND', 0), 0x16: ('XOR', 0),
}

def disassemble(code):
ip = 0
lines = []
while ip < len(code):
op = code[ip]
if op == 0x12:
lines.append((ip, 'HALT', None))
break
info = OPCODES.get(op)
if info is None:
lines.append((ip, f'UNK_0x{op:02X}', None))
ip += 1
continue
name, has_operand = info
if has_operand:
operand = code[ip+1] if ip+1 < len(code) else 0
lines.append((ip, name, operand))
ip += 2
else:
lines.append((ip, name, None))
ip += 1
return lines

def print_disasm(lines):
print("=" * 50)
print("VM 反汇编")
print("=" * 50)
for addr, name, operand in lines:
if operand is not None:
print(f" [{addr:3d}] {name:8s} {operand} (0x{operand:X})")
else:
print(f" [{addr:3d}] {name}")

# ============================================================
# 3. 模拟执行器:实际运行字节码,输出每步状态
# ============================================================
class VM:
def __init__(self, code, regs_init):
self.code = code
self.regs = list(regs_init) + [0] * (8 - len(regs_init)) # reg[0..7]
self.stack = []
self.ip = 0
self.trace = [] # 记录每步操作

def run(self, verbose=False):
while self.ip < len(self.code):
op = self.code[self.ip]
if op == 0x12: # HALT
self.trace.append(f"[{self.ip:3d}] HALT")
break

info = OPCODES.get(op)
if info is None:
raise Exception(f"Unknown opcode 0x{op:X} at IP={self.ip}")

name, has_operand = info
self.ip += 1

if has_operand:
operand = self.code[self.ip]
self.ip += 1
else:
operand = None

msg = self._exec(name, operand)
if verbose:
print(msg)
self.trace.append(msg)

def _exec(self, name, operand):
u32 = lambda x: x & 0xFFFFFFFF
ip_before = self.ip - (2 if operand is not None else 1)

if name == 'PUSH':
self.stack.append(operand)
return f"[{ip_before:3d}] PUSH {operand} (0x{operand:X}) stack={self._stack_str()}"

elif name == 'LOAD_I':
val = self.regs[operand]
self.stack.append(val)
return f"[{ip_before:3d}] LOAD_I reg[{operand}]={val} stack={self._stack_str()}"

elif name == 'LOAD_S':
addr = self.stack.pop()
val = self.regs[addr]
self.stack.append(val)
return f"[{ip_before:3d}] LOAD_S reg[{addr}]={val} stack={self._stack_str()}"

elif name == 'STORE_I':
val = self.stack.pop()
self.regs[operand] = u32(val)
return f"[{ip_before:3d}] STORE_I reg[{operand}] = {u32(val)} (0x{u32(val):X})"

elif name == 'STORE_S':
addr = self.stack.pop()
val = self.stack.pop()
self.regs[addr] = u32(val)
return f"[{ip_before:3d}] STORE_S reg[{addr}] = {u32(val)} (0x{u32(val):X})"

elif name in ('ADD', 'SUB', 'MUL', 'SHR', 'SHL', 'AND', 'XOR', 'CMP_LT', 'CMP_EQ'):
b = self.stack.pop() # top
a = self.stack.pop() # second
if name == 'ADD': r = u32(a + b)
elif name == 'SUB': r = u32(a - b)
elif name == 'MUL': r = u32(a * b)
elif name == 'SHR': r = u32(a >> (b & 31))
elif name == 'SHL': r = u32(a << (b & 31))
elif name == 'AND': r = u32(a & b)
elif name == 'XOR': r = u32(a ^ b)
elif name == 'CMP_LT':
r = 1 if (a if a < 0x80000000 else a - 0x100000000) < (b if b < 0x80000000 else b - 0x100000000) else 0
elif name == 'CMP_EQ': r = 1 if a == b else 0
self.stack.append(r)
return f"[{ip_before:3d}] {name:8s} 0x{a:X} op 0x{b:X} = 0x{r:X} stack={self._stack_str()}"

elif name == 'JZ':
cond = self.stack.pop()
if cond == 0:
self.ip = operand
return f"[{ip_before:3d}] JZ {operand} (cond=0, JUMP)"
return f"[{ip_before:3d}] JZ {operand} (cond={cond}, no jump)"

elif name == 'JNZ':
cond = self.stack.pop()
if cond != 0:
self.ip = operand
return f"[{ip_before:3d}] JNZ {operand} (cond={cond}, JUMP)"
return f"[{ip_before:3d}] JNZ {operand} (cond={cond}, no jump)"

elif name == 'JMP':
self.ip = operand
return f"[{ip_before:3d}] JMP {operand}"

elif name == 'POP':
self.stack.pop()
return f"[{ip_before:3d}] POP"

elif name == 'PRINT':
val = self.stack.pop()
return f"[{ip_before:3d}] PRINT {val}"

return f"[{ip_before:3d}] {name} (unhandled)"

def _stack_str(self):
if len(self.stack) <= 6:
return str([f"0x{v:X}" for v in self.stack])
return f"[...{len(self.stack)} items]"

# ============================================================
# 4. 运行
# ============================================================
if __name__ == '__main__':
code = load_bytecode()

# 反汇编
lines = disassemble(code)
print_disasm(lines)

# 模拟执行 (用示例输入)
test_input = 0xAABBCCDD
low32 = test_input & 0xFFFFFFFF

print(f"\n{'='*50}")
print(f"模拟执行第一段 (处理 low32 = 0x{low32:08X})")
print(f"输入字节: [{(low32>>24)&0xFF:02X}, {(low32>>16)&0xFF:02X}, {(low32>>8)&0xFF:02X}, {low32&0xFF:02X}]")
print(f"{'='*50}")

vm = VM(code, [low32, 0])
vm.run(verbose=True)

print(f"\n结果: reg[0] = 0x{vm.regs[0]:08X}")
print(f" reg[1] = 0x{vm.regs[1]:08X}")

# 验证
print(f"\n{'='*50}")
print("验证: 手动计算 vs VM执行")
print(f"{'='*50}")
b0, b1, b2, b3 = low32&0xFF, (low32>>8)&0xFF, (low32>>16)&0xFF, (low32>>24)&0xFF
r4 = (b0 - 27) & 0xFF
r5 = (b1 ^ 0xC2) & 0xFF
r6 = (b2 + 0xA8) & 0xFF
r7 = (b3 ^ 0x36) & 0xFF
manual = 0
for i, r in enumerate([r4, r5, r6, r7]):
shift = i * 8
manual |= ((r ^ shift) & 0xFF) << shift
manual &= 0xFFFFFFFF

print(f" byte0=0x{b0:02X} -27 -> reg4=0x{r4:02X}")
print(f" byte1=0x{b1:02X} ^C2 -> reg5=0x{r5:02X}")
print(f" byte2=0x{b2:02X} +A8 -> reg6=0x{r6:02X}")
print(f" byte3=0x{b3:02X} ^36 -> reg7=0x{r7:02X}")
print(f" 手动计算: 0x{manual:08X}")
print(f" VM执行: 0x{vm.regs[0]:08X}")
print(f" 匹配: {'PASS' if manual == vm.regs[0] else 'FAIL'}")

image-20260404203850803

image-20260404203939937

image-20260404203902124

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
import struct, sys

M32 = 0xFFFFFFFF
def u32(x): return x & M32

TEA_KEY = [0x7B777C63, 0xC56F6BF2, 0x2B670130, 0x76ABD7FE]
TEA_DELTA = 0xDEADBEEF
TEA_INIT_SUM = 0xBEEFBEEF
TEA_INIT_KS = 0x9D9D7DDE
TEA_ROUNDS = 64

def tea_encrypt(W21, W22):
s, ks = TEA_INIT_SUM, TEA_INIT_KS
for _ in range(TEA_ROUNDS):
t = u32(u32(u32(W22 << 7) ^ (W22 >> 8)) + W22)
W21 = u32(W21 + (u32(s - TEA_KEY[s & 3]) ^ t))
t2 = u32(u32(u32(W21 << 8) ^ (W21 >> 7)) - W21)
W22 = u32(W22 + (u32(ks + TEA_KEY[(ks >> 13) & 3]) ^ t2))
s, ks = u32(s + TEA_DELTA), u32(ks + TEA_DELTA)
return W21, W22

def tea_decrypt(W21, W22):
s = u32(TEA_INIT_SUM + TEA_ROUNDS * TEA_DELTA)
ks = u32(TEA_INIT_KS + TEA_ROUNDS * TEA_DELTA)
for _ in range(TEA_ROUNDS):
s, ks = u32(s - TEA_DELTA), u32(ks - TEA_DELTA)
t2 = u32(u32(u32(W21 << 8) ^ (W21 >> 7)) - W21)
W22 = u32(W22 - (u32(ks + TEA_KEY[(ks >> 13) & 3]) ^ t2))
t = u32(u32(u32(W22 << 7) ^ (W22 >> 8)) + W22)
W21 = u32(W21 - (u32(s - TEA_KEY[s & 3]) ^ t))
return W21, W22

# VM byte transformations (from bytecode analysis)
# Opcodes: 0x01=ADD, 0x02=SUB, 0x13=SHR, 0x14=SHL, 0x15=AND, 0x16=XOR
#
# First half (reg[0]): byte0-27, byte1^0xC2, byte2+0xA8, byte3^0x36
# Second half (reg[1]): byte0-0x2F, byte1^0xB6, byte2+0x37, byte3^0x98
# Reassembly: reg = sum( ((transformed_byte[i] ^ (i*8)) & 0xFF) << (i*8) )

def vm_forward(val32, sub_c, xor1_c, add_c, xor2_c):
b0, b1, b2, b3 = val32 & 0xFF, (val32>>8)&0xFF, (val32>>16)&0xFF, (val32>>24)&0xFF
regs = [(b0 - sub_c) & 0xFF, (b1 ^ xor1_c) & 0xFF, (b2 + add_c) & 0xFF, (b3 ^ xor2_c) & 0xFF]
result = 0
for i, r in enumerate(regs):
shift = i * 8
result |= ((r ^ shift) & 0xFF) << shift
return u32(result)

def vm_inverse(target, sub_c, xor1_c, add_c, xor2_c):
"""Fully invertible since XOR replaces OR."""
regs = []
for i in range(4):
shift = i * 8
b = (target >> shift) & 0xFF
regs.append(b ^ shift) # undo the XOR with shift
# regs = [reg4, reg5, reg6, reg7]
# Undo byte transformations
byte0 = (regs[0] + sub_c) & 0xFF
byte1 = (regs[1] ^ xor1_c) & 0xFF # XOR is its own inverse
byte2 = (regs[2] - add_c) & 0xFF
byte3 = (regs[3] ^ xor2_c) & 0xFF
return byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24)

def keygen(random_id_str):
target = int(random_id_str) & M32
# Step 1: Reverse TEA
reg0, reg1 = tea_decrypt(target, 0)
# Step 2: Reverse VM for each half
low32 = vm_inverse(reg0, 27, 0xC2, 0xA8, 0x36)
high32 = vm_inverse(reg1, 47, 0xB6, 0x37, 0x98)
key = (high32 << 32) | low32
return key

def verify(key, random_id_str):
target = int(random_id_str) & M32
low32, high32 = key & M32, (key >> 32) & M32
reg0 = vm_forward(low32, 27, 0xC2, 0xA8, 0x36)
reg1 = vm_forward(high32, 47, 0xB6, 0x37, 0x98)
enc = tea_encrypt(reg0, reg1)
return enc == (target, 0)

if __name__ == '__main__':
print(hex(tea_encrypt(0x3e90dddf, 0x61a7b627)[0]))
print(hex(tea_encrypt(0x3e90dddf, 0x61a7b627)[1]))
print(hex(tea_decrypt(0x48e193ed, 0xa3f3fe8)[0]))
print(hex(tea_decrypt(0x48e193ed, 0xa3f3fe8)[1]))

image-20260404204534844

libsec2023.so加密分析

通过上述代码的生成的key输入后发现无效果

image-20260404222203661

hook_vadlidate.js

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
// Frida hook: 验证 SmallKeyboard 注册码是否通过
function readIl2cppString(ptr) {
if (ptr.isNull()) return "<null>";
try {
var len = ptr.add(0x10).readInt();
if (len <= 0 || len > 1024) return "<invalid>";
return ptr.add(0x14).readUtf16String(len);
} catch (e) {
return "<error>";
}
}

function waitForLib(callback) {
var interval = setInterval(function () {
var base = Module.findBaseAddress("libil2cpp.so");
if (base) {
clearInterval(interval);
console.log("[+] libil2cpp.so: " + base);
callback(base);
}
}, 500);
}

waitForLib(function (base) {

// ============================================================
// Hook SmallKeyboard__ValidateKey (0x465AB4)
// 参数: this (X0), input_uint64 (X1)
// ============================================================
var validateKey = base.add(0x465AB4);
Interceptor.attach(validateKey, {
onEnter: function (args) {
this.thisPtr = args[0];
var inputLow = args[1].toUInt32();
var inputHigh = this.context.x1.shr(32).toUInt32();

// 读 this->oO0o0o0 (offset 0x38) = Token字符串
var tokenPtr = this.thisPtr.add(0x38).readPointer();
var token = readIl2cppString(tokenPtr);

// 读 this->iIIIi (offset 0x40) = 用户输入字符串
var inputPtr = this.thisPtr.add(0x40).readPointer();
var inputStr = readIl2cppString(inputPtr);

console.log("\n========================================");
console.log("[*] ValidateKey 被调用");
console.log(" Token (oO0o0o0): " + token);
console.log(" 用户输入 (iIIIi): " + inputStr);
console.log(" input uint64: " + args[1]);
},
onLeave: function (retval) {
// 验证后检查 SmallKeyboard.KeyboardNum
// SmallKeyboard_TypeInfo 的 static_fields->KeyboardNum
console.log(" 返回值: " + retval);
}
});
console.log("[+] Hooked ValidateKey at " + validateKey);

// ============================================================
// Hook 最终比较点 (0x465D14)
// 此时 W22=加密后右半, W21=加密后左半(在W0里是ToUInt32的返回值)
// 指令: CBNZ W22, fail 然后 CMP W0, W21
// ============================================================
var cmpAddr = base.add(0x465D14);
Interceptor.attach(cmpAddr, {
onEnter: function (args) {
var w21 = this.context.x21.toUInt32(); // 加密后的左半
var w22 = this.context.x22.toUInt32(); // 加密后的右半
var w0 = this.context.x0.toUInt32(); // Convert.ToUInt32(token)

console.log(" --- 最终比较 ---");
console.log(" TEA加密后: W21=0x" + w21.toString(16) + " W22=0x" + w22.toString(16));
console.log(" Token转uint32: W0=0x" + w0.toString(16) + " (" + w0 + ")");
console.log(" W22==0? " + (w22 === 0 ? "YES" : "NO"));
console.log(" W21==W0? " + (w21 === w0 ? "YES" : "NO"));
console.log(" 结果: " + (w22 === 0 && w21 === w0 ? "✓ PASS" : "✗ FAIL"));
console.log("========================================");
}
});
console.log("[+] Hooked 比较点 at " + cmpAddr);
var setKeyboardNum = base.add(0x465D2C);
Interceptor.attach(setKeyboardNum, {
onEnter: function (args) {
console.log("\n[!!!] KeyboardNum = -1 → 验证通过!无敌模式已激活!");
}
});
console.log("[+] Hooked KeyboardNum写入 at " + setKeyboardNum);

console.log("\n[+] 所有hook就绪,请在游戏中输入注册码并点击OK");
});

发现不能通过只能再debug下,hook_debug.js

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
// Debug hook: 追踪输入值在每一步的变化
function readIl2cppString(ptr) {
if (ptr.isNull()) return "<null>";
try {
var len = ptr.add(0x10).readInt();
if (len <= 0 || len > 1024) return "<invalid>";
return ptr.add(0x14).readUtf16String(len);
} catch (e) { return "<error>"; }
}

function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libil2cpp.so");
if (b) { clearInterval(i); console.log("[+] base: " + b); cb(b); }
}, 500);
}

waitForLib(function (base) {

// Hook iI1Ii 键盘处理 (0x465880) - Enter 键处理
var iI1Ii = base.add(0x465880);
Interceptor.attach(iI1Ii, {
onEnter: function(args) {
this.thisPtr = args[0];
var infoPtr = args[1];
if (!infoPtr.isNull()) {
var keyType = infoPtr.add(0x18).readInt();
if (keyType == 2) { // EnterKey
var iIIIi = readIl2cppString(this.thisPtr.add(0x40).readPointer());
var token = readIl2cppString(this.thisPtr.add(0x38).readPointer());
console.log("\n[Enter键] iIIIi = \"" + iIIIi + "\", token = \"" + token + "\"");
}
}
}
});
console.log("[+] Hooked iI1Ii at " + iI1Ii);

// Hook SmallKeyboard__iI1Ii_4610736 的调用点
// 在 iI1Ii 中: Convert.ToUInt64(iIIIi) 然后调用 iI1Ii_4610736(this, uint64)
// iI1Ii_4610736 在 0x465AB0 (thunk) → 实际到 0x2A8 → sec2023
var thunk = base.add(0x465AB0);
Interceptor.attach(thunk, {
onEnter: function(args) {
// X0 = this, X1 = uint64 value
var lo = this.context.x1.and(ptr(0xFFFFFFFF)).toUInt32();
var hi = this.context.x1.shr(32).and(ptr(0xFFFFFFFF)).toUInt32();
console.log("[iI1Ii_4610736] 被调用");
console.log(" X1 raw: " + this.context.x1);
console.log(" low32: 0x" + lo.toString(16));
console.log(" high32: 0x" + hi.toString(16));
}
});
console.log("[+] Hooked thunk at " + thunk);

// 也 hook 实际的 ValidateKey 函数体 (0x465AB4)
// 这个可能不会被调到(因为thunk跳走了)
var validateKey = base.add(0x465AB4);
try {
Interceptor.attach(validateKey, {
onEnter: function(args) {
var lo = this.context.x1.and(ptr(0xFFFFFFFF)).toUInt32();
var hi = this.context.x1.shr(32).and(ptr(0xFFFFFFFF)).toUInt32();
console.log("[ValidateKey 0x465AB4] 被调用!");
console.log(" X1 raw: " + this.context.x1);
console.log(" low32: 0x" + lo.toString(16));
console.log(" high32: 0x" + hi.toString(16));
}
});
console.log("[+] Hooked ValidateKey body at " + validateKey);
} catch(e) {
console.log("[-] 无法hook 0x465AB4: " + e);
}

// Hook 0x2A8 (sec2023跳板目标)
var jumpTarget = base.add(0x2A8);
try {
Interceptor.attach(jumpTarget, {
onEnter: function(args) {
var lo = this.context.x1.and(ptr(0xFFFFFFFF)).toUInt32();
var hi = this.context.x1.shr(32).and(ptr(0xFFFFFFFF)).toUInt32();
console.log("[0x2A8 sec2023入口] 被调用!");
console.log(" X1 raw: " + this.context.x1);
console.log(" low32: 0x" + lo.toString(16));
console.log(" high32: 0x" + hi.toString(16));
}
});
console.log("[+] Hooked 0x2A8");
} catch(e) {
console.log("[-] 无法hook 0x2A8: " + e);
}

// Hook VM执行前后 - 在 Oo0 构造函数中看传入的数据
var oo0Ctor = base.add(0x4660E8);
Interceptor.attach(oo0Ctor, {
onEnter: function(args) {
this.regsPtr = args[3]; // OOoOO0 数组
if (!this.regsPtr.isNull()) {
var r0 = this.regsPtr.add(0x20).readU32();
var r1 = this.regsPtr.add(0x24).readU32();
console.log("[Oo0构造] reg[0]=0x" + r0.toString(16) + " reg[1]=0x" + r1.toString(16));
}
}
});
console.log("[+] Hooked Oo0 ctor");

// Hook VM dispatch 执行完毕后读结果
var vmRun = base.add(0x46AD44);
Interceptor.attach(vmRun, {
onEnter: function(args) {
this.vmObj = args[0];
},
onLeave: function(retval) {
// 读 OOoOO0 (offset 0x18 in Oo0 object)
var regsPtr = this.vmObj.add(0x18).readPointer();
if (!regsPtr.isNull()) {
var r0 = regsPtr.add(0x20).readU32();
var r1 = regsPtr.add(0x24).readU32();
console.log("[VM执行完] reg[0]=0x" + r0.toString(16) + " reg[1]=0x" + r1.toString(16));
}
}
});
console.log("[+] Hooked VM dispatcher");

console.log("\n[+] 全部hook就绪,请输入注册码并点OK");
});

image-20260404222331353

发现

thunk (0x465AB0): X1 = 0xd704a6ef31bc018f 原始输入

sec2023 (0x2A8): X1 = 0xd704a6ef31bc018f 还是一样

image-20260404222433147

ValidateKey (0x465AB4): X1 = 0x1160f6d544d1b994 可能是被sec2023改了

sec2023 壳在 0x2A8到 0x465AB4 之间对输入做了额外变换。我们的 keygen 算出的值是给 0x465AB4 用的,但 sec2023 在传入前做了修改。

为什么会想到去hooksec2023?因为在字符串里看到了,此外loc_13B8DC8这个地址也跟进看过

image-20260404222541039

可能得分析另一个so文件

thunk il2cpp+0x465AB0 传 X1 进去,il2cpp+0x465AB4 X1 被改变。中间发生什么未知

一些尝试和发现

第一次尝试(失败):函数级 hook

sec2023 里大概有 17 个可疑函数,挨个 hook 看哪个被调。

hook_trace_sec2023.js

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
// 精准追踪: 按OK时 sec2023 中哪些函数被调用,X1在哪一步被修改
// 只在验证触发时启用跟踪

function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libil2cpp.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

waitForLib(function (il2cpp) {
var sec = Module.findBaseAddress("libsec2023.so");
if (!sec) { console.log("[-] libsec2023.so not found"); return; }
console.log("[+] libil2cpp.so: " + il2cpp);
console.log("[+] libsec2023.so: " + sec);

var tracing = false;
var origX1 = null;

// 在 thunk 处开始跟踪
Interceptor.attach(il2cpp.add(0x465AB0), {
onEnter: function(args) {
tracing = true;
origX1 = this.context.x1;
console.log("\n=== 开始跟踪 X1=" + origX1 + " ===");
}
});

// 在 ValidateKey 处结束跟踪
Interceptor.attach(il2cpp.add(0x465AB4), {
onEnter: function(args) {
if (tracing) {
console.log("=== 到达 ValidateKey X1=" + this.context.x1 + " ===");
tracing = false;
}
}
});

// 用 Stalker 追踪从 thunk 到 ValidateKey 之间 sec2023 中执行的代码
// 改用函数级 hook: hook sec2023 中所有被 sub_103D0 调用的关键函数
// 以及 sub_35870 注册的 handler 函数

var handlers = [
0x37060, 0x3C6A4, 0x3DF74, 0x3A054, 0x24364,
0x3852C, 0x38D9C, 0x3F2B0, 0x355F0, 0x35630,
0x3566C, 0x356B0, 0x356C4, 0xFABC, 0x103D0,
0x21098, 0x35870,
];

handlers.forEach(function(off) {
try {
Interceptor.attach(sec.add(off), {
onEnter: function(args) {
if (tracing) {
console.log(" [CALL] sec+0x" + off.toString(16) +
" X0=" + this.context.x0 +
" X1=" + this.context.x1 +
" X2=" + this.context.x2);
}
},
onLeave: function(retval) {
if (tracing) {
console.log(" [RET] sec+0x" + off.toString(16) +
" ret=" + retval +
" X1=" + this.context.x1);
}
}
});
} catch(e) {}
});

console.log("[+] 就绪 - 按OK触发");
});

只有 sub_3A054(dispatcher)被触发。其他函数没进去 ,说明 sec2023 用了间接跳转(dispatcher 表),直接 hook 地址抓不到

sec2023 用 VM-style dispatcher (BR X8/X10/X12),调用是间接跳转,IDA 里看到的函数地址根本不是实际执行的入口。

image-20260405101749529

第二次尝试:Stalker 指令级追踪

为什么用 Stalker: 静态分析看不到实际调用关系(因为间接跳转),只能动态跟。Frida Stalker 追踪 sec2023 范围内每条 BLR/BR/BL/RET,打印每条指令和 X1 的值。

hook_stalker.js

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
// 用 Stalker 追踪验证时 sec2023 执行的每个代码块
// 找出哪条指令修改了 X1

function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libil2cpp.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

waitForLib(function (il2cpp) {
var sec = Module.findBaseAddress("libsec2023.so");
console.log("[+] il2cpp: " + il2cpp);
console.log("[+] sec2023: " + sec);

var secBase = sec;
var secEnd = sec.add(0x80000); // 估算

Interceptor.attach(il2cpp.add(0x465AB0), {
onEnter: function (args) {
var tid = this.threadId;
var origX1 = this.context.x1;
console.log("\n=== Stalker X1=" + origX1 + " ===");

Stalker.follow(tid, {
events: { call: false, ret: false, exec: false, block: false, compile: false },
transform: function (iterator) {
var instruction;
while ((instruction = iterator.next()) !== null) {
var addr = instruction.address;
// 只关注 sec2023 范围内的 BLR 和 BR 指令
if (addr.compare(secBase) >= 0 && addr.compare(secEnd) < 0) {
var mnemonic = instruction.mnemonic;
if (mnemonic === 'blr' || mnemonic === 'br' || mnemonic === 'bl' || mnemonic === 'ret') {
var off = addr.sub(secBase).toUInt32();
iterator.putCallout(function (context) {
var x1val = context.x1;
// 打印 X1 变化时
console.log(" " + ptr(off) + " " + mnemonic + " X1=" + x1val);
});
}
}
iterator.keep();
}
}
});
}
});

// 在 ValidateKey 处停止 Stalker
Interceptor.attach(il2cpp.add(0x465AB4), {
onEnter: function (args) {
Stalker.unfollow(this.threadId);
console.log("=== Stalker停止 X1=" + this.context.x1 + " ===");
}
});

console.log("[+] 就绪 - 按OK");
});

Stalker 是 Frida 里的动态指令跟踪器

普通 Interceptor.attach()更像是在某个函数入口下断点进来时看参数出去时看返回值

而 Stalker是:

  • 这个线程接下来执行的每一小段代码我都盯着
  • 它跳到哪、call 到哪、ret 到哪,我都能跟
  • 甚至执行到某条指令时,我还能插桩看寄存器

比 Interceptor更细粒度

X1 在整个 chain 里基本不变,直到进入 sub_3A924 才被修改。发现调用链:

1
2
3
sub_3A924 → sub_3B4B8 (循环解密"encrypt")
→ sub_3B570 (循环解密"([B)[B")
→ JNI 调用 → X1 被改
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
[Remote::mouse ]-> [+] il2cpp: 0x7c0040f000
[+] sec2023: 0x7c7cb4a000
[+] 就绪 - 按OK

=== Stalker���动 X1=0x102cf ===
0x31188 bl X1=0x102cf
0x3b8fc bl X1=0x102cf
0x3ba00 br X1=0x102cf
0x3ba30 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3badc br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb4c br X1=0x102cf
0x3ba30 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3ba70 br X1=0x102cf
0x3badc br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb28 br X1=0x102cf
0x3bb4c br X1=0x102cf
0x3bb54 ret X1=0x102cf
0x3b90c bl X1=0x102cf
0x3a08c br X1=0x102cf
0x3a0f8 ret X1=0x102cf
0x3b920 bl X1=0x7c1a5d1d64
0x3a96c bl X1=0x7c1a5d1d64
0x10190 ret X1=0x7c1a5d1d64
0x3a978 bl X1=0xb400007c93090380
0x2d504 blr X1=0x7c1a5d1c30
0x2d564 ret X1=0xb400007c68fce710
0x3a980 bl X1=0xb400007c68fce710
0x2d58c ret X1=0xb400007c68fce710
0x3a990 bl X1=0xb400007c68efc040
0x125ec ret X1=0xb400007c68efc040
0x3a9a4 blr X1=0x4
0x3a9c8 blr X1=0x39
0x3a9f0 bl X1=0x7c1a5d1cd0
0x3b508 br X1=0x7c1a5d1cd0
0x3b54c br X1=0x7c1a5d1cd0
0x3b568 ret X1=0x7c1a5d1cd0
0x3aa28 bl X1=0x7c1a5d1cb8
0x3b5c0 br X1=0x7c1a5d1cb8
0x3b604 br X1=0x7c1a5d1cb8
0x3b620 ret X1=0x7c1a5d1cb8
0x3aa44 blr X1=0x2f66
Process terminated
[Remote::mouse ]->

Stalker打印的是 sec2023 基址的相对偏移和控制流指令。每条记录形如:

0x3a9f0 bl X1=… ← 在 sec2023+0x3a9f0 执行 BL 指令

之前 IDA 看 sub_3A924 反编译结果里有这几个关键点:

1
2
3
v10 = sub_3B4B8(&n176175656, v21);   // @ 0x3a9f8
v11 = sub_3B570(&v14, &n176175656); // @ 0x3aa28
v12 = (*(vtable[113]))(v7, v9, v10, v11); // GetStaticMethodID @ 0x3aa44

image-20260405142139150

现在对照 Stalker:

1
2
3
4
5
6
7
8
9
0x3a9f0 bl X1=0x7c1a5d1cd0   ← BL 到 sub_3B4B8
0x3b508 br
0x3b54c br ×19 ← sub_3B4B8 内部循环(dispatcher 跳转)
0x3b568 ret ← sub_3B4B8 返回
0x3aa28 bl X1=0x7c1a5d1cb8 ← BL 到 sub_3B570 (不同的 X1 buffer!)
0x3b5c0 br
0x3b604 br ×20 ← sub_3B570 内部循环
0x3b620 ret ← sub_3B570 返回
0x3aa44 blr X1=0x2f66 ← BLR = GetStaticMethodID (X1=0x2f66 是 jclass handle)

image-20260405142257203

推理步骤,不是 Stalker 直接看出来的:

1. 位置推断: 0x3a9f0 和 0x3aa28 两次 BL,正好对应 IDA 反编译里的 sub_3B4B8(...) 和 sub_3B570(...)。
2. 循环模式说明在处理字符串: sub_3B4B8 内部 0x3b54c br 出现 19 次,sub_3B570 内部 0x3b604 br 出现 20 次。这种重复 BR 到同一地址 = 循环体在跑。
3. 循环的目的是什么:
   - 下一个调用是 GetStaticMethodID(env, clazz, name_ptr, sig_ptr) (vtable[113])
   - 也就是说 sub_3B4B8/sub_3B570 的返回值是方法名指针和签名指针
   - IDA 里能看到这两个函数用大常量(0xF712A53DC141FF65 等)做 XOR 循环
   - 循环次数 ≈ 字符串长度:
       - "encrypt" = 7 字符 + 结尾 = 需要 ~7-8 次循环迭代
     - "([B)[B" = 6 字符 + 结尾 = 需要 ~6-7 次
   - 实际 BR 出现 19/20 次是因为每次循环体本身有多个 BR 跳转(典型 VM dispatch 一轮包含多次 BR)

4. 从后面的 hook_jni_call 确认:
   - JNI hook 抓到 [GetStaticMethodID] encrypt ([B)[B
   - 所以倒推回去,两次 BL 就是解密这两个字符串
1
2
3
4
5
6
7
8
9
10
Stalker trace 片段               推断的语义
──────────────────────── ──────────────
0x31188 bl X1=0x102cf sub_31164 → sub_3B8CC(user_input)
0x3ba00-0x3bb54 (多次 br/ret) sub_3B9D4 跑完预处理 (~4+4次 byte 循环)
0x3b920 bl X1=0x7c1a5d1d64 sub_3B8CC → sub_3A924(ctx, &v5, 4)
0x3a9a4 blr X1=0x4 JNI NewByteArray(size=4)
0x3a9c8 blr X1=0x39 JNI SetByteArrayRegion(arr, ...)
0x3a9f0 bl + 0x3b54c br ×19 解密 "encrypt" 方法名
0x3aa28 bl + 0x3b604 br ×20 解密 "([B)[B" 签名
0x3aa44 blr X1=0x2f66 JNI GetStaticMethodID → 得到 methodID
  • Stalker offset
  • BL/BR/RET 三类指令区分出 函数调用 / dispatcher 跳转 / 函数返回
  • 重复 BR 到同一地址 = 循环(可能是解密 / VM 指令 / byte-loop)
  • X1 值突变为大指针 = 作为函数调用的第 2 个参数传出去
  • JNI hook 结果 反向确认(”抓到 encrypt 方法名 → 所以前面的解密循环产生的就是它”)

sub3A924 分析

1
2
3
4
5
6
7
8
9
v6 = sub_1010C();                  // 拿 JNI env
sub_2D4B8(v16, *v6);
v7 = sub_2D588(v16); // v7 = JNIEnv*
v8 = (*(vtable[176]))(v7, a3); // NewByteArray(a3)
(*(vtable[208]))(v7, v8, 0, a3, a2); // SetByteArrayRegion
// ... 解密方法名 ...
v10 = sub_3B4B8(&..., ...); // 运行时解出 "encrypt"
v11 = sub_3B570(&..., ...); // 运行时解出 "([B)[B"
v12 = (*(vtable[113]))(v7, v9, v10, v11); // GetStaticMethodID

关键: 字符串是运行时用 XOR 常量(0xF712A53DC141FF65 等)解密的,静态看不到明文。

抓运行时的 JNI 调用,静态看不到方法名 ,动态 hook JNI vtable,截获 GetStaticMethodID 和字节数组操作

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
// 捕获 sub_3A924 中的 JNI 调用:方法名、签名、byte[] 内容
function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libil2cpp.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

waitForLib(function (il2cpp) {
var sec = Module.findBaseAddress("libsec2023.so");

// Hook sub_3A924 入口+出口,记录输入和输出
Interceptor.attach(sec.add(0x3A924), {
onEnter: function(args) {
console.log("\n[sub_3A924] a1=" + args[0] + " a2=" + args[1] + " a3=" + args[2]);
this.buf = args[1];
this.size = args[2].toUInt32();
if (this.size > 0 && this.size < 256) {
console.log(" IN bytes[" + this.size + "]: " + hexdump(this.buf, {length: this.size, ansi: false}));
}
},
onLeave: function(retval) {
if (this.size > 0 && this.size < 256) {
console.log(" OUT bytes[" + this.size + "]: " + hexdump(this.buf, {length: this.size, ansi: false}));
}
}
});

// Hook JNI GetMethodID / GetStaticMethodID 来捕获方法名
Java.perform(function() {
var env = Java.vm.getEnv();
var vtable = env.handle.readPointer();

// GetMethodID at vtable index 33 (offset 264)
Interceptor.attach(vtable.add(33 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
var name = args[2].readCString();
var sig = args[3].readCString();
console.log("[GetMethodID] " + name + " " + sig);
}
});

// GetStaticMethodID at vtable index 113 (offset 904)
Interceptor.attach(vtable.add(113 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
var name = args[2].readCString();
var sig = args[3].readCString();
console.log("[GetStaticMethodID] " + name + " " + sig);
}
});

// CallObjectMethod at vtable index 34 (offset 272)
Interceptor.attach(vtable.add(34 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
console.log("[CallObjectMethod] obj=" + args[1] + " methodID=" + args[2]);
},
onLeave: function(retval) {
console.log("[CallObjectMethod] return=" + retval);
}
});

// 也 hook NewByteArray 和 GetByteArrayRegion 来看返回的数据
// NewByteArray at index 176 (offset 1408)
Interceptor.attach(vtable.add(176 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
console.log("[NewByteArray] size=" + args[1]);
}
});

// GetByteArrayRegion at index 200 (offset 1600)
Interceptor.attach(vtable.add(200 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
this.buf = args[4];
this.len = args[3].toUInt32();
},
onLeave: function(retval) {
if (this.len > 0 && this.len < 256) {
console.log("[GetByteArrayRegion] output bytes[" + this.len + "]: " + hexdump(this.buf, {length: this.len}));
}
}
});

// CallStaticObjectMethod at index 114 (offset 912)
Interceptor.attach(vtable.add(114 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
console.log("[CallStaticObjectMethod] class=" + args[1] + " methodID=" + args[2] + " arg0=" + args[3]);
},
onLeave: function(retval) {
console.log("[CallStaticObjectMethod] return=" + retval);
}
});

// GetByteArrayElements at index 184 (offset 1472)
Interceptor.attach(vtable.add(184 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
this.arr = args[1];
},
onLeave: function(retval) {
// 读返回的字节指针,尝试读 4-16 字节看看
try {
console.log("[GetByteArrayElements] arr=" + this.arr + " ptr=" + retval + " bytes: " + hexdump(retval, {length: 16, ansi: false}));
} catch(e) {
console.log("[GetByteArrayElements] read err: " + e);
}
}
});

// SetByteArrayRegion at index 208 (offset 1664)
Interceptor.attach(vtable.add(208 * Process.pointerSize).readPointer(), {
onEnter: function(args) {
var len = args[3].toUInt32();
console.log("[SetByteArrayRegion] len=" + len);
if (len > 0 && len < 256) {
console.log(" data: " + hexdump(args[4], {length: len}));
}
}
});
});

console.log("[+] 就绪 - 按OK");
});

为什么 hook 这些:

  • GetStaticMethodID (vtable[113]) → 抓方法名和签名
  • NewByteArray (vtable[176]) → 看分配的大小
  • SetByteArrayRegion (vtable[208]) → 看传给 Java 的字节
  • GetByteArrayElements (vtable[184]) → 看 Java 返回的字节

image-20260405143115558

sec2023把 X1 拆成 2 段 4 字节, 各自调 Java 静态方法 encrypt(byte[]) -> byte[] , 合并回 X1

确认输入输出对齐

在分析 sec2023 黑盒,已经知道:

  • 用户输入 ulong 进去
  • sec2023 里调了两次 Java encrypt([B)[B],每次处理 4 字节
  • 出来后 X1 变成了另一个 64-bit 值

但不知道sec2023 内部是怎么把两次 encrypt 的结果拼接成新 X1 的。

可能的拼法有好几种:

方案 A: X1’ = (encrypt[0] << 32) | encrypt[1] → hi=out0, lo=out1

方案 B: X1’ = (encrypt[1] << 32) | encrypt[0] → hi=out1, lo=out0

方案 C: X1’ 的字节 = out0 字节 ‖ out1 字节(内存拼接,不管uint32)

方案 D: 更复杂的交错/异或

因此这里是验证这个的

hook_verify_encrypt.js

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
// 对齐验证: thunk X1 原值 → 2次 encrypt 输出 → ValidateKey X1 变换后值
// 按 OK 一次,看三组数据是否吻合

function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libil2cpp.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

function hex8(x) { return ("00000000" + (x >>> 0).toString(16)).slice(-8); }

waitForLib(function (il2cpp) {
var sec = Module.findBaseAddress("libsec2023.so");

var encryptCalls = [];
var thunkX1 = null;

// 1) thunk 入口: 原始 X1
Interceptor.attach(il2cpp.add(0x465AB0), {
onEnter: function(args) {
thunkX1 = this.context.x1;
encryptCalls = [];
var lo = thunkX1.and(ptr(0xFFFFFFFF)).toUInt32();
var hi = thunkX1.shr(32).and(ptr(0xFFFFFFFF)).toUInt32();
console.log("\n========== 开始 ==========");
console.log("[thunk] X1 = 0x" + hex8(hi) + "_" + hex8(lo) + " (hi_lo)");
}
});

// 2) sub_3A924: encrypt 输入
Interceptor.attach(sec.add(0x3A924), {
onEnter: function(args) {
var buf = args[1];
var size = args[2].toUInt32();
if (size === 4) {
var inBytes = buf.readByteArray(4);
var inArr = new Uint8Array(inBytes);
var inHex = "";
for (var i = 0; i < 4; i++) inHex += ("0" + inArr[i].toString(16)).slice(-2);
console.log("[encrypt " + encryptCalls.length + "] IN = " + inHex);
this.callIdx = encryptCalls.length;
encryptCalls.push({in: inHex, out: null});
}
}
});

// 3) 拦截 GetByteArrayElements 抓 encrypt 输出
Java.perform(function() {
var env = Java.vm.getEnv();
var vtable = env.handle.readPointer();
Interceptor.attach(vtable.add(184 * Process.pointerSize).readPointer(), {
onLeave: function(retval) {
try {
var bytes = retval.readByteArray(4);
var arr = new Uint8Array(bytes);
var hex = "";
for (var i = 0; i < 4; i++) hex += ("0" + arr[i].toString(16)).slice(-2);
// 记录到最后一次 encrypt 调用
if (encryptCalls.length > 0 && encryptCalls[encryptCalls.length-1].out === null) {
encryptCalls[encryptCalls.length-1].out = hex;
console.log("[encrypt " + (encryptCalls.length-1) + "] OUT = " + hex);
}
} catch(e) {}
}
});
});

// 4) ValidateKey: 变换后 X1
Interceptor.attach(il2cpp.add(0x465AB4), {
onEnter: function(args) {
var x1 = this.context.x1;
var lo = x1.and(ptr(0xFFFFFFFF)).toUInt32();
var hi = x1.shr(32).and(ptr(0xFFFFFFFF)).toUInt32();
console.log("[ValidateKey] X1 = 0x" + hex8(hi) + "_" + hex8(lo) + " (hi_lo)");

// 对齐验证
if (encryptCalls.length >= 2 && encryptCalls[0].out && encryptCalls[1].out) {
console.log("\n--- 对齐验证 ---");
console.log("encrypt[0] IN: " + encryptCalls[0].in + " OUT: " + encryptCalls[0].out);
console.log("encrypt[1] IN: " + encryptCalls[1].in + " OUT: " + encryptCalls[1].out);
var combo1 = encryptCalls[0].out + encryptCalls[1].out;
var combo2 = encryptCalls[1].out + encryptCalls[0].out;
var x1Hex = hex8(hi) + hex8(lo);
var x1HexSwap = hex8(lo) + hex8(hi);
console.log("X1 (hi|lo): " + x1Hex);
console.log("X1 (lo|hi): " + x1HexSwap);
console.log("out0|out1: " + combo1 + " " + (combo1===x1Hex?"MATCH hi|lo":combo1===x1HexSwap?"MATCH lo|hi":""));
console.log("out1|out0: " + combo2 + " " + (combo2===x1Hex?"MATCH hi|lo":combo2===x1HexSwap?"MATCH lo|hi":""));
}
console.log("========== 结束 ==========\n");
}
});

console.log("[+] 就绪 - 按一次 OK");
});

证明 X1’ = (encrypt(v4[0]) | encrypt(v4[1])),且搞清字节序。

image-20260405145229940

这验证确认了 sec2023 的拼接规则:

1
2
X1' = bswap32(encrypt[1]) : bswap32(encrypt[0])
↑ 放 hi ↑ 放 lo
1
2
3
4
enc0_output = bswap32(X1_target.lo)   # 逆第 1 次拼接
enc1_output = bswap32(X1_target.hi) # 逆第 2 次拼接
v4[0] = java_decrypt(enc0_output) # 逆 encrypt
v4[1] = java_decrypt(enc1_output)

Java encrypt

再classes.dex 没找到encrypt相关信息,因此可能是被动态解密出来执行的,继续分析so层代码,既然 sub_3A924 的 a1+32 存着 jclass(从反编译看到 v9 = *(a1+32)),直接读出来用 Class.getName() 反查。

hook_resolve_class.js

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
// 读 sub_3A924 的 context (a1+32) 处的 jclass,用 Java 反射查类名
function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libsec2023.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

waitForLib(function(sec) {
Java.perform(function() {
var Class = Java.use("java.lang.Class");

Interceptor.attach(sec.add(0x3A924), {
onEnter: function(args) {
try {
var ctx = args[0]; // a1
var jclassPtr = ctx.add(32).readPointer(); // *(a1+32)
console.log("\n[sub_3A924] ctx=" + ctx + " jclass=" + jclassPtr);
Java.scheduleOnMainThread(function() {
try {
var cls = Java.cast(jclassPtr, Class);
console.log(" ==> class name: " + cls.getName());
// 列出所有方法
var methods = cls.getDeclaredMethods();
for (var i = 0; i < methods.length; i++) {
console.log(" method: " + methods[i].toString());
}
} catch(e) {
console.log(" cast err: " + e);
}
});
} catch(e) { console.log("err: " + e); }
}
});
});
console.log("[+] 就绪 - 按一次 OK");
});

image-20260405152122704

类名 sec2023.Encrypt,但没找到这个类

手动dump(当然可以用frida-dexdump,当时忘记了)

从进程内存扫描 dex\n035\0 magic (DEX header 标识),把整个 DEX 结构 dump 下来。

var magic = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00];

Memory.scanSync(range.base, range.size, pattern);

dump_encrypt_dex.js

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
// 从内存找 encrypt.dex: 扫描进程内存中的 DEX magic 'dex\n035\0'
Java.perform(function() {
// 找所有 rw 段,搜索 DEX magic
var magic = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00];
var pattern = magic.map(function(b){return ("0"+b.toString(16)).slice(-2);}).join(" ");
console.log("scanning for DEX magic...");

var ranges = Process.enumerateRanges({protection: 'r--', coalesce: true})
.concat(Process.enumerateRanges({protection: 'rw-', coalesce: true}));

var found = [];
ranges.forEach(function(r) {
if (r.size > 500*1024*1024) return;
try {
var res = Memory.scanSync(r.base, r.size, pattern);
res.forEach(function(h) { found.push(h.address); });
} catch(e) {}
});
console.log("found " + found.length + " DEX headers");

// 每个 DEX: file_size at offset 0x20 (4 bytes LE)
for (var i = 0; i < found.length; i++) {
var addr = found[i];
try {
var fileSize = addr.add(0x20).readU32();
if (fileSize < 0x20 || fileSize > 10*1024*1024) continue;
// 读 header, 检查看起来像真 DEX
console.log("[" + i + "] " + addr + " size=" + fileSize);
// 只 dump < 1MB 的, 避免 classes.dex (197KB 可能也中)
var bytes = addr.readByteArray(fileSize);
send({type: "dex", idx: i, addr: addr.toString(), size: fileSize}, bytes);
} catch(e) {}
}
console.log("done");
});

反编译出源码

image-20260405152350870

源码是控制流扁平化 + 字符串哈希 switch

只看每个 case 的实际数据操作(忽略状态跳转):

1
2
3
4
5
6
7
8
def encrypt(b):           # 4 bytes in
x = int.from_bytes(b, 'big') # 大端拼 uint32
x = ((x >> 7) | ((x & 0x7f) << 25)) & 0xFFFFFFFF # rotr 7 bits
out = list(x.to_bytes(4, 'big'))
KEY = [0x32, 0xCD, 0xFF, 0x98]
for i in range(4):
out[i] = ((out[i] ^ KEY[i]) + i) & 0xff # XOR key + add index
return bytes(out)
1
2
3
4
5
6
输入 6d 94 ca e4:
1. x = 0x6d94cae4
2. rotr(x, 7) = 0xC8DB2995
3. BE bytes: C8 DB 29 95
4. 0xC8^0x32+0 = FA 0xDB^0xCD+1 = 17 0x29^0xFF+2 = D8 0x95^0x98+3 = 10
输出 fa 17 d8 10 — 与 hook 抓的完全一致。

sub_3B9D4 分析 [巧解]

sub_3A924 是真正调 Java encrypt 的函数

sub_3B8CC中调用了sub_3A924

image-20260405155229470

分析到3B9D4(预处理函数)

静态追 VM dispatcher太大了,都是一块块的函数,需要处理,sub_3B9D4 看起来只有 BR X10(一个间接跳转)。手动追每条 BR 的目标会太慢。

可以用Unicorn 模拟,加载 libsec2023.so 到 Unicorn,直接跑 sub_3B9D4

坑点: raw .so 里的 dispatcher 表项是相对偏移,不是绝对地址。没应用重定位就跑,指令会跳到错误地址崩溃。可以用 pyelftools 解析 ELF 的 R_AARCH64_RELATIVE 重定位,加载后应用:

1
2
for off, addend in relocs:
struct.pack_into("<Q", image, off, LIB_BASE + addend)

emu_sub_3B9D4.py,这个函数用来探测用的,探测是否使用成功这个unicorn跑出来的结果是否跟frida hook的结果一致为后面打基础的

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
#!/usr/bin/env python3
import struct, sys
from unicorn import *
from unicorn.arm64_const import *
from elftools.elf.elffile import ELFFile
from elftools.elf.relocation import RelocationSection

LIB_PATH = "/tmp/apk_full/lib/arm64-v8a/libsec2023.so"
LIB_BASE = 0x100000000

with open(LIB_PATH, "rb") as f:
lib_data_raw = f.read()
f.seek(0)
elf = ELFFile(f)
# 加载所有 PT_LOAD segments 到一个平坦镜像
max_vaddr = 0
segs = []
for seg in elf.iter_segments():
if seg['p_type'] == 'PT_LOAD':
va = seg['p_vaddr']
sz = seg['p_memsz']
data = seg.data()
segs.append((va, data, sz))
max_vaddr = max(max_vaddr, va + sz)
# 收集 RELATIVE 重定位
relocs = []
for section in elf.iter_sections():
if isinstance(section, RelocationSection):
for rel in section.iter_relocations():
info_type = rel['r_info_type']
if info_type == 1027: # R_AARCH64_RELATIVE
relocs.append((rel['r_offset'], rel['r_addend']))
print(f"lib size (max_vaddr) = 0x{max_vaddr:x}")
print(f"RELATIVE relocations: {len(relocs)}")

# 构造平坦镜像
image_size = (max_vaddr + 0xFFF) & ~0xFFF
image = bytearray(image_size)
for va, data, sz in segs:
image[va:va+len(data)] = data

# 应用 RELATIVE 重定位: *(offset) = LIB_BASE + addend
for off, addend in relocs:
struct.pack_into("<Q", image, off, LIB_BASE + addend)


def emulate_preprocess(hi, lo, verbose=False):
mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu.mem_map(LIB_BASE, image_size)
mu.mem_write(LIB_BASE, bytes(image))

STACK_BASE = 0x200000000
STACK_SIZE = 0x10000
mu.mem_map(STACK_BASE, STACK_SIZE)
sp = STACK_BASE + STACK_SIZE - 0x200

# TLS for stack canary (TPIDR_EL0+40)
TLS_BASE = 0x300000000
mu.mem_map(TLS_BASE, 0x1000)
mu.mem_write(TLS_BASE + 40, struct.pack("<Q", 0xCAFEBABEDEADBEEF))
mu.reg_write(UC_ARM64_REG_TPIDR_EL0, TLS_BASE)

# v4 数组在栈上
v4_addr = sp - 0x20
mu.mem_write(v4_addr, struct.pack("<III", hi, lo, 0))

mu.reg_write(UC_ARM64_REG_X0, v4_addr)
mu.reg_write(UC_ARM64_REG_SP, sp)
DONE_MARK = 0xDEAD0000
mu.reg_write(UC_ARM64_REG_LR, DONE_MARK)

stats = {'count': 0, 'last_pc': 0}
if verbose:
def code_hook(uc, addr, size, user_data):
user_data['count'] += 1
user_data['last_pc'] = addr
if user_data['count'] <= 50 or user_data['count'] % 100 == 0:
print(f" [{user_data['count']:4d}] 0x{addr - LIB_BASE:x}")
mu.hook_add(UC_HOOK_CODE, code_hook, stats)
else:
def code_hook(uc, addr, size, user_data):
user_data['count'] += 1
user_data['last_pc'] = addr
mu.hook_add(UC_HOOK_CODE, code_hook, stats)

try:
mu.emu_start(LIB_BASE + 0x3B9D4, DONE_MARK, timeout=5*1000*1000, count=500000)
except UcError as e:
rel = stats['last_pc'] - LIB_BASE
print(f" UcError: {e} last_pc=0x{rel:x} count={stats['count']}")
return None

v4_out = mu.mem_read(v4_addr, 12)
out0, out1, out2 = struct.unpack("<III", v4_out)
print(f" OUT: [0]=0x{out0:08x} [1]=0x{out1:08x} [2]=0x{out2:08x} ({stats['count']} insns)")
return (out0, out1, out2)


if __name__ == "__main__":
print("Test 1: hi=0 lo=0x0000d828 (expect v4[1]=0x6d94020c)")
emulate_preprocess(0, 0x0000d828, verbose=True)
print("\nTest 2: hi=0 lo=0x000102b9 (expect v4[1]=0x6d95c89d)")
emulate_preprocess(0, 0x000102b9)
print("\nTest 3: hi=0 lo=0x00a98ac7 (expect v4[1]=0x6d3d50ab)")
emulate_preprocess(0, 0x00a98ac7)

image-20260405161816133

image-20260405161850629

用unicorn做函数结构探测,sub_3B9D4 对我们是个黑盒:能给它输入,能拿到输出,但不知道里面在算什么。

目标是搞清楚这个黑盒的结构,最好能写个 Python 版本代替它。最懒的方法不用小变化戳一下函数,看它怎么反应。改一个比特,看输出变多少,从而猜函数的结构

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
#!/usr/bin/env python3
"""Single-bit flip 测试: 探测 sub_3B9D4 的函数结构
结论: 每个输入字节只影响对应位置的输出字节 → 可以建字节查找表
"""
import struct
import sec2023
from unicorn import *
from unicorn.arm64_const import *

mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu.mem_map(sec2023.LIB_BASE, sec2023._IMAGE_SIZE)
mu.mem_write(sec2023.LIB_BASE, sec2023._IMAGE)
mu.mem_map(sec2023.STACK_BASE, sec2023.STACK_SIZE)
mu.mem_map(sec2023.TLS_BASE, 0x1000)
mu.mem_write(sec2023.TLS_BASE + 40, struct.pack("<Q", 0xCAFEBABE))
mu.reg_write(UC_ARM64_REG_TPIDR_EL0, sec2023.TLS_BASE)

sp = sec2023.STACK_BASE + sec2023.STACK_SIZE - 0x200
v4_addr = sp - 0x20

def f(hi, lo):
"""调用 sub_3B9D4, 返回 v4[1]"""
mu.mem_write(v4_addr, struct.pack("<III", hi, lo, 0))
mu.reg_write(UC_ARM64_REG_X0, v4_addr)
mu.reg_write(UC_ARM64_REG_SP, sp)
mu.reg_write(UC_ARM64_REG_LR, 0xDEAD0000)
mu.emu_start(sec2023.LIB_BASE + 0x3B9D4, 0xDEAD0000, count=500000)
return struct.unpack("<III", mu.mem_read(v4_addr, 12))[1]


if __name__ == "__main__":
print("=== single-bit flip tests (hi=0, 翻转 lo 的某一位) ===")
base = f(0, 0)
print(f"f(0, 0) = 0x{base:08x} (base)\n")
for bit in range(32):
val = 1 << bit
out = f(0, val)
diff = out ^ base
which_byte = None
for bp in range(4):
if diff & (0xff << (bp*8)):
which_byte = bp
break
print(f"bit {bit:2d} (val={val:#010x}) → out=0x{out:08x} diff=0x{diff:08x} 影响byte[{which_byte}]")

print("\n=== 结论 ===")
print("bits 0-7 → 只影响 byte[0]")
print("bits 8-15 → 只影响 byte[1]")
print("bits 16-23 → 只影响 byte[2]")
print("bits 24-31 → 只影响 byte[3]")
print("→ 函数是字节独立的, 可以用 4 张 256-entry 查找表表示")

基准点:先跑一次,记下啥都不输入的结果:f(0, 0) = 0x6d94cae4 base(基准输出)

然后每次只翻一位(bit flip),看输出变什么:

输入 lo = 0x00000001 (只在 bit 0 上是 1,其他 31 位都是 0)

基准: 0x6d94cae4 = 0110_1101 1001_0100 1100_1010 1110_0100

新的: 0x6d94cae5 = 0110_1101 1001_0100 1100_1010 1110_0101

只差一位

image-20260405165851338

输入 byte[i] 只影响输出 byte[i],其他字节不受影响

假如 sub_3B9D4 是加密算法(AES、DES),翻 1 位输入,整个 32 位输出都会变得面目全非(这叫扩散,是加密算法的特征)。

但我们看到的情况翻 1 位输入,只影响 1 个字节输出。这说明 sub_3B9D4 不是加密,而是 4 个独立的字节变换串起来,每个 fi 是一个独立的 256 → 256 映射函数。

既然是 4 个独立的 f0, f1, f2, f3,每个只关心一个字节(256 种可能),穷举所有输入就能完整记录函数行为:

1
2
3
4
for bp in 0..3:                  # 4 个字节位置
for val in 0..255: # 该位置的所有可能字节值
测试输入 val << (bp*8)
记录: table[bp][val] = 对应输出字节

总共 4 × 256 = 1024 次调用 Unicorn,就能完整覆盖这个函数。

然后这 4 张表就代替了整个 sub_3B9D4 函数,keygen 只需要查表,不需要 Unicorn 了。 注意是 8 个 bit 影响同一个字节。这 8 个 bit 合起来就是输入 byte[0]。输入 byte[i] 的 8 个 bit 合起来,决定输出 byte[i] 的 8 个 bit,是一一对应的关系。

输入 byte[0] (8 bit 当一个 0255 的数) → 输出 byte[0] (8 bit 当一个 0255 的数)

建表

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
#!/usr/bin/env python3
import struct, json
import sec2023
from unicorn import *
from unicorn.arm64_const import *

# 初始化 Unicorn (只创建一次, 重用 mu 实例)
mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu.mem_map(sec2023.LIB_BASE, sec2023._IMAGE_SIZE)
mu.mem_write(sec2023.LIB_BASE, sec2023._IMAGE)
mu.mem_map(sec2023.STACK_BASE, sec2023.STACK_SIZE)
mu.mem_map(sec2023.TLS_BASE, 0x1000)
mu.mem_write(sec2023.TLS_BASE + 40, struct.pack("<Q", 0xCAFEBABE))
mu.reg_write(UC_ARM64_REG_TPIDR_EL0, sec2023.TLS_BASE)

sp = sec2023.STACK_BASE + sec2023.STACK_SIZE - 0x200
v4_addr = sp - 0x20

def f(hi, lo):
"""调 sub_3B9D4(v4), 返回 v4[1]"""
mu.mem_write(v4_addr, struct.pack("<III", hi, lo, 0))
mu.reg_write(UC_ARM64_REG_X0, v4_addr)
mu.reg_write(UC_ARM64_REG_SP, sp)
mu.reg_write(UC_ARM64_REG_LR, 0xDEAD0000)
mu.emu_start(sec2023.LIB_BASE + 0x3B9D4, 0xDEAD0000, count=500000)
return struct.unpack("<III", mu.mem_read(v4_addr, 12))[1]

# ===== 建正表 =====
print("Building forward tables (1024 Unicorn calls)...")
tables = [[0]*256 for _ in range(4)]
for bp in range(4): # 4 个字节位置
for val in range(256): # 每个位置 256 种输入
inp = val << (bp*8) # 只在 byte[bp] 放 val, 其他字节 = 0
out = f(0, inp)
tables[bp][val] = (out >> (bp*8)) & 0xFF

# 验证: 每张表是否双射 (256 个输出值全不同)
for bp in range(4):
unique = len(set(tables[bp]))
status = "BIJECTIVE" if unique == 256 else "NOT BIJECTIVE"
print(f" table[{bp}]: {unique}/256 unique [{status}]")

# ===== 建逆表 =====
inv = [[0]*256 for _ in range(4)]
for bp in range(4):
for in_byte in range(256):
out_byte = tables[bp][in_byte]
inv[bp][out_byte] = in_byte

# ===== 保存 JSON =====
with open('sub_3b9d4_tables.json', 'w') as fp:
json.dump({'forward': tables, 'inverse': inv}, fp)
print("Saved to sub_3b9d4_tables.json")

# ===== 自测: 用表预测, 和 Unicorn 对比 =====
print("\n=== Self-test ===")
def apply_table(x, t):
out = 0
for bp in range(4):
out |= t[bp][(x >> (bp*8)) & 0xFF] << (bp*8)
return out

for test_lo in [0, 0x0000d828, 0x12345678, 0xDEADBEEF, 0xFFFFFFFF]:
actual = f(0, test_lo)
predicted = apply_table(test_lo, tables)
# 逆向测试: 用逆表还原
recovered = apply_table(predicted, inv)
ok_fwd = "OK" if actual == predicted else "FAIL"
ok_inv = "OK" if recovered == test_lo else "FAIL"
print(f" lo=0x{test_lo:08x} actual=0x{actual:08x} predict=0x{predicted:08x} [{ok_fwd}] recover=0x{recovered:08x} [{ok_inv}]")

这个{“forward”: [[228, 229, 230, 231 的前几个意思就是0对应228,1对应229.3对应230

这个表是二维数组sub_3B9D4 是 4 个字节独立映射,4 个不同的函数,每个是 256 → 256 的映射,inverse是逆表

1
{"forward": [[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, 0, 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], [202, 203, 200, 201, 206, 207, 204, 205, 210, 211, 208, 209, 214, 215, 212, 213, 186, 187, 184, 185, 190, 191, 188, 189, 194, 195, 192, 193, 198, 199, 196, 197, 234, 235, 232, 233, 238, 239, 236, 237, 242, 243, 240, 241, 246, 247, 244, 245, 218, 219, 216, 217, 222, 223, 220, 221, 226, 227, 224, 225, 230, 231, 228, 229, 138, 139, 136, 137, 142, 143, 140, 141, 146, 147, 144, 145, 150, 151, 148, 149, 122, 123, 120, 121, 126, 127, 124, 125, 130, 131, 128, 129, 134, 135, 132, 133, 170, 171, 168, 169, 174, 175, 172, 173, 178, 179, 176, 177, 182, 183, 180, 181, 154, 155, 152, 153, 158, 159, 156, 157, 162, 163, 160, 161, 166, 167, 164, 165, 74, 75, 72, 73, 78, 79, 76, 77, 82, 83, 80, 81, 86, 87, 84, 85, 58, 59, 56, 57, 62, 63, 60, 61, 66, 67, 64, 65, 70, 71, 68, 69, 106, 107, 104, 105, 110, 111, 108, 109, 114, 115, 112, 113, 118, 119, 116, 117, 90, 91, 88, 89, 94, 95, 92, 93, 98, 99, 96, 97, 102, 103, 100, 101, 10, 11, 8, 9, 14, 15, 12, 13, 18, 19, 16, 17, 22, 23, 20, 21, 250, 251, 248, 249, 254, 255, 252, 253, 2, 3, 0, 1, 6, 7, 4, 5, 42, 43, 40, 41, 46, 47, 44, 45, 50, 51, 48, 49, 54, 55, 52, 53, 26, 27, 24, 25, 30, 31, 28, 29, 34, 35, 32, 33, 38, 39, 36, 37], [148, 149, 146, 147, 152, 153, 150, 151, 156, 157, 154, 155, 160, 161, 158, 159, 164, 165, 162, 163, 168, 169, 166, 167, 172, 173, 170, 171, 176, 177, 174, 175, 180, 181, 178, 179, 184, 185, 182, 183, 188, 189, 186, 187, 192, 193, 190, 191, 196, 197, 194, 195, 200, 201, 198, 199, 204, 205, 202, 203, 208, 209, 206, 207, 212, 213, 210, 211, 216, 217, 214, 215, 220, 221, 218, 219, 224, 225, 222, 223, 228, 229, 226, 227, 232, 233, 230, 231, 236, 237, 234, 235, 240, 241, 238, 239, 244, 245, 242, 243, 248, 249, 246, 247, 252, 253, 250, 251, 0, 1, 254, 255, 4, 5, 2, 3, 8, 9, 6, 7, 12, 13, 10, 11, 16, 17, 14, 15, 20, 21, 18, 19, 24, 25, 22, 23, 28, 29, 26, 27, 32, 33, 30, 31, 36, 37, 34, 35, 40, 41, 38, 39, 44, 45, 42, 43, 48, 49, 46, 47, 52, 53, 50, 51, 56, 57, 54, 55, 60, 61, 58, 59, 64, 65, 62, 63, 68, 69, 66, 67, 72, 73, 70, 71, 76, 77, 74, 75, 80, 81, 78, 79, 84, 85, 82, 83, 88, 89, 86, 87, 92, 93, 90, 91, 96, 97, 94, 95, 100, 101, 98, 99, 104, 105, 102, 103, 108, 109, 106, 107, 112, 113, 110, 111, 116, 117, 114, 115, 120, 121, 118, 119, 124, 125, 122, 123, 128, 129, 126, 127, 132, 133, 130, 131, 136, 137, 134, 135, 140, 141, 138, 139, 144, 145, 142, 143], [109, 108, 111, 110, 105, 104, 107, 106, 117, 116, 119, 118, 113, 112, 115, 114, 125, 124, 127, 126, 121, 120, 123, 122, 133, 132, 135, 134, 129, 128, 131, 130, 141, 140, 143, 142, 137, 136, 139, 138, 149, 148, 151, 150, 145, 144, 147, 146, 157, 156, 159, 158, 153, 152, 155, 154, 165, 164, 167, 166, 161, 160, 163, 162, 173, 172, 175, 174, 169, 168, 171, 170, 181, 180, 183, 182, 177, 176, 179, 178, 189, 188, 191, 190, 185, 184, 187, 186, 197, 196, 199, 198, 193, 192, 195, 194, 205, 204, 207, 206, 201, 200, 203, 202, 213, 212, 215, 214, 209, 208, 211, 210, 221, 220, 223, 222, 217, 216, 219, 218, 229, 228, 231, 230, 225, 224, 227, 226, 237, 236, 239, 238, 233, 232, 235, 234, 245, 244, 247, 246, 241, 240, 243, 242, 253, 252, 255, 254, 249, 248, 251, 250, 5, 4, 7, 6, 1, 0, 3, 2, 13, 12, 15, 14, 9, 8, 11, 10, 21, 20, 23, 22, 17, 16, 19, 18, 29, 28, 31, 30, 25, 24, 27, 26, 37, 36, 39, 38, 33, 32, 35, 34, 45, 44, 47, 46, 41, 40, 43, 42, 53, 52, 55, 54, 49, 48, 51, 50, 61, 60, 63, 62, 57, 56, 59, 58, 69, 68, 71, 70, 65, 64, 67, 66, 77, 76, 79, 78, 73, 72, 75, 74, 85, 84, 87, 86, 81, 80, 83, 82, 93, 92, 95, 94, 89, 88, 91, 90, 101, 100, 103, 102, 97, 96, 99, 98]], "inverse": [[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, 0, 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], [218, 219, 216, 217, 222, 223, 220, 221, 194, 195, 192, 193, 198, 199, 196, 197, 202, 203, 200, 201, 206, 207, 204, 205, 242, 243, 240, 241, 246, 247, 244, 245, 250, 251, 248, 249, 254, 255, 252, 253, 226, 227, 224, 225, 230, 231, 228, 229, 234, 235, 232, 233, 238, 239, 236, 237, 146, 147, 144, 145, 150, 151, 148, 149, 154, 155, 152, 153, 158, 159, 156, 157, 130, 131, 128, 129, 134, 135, 132, 133, 138, 139, 136, 137, 142, 143, 140, 141, 178, 179, 176, 177, 182, 183, 180, 181, 186, 187, 184, 185, 190, 191, 188, 189, 162, 163, 160, 161, 166, 167, 164, 165, 170, 171, 168, 169, 174, 175, 172, 173, 82, 83, 80, 81, 86, 87, 84, 85, 90, 91, 88, 89, 94, 95, 92, 93, 66, 67, 64, 65, 70, 71, 68, 69, 74, 75, 72, 73, 78, 79, 76, 77, 114, 115, 112, 113, 118, 119, 116, 117, 122, 123, 120, 121, 126, 127, 124, 125, 98, 99, 96, 97, 102, 103, 100, 101, 106, 107, 104, 105, 110, 111, 108, 109, 18, 19, 16, 17, 22, 23, 20, 21, 26, 27, 24, 25, 30, 31, 28, 29, 2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9, 14, 15, 12, 13, 50, 51, 48, 49, 54, 55, 52, 53, 58, 59, 56, 57, 62, 63, 60, 61, 34, 35, 32, 33, 38, 39, 36, 37, 42, 43, 40, 41, 46, 47, 44, 45, 210, 211, 208, 209, 214, 215, 212, 213], [108, 109, 114, 115, 112, 113, 118, 119, 116, 117, 122, 123, 120, 121, 126, 127, 124, 125, 130, 131, 128, 129, 134, 135, 132, 133, 138, 139, 136, 137, 142, 143, 140, 141, 146, 147, 144, 145, 150, 151, 148, 149, 154, 155, 152, 153, 158, 159, 156, 157, 162, 163, 160, 161, 166, 167, 164, 165, 170, 171, 168, 169, 174, 175, 172, 173, 178, 179, 176, 177, 182, 183, 180, 181, 186, 187, 184, 185, 190, 191, 188, 189, 194, 195, 192, 193, 198, 199, 196, 197, 202, 203, 200, 201, 206, 207, 204, 205, 210, 211, 208, 209, 214, 215, 212, 213, 218, 219, 216, 217, 222, 223, 220, 221, 226, 227, 224, 225, 230, 231, 228, 229, 234, 235, 232, 233, 238, 239, 236, 237, 242, 243, 240, 241, 246, 247, 244, 245, 250, 251, 248, 249, 254, 255, 252, 253, 2, 3, 0, 1, 6, 7, 4, 5, 10, 11, 8, 9, 14, 15, 12, 13, 18, 19, 16, 17, 22, 23, 20, 21, 26, 27, 24, 25, 30, 31, 28, 29, 34, 35, 32, 33, 38, 39, 36, 37, 42, 43, 40, 41, 46, 47, 44, 45, 50, 51, 48, 49, 54, 55, 52, 53, 58, 59, 56, 57, 62, 63, 60, 61, 66, 67, 64, 65, 70, 71, 68, 69, 74, 75, 72, 73, 78, 79, 76, 77, 82, 83, 80, 81, 86, 87, 84, 85, 90, 91, 88, 89, 94, 95, 92, 93, 98, 99, 96, 97, 102, 103, 100, 101, 106, 107, 104, 105, 110, 111], [157, 156, 159, 158, 153, 152, 155, 154, 165, 164, 167, 166, 161, 160, 163, 162, 173, 172, 175, 174, 169, 168, 171, 170, 181, 180, 183, 182, 177, 176, 179, 178, 189, 188, 191, 190, 185, 184, 187, 186, 197, 196, 199, 198, 193, 192, 195, 194, 205, 204, 207, 206, 201, 200, 203, 202, 213, 212, 215, 214, 209, 208, 211, 210, 221, 220, 223, 222, 217, 216, 219, 218, 229, 228, 231, 230, 225, 224, 227, 226, 237, 236, 239, 238, 233, 232, 235, 234, 245, 244, 247, 246, 241, 240, 243, 242, 253, 252, 255, 254, 249, 248, 251, 250, 5, 4, 7, 6, 1, 0, 3, 2, 13, 12, 15, 14, 9, 8, 11, 10, 21, 20, 23, 22, 17, 16, 19, 18, 29, 28, 31, 30, 25, 24, 27, 26, 37, 36, 39, 38, 33, 32, 35, 34, 45, 44, 47, 46, 41, 40, 43, 42, 53, 52, 55, 54, 49, 48, 51, 50, 61, 60, 63, 62, 57, 56, 59, 58, 69, 68, 71, 70, 65, 64, 67, 66, 77, 76, 79, 78, 73, 72, 75, 74, 85, 84, 87, 86, 81, 80, 83, 82, 93, 92, 95, 94, 89, 88, 91, 90, 101, 100, 103, 102, 97, 96, 99, 98, 109, 108, 111, 110, 105, 104, 107, 106, 117, 116, 119, 118, 113, 112, 115, 114, 125, 124, 127, 126, 121, 120, 123, 122, 133, 132, 135, 134, 129, 128, 131, 130, 141, 140, 143, 142, 137, 136, 139, 138, 149, 148, 151, 150, 145, 144, 147, 146]]}

去混淆 [常规解法]

IDA 只能看到 br X10,根本不知道跳到哪,因为目标地址是运行时算出来的, 既然 IDA 搞不定,就用 Unicorn 跑一遍,可以装载 .so 文件、设置寄存器、从指定地址开始执行。每执行一条指令都可以”钩住”(hook),让我们打印出来。混淆代码跑起来的时候,br X10 里的 X10 是个具体的值(比如 0x3ba34),CPU 会照着跳。我们 hook 住每条指令就行

找基本块(从 BR 边构建 CFG)

基本块 = 一段指令的直线序列,中间没有跳转,末尾以跳转结尾。

扫描 trace,找所有 br/b/ret 指令。这些跳转的目标地址就是一个新块的起点。

目标地址 = 7 个基本块的起始地址。看到自循环(0x3ba70 → 0x3ba34)说明这是内部循环。

1
2
3
4
for i, ins in enumerate(trace):
if ins.is_jump:
edge = (ins.addr, trace[i+1].addr) # 从当前地址跳到下一条指令的地址
edges.add(edge)

混淆后的每个基本块里,指令分两类

dispatcher 指令(骨架,没用):

  • adrl/adrp X9, 0x72C40 — 加载跳转表基址
  • ldr X10, [X9, X12] — 从跳转表查地址
  • mov W11, #0x740078FC — 加固定常量
  • add X10, X10, X11 — 算出真实目标
  • br X10 — 跳
  • csel / cset / cmp — 分支选择

业务指令(真正的函数逻辑):

  • ldrb / strb — 读写字节
  • eor / and / or — 异或与或
  • add w10, w14, w10 — 算术
  • lsr / lsl — 移位
  • sub w11, w14, #0x5e — 减法(带小常量)

原理: dispatcher 指令都有明显的模式(用特定寄存器如 X9/X10、涉及固定常量 0x740078FC 等)。过滤掉这些,剩下的就是真实逻辑。

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
import struct
from unicorn import *
from unicorn.arm64_const import *
from capstone import *
import sec2023

md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)

# -- Unicorn trace --
mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu.mem_map(sec2023.LIB_BASE, sec2023._IMAGE_SIZE)
mu.mem_write(sec2023.LIB_BASE, sec2023._IMAGE)
mu.mem_map(sec2023.STACK_BASE, sec2023.STACK_SIZE)
mu.mem_map(sec2023.TLS_BASE, 0x1000)
mu.mem_write(sec2023.TLS_BASE + 40, struct.pack("<Q", 0xCAFEBABE))
mu.reg_write(UC_ARM64_REG_TPIDR_EL0, sec2023.TLS_BASE)
sp = sec2023.STACK_BASE + sec2023.STACK_SIZE - 0x200
v4_addr = sp - 0x20

trace = []
def hook(uc, addr, size, _):
rel = addr - sec2023.LIB_BASE
ins_bytes = bytes(uc.mem_read(addr, size))
for ins in md.disasm(ins_bytes, addr):
trace.append((rel, ins.mnemonic, ins.op_str))

mu.hook_add(UC_HOOK_CODE, hook)
mu.mem_write(v4_addr, struct.pack("<III", 0, 0x0000d828, 0))
mu.reg_write(UC_ARM64_REG_X0, v4_addr)
mu.reg_write(UC_ARM64_REG_SP, sp)
mu.reg_write(UC_ARM64_REG_LR, 0xDEAD0000)
mu.emu_start(sec2023.LIB_BASE + 0x3B9D4, 0xDEAD0000, count=500000)


# -- 识别 dispatcher 指令 (要删掉的) --
DISPATCH_ADDRS = set() # 记录哪些地址的指令是 dispatcher 骨架

# dispatcher 通用模式:
# ADRL X9, off_72XXX (加载跳转表)
# LDR Xn, [X9, Xm] (查表)
# MOV Wn, #0x740078FC (加常量)
# ADD Xn, Xn, Xm (计算目标)
# BR Xn (跳)
# CSEL / CSET / CMP (选择分支)
# MOV X8/X9/X10/X11/X13 立即数 用于 dispatch 状态
for off, mnem, ops in trace:
# ADRL/ADRP 加载表
if mnem in ('adrl', 'adrp'):
DISPATCH_ADDRS.add(off)
# LDR xN, [x9, ...] 是表查
elif mnem == 'ldr' and '[x9' in ops:
DISPATCH_ADDRS.add(off)
# MOV wN, #0x740078FC (dispatcher 常量)
elif mnem == 'mov' and '#0x740078fc' in ops.lower():
DISPATCH_ADDRS.add(off)
# MOV wN, #0x78fc / movk 构造 0x740078fc
elif mnem in ('mov', 'movk') and ('#0x78fc' in ops or '#0x7400' in ops):
DISPATCH_ADDRS.add(off)
# ADD xN, xN, xM 最后计算跳转目标 (目标寄存器是 x12/x13)
elif mnem == 'add' and ops.startswith(('x12,', 'x13,')) and ', x' in ops and '#' not in ops:
DISPATCH_ADDRS.add(off)
# BR xN 间接跳转
elif mnem == 'br':
DISPATCH_ADDRS.add(off)
# CSEL / CSET / CMP 分支逻辑
elif mnem in ('csel', 'cset', 'cmp') and 'sp' not in ops:
DISPATCH_ADDRS.add(off)
# MOV x8, xzr 和 add x8, x8, #1 是 phase counter
elif mnem in ('mov',) and ('xzr' in ops and 'x8' in ops.split(',')[0]):
pass # 保留
# STR WZR, [x0, x8, lsl #2] 是写 v4[x8] 前的清零, 保留
# MOV wN, #常量 中 0x78fc 部分已经排除, 剩下小常量 (#3, #0x18, #8, #0x10, #0x20) 是 loop 计数器
# 这些是 LOOP STATE, 保留

# -- 按"出现顺序"去重输出 --
# 第一次到的指令保留原始顺序, 之后重复的都跳过
seen = set()
uniq_trace = []
for off, mnem, ops in trace:
key = (off, mnem, ops)
if key not in seen:
seen.add(key)
uniq_trace.append((off, mnem, ops))

# -- 按基本块分组 --
# 基本块起始 = BR 的目标 (再加上函数入口)
block_starts = {0x3b9d4}
for i, (off, mnem, ops) in enumerate(trace[:-1]):
if mnem in ('br', 'b', 'bl', 'blr', 'ret'):
nxt = trace[i+1][0]
if nxt < 0x70000:
block_starts.add(nxt)

# 每块的业务指令 (按地址排)
block_ins = {}
for off, mnem, ops in sorted(uniq_trace):
# 找属于哪个块: 最大的不超过 off 的 block_start
owner = max((bs for bs in block_starts if bs <= off), default=None)
if owner is None:
continue
# 跳过 dispatcher
if off in DISPATCH_ADDRS:
continue
# 跳过 SP 相关
if mnem in ('sub', 'add') and 'sp,' in ops:
continue
if mnem == 'ret':
continue
block_ins.setdefault(owner, []).append((off, mnem, ops))

# -- 输出去混淆后的汇编 --
print(";" + "="*68)
print("; libsec2023.so::sub_3B9D4 - 去混淆后的汇编 (只保留业务指令)")
print("; 原函数: 390 条指令 (含 dispatcher), 逆向后: 下面这些")
print(";" + "="*68)
print()

labels = {
0x3b9d4: 'sub_3B9D4',
0x3ba04: 'phase_start', # 循环回到这里 (x8 < 2 时)
0x3ba34: 'extract_loop', # byte-extract 循环头
0x3ba74: 'byte_transform', # 字节变换
0x3bae0: 'recompose_loop', # 重组循环头
0x3bb2c: 'phase_check', # x8++ 检查
0x3bb50: 'epilog', # 函数尾
}

for bs in sorted(block_starts):
label = labels.get(bs, f'block_{bs:x}')
print(f"{label}: ; 0x{bs:05x}")
ins_list = block_ins.get(bs, [])
if not ins_list:
print(f" ; (pure dispatcher, no business code)")
else:
for off, mnem, ops in ins_list:
print(f" {mnem:7s} {ops:30s} ; 0x{off:05x}")
print()

print(";" + "="*68)
print("; 注: sub_3B9D4 是外层 2 次 phase 循环 (处理 hi, lo)")
print("; 每 phase 里有 2 个内循环: extract_loop 和 recompose_loop 各 4 次")
print(";" + "="*68)

image-20260405200332685

实际逻辑

image-20260405200822095

实现注册机

python

全流程是user_input → sec2023 → X1’ → VM_forward → TEA_encrypt → (token, 0)

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
import json, os, sys

M32 = 0xFFFFFFFF
def u32(x): return x & M32

# ===================== TEA =====================
TEA_KEY = [0x7B777C63, 0xC56F6BF2, 0x2B670130, 0x76ABD7FE]
TEA_DELTA = 0xDEADBEEF
TEA_INIT_SUM = 0xBEEFBEEF
TEA_INIT_KS = 0x9D9D7DDE
TEA_ROUNDS = 64

def tea_encrypt(W21, W22):
s, ks = TEA_INIT_SUM, TEA_INIT_KS
for _ in range(TEA_ROUNDS):
t = u32(u32(u32(W22 << 7) ^ (W22 >> 8)) + W22)
W21 = u32(W21 + (u32(s - TEA_KEY[s & 3]) ^ t))
t2 = u32(u32(u32(W21 << 8) ^ (W21 >> 7)) - W21)
W22 = u32(W22 + (u32(ks + TEA_KEY[(ks >> 13) & 3]) ^ t2))
s, ks = u32(s + TEA_DELTA), u32(ks + TEA_DELTA)
return W21, W22

def tea_decrypt(W21, W22):
s = u32(TEA_INIT_SUM + TEA_ROUNDS * TEA_DELTA)
ks = u32(TEA_INIT_KS + TEA_ROUNDS * TEA_DELTA)
for _ in range(TEA_ROUNDS):
s, ks = u32(s - TEA_DELTA), u32(ks - TEA_DELTA)
t2 = u32(u32(u32(W21 << 8) ^ (W21 >> 7)) - W21)
W22 = u32(W22 - (u32(ks + TEA_KEY[(ks >> 13) & 3]) ^ t2))
t = u32(u32(u32(W22 << 7) ^ (W22 >> 8)) + W22)
W21 = u32(W21 - (u32(s - TEA_KEY[s & 3]) ^ t))
return W21, W22

# ===================== VM (Oo0 class bytecode) =====================

def vm_forward(val32, sub_c, xor1_c, add_c, xor2_c, mix_op='xor'):
b0, b1, b2, b3 = val32 & 0xFF, (val32>>8)&0xFF, (val32>>16)&0xFF, (val32>>24)&0xFF
regs = [(b0 - sub_c) & 0xFF, (b1 ^ xor1_c) & 0xFF, (b2 + add_c) & 0xFF, (b3 ^ xor2_c) & 0xFF]
result = 0
for i, r in enumerate(regs):
shift = i * 8
mixed = (r ^ shift) & 0xFF if mix_op == 'xor' else (r + shift) & 0xFF
result |= mixed << shift
return u32(result)

def vm_inverse(target, sub_c, xor1_c, add_c, xor2_c, mix_op='xor'):
regs = []
for i in range(4):
shift = i * 8
b = (target >> shift) & 0xFF
if mix_op == 'xor':
regs.append(b ^ shift)
else:
regs.append((b - shift) & 0xFF)
byte0 = (regs[0] + sub_c) & 0xFF
byte1 = (regs[1] ^ xor1_c) & 0xFF
byte2 = (regs[2] - add_c) & 0xFF
byte3 = (regs[3] ^ xor2_c) & 0xFF
return byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24)

# ===================== Java encrypt (encrypt.dex) =====================
_JENC_KEY = [0x32, 0xCD, 0xFF, 0x98]

def java_encrypt(x):
x = ((x >> 7) | ((x & 0x7f) << 25)) & 0xFFFFFFFF
bs = [(x>>24)&0xff, (x>>16)&0xff, (x>>8)&0xff, x&0xff]
for i in range(4):
bs[i] = ((bs[i] ^ _JENC_KEY[i]) + i) & 0xff
return (bs[0]<<24) | (bs[1]<<16) | (bs[2]<<8) | bs[3]

def java_decrypt(y):
bs = [(y>>24)&0xff, (y>>16)&0xff, (y>>8)&0xff, y&0xff]
for i in range(4):
bs[i] = ((bs[i] - i) & 0xff) ^ _JENC_KEY[i]
y = (bs[0]<<24)|(bs[1]<<16)|(bs[2]<<8)|bs[3]
return ((y << 7) | (y >> 25)) & 0xFFFFFFFF

# ===================== sub_3B9D4 (字节查找表) =====================
_tbl_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sub_3b9d4_tables.json')
with open(_tbl_path) as _f:
_T = json.load(_f)
_FWD = _T['forward'] # _FWD[byte_pos][in_byte] = out_byte
_INV = _T['inverse'] # _INV[byte_pos][out_byte] = in_byte

def sub3b9d4_fwd(x):
out = 0
for bp in range(4):
out |= _FWD[bp][(x >> (bp*8)) & 0xFF] << (bp*8)
return out

def sub3b9d4_inv(x):
out = 0
for bp in range(4):
out |= _INV[bp][(x >> (bp*8)) & 0xFF] << (bp*8)
return out

# ===================== sec2023 完整变换 =====================
def _bswap(u):
return ((u & 0xff) << 24) | (((u >> 8) & 0xff) << 16) | (((u >> 16) & 0xff) << 8) | ((u >> 24) & 0xff)

def sec2023_forward(user_ulong):
"""给用户输入 ulong, 返回 sec2023 变换后的 X1' (lo_u32, hi_u32)"""
hi = (user_ulong >> 32) & M32
lo = user_ulong & M32
v0 = sub3b9d4_fwd(hi)
v1 = sub3b9d4_fwd(lo)
x1_lo = _bswap(java_encrypt(v0))
x1_hi = _bswap(java_encrypt(v1))
return x1_lo, x1_hi

def sec2023_inverse(x1_lo, x1_hi):
"""给定 X1' (lo_u32, hi_u32), 反推出用户输入 (hi_u32, lo_u32)"""
v0 = java_decrypt(_bswap(x1_lo))
v1 = java_decrypt(_bswap(x1_hi))
user_hi = sub3b9d4_inv(v0)
user_lo = sub3b9d4_inv(v1)
return user_hi, user_lo

# ===================== keygen 主函数 =====================
def keygen(token):
"""token (int) -> user_input (int, 64-bit)"""
target = token & M32
# TEA 逆: TEA 输出 (target, 0) 对应的输入
reg0, reg1 = tea_decrypt(target, 0)
# VM 逆: 两段 mixing 不同
x1_lo = vm_inverse(reg0, 27, 0xC2, 0xA8, 0x36, mix_op='xor')
x1_hi = vm_inverse(reg1, 47, 0xB6, 0x37, 0x98, mix_op='add')
# sec2023 逆
user_hi, user_lo = sec2023_inverse(x1_lo, x1_hi)
return (user_hi << 32) | user_lo

def verify(user_input, token):
"""校验: user_input 经过完整正向变换是否等于 (token, 0)"""
target = token & M32
x1_lo, x1_hi = sec2023_forward(user_input)
reg0 = vm_forward(x1_lo, 27, 0xC2, 0xA8, 0x36, mix_op='xor')
reg1 = vm_forward(x1_hi, 47, 0xB6, 0x37, 0x98, mix_op='add')
return tea_encrypt(reg0, reg1) == (target, 0)

# ===================== CLI =====================
if __name__ == '__main__':
if len(sys.argv) > 1:
tokens = [int(x, 0) for x in sys.argv[1:]]
else:
print("用法: python3 keygen_standalone.py <token> [<token>...]")
print("示例: python3 keygen_standalone.py 13732582\n")
tokens = [13732582, 55336, 12345]

for tok in tokens:
key = keygen(tok)
ok = verify(key, tok)
print(f"token = {tok} (0x{tok:x})")
print(f"user = {key} (0x{key:x}) [{'PASS' if ok else 'FAIL'}]")
print()

C++

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
// 编译: g++ -O2 -o keygen keygen_standalone.cpp
// 运行: ./keygen 13732582
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>

typedef uint8_t u8;
typedef uint32_t u32;
typedef uint64_t u64;

// ===================== sub_3B9D4 查表 (embedded) =====================
static const uint8_t FWD_TBL[4][256] = {
{ 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3 },
{ 0xca, 0xcb, 0xc8, 0xc9, 0xce, 0xcf, 0xcc, 0xcd, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5, 0xba, 0xbb, 0xb8, 0xb9, 0xbe, 0xbf, 0xbc, 0xbd, 0xc2, 0xc3, 0xc0, 0xc1, 0xc6, 0xc7, 0xc4, 0xc5, 0xea, 0xeb, 0xe8, 0xe9, 0xee, 0xef, 0xec, 0xed, 0xf2, 0xf3, 0xf0, 0xf1, 0xf6, 0xf7, 0xf4, 0xf5, 0xda, 0xdb, 0xd8, 0xd9, 0xde, 0xdf, 0xdc, 0xdd, 0xe2, 0xe3, 0xe0, 0xe1, 0xe6, 0xe7, 0xe4, 0xe5, 0x8a, 0x8b, 0x88, 0x89, 0x8e, 0x8f, 0x8c, 0x8d, 0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94, 0x95, 0x7a, 0x7b, 0x78, 0x79, 0x7e, 0x7f, 0x7c, 0x7d, 0x82, 0x83, 0x80, 0x81, 0x86, 0x87, 0x84, 0x85, 0xaa, 0xab, 0xa8, 0xa9, 0xae, 0xaf, 0xac, 0xad, 0xb2, 0xb3, 0xb0, 0xb1, 0xb6, 0xb7, 0xb4, 0xb5, 0x9a, 0x9b, 0x98, 0x99, 0x9e, 0x9f, 0x9c, 0x9d, 0xa2, 0xa3, 0xa0, 0xa1, 0xa6, 0xa7, 0xa4, 0xa5, 0x4a, 0x4b, 0x48, 0x49, 0x4e, 0x4f, 0x4c, 0x4d, 0x52, 0x53, 0x50, 0x51, 0x56, 0x57, 0x54, 0x55, 0x3a, 0x3b, 0x38, 0x39, 0x3e, 0x3f, 0x3c, 0x3d, 0x42, 0x43, 0x40, 0x41, 0x46, 0x47, 0x44, 0x45, 0x6a, 0x6b, 0x68, 0x69, 0x6e, 0x6f, 0x6c, 0x6d, 0x72, 0x73, 0x70, 0x71, 0x76, 0x77, 0x74, 0x75, 0x5a, 0x5b, 0x58, 0x59, 0x5e, 0x5f, 0x5c, 0x5d, 0x62, 0x63, 0x60, 0x61, 0x66, 0x67, 0x64, 0x65, 0x0a, 0x0b, 0x08, 0x09, 0x0e, 0x0f, 0x0c, 0x0d, 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0xfa, 0xfb, 0xf8, 0xf9, 0xfe, 0xff, 0xfc, 0xfd, 0x02, 0x03, 0x00, 0x01, 0x06, 0x07, 0x04, 0x05, 0x2a, 0x2b, 0x28, 0x29, 0x2e, 0x2f, 0x2c, 0x2d, 0x32, 0x33, 0x30, 0x31, 0x36, 0x37, 0x34, 0x35, 0x1a, 0x1b, 0x18, 0x19, 0x1e, 0x1f, 0x1c, 0x1d, 0x22, 0x23, 0x20, 0x21, 0x26, 0x27, 0x24, 0x25 },
{ 0x94, 0x95, 0x92, 0x93, 0x98, 0x99, 0x96, 0x97, 0x9c, 0x9d, 0x9a, 0x9b, 0xa0, 0xa1, 0x9e, 0x9f, 0xa4, 0xa5, 0xa2, 0xa3, 0xa8, 0xa9, 0xa6, 0xa7, 0xac, 0xad, 0xaa, 0xab, 0xb0, 0xb1, 0xae, 0xaf, 0xb4, 0xb5, 0xb2, 0xb3, 0xb8, 0xb9, 0xb6, 0xb7, 0xbc, 0xbd, 0xba, 0xbb, 0xc0, 0xc1, 0xbe, 0xbf, 0xc4, 0xc5, 0xc2, 0xc3, 0xc8, 0xc9, 0xc6, 0xc7, 0xcc, 0xcd, 0xca, 0xcb, 0xd0, 0xd1, 0xce, 0xcf, 0xd4, 0xd5, 0xd2, 0xd3, 0xd8, 0xd9, 0xd6, 0xd7, 0xdc, 0xdd, 0xda, 0xdb, 0xe0, 0xe1, 0xde, 0xdf, 0xe4, 0xe5, 0xe2, 0xe3, 0xe8, 0xe9, 0xe6, 0xe7, 0xec, 0xed, 0xea, 0xeb, 0xf0, 0xf1, 0xee, 0xef, 0xf4, 0xf5, 0xf2, 0xf3, 0xf8, 0xf9, 0xf6, 0xf7, 0xfc, 0xfd, 0xfa, 0xfb, 0x00, 0x01, 0xfe, 0xff, 0x04, 0x05, 0x02, 0x03, 0x08, 0x09, 0x06, 0x07, 0x0c, 0x0d, 0x0a, 0x0b, 0x10, 0x11, 0x0e, 0x0f, 0x14, 0x15, 0x12, 0x13, 0x18, 0x19, 0x16, 0x17, 0x1c, 0x1d, 0x1a, 0x1b, 0x20, 0x21, 0x1e, 0x1f, 0x24, 0x25, 0x22, 0x23, 0x28, 0x29, 0x26, 0x27, 0x2c, 0x2d, 0x2a, 0x2b, 0x30, 0x31, 0x2e, 0x2f, 0x34, 0x35, 0x32, 0x33, 0x38, 0x39, 0x36, 0x37, 0x3c, 0x3d, 0x3a, 0x3b, 0x40, 0x41, 0x3e, 0x3f, 0x44, 0x45, 0x42, 0x43, 0x48, 0x49, 0x46, 0x47, 0x4c, 0x4d, 0x4a, 0x4b, 0x50, 0x51, 0x4e, 0x4f, 0x54, 0x55, 0x52, 0x53, 0x58, 0x59, 0x56, 0x57, 0x5c, 0x5d, 0x5a, 0x5b, 0x60, 0x61, 0x5e, 0x5f, 0x64, 0x65, 0x62, 0x63, 0x68, 0x69, 0x66, 0x67, 0x6c, 0x6d, 0x6a, 0x6b, 0x70, 0x71, 0x6e, 0x6f, 0x74, 0x75, 0x72, 0x73, 0x78, 0x79, 0x76, 0x77, 0x7c, 0x7d, 0x7a, 0x7b, 0x80, 0x81, 0x7e, 0x7f, 0x84, 0x85, 0x82, 0x83, 0x88, 0x89, 0x86, 0x87, 0x8c, 0x8d, 0x8a, 0x8b, 0x90, 0x91, 0x8e, 0x8f },
{ 0x6d, 0x6c, 0x6f, 0x6e, 0x69, 0x68, 0x6b, 0x6a, 0x75, 0x74, 0x77, 0x76, 0x71, 0x70, 0x73, 0x72, 0x7d, 0x7c, 0x7f, 0x7e, 0x79, 0x78, 0x7b, 0x7a, 0x85, 0x84, 0x87, 0x86, 0x81, 0x80, 0x83, 0x82, 0x8d, 0x8c, 0x8f, 0x8e, 0x89, 0x88, 0x8b, 0x8a, 0x95, 0x94, 0x97, 0x96, 0x91, 0x90, 0x93, 0x92, 0x9d, 0x9c, 0x9f, 0x9e, 0x99, 0x98, 0x9b, 0x9a, 0xa5, 0xa4, 0xa7, 0xa6, 0xa1, 0xa0, 0xa3, 0xa2, 0xad, 0xac, 0xaf, 0xae, 0xa9, 0xa8, 0xab, 0xaa, 0xb5, 0xb4, 0xb7, 0xb6, 0xb1, 0xb0, 0xb3, 0xb2, 0xbd, 0xbc, 0xbf, 0xbe, 0xb9, 0xb8, 0xbb, 0xba, 0xc5, 0xc4, 0xc7, 0xc6, 0xc1, 0xc0, 0xc3, 0xc2, 0xcd, 0xcc, 0xcf, 0xce, 0xc9, 0xc8, 0xcb, 0xca, 0xd5, 0xd4, 0xd7, 0xd6, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0xd9, 0xd8, 0xdb, 0xda, 0xe5, 0xe4, 0xe7, 0xe6, 0xe1, 0xe0, 0xe3, 0xe2, 0xed, 0xec, 0xef, 0xee, 0xe9, 0xe8, 0xeb, 0xea, 0xf5, 0xf4, 0xf7, 0xf6, 0xf1, 0xf0, 0xf3, 0xf2, 0xfd, 0xfc, 0xff, 0xfe, 0xf9, 0xf8, 0xfb, 0xfa, 0x05, 0x04, 0x07, 0x06, 0x01, 0x00, 0x03, 0x02, 0x0d, 0x0c, 0x0f, 0x0e, 0x09, 0x08, 0x0b, 0x0a, 0x15, 0x14, 0x17, 0x16, 0x11, 0x10, 0x13, 0x12, 0x1d, 0x1c, 0x1f, 0x1e, 0x19, 0x18, 0x1b, 0x1a, 0x25, 0x24, 0x27, 0x26, 0x21, 0x20, 0x23, 0x22, 0x2d, 0x2c, 0x2f, 0x2e, 0x29, 0x28, 0x2b, 0x2a, 0x35, 0x34, 0x37, 0x36, 0x31, 0x30, 0x33, 0x32, 0x3d, 0x3c, 0x3f, 0x3e, 0x39, 0x38, 0x3b, 0x3a, 0x45, 0x44, 0x47, 0x46, 0x41, 0x40, 0x43, 0x42, 0x4d, 0x4c, 0x4f, 0x4e, 0x49, 0x48, 0x4b, 0x4a, 0x55, 0x54, 0x57, 0x56, 0x51, 0x50, 0x53, 0x52, 0x5d, 0x5c, 0x5f, 0x5e, 0x59, 0x58, 0x5b, 0x5a, 0x65, 0x64, 0x67, 0x66, 0x61, 0x60, 0x63, 0x62 },
};

static const uint8_t INV_TBL[4][256] = {
{ 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b },
{ 0xda, 0xdb, 0xd8, 0xd9, 0xde, 0xdf, 0xdc, 0xdd, 0xc2, 0xc3, 0xc0, 0xc1, 0xc6, 0xc7, 0xc4, 0xc5, 0xca, 0xcb, 0xc8, 0xc9, 0xce, 0xcf, 0xcc, 0xcd, 0xf2, 0xf3, 0xf0, 0xf1, 0xf6, 0xf7, 0xf4, 0xf5, 0xfa, 0xfb, 0xf8, 0xf9, 0xfe, 0xff, 0xfc, 0xfd, 0xe2, 0xe3, 0xe0, 0xe1, 0xe6, 0xe7, 0xe4, 0xe5, 0xea, 0xeb, 0xe8, 0xe9, 0xee, 0xef, 0xec, 0xed, 0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94, 0x95, 0x9a, 0x9b, 0x98, 0x99, 0x9e, 0x9f, 0x9c, 0x9d, 0x82, 0x83, 0x80, 0x81, 0x86, 0x87, 0x84, 0x85, 0x8a, 0x8b, 0x88, 0x89, 0x8e, 0x8f, 0x8c, 0x8d, 0xb2, 0xb3, 0xb0, 0xb1, 0xb6, 0xb7, 0xb4, 0xb5, 0xba, 0xbb, 0xb8, 0xb9, 0xbe, 0xbf, 0xbc, 0xbd, 0xa2, 0xa3, 0xa0, 0xa1, 0xa6, 0xa7, 0xa4, 0xa5, 0xaa, 0xab, 0xa8, 0xa9, 0xae, 0xaf, 0xac, 0xad, 0x52, 0x53, 0x50, 0x51, 0x56, 0x57, 0x54, 0x55, 0x5a, 0x5b, 0x58, 0x59, 0x5e, 0x5f, 0x5c, 0x5d, 0x42, 0x43, 0x40, 0x41, 0x46, 0x47, 0x44, 0x45, 0x4a, 0x4b, 0x48, 0x49, 0x4e, 0x4f, 0x4c, 0x4d, 0x72, 0x73, 0x70, 0x71, 0x76, 0x77, 0x74, 0x75, 0x7a, 0x7b, 0x78, 0x79, 0x7e, 0x7f, 0x7c, 0x7d, 0x62, 0x63, 0x60, 0x61, 0x66, 0x67, 0x64, 0x65, 0x6a, 0x6b, 0x68, 0x69, 0x6e, 0x6f, 0x6c, 0x6d, 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1a, 0x1b, 0x18, 0x19, 0x1e, 0x1f, 0x1c, 0x1d, 0x02, 0x03, 0x00, 0x01, 0x06, 0x07, 0x04, 0x05, 0x0a, 0x0b, 0x08, 0x09, 0x0e, 0x0f, 0x0c, 0x0d, 0x32, 0x33, 0x30, 0x31, 0x36, 0x37, 0x34, 0x35, 0x3a, 0x3b, 0x38, 0x39, 0x3e, 0x3f, 0x3c, 0x3d, 0x22, 0x23, 0x20, 0x21, 0x26, 0x27, 0x24, 0x25, 0x2a, 0x2b, 0x28, 0x29, 0x2e, 0x2f, 0x2c, 0x2d, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5 },
{ 0x6c, 0x6d, 0x72, 0x73, 0x70, 0x71, 0x76, 0x77, 0x74, 0x75, 0x7a, 0x7b, 0x78, 0x79, 0x7e, 0x7f, 0x7c, 0x7d, 0x82, 0x83, 0x80, 0x81, 0x86, 0x87, 0x84, 0x85, 0x8a, 0x8b, 0x88, 0x89, 0x8e, 0x8f, 0x8c, 0x8d, 0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94, 0x95, 0x9a, 0x9b, 0x98, 0x99, 0x9e, 0x9f, 0x9c, 0x9d, 0xa2, 0xa3, 0xa0, 0xa1, 0xa6, 0xa7, 0xa4, 0xa5, 0xaa, 0xab, 0xa8, 0xa9, 0xae, 0xaf, 0xac, 0xad, 0xb2, 0xb3, 0xb0, 0xb1, 0xb6, 0xb7, 0xb4, 0xb5, 0xba, 0xbb, 0xb8, 0xb9, 0xbe, 0xbf, 0xbc, 0xbd, 0xc2, 0xc3, 0xc0, 0xc1, 0xc6, 0xc7, 0xc4, 0xc5, 0xca, 0xcb, 0xc8, 0xc9, 0xce, 0xcf, 0xcc, 0xcd, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5, 0xda, 0xdb, 0xd8, 0xd9, 0xde, 0xdf, 0xdc, 0xdd, 0xe2, 0xe3, 0xe0, 0xe1, 0xe6, 0xe7, 0xe4, 0xe5, 0xea, 0xeb, 0xe8, 0xe9, 0xee, 0xef, 0xec, 0xed, 0xf2, 0xf3, 0xf0, 0xf1, 0xf6, 0xf7, 0xf4, 0xf5, 0xfa, 0xfb, 0xf8, 0xf9, 0xfe, 0xff, 0xfc, 0xfd, 0x02, 0x03, 0x00, 0x01, 0x06, 0x07, 0x04, 0x05, 0x0a, 0x0b, 0x08, 0x09, 0x0e, 0x0f, 0x0c, 0x0d, 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1a, 0x1b, 0x18, 0x19, 0x1e, 0x1f, 0x1c, 0x1d, 0x22, 0x23, 0x20, 0x21, 0x26, 0x27, 0x24, 0x25, 0x2a, 0x2b, 0x28, 0x29, 0x2e, 0x2f, 0x2c, 0x2d, 0x32, 0x33, 0x30, 0x31, 0x36, 0x37, 0x34, 0x35, 0x3a, 0x3b, 0x38, 0x39, 0x3e, 0x3f, 0x3c, 0x3d, 0x42, 0x43, 0x40, 0x41, 0x46, 0x47, 0x44, 0x45, 0x4a, 0x4b, 0x48, 0x49, 0x4e, 0x4f, 0x4c, 0x4d, 0x52, 0x53, 0x50, 0x51, 0x56, 0x57, 0x54, 0x55, 0x5a, 0x5b, 0x58, 0x59, 0x5e, 0x5f, 0x5c, 0x5d, 0x62, 0x63, 0x60, 0x61, 0x66, 0x67, 0x64, 0x65, 0x6a, 0x6b, 0x68, 0x69, 0x6e, 0x6f },
{ 0x9d, 0x9c, 0x9f, 0x9e, 0x99, 0x98, 0x9b, 0x9a, 0xa5, 0xa4, 0xa7, 0xa6, 0xa1, 0xa0, 0xa3, 0xa2, 0xad, 0xac, 0xaf, 0xae, 0xa9, 0xa8, 0xab, 0xaa, 0xb5, 0xb4, 0xb7, 0xb6, 0xb1, 0xb0, 0xb3, 0xb2, 0xbd, 0xbc, 0xbf, 0xbe, 0xb9, 0xb8, 0xbb, 0xba, 0xc5, 0xc4, 0xc7, 0xc6, 0xc1, 0xc0, 0xc3, 0xc2, 0xcd, 0xcc, 0xcf, 0xce, 0xc9, 0xc8, 0xcb, 0xca, 0xd5, 0xd4, 0xd7, 0xd6, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0xd9, 0xd8, 0xdb, 0xda, 0xe5, 0xe4, 0xe7, 0xe6, 0xe1, 0xe0, 0xe3, 0xe2, 0xed, 0xec, 0xef, 0xee, 0xe9, 0xe8, 0xeb, 0xea, 0xf5, 0xf4, 0xf7, 0xf6, 0xf1, 0xf0, 0xf3, 0xf2, 0xfd, 0xfc, 0xff, 0xfe, 0xf9, 0xf8, 0xfb, 0xfa, 0x05, 0x04, 0x07, 0x06, 0x01, 0x00, 0x03, 0x02, 0x0d, 0x0c, 0x0f, 0x0e, 0x09, 0x08, 0x0b, 0x0a, 0x15, 0x14, 0x17, 0x16, 0x11, 0x10, 0x13, 0x12, 0x1d, 0x1c, 0x1f, 0x1e, 0x19, 0x18, 0x1b, 0x1a, 0x25, 0x24, 0x27, 0x26, 0x21, 0x20, 0x23, 0x22, 0x2d, 0x2c, 0x2f, 0x2e, 0x29, 0x28, 0x2b, 0x2a, 0x35, 0x34, 0x37, 0x36, 0x31, 0x30, 0x33, 0x32, 0x3d, 0x3c, 0x3f, 0x3e, 0x39, 0x38, 0x3b, 0x3a, 0x45, 0x44, 0x47, 0x46, 0x41, 0x40, 0x43, 0x42, 0x4d, 0x4c, 0x4f, 0x4e, 0x49, 0x48, 0x4b, 0x4a, 0x55, 0x54, 0x57, 0x56, 0x51, 0x50, 0x53, 0x52, 0x5d, 0x5c, 0x5f, 0x5e, 0x59, 0x58, 0x5b, 0x5a, 0x65, 0x64, 0x67, 0x66, 0x61, 0x60, 0x63, 0x62, 0x6d, 0x6c, 0x6f, 0x6e, 0x69, 0x68, 0x6b, 0x6a, 0x75, 0x74, 0x77, 0x76, 0x71, 0x70, 0x73, 0x72, 0x7d, 0x7c, 0x7f, 0x7e, 0x79, 0x78, 0x7b, 0x7a, 0x85, 0x84, 0x87, 0x86, 0x81, 0x80, 0x83, 0x82, 0x8d, 0x8c, 0x8f, 0x8e, 0x89, 0x88, 0x8b, 0x8a, 0x95, 0x94, 0x97, 0x96, 0x91, 0x90, 0x93, 0x92 },
};

// ===================== TEA =====================
static const u32 TEA_KEY[4] = { 0x7B777C63u, 0xC56F6BF2u, 0x2B670130u, 0x76ABD7FEu };
static const u32 TEA_DELTA = 0xDEADBEEFu;
static const u32 TEA_INIT_SUM = 0xBEEFBEEFu;
static const u32 TEA_INIT_KS = 0x9D9D7DDEu;
static const int TEA_ROUNDS = 64;

static void tea_encrypt(u32 &W21, u32 &W22) {
u32 s = TEA_INIT_SUM, ks = TEA_INIT_KS;
for (int i = 0; i < TEA_ROUNDS; ++i) {
u32 t = ((W22 << 7) ^ (W22 >> 8)) + W22;
W21 = W21 + ((s - TEA_KEY[s & 3]) ^ t);
u32 t2 = ((W21 << 8) ^ (W21 >> 7)) - W21;
W22 = W22 + ((ks + TEA_KEY[(ks >> 13) & 3]) ^ t2);
s += TEA_DELTA;
ks += TEA_DELTA;
}
}

static void tea_decrypt(u32 &W21, u32 &W22) {
u32 s = TEA_INIT_SUM + (u32)TEA_ROUNDS * TEA_DELTA;
u32 ks = TEA_INIT_KS + (u32)TEA_ROUNDS * TEA_DELTA;
for (int i = 0; i < TEA_ROUNDS; ++i) {
s -= TEA_DELTA;
ks -= TEA_DELTA;
u32 t2 = ((W21 << 8) ^ (W21 >> 7)) - W21;
W22 = W22 - (((ks + TEA_KEY[(ks >> 13) & 3]) ^ t2));
u32 t = ((W22 << 7) ^ (W22 >> 8)) + W22;
W21 = W21 - (((s - TEA_KEY[s & 3]) ^ t));
}
}

// ===================== VM (Oo0 bytecode) =====================
// 前半: sub=27, xor1=0xC2, add=0xA8, xor2=0x36, 重组用 XOR shift
// 后半: sub=0x2F, xor1=0xB6, add=0x37, xor2=0x98, 重组用 ADD shift
enum MixOp { MIX_XOR, MIX_ADD };

static u32 vm_inverse(u32 target, u8 sub_c, u8 xor1_c, u8 add_c, u8 xor2_c, MixOp mix) {
u8 regs[4];
for (int i = 0; i < 4; ++i) {
int shift = i * 8;
u8 b = (u8)(target >> shift);
regs[i] = (mix == MIX_XOR) ? (u8)(b ^ shift) : (u8)(b - shift);
}
u8 b0 = (u8)(regs[0] + sub_c);
u8 b1 = (u8)(regs[1] ^ xor1_c);
u8 b2 = (u8)(regs[2] - add_c);
u8 b3 = (u8)(regs[3] ^ xor2_c);
return (u32)b0 | ((u32)b1 << 8) | ((u32)b2 << 16) | ((u32)b3 << 24);
}

static u32 vm_forward(u32 val32, u8 sub_c, u8 xor1_c, u8 add_c, u8 xor2_c, MixOp mix) {
u8 b0 = (u8)val32, b1 = (u8)(val32 >> 8), b2 = (u8)(val32 >> 16), b3 = (u8)(val32 >> 24);
u8 regs[4] = { (u8)(b0 - sub_c), (u8)(b1 ^ xor1_c), (u8)(b2 + add_c), (u8)(b3 ^ xor2_c) };
u32 result = 0;
for (int i = 0; i < 4; ++i) {
int shift = i * 8;
u8 mixed = (mix == MIX_XOR) ? (u8)(regs[i] ^ shift) : (u8)(regs[i] + shift);
result |= (u32)mixed << shift;
}
return result;
}

// ===================== Java encrypt (encrypt.dex) =====================
static const u8 JENC_KEY[4] = { 0x32, 0xCD, 0xFF, 0x98 };

static u32 java_encrypt(u32 x) {
x = (x >> 7) | ((x & 0x7F) << 25); // rotr 7
u8 b[4] = { (u8)(x >> 24), (u8)(x >> 16), (u8)(x >> 8), (u8)x };
for (int i = 0; i < 4; ++i)
b[i] = (u8)((b[i] ^ JENC_KEY[i]) + i);
return ((u32)b[0] << 24) | ((u32)b[1] << 16) | ((u32)b[2] << 8) | b[3];
}

static u32 java_decrypt(u32 y) {
u8 b[4] = { (u8)(y >> 24), (u8)(y >> 16), (u8)(y >> 8), (u8)y };
for (int i = 0; i < 4; ++i)
b[i] = (u8)(((u8)(b[i] - i)) ^ JENC_KEY[i]);
u32 r = ((u32)b[0] << 24) | ((u32)b[1] << 16) | ((u32)b[2] << 8) | b[3];
return (r << 7) | (r >> 25); // rotl 7
}

// ===================== sub_3B9D4 查表 =====================
static u32 sub3b9d4_fwd(u32 x) {
u32 out = 0;
for (int bp = 0; bp < 4; ++bp)
out |= (u32)FWD_TBL[bp][(u8)(x >> (bp * 8))] << (bp * 8);
return out;
}

static u32 sub3b9d4_inv(u32 x) {
u32 out = 0;
for (int bp = 0; bp < 4; ++bp)
out |= (u32)INV_TBL[bp][(u8)(x >> (bp * 8))] << (bp * 8);
return out;
}

// ===================== sec2023 =====================
static u32 bswap32(u32 u) {
return ((u & 0xff) << 24) | (((u >> 8) & 0xff) << 16) | (((u >> 16) & 0xff) << 8) | (u >> 24);
}

static void sec2023_forward(u64 user_ulong, u32 &x1_lo, u32 &x1_hi) {
u32 hi = (u32)(user_ulong >> 32);
u32 lo = (u32)user_ulong;
u32 v0 = sub3b9d4_fwd(hi);
u32 v1 = sub3b9d4_fwd(lo);
x1_lo = bswap32(java_encrypt(v0));
x1_hi = bswap32(java_encrypt(v1));
}

static void sec2023_inverse(u32 x1_lo, u32 x1_hi, u32 &user_hi, u32 &user_lo) {
u32 v0 = java_decrypt(bswap32(x1_lo));
u32 v1 = java_decrypt(bswap32(x1_hi));
user_hi = sub3b9d4_inv(v0);
user_lo = sub3b9d4_inv(v1);
}

// ===================== keygen =====================
static u64 keygen(u32 token) {
u32 reg0 = token, reg1 = 0;
tea_decrypt(reg0, reg1);
u32 x1_lo = vm_inverse(reg0, 27, 0xC2, 0xA8, 0x36, MIX_XOR);
u32 x1_hi = vm_inverse(reg1, 0x2F, 0xB6, 0x37, 0x98, MIX_ADD);
u32 user_hi, user_lo;
sec2023_inverse(x1_lo, x1_hi, user_hi, user_lo);
return ((u64)user_hi << 32) | user_lo;
}

static bool verify(u64 user_input, u32 token) {
u32 x1_lo, x1_hi;
sec2023_forward(user_input, x1_lo, x1_hi);
u32 reg0 = vm_forward(x1_lo, 27, 0xC2, 0xA8, 0x36, MIX_XOR);
u32 reg1 = vm_forward(x1_hi, 0x2F, 0xB6, 0x37, 0x98, MIX_ADD);
tea_encrypt(reg0, reg1);
return reg0 == token && reg1 == 0;
}

// ===================== main =====================
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "用法: %s <token> [<token>...]\n", argv[0]);
fprintf(stderr, "示例: %s 13732582\n", argv[0]);
return 1;
}
for (int i = 1; i < argc; ++i) {
u32 tok = (u32)strtoul(argv[i], nullptr, 0);
u64 key = keygen(tok);
bool ok = verify(key, tok);
printf("token = %u (0x%x)\n", tok, tok);
printf("user = %llu (0x%llx) [%s]\n\n",
(unsigned long long)key, (unsigned long long)key, ok ? "PASS" : "FAIL");
}
return 0;
}

无敌模式触发flag

输入注册机生成的key,就触发无敌模式了

image-20260405123249310

反调试分析

整个会话里我们多次看到Process terminated

不管 hook 什么,进程经常点 OK 后就被 kill

很可能代码完整性校验,不然应该一进游戏就被kill了

写一个脚本去trace,故意触发反调试,再去hook了libc.so的退出的操作

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
// v2: 先 hook sec2023 触发反调试, 再监控 exit 类函数被谁调用
Java.perform(function() {
console.log("[+] installing exit monitors first...");

// 1. hook 所有退出函数 (必须在触发反调试的 hook 之前装!)
['exit', '_exit', 'abort', 'raise'].forEach(function(name) {
var addr = Module.findExportByName("libc.so", name);
if (!addr) return;
Interceptor.attach(addr, {
onEnter: function(args) {
console.log("\n[!!!] libc." + name + "(" + args[0] + ")");
console.log("backtrace:");
Thread.backtrace(this.context, Backtracer.ACCURATE).forEach(function(a){
var sec = Module.findBaseAddress("libsec2023.so");
var il2cpp = Module.findBaseAddress("libil2cpp.so");
var rel_sec = a.sub(sec).toUInt32 ? a.sub(sec).toUInt32() : 0xFFFFFFFF;
var rel_il = a.sub(il2cpp).toUInt32 ? a.sub(il2cpp).toUInt32() : 0xFFFFFFFF;
if (rel_sec < 0x100000) {
console.log(" sec2023+0x" + rel_sec.toString(16));
} else if (rel_il < 0x2000000) {
console.log(" il2cpp+0x" + rel_il.toString(16));
} else {
console.log(" " + a + " " + DebugSymbol.fromAddress(a));
}
});
}
});
});
console.log("[+] exit monitors ready");

// 2. 装一个触发反调试的 hook
var sec = Module.findBaseAddress("libsec2023.so");
if (sec) {
Interceptor.attach(sec.add(0x3A924), {
onEnter: function(args) {
console.log("[hook@3A924] args[0]=" + args[0]);
}
});
console.log("[+] trigger hook on sec2023+0x3A924 installed");
}

console.log("[+] press OK now, watch for exit trace");
});

image-20260405203730242

sec2023 绕过 libc,用直接 syscall 杀自己

静态找 SVC 指令,既然是裸 syscall,那必然有 SVC 指令。用 IDA 扫整个 libsec2023.so 的 .text 段:

1
2
3
for ea in .text:                                                
if mnem == 'SVC':
记录

整个 libsec2023.so 里只有 1 条 SVC 指令,在 0x20FEC。

image-20260405204551208

image-20260405204607240

这是 syscall wrapper。参数从 X0 拿 syscall number,其他参数在 X1-X6。所有 syscall 都通过它。

xref 到 sub_20FD0,找出 12 个 caller:

1
2
3
4
5
6
7
sub_1F3A4 : syscall 0x38 (56=openat)                            
sub_1F3BC : syscall 0x38 (openat)
sub_1F3D8 : syscall 0x40 (64=write)
sub_1F400 : syscall 0x3F (63=read)
sub_1F42C : syscall 0x39 (57=close)
sub_36784 : ???
sub_367B0 : syscall 0xAC, 0x81 ← 可疑!

每个 caller 在调 sub_20FD0 前会 MOV W0, #syscall_nr。看每个的 syscall 号,对照 AArch64 Linux syscall table。sub_367B0 用 0xAC (172=getpid) 和 0x81 (129=kill) — 完全符合自杀模式。

image-20260405204917715

1
2
3
4
5
6
7
BL  sub_36818           ; 做点事 (后面会看)                 
MOV W0, #0xAC ; syscall 172 = getpid
BL sub_20FD0 ; → 返回 pid 到 X0
MOV W1, W0 ; W1 = pid
MOV W0, #0x81 ; syscall 129 = kill
MOV W2, #9 ; signal 9 = SIGKILL
B sub_20FD0 ; kill(pid, 9)

sub_3681中

image-20260405205006365

还会sleep2这也是为什么2s后才会退出

trace确认下

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
// 检查 sub_367B0 是否真的走到 getpid/kill
function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libsec2023.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

waitForLib(function(sec) {
// hook sub_367B0 入口
Interceptor.attach(sec.add(0x367B0), {
onEnter: function(args) {
console.log("\n[+] sub_367B0 ENTER, arg=0x" + args[0].toString(16));
console.log("backtrace:");
Thread.backtrace(this.context, Backtracer.ACCURATE).forEach(function(a){
var rel = a.sub(sec).toUInt32 ? a.sub(sec).toUInt32() : 0xFFFFFFFF;
if (rel < 0x100000) console.log(" sec+0x" + rel.toString(16));
else console.log(" " + a);
});
},
onLeave: function(retval) {
console.log("[+] sub_367B0 LEAVE ret=" + retval);
}
});

// hook 0x367F8 (MOV W0, #0xAC 即 getpid 前)
Interceptor.attach(sec.add(0x367F8), {
onEnter: function() {
console.log("[!!!] reached 0x367F8 (about to call getpid)");
Thread.backtrace(this.context, Backtracer.ACCURATE).forEach(function(a){
var rel = a.sub(sec).toUInt32 ? a.sub(sec).toUInt32() : 0xFFFFFFFF;
console.log(" sec+0x" + rel.toString(16));
});
}
});

// hook 0x36808 (kill syscall)
Interceptor.attach(sec.add(0x36808), {
onEnter: function() {
console.log("[!!!] reached 0x36808 (about to kill(pid, 9))");
}
});

// hook sub_36818 - 中间的函数
Interceptor.attach(sec.add(0x36818), {
onEnter: function() {
console.log(" [+] sub_36818 ENTER");
},
onLeave: function(rv) {
console.log(" [+] sub_36818 LEAVE ret=" + rv);
}
});

console.log("[+] press OK");
});

image-20260405205105555

sec+0x364c4 调 sub_367B0 的地方

image-20260405205357663

1
2
3
4
5
6
sec+0x364c4 (在 sub_364B0 里):                                  
LDR X8, [X19,#0x88] ; 从对象 +0x88 取函数指针
LDR W9, [X28,#0x20] ; 取密钥
LDR W0, [X20,#8] ; 取 arg = 1
EOR X8, X8, X23 ; 解密函数指针
BLR X8 ; 调用! X8 = sub_367B0

函数指针是加密存储的,XOR 解密后才调。这就是为什么静态 xref 找不到这条调用路径。

hooksyscall下看看

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
// Hook sec2023 的 syscall wrapper, 记录所有 syscall
function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libsec2023.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

// ARM64 Linux syscall number table
var SYSCALLS = {
17: 'getcwd', 29: 'ioctl', 38: 'renameat', 48: 'faccessat',
49: 'chdir', 56: 'openat', 57: 'close', 62: 'lseek', 63: 'read',
64: 'write', 65: 'readv', 66: 'writev', 78: 'readlinkat', 79: 'fstatat',
80: 'fstat', 93: 'exit', 94: 'exit_group', 96: 'set_tid_address',
99: 'set_robust_list', 113: 'clock_gettime', 115: 'clock_nanosleep',
117: 'ptrace', 129: 'kill', 130: 'tkill', 131: 'tgkill',
134: 'sigaction', 135: 'sigprocmask', 139: 'sigreturn',
147: 'setpriority', 153: 'times', 160: 'uname', 167: 'prctl',
172: 'getpid', 178: 'gettid', 179: 'sysinfo', 183: 'getcwd',
214: 'brk', 215: 'munmap', 220: 'clone', 221: 'execve',
222: 'mmap', 226: 'mprotect', 281: 'getrandom'
};

waitForLib(function(sec) {
var il = Module.findBaseAddress("libil2cpp.so");

Interceptor.attach(sec.add(0x20FD0), {
onEnter: function(args) {
// syscall number is in X0 at entry
var nr = this.context.x0.toInt32();
var x1 = this.context.x1;
var x2 = this.context.x2;
var x3 = this.context.x3;
var name = SYSCALLS[nr] || ('sys' + nr);

// 特别关注的 syscall: 读文件 / 检测
var interesting = [56, 57, 63, 64, 78, 79, 80, 117, 167, 226];

// 非关注的只记计数
if (interesting.indexOf(nr) < 0) {
return;
}

var info = "[syscall " + nr + "=" + name + "] ";

// 对 openat / readlinkat 读路径字符串
if (nr === 56 || nr === 78) {
// openat(dirfd, pathname, flags, mode)
try {
var path = x2.readCString();
info += 'path="' + path + '"';
} catch(e) {}
} else if (nr === 63) {
// read(fd, buf, count)
info += 'fd=' + x1 + ' count=' + x3;
} else if (nr === 226) {
// mprotect(addr, len, prot)
info += 'addr=' + x1 + ' len=' + x2 + ' prot=' + x3;
} else if (nr === 117) {
// ptrace(request, pid, addr, data)
info += 'req=' + x1 + ' pid=' + x2;
} else if (nr === 167) {
// prctl
info += 'opt=' + x1 + ' arg=' + x2;
}

console.log(info);

// backtrace for context
Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0, 4).forEach(function(a){
var rs = a.sub(sec).toUInt32 ? a.sub(sec).toUInt32() : 0xFFFFFFFF;
if (rs < 0x100000) console.log(" from sec+0x" + rs.toString(16));
});
}
});

// 也 hook sub_3A924 触发反调试
Interceptor.attach(sec.add(0x3A924), {
onEnter: function() { console.log("[trigger: sub_3A924 called]"); }
});

console.log("[+] syscall monitor ready, press OK");
});

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
[syscall 56=openat] path="/proc/self/maps"
from sec+0x1f5a4
from sec+0x2db18
from sec+0x2e2a4
from sec+0x2e358

[syscall 56=openat] path="/data/app/~~xSj5XGBgAVR1wIwm0PSo0w==/com.com.sec2023.rocketmouse.mouse-PPG-iEoicy-mM_SJhKWuKQ==/lib/arm64/libsec2023.so"
from sec+0x1f5a4
from sec+0x373d8
from sec+0x3710c
from sec+0x3710c

[syscall 63=read] fd=0x92 count=0x1000
from sec+0x1fba4
from sec+0x1fab0
from sec+0x37548
from sec+0x3710c
[syscall 63=read] fd=0x92 count=0x1000
from sec+0x1fba4
from sec+0x1fab0
from sec+0x37548
from sec+0x3710c

Process terminated
[Remote::mouse ]->

触发条件就是内存中 libsec2023.so 的 .text 段和磁盘上原版不一致, 立刻 kill。

从 syscall trace看到:

  • 读 /proc/self/maps
  • 读 libsec2023.so(从磁盘)
  • 每次读 4KB (count=0x1000)
  • 循环很多次(读完整个 .so 文件)
  • 读完就死

syscall trace 里 read 调用的 backtrace里有sub_370AC和0x37548

跟到sub_37940

image-20260405212406001

立刻认出来:这是 CRC32 的标准算法:

crc = table[(crc ^ byte) & 0xFF] ^ (crc >> 8)

查表位置 X13 = off_70F70 指向 unk_5B3C8,这是 CRC32 的 256 项查找表。

image-20260405212758938

检测函数 vtable[2]obj 返回检测结果(0=clean, 非0=detected)。它不自己杀,只是返回结果。杀不杀由调用者决定。

我们只需要在这做hook即可,假如反反调试

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
// Bypass: 在 CMP W0, #0 之前强制 W0 = 0
// 让检测函数的结果被忽略, 即使 CRC 不匹配也不 kill
function waitForLib(cb) {
var i = setInterval(function () {
var b = Module.findBaseAddress("libsec2023.so");
if (b) { clearInterval(i); cb(b); }
}, 500);
}

waitForLib(function(sec) {
var hook_count = 0;
// hook 在 CMP W0, #0 那条指令前 (sec+0x3649c)
// 此时 W0 = vtable[2](obj) 的返回值
Interceptor.attach(sec.add(0x3649c), {
onEnter: function() {
hook_count++;
var w0 = this.context.x0.toInt32();
console.log("[check #" + hook_count + "] detector returned w0=" + w0 + (w0 !== 0 ? " (TAMPERED!) → force 0" : " (clean)"));
if (w0 !== 0) {
// 强制返回值为 0, 骗消费者说"没事"
this.context.x0 = ptr(0);
}
}
});

// 装一个触发反调试的 hook (hook sec2023 某个函数)
Interceptor.attach(sec.add(0x3A924), {
onEnter: function(args) {
console.log("[hook@3A924] args[0]=" + args[0]);
}
});
console.log("[+] trigger hook on sec2023+0x3A924 installed");

console.log("[+] press OK now, watch if it survives or dies");
});

没有被kill

image-20260405213034943