强网拟态2025 mobile just WP

看到了 twogoat 师傅的文章 强网拟态2025初赛 Mobile方向just Writeup,然后自己复现了一下,记录下学习过程

题目分析

查看文件结构发现是 unity il2cpp 编译的

il2cpp 编译过程中,原始的 C# 代码被提前编译(AOT)成了原生 c++ 代码,再由各个平台的C++编译器直接编译成机器码,最终打包进了 libil2cpp.so 这个动态链接库文件中,失去了原来的类名、函数名等符号信息;而 global-metadata.dat 是 Unity 的元数据文件,存储了从 C# 脚本中提取的 类型、方法名、类名、字段、属性、字符串常量、参数信息等描述性数据,通常在通常在 apk 的如下路径:assets/bin/Data/Managed/Metadata/global-metadata.dat

利用 Il2CppDumper 把 libil2cpp.so 和 global-metadata.dat 进行联合分析,可以还原出原本的 C# 结构信息

这道题先用 Il2CppDumper 尝试,结果失败了

推测可能是 global-metadata.dat 或者 libil2cpp.so 被加密了,把这道题的这两个文件与另外题目的进行对比,发现两个都加密了(

global-metadata.dat 从 0x400 开始被加密了;il2cpp.so 连 ELF 头都没有

打算使用 frida-il2cpp-bridge dump 出运行时已经解密的 libil2cpp.so

frida 检测绕过

apk 有 frida 检测,直接 hook 会显示失败

端口转发连接 frida

1
2
3
./fs -l 0.0.0.0:11223

adb forward tcp:11223 tcp:11223

先 hook 一下 android_dlopen_ext 看看 so 的加载流程,找到检测 frida 的 so 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
}, onLeave: function(retval){
console.log(`dlopen onLeave fileName: ${this.fileName}`)
}
}
);
}

setImmediate(hook_dlopen)

可以看到在应用崩溃前最后加载的是 libjust.so, frida 的检测应该是在这里面实现的
frida 注入一次之后再次打开 apk 会直接闪退,需要重启手机才能正常打开

在 js 脚本的 onLeave 部分加上如下代码再次 hook 得到的依旧是上面的结果,可以判断检测点在 JNI_OnLoad 之前

1
2
3
4
if(this.fileName != null && this.fileName.indexOf("libjust.so") >= 0){
let JNI_OnLoad = Module.getExportByName(this.fileName, 'JNI_OnLoad')
console.log(`dlopen onLeave JNI_OnLoad: ${JNI_OnLoad}`)
}

hook call_constructors

由于 call_constructors(主要负责在.so 文件加载后,调用其中 .init_array里定义的函数) 是 Android 动态链接器内部的私有函数,不是一个导出函数,因此在 hook 前要先找到它相对与 linker 的 偏移地址

readelf -sW /apex/com.android.runtime/bin/linker64 | grep call_constructors

找到设备中 call_constructors 函数的 offset 为 0x52838

1
2
renoir:/ # readelf -sW /apex/com.android.runtime/bin/linker64 | grep call_constructors
771: 0000000000052838 888 FUNC LOCAL HIDDEN 11 __dl__ZN6soinfo17call_constructorsE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook_linker_call_constructors() {
let linker64_base_addr = Module.getBaseAddress('linker64')
let offset = 0x52838 // __dl__ZN6soinfo17call_constructorsEv offset
let call_constructors = linker64_base_addr.add(offset)
let listener = Interceptor.attach(call_constructors,{
onEnter:function(args){
console.log('hook_linker_call_constructors onEnter')
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
if (secmodule != null){
// TODO
}
}
})
}

确定了 hook 点之后,继续定位 frida 的具体检测点
frida 检测中常用的 native 函数

  • openat() / open() 打开 /proc/self/maps 文件,看发现文件中包含 frida、gumjs、agent.so 等与 Frida 相关的字符串
  • readlinkat() /proc/self/fd 目录下的文件描述符,或读取 /proc/self/exe (进程可执行文件路径)来检测进程环境
  • strstr() 匹配关键字符串
  • pthread_create() 监控是否有可疑线程被创建
  • snprintf() / sprintf() 应用程序在进行 Frida 检测时,需要使用它们来构造 /proc/self/maps 这样的字符串

hook pthread_create(),应用要检测 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
function hook_pthread_create(){
console.log("libjust.so --- " + Process.findModuleByName("libjust.so").base)
Interceptor.attach(Module.findExportByName('libc.so','pthread_create'),{
onEnter(args){
let func_addr = args[2]
console.log(`The thread Called function address is: ${func_addr}`)
}
})
}

function hook_linker_call_constructors() {
let linker64_base_addr = Module.getBaseAddress('linker64')
let offset = 0x52838 // __dl__ZN6soinfo17call_constructorsEv
let call_constructors = linker64_base_addr.add(offset)
let listener = Interceptor.attach(call_constructors,{
onEnter:function(args){
console.log('hook_linker_call_constructors onEnter')
let secmodule = Process.findModuleByName("libjust.so")
if (secmodule != null){
// TODO
console.log('the size of libjust.so is ' + secmodule.size)
hook_pthread_create()
}
}
})
}

setImmediate(hook_linker_call_constructors)

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
hook_linker_call_constructors onEnter
the size of libjust.so is 229376
libjust.so --- 0x7cf96c1000
hook_linker_call_constructors onEnter
the size of libjust.so is 229376
libjust.so --- 0x7cf96c1000
hook_linker_call_constructors onEnter
the size of libjust.so is 229376
libjust.so --- 0x7cf96c1000
hook_linker_call_constructors onEnter
the size of libjust.so is 229376
libjust.so --- 0x7cf96c1000
hook_linker_call_constructors onEnter
the size of libjust.so is 229376
libjust.so --- 0x7cf96c1000
hook_linker_call_constructors onEnter
the size of libjust.so is 229376
libjust.so --- 0x7cf96c1000
The thread Called function address is: 0x7cf96cf510
The thread Called function address is: 0x7cf96cf510
The thread Called function address is: 0x7cf96cf510
The thread Called function address is: 0x7cf96cf510
The thread Called function address is: 0x7cf96cf510
The thread Called function address is: 0x7cf96cf510
The thread Called function address is: 0x7db41dbeb0
The thread Called function address is: 0x7db41dbeb0
The thread Called function address is: 0x7db41dbeb0
The thread Called function address is: 0x7db41dbeb0
The thread Called function address is: 0x7db41dbeb0
The thread Called function address is: 0x7db41dbeb0

可以看出 libjust.so 创建了六个线程,涉及到的函数地址一个是 0x7cf96d6510,相对于基址的偏移为 0xE510;另一个 0x7db41dbeb0 明显不属于 libjust.so 模块

反编译 libjust.so,定位到函数 sub_E510,里面有 frida 检测相关代码

对这个函数交叉引用找到 pthread_create 的调用点,这是在 sub_C1B4 里面

尝试把sub_C1B4 replace 掉

1
2
3
4
5
6
7
8
9
10
11
12
let isHooked = false
function hook_c1b4(){
if(isHooked){
console.log("function already hooked,skip");
return;
}
let mod = Process.findModuleByName("libjust.so")
Interceptor.replace(mod.base.add(0xC1B4), new NativeCallback(function(){
console.log("replace sub_c1b4");
},'void',[]));
isHooked = true;
}

但是现实情况是这样 replace 之后依旧不能绕过 frida 检测

得知 pthread_create() 最终会执行 clone() 系统调用,那就顺便学习一下 hook clone

clone 函数的声明

1
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);

使用 clone() 系统调用创建线程的简单实例

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
#define _GNU_SOURCE // 必须在包含任何头文件之前定义
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/wait.h>

#define STACK_SIZE (1024 * 1024) // 为子线程分配1MB的栈空间

// 线程将要执行的函数
int thread_function(void *arg) {
// 使用 syscall(SYS_gettid) 获取线程ID (TID)
printf(" 子线程 -> PID: %d, TID: %ld\n", getpid(), syscall(SYS_gettid));
printf(" 子线程 -> 参数: %s\n", (char *)arg);
sleep(2);
printf(" 子线程 -> 退出\n");
return 0;
}

int main() {
void *child_stack;
int flags;
pid_t pid;

printf("主线程 -> PID: %d, TID: %ld\n", getpid(), syscall(SYS_gettid));

// 为子线程分配栈空间
child_stack = malloc(STACK_SIZE);
if (child_stack == NULL) {
perror("malloc");
exit(1);
}

// 设置 clone 的标志位来创建线程
// CLONE_VM: 共享父进程的虚拟内存空间
// CLONE_FS: 共享父进程的文件系统信息
// CLONE_FILES: 共享父进程打开的文件
// CLONE_SIGHAND: 共享父进程的信号处理程序
// CLONE_THREAD: 将子线程放入与父线程相同的线程组 (使其行为更像线程)
flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD;

char *arg = "这是一个传递给线程的参数";

// 创建线程
// 注意:栈是向下增长的,所以我们需要传递栈的顶部地址
pid = clone(thread_function, (char *)child_stack + STACK_SIZE, flags, arg);
if (pid == -1) {
perror("clone");
free(child_stack);
exit(1);
}

printf("主线程 -> 创建了TID为 %d 的子线程\n", pid);

// 主线程休眠,等待子线程执行
sleep(5);

printf("主线程 -> 退出\n");

// 释放为子线程分配的栈空间
free(child_stack);

return 0;
}

hook clone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hook_clone(){
Interceptor.attach(Module.findExportByName(null, 'clone'), {
onEnter: function (args) {
let thread_func = args[0];
console.log('Thread function addr: ' + thread_func);

// 查找线程函数所属的模块(.so 文件)
let module = Process.findModuleByAddress(thread_func);
if (module) {
console.log('Thread function is located in module: ' + module.name);
} else {
console.log('Fail to find module');
}

console.log('backtrace:\n' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'));
},
onLeave: function (retval) {
}
});
}

setImmediate(hook_clone);

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
PS D:\aaa\强网拟态2025> frida -H 127.0.0.1:11223 -f com.DefaultCompany.just -l .\hook_clone.js
____
/ _ | Frida 16.6.6 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to 127.0.0.1:11223 (id=socket@127.0.0.1:11223)
Spawned `com.DefaultCompany.just`. Resuming main thread!
[Remote::com.DefaultCompany.just ]-> Thread function addr: 0x74afda84f8
Thread function is located in module: libc.so
backtrace:
0x74afda82bc libc.so!pthread_create+0x260
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x74afda84f8
Thread function is located in module: libc.so
Thread function addr: 0x74afda84f8
Thread function is located in module: libc.so
backtrace:
0x74afda82bc libc.so!pthread_create+0x260
0x74afda82bc libc.so!pthread_create+0x260
backtrace:
0x74afda82bc libc.so!pthread_create+0x260
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x74afda84f8
Thread function is located in module: libc.so
backtrace:
0x74afda82bc libc.so!pthread_create+0x260
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
Fail to find module
backtrace:
0x74afdac3d4 libc.so!fork+0x40
0x74afdac3d4 libc.so!fork+0x40
Thread function addr: 0x74afda84f8
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
Fail to find module
backtrace:
0x74afdac3d4 libc.so!fork+0x40
0x74afdac3d4 libc.so!fork+0x40
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
Fail to find module
backtrace:
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
0x74afda82bc libc.so!pthread_create+0x260
Thread function addr: 0x0
Fail to find module
backtrace:
0x74afdac3d4 libc.so!fork+0x40
0x74afdac3d4 libc.so!fork+0x40
Thread function addr: 0x74afda84f8
Thread function is located in module: libc.so
Thread function addr: 0x74afda84f8
[Remote::com.DefaultCompany.just ]->

提取出 libc.so

1
adb pull /apex/com.android.runtime/lib64/bionic/libc.so D:\aaa\强网拟态2025

找到 pthread_create 函数,跳转到其 offset 0x260 处,可以看到调用了 clone

pthread_create 的函数声明

1
2
3
4
5
6
#include <pthread.h>

int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);

pthread_create 调用示例,第三个参数就是要让新线程执行的线程函数的地址

1
2
3
4
int result = pthread_create(&thread_id,          // 参数1: 存储线程ID的地址
NULL, // 参数2: 使用默认属性
my_thread_function, // 参数3: 线程要执行的函数
(void *)message); // 参数4: 传递给线程函数的参数

在 ida 中关注 a3 的传递

v28 + 96 处存的就是线程函数地址,v28 是 clone 的第四个参数
继续 hook clone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hook_clone1(){
const clone = Module.findExportByName(null, 'clone');
Interceptor.attach(clone, {
onEnter: function(args){
const thread_arg = args[3];
if(!thread_arg.isNull()){
let func_addr = thread_arg.add(96).readPointer();
let module = Process.findModuleByAddress(func_addr);
if (module){
let so_name = module.name;
let so_base = module.base;
let offset = func_addr.sub(so_base);
console.log(`Thread created by: ${so_name}!${offset},RealAddr: ${func_addr}`);

}
}
},
onLeave: function(retval){

}
});
}

hook 结果

1
2
3
4
5
6
7
8
Spawned `com.DefaultCompany.just`. Resuming main thread!
[Remote::com.DefaultCompany.just ]-> Thread created by: libutils.so!0x12eb0,RealAddr: 0x7794ee0eb0
Thread created by: libart.so!0x6600c0,RealAddr: 0x76e60600c0
Thread created by: libart.so!0x42e9a0,RealAddr: 0x76e5e2e9a0
Thread created by: libgraphicsenv.so!0x9474,RealAddr: 0x7779a57474
Thread created by: libjust.so!0xe510,RealAddr: 0x766fe4e510
Process terminated
[Remote::com.DefaultCompany.just ]->

可以看到这里已经可以得到我们想要的地址了
接着 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
function nop_64(addr) {
Memory.protect(addr, 4 , 'rwx');
var w = new Arm64Writer(addr);
w.putRet();
w.flush();
w.dispose();
}

function hook_and_nop_thread(soname){
const clone = Module.findExportByName(null, 'clone');
Interceptor.attach(clone, {
onEnter: function(args){
const thread_arg = args[3];
if(!thread_arg.isNull()){
let func_addr = thread_arg.add(96).readPointer();
let module = Process.findModuleByAddress(func_addr);
if (module){
let so_name = module.name;
let so_base = module.base;
let offset = func_addr.sub(so_base);
// console.log(`Thread created by: ${so_name}!${offset},RealAddr: ${func_addr}`);
if(so_name.indexOf(soname) >= 0){
nop_64(func_addr);
}
}
}
},
onLeave: function(retval){

}
});
}

// setImmediate(hook_and_nop_thread,"libjust.so");
setImmediate(() => hook_and_nop_thread("libjust.so"));

但是这样 hook 还是会崩溃

推测还有其他检测的地方,了解到 android crc 检测,其中一种的实现原理是把本地文件 /apex/com.android.runtime/lib64/bionic/libc.so 中的可执行数据与 apk 中/proc/{id}/maps下映射的libc.so可执行段内存进行比较(比较算法大多是 crc32,因为比较简单,计算耗能小,也可以选择其他如 md5、aes等,但很少见),从而判断对应的 so 是否被 inline hook 修改过(inline hook 会修改函数入口处的指令,让其跳转到自定义的 hook 函数)

在 libjust.so 中找到 crc 校验相关的代码,在函数 sub_119F8 中

绕过这个函数

最终绕过检测的 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
function nop_64(addr) {
Memory.protect(addr, 4 , 'rwx');
var w = new Arm64Writer(addr);
w.putRet();
w.flush();
w.dispose();
}

let ispatched = false;
function patch_crcCheck(){
if (ispatched) {
return;
}

const base = Module.findBaseAddress("libjust.so");
let crc_addr = null;
if(base){
crc_addr = base.add(0x119F8);
}
nop_64(crc_addr); // crc check
ispatched = true;
}

function hook_and_nop_thread(soname){
const clone = Module.findExportByName(null, 'clone');
Interceptor.attach(clone, {
onEnter: function(args){
const thread_arg = args[3];
if(!thread_arg.isNull()){
let func_addr = thread_arg.add(96).readPointer();
let module = Process.findModuleByAddress(func_addr);
if (module){
let so_name = module.name;
let so_base = module.base;
let offset = func_addr.sub(so_base);
// console.log(`Thread created by: ${so_name}!${offset},RealAddr: ${func_addr}`);
if(so_name.indexOf(soname) >= 0){
nop_64(func_addr);
patch_crcCheck();
}
}
}
},
onLeave: function(retval){

}
});
}

// setImmediate(hook_and_nop_thread,"libjust.so");
setImmediate(() => hook_and_nop_thread("libjust.so"));

到这里 frida 检测算是成功绕过了,但是 frida-il2cpp-bridge 依旧使用失败
我只能说通过这道题学到了不少绕过 frida 检测的的技巧吧

真正开始

对于这道题目本身而言,后续不用 frida dump 数据的话,上面做了那么多都没啥用处,下面才是真正开始解题

解密 libil2cpp.so

在 libjust.so 中搜索字符串 il2cpp 看到有一个 dec_il2cpp,进入相关函数,在 “dec_il2cpp” 处网上看找到加密方式是 RC4 + 异或 0x33

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
LABEL_107:
for ( j = 0LL; j != 256; ++j )
{
v79 = *((unsigned __int8 *)v137 + j);
v76 += v79 + (unsigned __int8)aNihaounity[j % v59];
*((_BYTE *)v137 + j) = *((_BYTE *)v137 + (unsigned __int8)v76);
*((_BYTE *)v137 + (unsigned __int8)v76) = v79;
}
++v77;
continue;
}
break;
}
if ( v64 )
{
v80 = 0;
v81 = 0;
for ( k = 0; ; ++k )
{
LOBYTE(v73) = byte_37010;
LOBYTE(v74) = byte_3700C;
v74 = (double)*(unsigned __int64 *)&v74 + (double)*(unsigned __int64 *)&v74;
v73 = ((double)*(unsigned __int64 *)&v73 - v74)
* (double)(((unsigned int)(unsigned __int8)byte_37000 + 7) / (unsigned __int8)byte_37004
- (unsigned __int8)byte_37008);
v136 = v73;
if ( ((unsigned int)(unsigned __int8)byte_37000 + 7) / (unsigned __int8)byte_37004 == (unsigned __int8)byte_37008
|| (unsigned __int8)byte_3700C + 6 * (unsigned __int8)byte_37060 == 3 )
{
v74 = (double)(((unsigned int)(unsigned __int8)byte_37000 + 7) / (unsigned __int8)byte_37004
- (unsigned __int8)byte_37008);
v73 = v136 * v74;
if ( k >= (int)(v136 * v74) )
goto LABEL_125;
}
else if ( k >= 7 )
{
goto LABEL_125;
}
v83 = (_BYTE *)v57;
v84 = v58 - v57;
do
{
v85 = *((unsigned __int8 *)v137 + (unsigned __int8)++v81);
--v84;
v80 += v85;
*((_BYTE *)v137 + (unsigned __int8)v81) = *((_BYTE *)v137 + (unsigned __int8)v80);
*((_BYTE *)v137 + (unsigned __int8)v80) = v85;
*v83++ ^= *((_BYTE *)v137 + (unsigned __int8)(*((_BYTE *)v137 + (unsigned __int8)v81) + v85)) ^ 0x33;
}
while ( v84 );
}
}
v86 = -1;
do
{
while ( 1 )
{
LOBYTE(v73) = byte_37010;
LOBYTE(v74) = byte_3700C;
v74 = (double)*(unsigned __int64 *)&v74 + (double)*(unsigned __int64 *)&v74;
v73 = ((double)*(unsigned __int64 *)&v73 - v74)
* (double)(((unsigned int)(unsigned __int8)byte_37000 + 7) / (unsigned __int8)byte_37004
- (unsigned __int8)byte_37008);
v136 = v73;
if ( ((unsigned int)(unsigned __int8)byte_37000 + 7) / (unsigned __int8)byte_37004 != (unsigned __int8)byte_37008
&& (unsigned __int8)byte_3700C + 6 * (unsigned __int8)byte_37060 != 3 )
{
break;
}
v74 = (double)(((unsigned int)(unsigned __int8)byte_37000 + 7) / (unsigned __int8)byte_37004
- (unsigned __int8)byte_37008);
v73 = v136 * v74;
if ( ++v86 >= (int)(v136 * v74) )
goto LABEL_125;
}
++v86;
}
while ( v86 < 7 );
LABEL_125:
v87 = syscall(279LL, "dec_il2cpp", 0LL);

在 ciberchef 中解密得到解密后的 libil2cpp.so

解密 global-metadata.dat

global-metadata.dat 在 libil2cpp.so 中加密
同样是字符串搜索定位到 global-metadata.dat 相关的函数

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
__int64 __fastcall sub_211B8C(_DWORD *a1, int *a2)
{
int v4; // w1
int v5; // w2
int v6; // w3
int v7; // w4
int v8; // w5
int v9; // w6
int v10; // w7
int *v11; // x0
__int64 result; // x0
int v13; // w1
int v14; // w2
int v15; // w3
int v16; // w4
int v17; // w5
int v18; // w6
int v19; // w7
unsigned __int64 v20; // kr00_8
__int64 v21; // x0
__int64 v22; // x12
__int64 v23; // x8
int *v24; // x12
unsigned int v25; // w13
int v26; // w13
__int64 v27; // x13
__int64 v28; // x13
__int64 v29; // x13
int v30[4]; // [xsp+0h] [xbp-30h] BYREF
char v31[8]; // [xsp+10h] [xbp-20h]
__int64 v32; // [xsp+18h] [xbp-18h]
void *v33; // [xsp+20h] [xbp-10h]
__int64 v34; // [xsp+28h] [xbp-8h]
int vars0[2]; // [xsp+30h] [xbp+0h]
void *vars8; // [xsp+38h] [xbp+8h]

if ( (sub_211AF8() & 1) != 0 )
{
v11 = v30;
strcpy((char *)v30, "game.dat");
}
else
{
v11 = (int *)"global-metadata.dat";
}
result = sub_211D94((int)v11, v4, v5, v6, v7, v8, v9, v10, v30[0], v30[2], v31[0], v32, v33, v34, vars0[0], vars8);
qword_A322E8 = result;
if ( result
|| (result = sub_211D94(
(int)"global-metadata.dat",
v13,
v14,
v15,
v16,
v17,
v18,
v19,
v30[0],
v30[2],
v31[0],
v32,
v33,
v34,
vars0[0],
vars8),
qword_A322E8 = result,
off_A322D0 = 0LL,
result) )
{
qword_A322F0 = result;
v20 = *(int *)(result + 172);
*a1 = v20 / 0x28;
dword_A322F8 = v20 / 0x28;
*a2 = *(int *)(result + 180) >> 6;
qword_A32300 = sub_267DFC((int)(v20 / 0x28), 24LL);
qword_A32308 = sub_267DFC(*(int *)(qword_A322E0 + 48), 8LL);
qword_A32310 = sub_267DFC(*(int *)(qword_A322F0 + 164) / 0x58uLL, 8LL);
qword_A32318 = sub_267DFC((unsigned __int64)*(int *)(qword_A322F0 + 52) >> 5, 8LL);
v21 = sub_267DFC(*(int *)(qword_A322E0 + 64), 8LL);
v22 = qword_A322E0;
qword_A32320 = v21;
result = 1LL;
if ( *(int *)(qword_A322E0 + 48) >= 1 )
{
v23 = 0LL;
while ( 1 )
{
v24 = *(int **)(*(_QWORD *)(v22 + 56) + 8 * v23);
v25 = *((unsigned __int8 *)v24 + 10);
if ( v25 <= 0x1E )
{
v26 = 1 << v25;
if ( (v26 & 0x13467FFE) != 0 )
{
v27 = *v24;
if ( (_DWORD)v27 != -1 )
{
v28 = qword_A322E8 + *(int *)(qword_A322F0 + 160) + 88 * v27;
LABEL_16:
*(_QWORD *)v24 = v28;
goto LABEL_17;
}
goto LABEL_15;
}
if ( (v26 & 0x40080000) != 0 )
{
v29 = *v24;
if ( (_DWORD)v29 != -1 )
{
v28 = qword_A322E8 + *(int *)(qword_A322F0 + 104) + 16 * v29;
goto LABEL_16;
}
LABEL_15:
v28 = 0LL;
goto LABEL_16;
}
}
LABEL_17:
v22 = qword_A322E0;
if ( ++v23 >= *(int *)(qword_A322E0 + 48) )
return 1LL;
}
}
}
return result;
}

result 应该是解密后的 global-metadata.dat,关注函数 sub_211D94

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
__int64 __fastcall sub_211D94(
const char *a1,
int a2,
int a3,
int a4,
int a5,
int a6,
int a7,
int a8,
int a9,
int a10,
char a11,
int a12,
void *a13,
char a14,
int a15,
void *a16)
{
void *v17; // x8
char *v18; // x9
size_t v19; // x0
char *v20; // x8
void *v21; // x9
__int64 v22; // x0
__int64 v23; // x20
__int64 v24; // x21
__int64 v25; // x0
__int64 v26; // x19
char *v28; // [xsp+0h] [xbp-70h] BYREF
__int64 v29; // [xsp+8h] [xbp-68h]
__int64 v30; // [xsp+10h] [xbp-60h] BYREF
void *v31; // [xsp+18h] [xbp-58h]
void *ptr; // [xsp+20h] [xbp-50h]
__int64 v33; // [xsp+28h] [xbp-48h] BYREF
void *v34; // [xsp+30h] [xbp-40h]
void *v35; // [xsp+38h] [xbp-38h]
char *v36; // [xsp+40h] [xbp-30h] BYREF
void *v37; // [xsp+48h] [xbp-28h]

sub_26850C(
&v30,
(int)a1,
a2,
a3,
a4,
a5,
a6,
a7,
a8,
(int)v28,
v29,
v30,
(int)v31,
(char)ptr,
v33,
v34,
(char)v35,
(int)v36,
v37);
v28 = "Metadata";
v29 = 8LL;
v17 = (void *)((unsigned __int64)(unsigned __int8)v30 >> 1);
if ( (v30 & 1) != 0 )
v18 = (char *)ptr;
else
v18 = (char *)&v30 + 1;
if ( (v30 & 1) != 0 )
v17 = v31;
v36 = v18;
v37 = v17;
sub_1F3DA0(&v33, &v36, &v28);
if ( (v30 & 1) != 0 )
operator delete(ptr);
v19 = strlen(a1);
if ( (v33 & 1) != 0 )
v20 = (char *)v35;
else
v20 = (char *)&v33 + 1;
if ( (v33 & 1) != 0 )
v21 = v34;
else
v21 = (void *)((unsigned __int64)(unsigned __int8)v33 >> 1);
v28 = (char *)a1;
v29 = v19;
v36 = v20;
v37 = v21;
sub_1F3DA0(&v30, &v36, &v28);
LODWORD(v36) = 0;
v22 = sub_1EDFAC(&v30, 3LL, 1LL, 1LL, 0LL, &v36);
if ( (_DWORD)v36 )
{
sub_267C8C("ERROR: Could not open %s");
}
else
{
v23 = v22;
v24 = sub_267E30();
qword_A327F0 = v24;
v25 = sub_1F08B0(v23, &v28);
v26 = sub_21A2C8(v24, v25);
qword_A327F8 = v26;
sub_1EE398(v23, &v36);
if ( !(_DWORD)v36 )
goto LABEL_19;
sub_267E40(v24);
}
v26 = 0LL;
LABEL_19:
if ( (v30 & 1) != 0 )
operator delete(ptr);
if ( (v33 & 1) != 0 )
operator delete(v35);
return v26;
}

最后返回的是 v26,对 v26 进行操作的函数是 sub_21A2C8

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
char *__fastcall sub_21A2C8(unsigned __int16 *a1, __int64 a2)
{
__int64 v2; // x21
__int64 v4; // x8
__int64 v5; // x22
char *v6; // x19
__int64 i; // x8
__int64 v8; // x13
__int64 v9; // x12

v2 = a1[512];
v4 = a2 - 4 * v2;
v5 = v4 - 1028;
v6 = (char *)malloc(v4 - 4);
memcpy(v6, a1, 0x400uLL);
if ( v5 >= 1 )
{
for ( i = 0LL; i < v5; i += 4LL )
{
v8 = i + 3;
v9 = i + i / v2;
if ( i >= 0 )
v8 = i;
*(_DWORD *)&v6[(v8 & 0xFFFFFFFFFFFFFFFCLL) + 1024] = *(_DWORD *)((char *)&a1[2 * v2 + 514]
+ (v8 & 0xFFFFFFFFFFFFFFFCLL)) ^ *(_DWORD *)&a1[2 * (v9 % v2) + 514];
}
}
return v6;
}

写个解密脚本

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *decrypt_metadata(unsigned short *data, long long file_size)
{
long long v2 = data[512]; // 密钥长度
long long v4 = file_size - 4 * v2;
long long v5 = v4 - 1028;
char *decrypted = (char *)malloc(v4 - 4);

// 复制前1024字节(密钥部分)
memcpy(decrypted, data, 0x400);

if (v5 >= 1)
{
for (long long i = 0; i < v5; i += 4)
{
long long v8 = i + 3;
long long v9 = i + i / v2;
if (i >= 0)
v8 = i;

// 解密:再次XOR相同的值即可还原
*(unsigned int *)&decrypted[(v8 & 0xFFFFFFFFFFFFFFFCLL) + 1024] =
*(unsigned int *)((char *)&data[2 * v2 + 514] + (v8 & 0xFFFFFFFFFFFFFFFCLL)) ^
*(unsigned int *)&data[2 * (v9 % v2) + 514];
}
}

return decrypted;
}

int main()
{
FILE *file = fopen("global-metadata.dat", "rb");
if (!file) {
printf("无法打开 global-metadata.dat 文件\n");
return 1;
}

// 获取文件大小
fseek(file, 0, SEEK_END);
long long file_size = ftell(file);
fseek(file, 0, SEEK_SET);

// 读取文件内容
unsigned short *data = (unsigned short *)malloc(file_size);
fread(data, 1, file_size, file);
fclose(file);

// 解密
char *decrypted = decrypt_metadata(data, file_size);

// 写入解密后的文件
FILE *output = fopen("dec_global-metadata.dat", "wb");
if (output) {
long long v2 = data[512];
long long decrypted_size = file_size - 4 * v2 - 4;
fwrite(decrypted, 1, decrypted_size, output);
fclose(output);
printf("解密完成,输出文件:dec_global-metadata.dat\n");
} else {
printf("无法创建输出文件\n");
}

free(data);
free(decrypted);
return 0;
}

il2cppdumper

现在我们已经拿到了解密后的 libil2cpp.so 和 global-metadata.dat,接下来就可以用 il2cppdumper 了

dump 成功,yeah

ida 打开 dec_libil2cpp.so, File->Script file 选择 Il2CppDumper 里面的 ida_with_struct_py3.py,然后再选择刚刚 dump 得到的文件里面的 script.json 和 il2cpp.h 就可以恢复符号表了

关注 FlagChecker 类

找到 FlagChecker__TeaEncrypt

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
void FlagChecker__TeaEncrypt(System_UInt32_array *v, System_UInt32_array *k, const MethodInfo *method)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

if ( !v )
LABEL_17:
sub_2745C8();
v9 = sub_1B5D88((__int64)v, 0LL, v3, v4, v5, v6);
v36 = v;
v14 = sub_1B5D88((__int64)v, 1LL, v10, v11, v12, v13);
if ( ((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 != (unsigned __int8)byte_9D8048 )
{
v18 = 0;
v19 = 0;
do
{
LOBYTE(v15) = byte_9D804C;
if ( v19 >= 16 )
v20 = 0.0;
else
v20 = 1.0;
LOBYTE(v16) = byte_9D8050;
*(double *)&v16 = (double)*(unsigned __int64 *)&v16 + (double)*(unsigned __int64 *)&v16;
*(double *)&v15 = ((double)*(unsigned __int64 *)&v15 - *(double *)&v16) * v20;
v37 = *(double *)&v15;
if ( ((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 == (unsigned __int8)byte_9D8048
|| (unsigned __int8)byte_9D8050 + 30 * (unsigned __int8)byte_A2F928 == 3 )
{
if ( ((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 == (unsigned __int8)byte_9D8048 )
{
v21 = 0;
}
else
{
*(double *)&v16 = (double)(((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044
- (unsigned __int8)byte_9D8048);
*(double *)&v15 = *(double *)&v15 * *(double *)&v16;
v21 = (int)(v37 * *(double *)&v16);
}
}
else
{
v21 = 9;
}
if ( !((((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044
- (unsigned __int8)byte_9D8048)
* v21) )
break;
if ( !k )
goto LABEL_17;
v22 = sub_1B5D88((__int64)k, 0LL, v15, v16, v20, v17);
v9 += (v22 + 16 * v14) ^ (v14 + v18) ^ (sub_1B5D88((__int64)k, 1LL, v23, v24, v25, v26) + (v14 >> 5));
v31 = sub_1B5D88((__int64)k, 2LL, v27, v28, v29, v30);
v14 += (v31 + 16 * v9) ^ (v18 + v9) ^ (sub_1B5D88((__int64)k, 3LL, v32, v33, v34, v35) + (v9 >> 5));
v18 -= 0x61C88647;
++v19;
}
while ( ((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 != (unsigned __int8)byte_9D8048 );
}
sub_1B5B88(v36, 0LL, v9);
sub_1B5B88(v36, 1LL, v14);
}

魔改 tea

获取数据

在 FlagChecker___cctor 中可以看到初始化 Key 和 ReallyCompare

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
void FlagChecker___cctor(const MethodInfo *method)
{
unsigned __int64 v1; // d0
unsigned __int64 v2; // d1
int v3; // w8
System_Array_o *v4; // x0
System_RuntimeFieldHandle_o v5; // x1
System_Array_o *v6; // x19
struct FlagChecker_StaticFields *static_fields; // x0
System_Array_o *v8; // x0
System_RuntimeFieldHandle_o v9; // x1
System_Array_o *v10; // x19
struct FlagChecker_StaticFields *v11; // x0

if ( ((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 != (unsigned __int8)byte_9D8048 )
{
LOBYTE(v1) = byte_9D804C;
LOBYTE(v2) = byte_9D8050;
if ( (unsigned __int8)byte_9D8050 + 30 * (unsigned __int8)byte_A2F928 == 3 )
{
if ( ((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 == (unsigned __int8)byte_9D8048 )
v3 = 0;
else
v3 = (int)(((double)v1 - ((double)v2 + (double)v2))
* (double)((byte_A2F8C2 & 1) == 0)
* (double)(((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044
- (unsigned __int8)byte_9D8048));
}
else
{
v3 = 9;
}
if ( (((unsigned int)(unsigned __int8)byte_9D8040 + 7) / (unsigned __int8)byte_9D8044 - (unsigned __int8)byte_9D8048)
* v3 )
{
sub_2744F4((__int64)&byte___TypeInfo);
sub_2744F4((__int64)&FlagChecker_TypeInfo);
sub_2744F4((__int64)&Field__PrivateImplementationDetails__29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2);
sub_2744F4((__int64)&Field__PrivateImplementationDetails__C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF);
sub_2744F4((__int64)&uint___TypeInfo);
byte_A2F8C2 = 1;
}
}
v4 = (System_Array_o *)sub_274508((__int64)uint___TypeInfo, 4u);
v5.fields.value = Field__PrivateImplementationDetails__C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF;
v6 = v4;
System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_4166724(v4, v5, 0LL);
static_fields = FlagChecker_TypeInfo->static_fields;
static_fields->Key = (struct System_UInt32_array *)v6;
sub_274498(static_fields, v6);
v8 = (System_Array_o *)sub_274508((__int64)byte___TypeInfo, 0x28u);
v9.fields.value = Field__PrivateImplementationDetails__29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2;
v10 = v8;
System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_4166724(v8, v9, 0LL);
v11 = FlagChecker_TypeInfo->static_fields;
v11->ReallyCompare = (struct System_Byte_array *)v10;
sub_274498(&v11->ReallyCompare, v10);
}

在 C# 代码中定义一个静态数组并使用数组初始化器给它赋初值时,例如

1
private static readonly byte[] MyData = new byte[] { 0xAF, 0x58, ..., 0x1A };

编译器不会将这些字节值作为 C# 源代码的一部分存储在 IL 中,相反,它会将这些原始字节数据放在一个特殊的元数据(Metadata)部分,并生成一个指向这块数据的静态字段,这个静态字段就存在于 类中

29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF 分别对应 40 字节(密文)和 16 字节(密钥)数据的哈希值

在 dump.cs 中可以找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 // Namespace: 
private struct <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 // TypeDefIndex: 2221
{}

// Namespace:
private struct <PrivateImplementationDetails>.__StaticArrayInitTypeSize=40 // TypeDefIndex: 2222
{}

// Namespace:
internal sealed class <PrivateImplementationDetails> // TypeDefIndex: 2223
{
// Fields
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=40 29FC2CC7503351583297C73AC477A5D7AA78899F3C0D66E1F909139D4AA1FFB2 /*Metadata offset 0xF901D*/; // 0x0
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 C8E4E9E3F34C25560172B0D40B6DF4823260AA87EC6866054AA4691711E5D7BF /*Metadata offset 0xF9045*/; // 0x28
}

根据 offset 在 global-metadata.dat 中提取出对应的数据

解密脚本

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
cipher = [0xAF,0x58,0x64,0x40,0x9D,0xB9,0x21,0x67,0xAE,0xB5,0x29,0x04,0x9E,0x86,0xC5,0x43,
0x23,0x0F,0xBF,0xA6,0xB2,0xAE,0x4A,0xB5,0xC5,0x69,0xB7,0xA8,0x03,0xD1,0xAE,0xCF,
0xC6,0x2C,0x5B,0x7F,0xA2,0x86,0x1E,0x1A]

key = [0x12345678, 0x09101112, 0x13141516, 0x15161718]

def tea_encrypt(v, k):
delta = 0x61C88647
sum = 0
k0, k1, k2, k3 = k[0], k[1], k[2], k[3]
v0, v1 = v[0], v[1]

for _ in range(16):
v0 = (v0 + (((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1))) & 0xFFFFFFFF
v1 = (v1 + (((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3))) & 0xFFFFFFFF
sum = (sum - delta) & 0xFFFFFFFF
return [v0, v1]


def tea_decrypt(v, k):
delta = 0x61C88647
sum = ((-16) * delta) & 0xFFFFFFFF
k0, k1, k2, k3 = k[0], k[1], k[2], k[3]
v0, v1 = v[0], v[1]

for _ in range(16):
sum = (sum + delta) & 0xFFFFFFFF
v1 = (v1 - (((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1))) & 0xFFFFFFFF

return [v0, v1]

def b2dle(byte_array):
return [int.from_bytes(byte_array[i:i+4], byteorder='little', signed=False) for i in range(0, len(byte_array), 4)]

def d2ble(dword_array):
byte_list = []
for dword in dword_array:
byte_list.extend(dword.to_bytes(4, byteorder='little'))
return byte_list

cipher_copy = cipher.copy()
v = b2dle(cipher_copy)
for i in range(32, 7, -8):
for j in range(8):
cipher_copy[j + i] ^= cipher_copy[j]

v = tea_decrypt(v, key)
dec_bytes = d2ble(v)
for i in range(8):
cipher_copy[i] = dec_bytes[i]

v = tea_decrypt(v, key)
dec_bytes = d2ble(v)
for i in range(8):
cipher_copy[i] = dec_bytes[i]
print(''.join(chr(b) for b in cipher_copy))

flag 为 flag{unitygame_I5S0ooFunny_Isnotit?????}


强网拟态2025 mobile just WP
http://example.com/2025/11/03/just/
作者
Eleven
发布于
2025年11月3日
许可协议