腾讯游戏安全大赛 2023 安卓初赛题解

分析

unity il2cpp 编译的游戏,先解包找出 global-metadata.dat 和 libil2cpp.so 文件,尝试用 Il2CppDumper 进行分析,但是分析失败了

应该是 libil2cpp.so 被加密了(在 010 editor 中对 global-metadata.dat 运行 UnityMetadata 模板,显示模板执行成功;对 libil2cpp.so 运行 ELF 模板报错)

解密 libil2cpp.so

在 libmain.so 中找到如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool __fastcall sub_9FC(__int64 a1, __int64 a2, __int64 a3)
{
size_t v5; // x21
void *v6; // x22
const void *v7; // x23

v5 = (int)((*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)a1 + 1344LL))(a1, a3) + 1);
v6 = malloc(v5);
v7 = (const void *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
memcpy(v6, v7, v5);
(*(void (__fastcall **)(__int64, __int64, const void *))(*(_QWORD *)a1 + 1360LL))(a1, a3, v7);
sub_BD4(a1, v6, "libunity.so", &qword_3030);
sub_BD4(a1, v6, "libil2cpp.so", &qword_3038);
free(v6);
return qword_3030 != 0;
}

最开始的想法是 hook dlopen,在 libil2cpp.so 加载完整时 dump 出来,这时候得到的 libil2cpp.so 就是解密后的,但是测试的时候发现有 frida 检测,直接运行 dump 脚本会显示 Process terminated

尝试绕过 frida 检测
frida-server 会默认开启端口 27042(主控制流端口) 和 27043(文件传输端口),app 可能会通过检测这些端口是否被占用来判断 frida 是否存在

进行端口转发

1
2
3
./fs -l 0.0.0.0:11223
adb forward tcp:11223 tcp:11223
frida -H 127.0.0.1:11223 -f com.com.sec2023.rocketmouse.mouse -l .\hook_dec_so.js

dump libil2cpp.so 的 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
// hook_dec_so.js
function dumpSo(soName) {
const mod = Process.findModuleByName(soName);
if (!mod) {
console.log("[-] Module not found: " + soName);
return;
}

console.log("[*] Base: " + mod.base + ", Size: " + mod.size);
const dump_path = "/data/data/com.com.sec2023.rocketmouse.mouse/files/" + soName;
const buffer = new ArrayBuffer(mod.size);
const view = new Uint8Array(buffer);

mod.enumerateRanges('r--').forEach(function(range){
const offset = range.base.sub(mod.base).toInt32();
console.log("[*] Reading range: 0x" + offset.toString(16) + ",size:" + range.size);
try{
const data = ptr(range.base).readByteArray(range.size);
const dataView = new Uint8Array(data);
view.set(dataView, offset);
} catch(e) {
console.log("[-] Failed to read range" + e.message);
}
});

const file = new File(dump_path, "wb");
file.write(buffer);
file.close();
console.log("[+] Dumped to: " + dump_path);
}

Interceptor.attach(Module.findExportByName(null, 'dlopen'), {
onEnter: function(args) {
this.path = Memory.readUtf8String(args[0]);
},
onLeave: function(retval) {
if (!retval.isNull() && this.path && this.path.indexOf("libil2cpp.so") !== -1) {
setTimeout(function() {
dumpSo("libil2cpp.so");
}, 2000);
}
}
});

成功 dump

1
2
3
4
5
6
7
8
9
10
[Remote::com.com.sec2023.rocketmouse.mouse ]-> [*] Base: 0x6ef3e0f000, Size: 20758528
[*] Reading range: 0x0,size:2842624
[*] Reading range: 0x2b6000,size:11878400
[*] Reading range: 0xe0a000,size:2453504
[*] Reading range: 0x1062000,size:655360
[*] Reading range: 0x1102000,size:626688
[*] Reading range: 0x13b8000,size:8192
[*] Reading range: 0x13ba000,size:8192
[*] Reading range: 0x13bc000,size:65536
[+] Dumped to: /data/data/com.com.sec2023.rocketmouse.mouse/files/libil2cpp.so

直接 adb pull /data/data/com.com.sec2023.rocketmouse.mouse/files/libil2cpp.so 到本地会显示没有权限
所以先在 /data/data/com.com.sec2023.rocketmouse.mouse/files 目录下把 dump 下来的 so 移动到有访问权限的 /sdcard/Download 目录下,然后在传到本地

1
2
mv libil2cpp.so /sdcard/Download
adb pull /sdcard/Download/libil2cpp.so D:\aaa\TXgame2023-1

这时候再去 010 里面对 dump 下来的 libil2cpp.so 运行 ELF 模板,显示模板执行成功

用 Il2CppDumper 对 dump 下来的 libil2cpp.so 和 global-metadata.dat 进行分析

修复 dump 下来的 so 文件

但是直接把这个文件扔进 ida 里面去恢复符号会分析失败 o.o
是因为 dump 下来的文件段和节的偏移是在虚拟空间中的偏移,而 ida 分析时的偏移是按照实际文件中的偏移来算的
这里可以使用 SoFixer 来修复

ida 打开 fix.so, File->Script file 选择 Il2CppDumper 里面的 ida_with_struct_py3.py,然后再选择刚刚 dump 得到的文件里面的 script.json 和 il2cpp.h 就可以恢复符号表了。除此之外,ida 中也要设置基地址,在 Edit->Segments->Rebase program…填入 hook 出的 base 地址

修改金币数量

在 ida 中搜索 MouseController 找到相关函数,看到有个 CollectCoin

写 frida 脚本 hook TssSdtInt__op_Implicit 返回值让它等于 1000
先在 dump.cs 中找到相关的地址偏移

1
2
3
4
5
// RVA: 0x464794 Offset: 0x464794 VA: 0x6EF526C794
public static int op_Implicit(TssSdtInt v) { }

// RVA: 0x4652E4 Offset: 0x4652E4 VA: 0x6EF526D2E4
private void CollectCoin(Collider2D coinCollider) { }

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
function hook_coin(){
const mod = Process.findModuleByName("libil2cpp.so");
if(!mod){
console.log("[-] Module not found");
return;
}

let baseAddr = mod.base;
console.log("[*] libil2cpp.so base address: " + baseAddr);
let coin_Offset = 0x4652E4;
let op_Implicit_Offset = 0x464794;
let coin_addr = baseAddr.add(coin_Offset);
let op_Implicit_addr = baseAddr.add(op_Implicit_Offset);

let coin_end_addr = coin_addr.add(0x160) // 用于判断调用来源
Interceptor.attach(op_Implicit_addr,{
onEnter:function(args){
this.returnAddr = this.context.lr // 这里的 lr (Linker Regiater) 保存了当前函数调用者的返回地址
},
onLeave:function(retval){
if(this.returnAddr >= coin_addr && this.returnAddr < coin_end_addr){
console.log("[*] Original coin value: " + retval.toInt32());
retval.replace(1000);
console.log("[*] Modified coin value: " + retval.toInt32());
}
}
});
}

setImmediate(hook_coin,2000);

成功 hook,游戏界面也显示了 flag

小键盘分析

游戏还有个 mod menu 菜单

本题目标为制作 RocketMouse 注册机, 获取 flag,其中注册机算法需要使用 C、C++ 语言实现,要求根据 Mod Menu 中生成的任意随机 Token,均能计算出相应的正确结果,所以现在需要分析注册机算法

ida 搜索 keyboard 可以看到好几个混淆函数,在 dump.cs 中找到相关的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// RVA: 0x465880 Offset: 0x465880 VA: 0x6EF526D880
private void iI1Ii(SmallKeyboard.iII1i _info) { }

// RVA: 0x465FDC Offset: 0x465FDC VA: 0x6EF526DFDC
private void iI1Ii(GameObject go) { }

// RVA: 0x465AB0 Offset: 0x465AB0 VA: 0x6EF526DAB0
private void iI1Ii(ulong i1I) { }

// RVA: 0x465E90 Offset: 0x465E90 VA: 0x6EF526DE90
private void oO0oOo0() { }

// RVA: 0x466184 Offset: 0x466184 VA: 0x6EF526E184
private string oO0oOoO() { }

写脚本跟踪一下这些函数

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
// trace.js
function trace() {
let mod = Process.findModuleByName("libil2cpp.so");

if (mod == null) {
setTimeout(trace, 1000);
return;
}

console.log("libil2cpp.so Base Address: " + mod);

function readIl2CppString(ptr){
if (ptr.isNull()) return "null";
try{
return ptr.add(0x14).readUtf16String();
} catch (e) {
return "Cannot read string (" + ptr + ")";
}
}

function traceMethod(offset, className, methodSignature) {

let funcAddr = mod.base.add(offset);

try {
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("method call");
console.log(" nameSpaze: class:" + className);
console.log(" methodPointer offset :" + offset.toString(16).toUpperCase()); // 转成16进制
console.log(" " + methodSignature);
console.log(" method end");
console.log("");
},
onLeave: function(retval) {

}
});
console.log(`[+] Hooked ${className} at ${offset.toString(16)}`);
} catch (e) {
console.log(`[-] Failed to hook ${offset.toString(16)}: ${e.message}`);
}
}

let cls = "SmallKeyboard";
traceMethod(0x465880, cls, "private void iI1Ii(iII1i _info)");

traceMethod(0x465FDC, cls, "private void iI1Ii(GameObject go)");

traceMethod(0x465AB0, cls, "private void iI1Ii(UInt64 i1I)");

traceMethod(0x465E90, cls, "private void oO0oOo0()");

traceMethod(0x466184, cls, "private string oO0oOoO()");

traceMethod(0x46618C, cls, "private void Start()");

traceMethod(0x466300, cls, "public void _ctor()");
}

setImmediate(trace);

点击小键盘上的数字时触发

1
2
3
4
5
6
7
8
9
10
11
method  call
nameSpaze: class:SmallKeyboard
methodPointer offset :465FDC
private void iI1Ii(GameObject go)
method end

method call
nameSpaze: class:SmallKeyboard
methodPointer offset :465880
private void iI1Ii(iII1i _info)
method end

点击 OK 键时触发

1
2
3
4
5
6
7
8
9
10
11
method  call
nameSpaze: class:SmallKeyboard
methodPointer offset :465AB0
private void iI1Ii(UInt64 i1I)
method end

method call
nameSpaze: class:SmallKeyboard
methodPointer offset :465E90
private void oO0oOo0()
method end

ida 中看到

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
void SmallKeyboard__iI1Ii(SmallKeyboard_o *this, SmallKeyboard_iII1i_o *info, const MethodInfo *method)
{
__int64 v5; // x1
__int64 v6; // x1
__int64 v7; // x1
unsigned int KeyType; // w8
struct System_String_o *v9; // x0
struct System_String_o *v10; // x0
__int64 *v11; // x20
_QWORD *v12; // x21
System_String_o *v13; // x0
__int64 v14; // x8
Il2CppObject *v15; // x20
System_String_o *iIIIi; // x20
uint64_t v17; // x0
const MethodInfo *v18; // x2
const MethodInfo *v19; // x1
UnityEngine_GameObject_o *inputObj; // x0
Il2CppObject *Component_object; // x0

if ( (qword_6EF5FA2378 & 0x100000000000000LL) == 0 )
{
sub_6EF5191B58(off_6EF5EF9A68, info);
sub_6EF5191B58(off_6EF5EFDB28, v5);
sub_6EF5191B58(off_6EF5EEB4F0[0], v6);
sub_6EF5191B58(off_6EF5EF5E40[0], v7);
HIBYTE(qword_6EF5FA2378) = 1;
}
if ( !info )
LABEL_19:
sub_6EF5191C40();
KeyType = info->fields.KeyType;
if ( KeyType < 2 )
{
v10 = System_String__Concat_8591696(this->fields.iIIIi, info->fields.SValue, 0LL);
LABEL_10:
this->fields.iIIIi = v10;
goto LABEL_16;
}
if ( KeyType == 2 )
{
v11 = (__int64 *)off_6EF5EFDB28;
v12 = off_6EF5EF9A68;
v13 = System_String__Concat_8591696(*(System_String_o **)off_6EF5EF5E40[0], this->fields.iIIIi, 0LL);
v14 = *v11;
v15 = (Il2CppObject *)v13;
if ( !*(_DWORD *)(v14 + 224) )
j_il2cpp_runtime_class_init_0(v14);
UnityEngine_Debug__LogWarning(v15, 0LL);
iIIIi = this->fields.iIIIi;
if ( !*(_DWORD *)(*v12 + 224LL) )
j_il2cpp_runtime_class_init_0(*v12);
v17 = System_Convert__ToUInt64_8763844(iIIIi, 0LL);
SmallKeyboard__iI1Ii_4610736(this, v17, v18);
SmallKeyboard__oO0oOo0(this, v19);
}
else if ( KeyType == 3 )
{
v9 = this->fields.iIIIi;
if ( !v9 )
goto LABEL_19;
v10 = System_String__Remove_8642964(v9, v9->fields._stringLength - 1, 0LL);
goto LABEL_10;
}
LABEL_16:
inputObj = this->fields.inputObj;
if ( !inputObj )
goto LABEL_19;
Component_object = UnityEngine_GameObject__GetComponent_object_(
inputObj,
*(const MethodInfo_521C18 **)off_6EF5EEB4F0[0]);
if ( !Component_object )
goto LABEL_19;
((void (__fastcall *)(Il2CppObject *, struct System_String_o *, const MethodInfo *))Component_object->klass->vtable[75].methodPtr)(
Component_object,
this->fields.iIIIi,
Component_object->klass->vtable[75].method);
}

dump.cs 中找到 KeyType

1
2
3
4
5
6
7
8
9
public enum SmallKeyboard.KeyboardType // TypeDefIndex: 3310
{
// Fields
public int value__; // 0x0
public const SmallKeyboard.KeyboardType Number = 0;
public const SmallKeyboard.KeyboardType Character = 1;
public const SmallKeyboard.KeyboardType EnterKey = 2;
public const SmallKeyboard.KeyboardType BackSpace = 3;
}

可以看出 EnterKey 对应的值是 2,这个应该就对应的是小键盘中的 OK

hook 一下 SmallKeyboard__iI1Ii_4610736;SmallKeyboard__oO0oOo0;的参数

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
function readil2cppString(ptr){
if(ptr.isNull()){
return "";
}
try{
// Il2Cpp String 结构:
// 32位: [Header 12 bytes] + [Length 4 bytes] + [Chars...]
// 64位: [Header 16 bytes] + [Length 4 bytes] + [Chars...]
// 通常 length 在偏移 0x10 处 (64位)
let length = ptr.add(0x10).readInt();
return ptr.add(0x14).readUtf16String(length);
}catch(e){
return "(error reading string)";
}
}

function hook_SmallKeyboard(){
let mod = Process.findModuleByName("libil2cpp.so");
if(!mod){
console.log("[-] Module not found");
return;
}
let baseAddr = mod.base;
console.log("[*] libil2cpp.so base address: " + baseAddr);
// let func_offset = 0x465880;
let func_offsets = [0x465AB0, 0x465E90];

// let func_addr = baseAddr.add(func_offset);
func_offsets.forEach(function(offset, index){
let func_addr = baseAddr.add(offset);
console.log("[*] Hooking function" + index + " at offset: " + offset.toString(16));
Interceptor.attach(func_addr, {
onEnter: function(args){
console.log("[*] Function " + index + " (offset: 0x" + offset.toString(16) + ") called");
this.instance = args[0];
this.arg2 = args[1];
this.arg3 = args[2];
console.log(" |-- instance (this): " + this.instance);
console.log(" |-- arg2: " + this.arg2);
console.log(" |-- arg3: " + this.arg3);

let str = readil2cppString(this.arg2);
console.log(" |-- arg2 string value: " + str);
},
onLeave: function(retval){
console.log("[*] Function " + index + " returned");
}
});
})

// Interceptor.attach(func_addr,{
// onEnter:function(args){
// console.log("[*] SmallKeyboard function called");
// this.instance = args[0];
// this.arg2 = args[1];
// this.arg3 = args[2];
// console.log(" |-- instance (this): " + this.instance);
// console.log(" |-- arg2: " + this.arg2);
// console.log(" |-- arg3: " + this.arg3);
// let str = readil2cppString(this.arg2);
// console.log(" |-- arg2 string value: " + str);


// },
// onLeave:function(retval){
// console.log("[*] SmallKeyboard function returned");

// }
// })

}

setImmediate(hook_SmallKeyboard);

输出中可以看到 function 0 (SmallKeyboard__iI1Ii_4610736) 其中的 args2 就是小键盘中的输入(输入为 1111)

1
2
3
4
5
6
7
8
9
10
11
12
[*] Function 0 (offset: 0x465ab0) called
|-- instance (this): 0x70a2458a00
|-- arg2: 0x457
|-- arg3: 0x0
|-- arg2 string value: (error reading string)
[*] Function 0 returned
[*] Function 1 (offset: 0x465e90) called
|-- instance (this): 0x70a2458a00
|-- arg2: 0x0
|-- arg3: 0x0
|-- arg2 string value:
[*] Function 1 returned

在 ida 中点进 SmallKeyboard__iI1Ii_4610736 函数

它从一个叫 g_sec2023_p_array_ptr 的全局结构体中,读取偏移量为 0x48 的函数指针,并跳转执行该函数; U P 重新反编译一下可以看到它是全局表 g_sec2023_p_array_ptr 里面的第十个函数
那么现在就去看看 libsec2023.so

libsec2023.so 分析

libsec2023.so 的导出表中看到 g_sec2023_p_array_ptr,点进去可以看到一个跳转表,我们要找的 0x48 偏移处的就是 sub_31164 这个函数

用 frida hook 这个函数游戏会显示 hack detected,然后闪退,应该是有检测

由于目前没法使用 stackplz 或者 rwProcmem33,原因详见记一次失败的把 rwProcMem33 编译进 MI MIX2 内核的尝试,没法找到具体的检测函数,这部分的内容就暂且跳过

emmm 后续来了,买了台 pixel6,我爽学内核爽用 stackplz,回顾上文 hook 函数退出,可能是有 crc 校验,之后又去尝试在 libsec2023.so 里面 hook openat 等函数但没 hook 到,估计是通过 SVC 调用来对抗 frida hook
ps:
SVC(SuperVisor Call) 是 ARM 架构中的一条汇编指令,能够从用户态切换到内核态,允许应用程序请求操作系统执行特权操作
如果直接调用系统库的的函数,frida 可以轻松拦截到,但是如果通过 SVC 指令调用内核提供的系统调用接口,frida 就无法直接拦截,因为它是直接写汇编,不调用 libc 的 open

1
2
3
4
5
6
7
8
9
10
11
long raw_syscall_openat(int dirfd, const char *pathname, int flags) {
long ret;
__asm__ volatile(
"mov x8, #56\n" // 56 是 openat 在 ARM64 里的系统调用号
"svc #0\n" // <--- 这里的 SVC 就是直接进内核,不经过 libc
: "=r"(ret)
: "r"(dirfd), "r"(pathname), "r"(flags)
: "x8", "memory"
);
return ret;
}

于是用 stackplz 追踪内核,果然发现 libsec2023.so 在时调用 openat 读取 /proc/self/maps

1
2
3
4
5
6
7
8
9
10
11
12
13
[12070|12092|Thread-927] openat(dirfd=-100, *pathname=0x79e1e7cbd9(/proc/self/maps), flags=0x0, mode=0o666(S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)) LR:0x79e1e285a4 PC:0x79e1e29ff0 SP:0x79e1cfdb70, Backtrace:
#00 pc 0000000000020ff0 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#01 pc 000000000001f5a0 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#02 pc 000000000002db14 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#03 pc 000000000002e2a0 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#04 pc 000000000002e354 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#05 pc 0000000000039630 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#06 pc 0000000000038ea4 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#07 pc 0000000000036498 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#08 pc 000000000003633c /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#09 pc 0000000000011f40 /data/app/~~t9YtPNrPVvV0x4t9bzj8HQ==/com.com.sec2023.rocketmouse.mouse-PXj9C5mqeJs5_s7TSF3z3w==/lib/arm64/libsec2023.so
#10 pc 0000000000080e6c /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+236)
#11 pc 00000000000736d0 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)

SVC 调用

根据栈回溯往前寻找,在 0x36498 处跳转的地址不是常量,而是寄存器 x8 中保存的地址

hook x8 寄存的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let soName = "libsec2023.so";
let offset_instr = 0x36498;
function hookCmpInstruction() {
const mod = Process.findModuleByName(soName);
if (!mod) {
console.log("[-] Module not found: " + soName);
return;
}
let moduleBase = mod.base;
let targetAddress = moduleBase.add(offset_instr);
console.log("Found " + soName + " at: " + moduleBase);
Interceptor.attach(targetAddress, {
onEnter: function(args) {
let x8 = this.context.x8
console.log("x8 " + x8);
let x0 = this.context.x0;

}
});
}
setImmediate(hookCmpInstruction,0);

hook 结果:0x6de98390ac - 0x6de9802000 = 0x370ac

1
2
3
[Remote::com.com.sec2023.rocketmouse.mouse ]-> Found libsec2023.so at: 0x6de9802000
Found libsec2023.so at: 0x6de9802000
x8 0x6de98390ac

分析 sub_370ac

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
000370ac        uint64_t sub_370ac(void* arg1)

000370c4 uint64_t x20 = _ReadMSR(SystemReg: tpidr_el0)
000370c8 int64_t x8 = *(x20 + 0x28)
000370e0 *(arg1 + 0x38) += 1
000370e8 void var_50
000370e8 sub_36558(&var_50, arg1)
000370f0 int64_t x0_1 = *(arg1 + 0x20)
000370f0
000370f4 if (x0_1 != 0)
000370f8 sub_413a4(mem: x0_1)
000370f8
00037100 __builtin_memset(dest: arg1 + 0x20, ch: 0, count: 0x18)
00037130 int32_t x19_1
00037130
00037130 switch (sub_37254(arg1) != 0 ? 1 : 0)
00037134 case 0
00037134 int64_t* x22_1 = *(arg1 + 0x20)
00037134 int64_t x23_1 = *(arg1 + 0x28)
00037144 int64_t x8_7
00037144
00037144 x8_7 = x23_1 == x22_1 ? 8 : 0x18
00037144
00037158 switch (x8_7)
00037184 case 0x18
00037184 while (true)
00037184 int64_t x8_13 = x22_1[1]
00037194 int64_t x9_1
00037194
00037194 x9_1 = x8_13 == 0 ? 0x30 : 0x20
00037194
000371a8 switch (x9_1)
000371b0 case 0x20
000371b0 uint64_t x2_1 = zx.q(x22_1[2].d)
000371bc int64_t x1_1 = *x22_1 + x8_13
000371c4 int32_t var_54 = 0xeecf7326
000371dc int64_t x8_15
000371dc
000371dc if (sub_376cc(&var_54, x1_1, x2_1) == x22_1[3].d)
000371dc x8_15 = 0x28
000371dc else
000371dc x8_15 = 0x38
000371dc
000371f0 switch (x8_15)
000371f0 case 0x38
000371f0 goto label_3720c
000371f0
0003715c x22_1 = &x22_1[4]
0003716c int64_t x8_10
0003716c
0003716c x8_10 = x23_1 == x22_1 ? 8 : 0x18
0003716c
00037180 switch (x8_10)
00037180 case 8
00037180 break
00037180 case 0x18
00037180 continue
00037180
00037218 x19_1 = 0
0003720c case 1
0003720c label_3720c:
0003720c (*(arg1 + 0x18) ^ 0x1d3a3590)(zx.q(*(arg1 + 8)))
00037210 x19_1 = -1
00037210
00037220 sub_36580(&var_50)
00037220
00037230 if (*(x20 + 0x28) == x8)
0003724c return zx.q(x19_1)
0003724c
00037250 __stack_chk_fail()
00037250 noreturn

核心逻辑大概是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch (sub_37254(arg1) != 0 ? 1 : 0) // sub_37254 负责重新填充校验列表
case 0: // 如果 sub_37254 返回 0 (成功)
// 开始遍历列表
while (true)
// ... 边界检查 ...

// 读取条目数据
int64_t offset = x22_1[1];
int64_t size = x22_1[2];
int64_t ptr = *x22_1 + offset; // 计算实际内存地址

// 计算哈希
int32_t seed = 0xeecf7326; // 魔法常数/种子
// sub_376cc 是哈希计算函数 (可能是 CRC32 变种)
if (sub_376cc(&seed, ptr, size) != x22_1[3]) {
goto label_3720c; // 哈希不匹配,跳转到失败处理
}

x22_1 = &x22_1[4]; // 移动到下一个条目 (4 * 8 = 32 bytes)

x19_1 = 0; // 校验通过,返回 0

查看 sub_376cc 函数验证推测,这个函数里面有两个间接跳转,通过 hook br 跳转的寄存器值再在 bn 的 mlil 中 Set Value,得到完整的反编译代码,是一个标准的 crc32 算法

绕过 crc 校验:

csel x8, x8, x9, eq 指令改成 csel x8, x8, x8, eq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function anti_sec2023() {
Java.perform(function () {
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var libso = Process.findModuleByName("libsec2023.so");
console.log("[name]:", libso.name);
console.log("[base]:", libso.base);
console.log("[size]:", ptr(libso.size));
console.log("[path]:", libso.path);
const basePtr = ptr(libso.base);
Memory.protect(basePtr, libso.size, 'rwx');
basePtr.add(0x371DC).writeByteArray([0x08,0x01,0x88,0x9A]);
console.log("Patched libsec2023.so at offset 0x371DC");

//后续可以插入 hook 函数

});
}

setImmediate(anti_sec2023,1000);

后面发现 0x36498 处指令所在的函数开头就有个 sleep,从这里其实就能够看出端倪了

解混淆

ps:bn 中 rebaseAnalysis
sub_31164 中调用了 sub_3b8cc,点进去看到有跳转 bn 没有分析出来,可以通过把 Sections 中的 .data 设置为 Read-only data 解决

对于去除 CSEL-BR/CSET-BR 这类结构的间接跳转,一种思路通过模拟执行计算出跳转地址,再 patch 汇编指令,但是这种方式对于期望结构外的跳转汇编不好处理;另外一种方式,通过根据 workflow 相关原理修改 bn 中的 IL 来实现

加密算法

加密一 sub_3b9d4

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
0003b9d4    void sub_3b9d4(int64_t arg1)

0003b9d8 int64_t x8 = 0
0003b9f0 jump_table_72c40[0]
0003b9f0
0003ba04 while (true)
0003ba04 int64_t x10_3 = 3
0003ba18 jump_table_72c40[2]
0003ba24 int32_t var_4 = 0
0003ba2c int32_t x11_3 = 0x18
0003ba2c
0003ba48 while (true)
0003ba48 *(&var_4 + x10_3) =
0003ba48 (*(arg1 + (x8 << 2)) u>> x11_3).b ^ x10_3.b
0003ba4c x10_3 -= 1
0003ba58 int64_t x12_5
0003ba58
0003ba58 x12_5 = x10_3 s>= 0 ? 0x10 : 0x18
0003ba58
0003ba68 x11_3 -= 8
0003ba68
0003ba70 switch (x12_5)
0003ba70 case 0x10
0003ba70 continue
0003ba70 case 0x18
0003ba70 break
0003ba70
0003ba94 var_4:3.b ^= 0x86
0003ba9c var_4:2.b -= 0x5e
0003baa0 int64_t x11_7 = 3
0003baa8 var_4:1.b ^= 0xd3
0003bab0 var_4.b -= 0x1c
0003babc *(arg1 + (x8 << 2)) = 0
0003bac4 jump_table_72c40[4]
0003bad0 int32_t x10_4 = 0
0003bad8 int32_t x12_11 = 0x18
0003bad8
0003bae8 while (true)
0003bae8 char x14_3 = *(&var_4 + x11_7) - x12_11.b
0003baec *(&var_4 + x11_7) = x14_3
0003baf8 x11_7 -= 1
0003bb00 x10_4 += zx.d(x14_3) << x12_11
0003bb0c *(arg1 + (x8 << 2)) = x10_4
0003bb10 int64_t x13_5
0003bb10
0003bb10 x13_5 = x11_7 s>= 0 ? 0x20 : 8
0003bb10
0003bb20 x12_11 -= 8
0003bb20
0003bb28 switch (x13_5)
0003bb28 case 8
0003bb28 break
0003bb28 case 0x20
0003bb28 continue
0003bb28
0003bb2c x8 += 1
0003bb38 int64_t x10_5
0003bb38
0003bb38 x10_5 = x8 u< 2 ? 0 : 0x28
0003bb38
0003bb4c switch (x10_5)
0003bb4c case 0
0003bb4c continue
0003bb4c case 0x28
0003bb4c break

frida hook 一下函数的参数和返回值,输入 11111111(即 A98AC7)

用 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
def _u8(x): return x & 0xFF

def encrypt_word(w: int) -> int:
res = 0
for i in range(4):
b = (w >> (8 * i)) & 0xFF
t = b ^ i
if i == 3:
t = t ^ 0x86
elif i == 2:
t = _u8(t - 0x5E)
elif i == 1:
t = t ^ 0xD3
else: # i == 0
t = _u8(t - 0x1C)
v = _u8(t - (i * 8))
res |= (v << (8 * i))
return res & 0xFFFFFFFF

def decrypt_word(w_enc: int) -> int:
res = 0
for i in range(4):
v = (w_enc >> (8 * i)) & 0xFF
t = _u8(v + (i * 8))
if i == 3:
t = t ^ 0x86
elif i == 2:
t = _u8(t + 0x5E)
elif i == 1:
t = t ^ 0xD3
else:
t = _u8(t + 0x1C)
b = t ^ i
res |= (b << (8 * i))
return res & 0xFFFFFFFF

if __name__ == "__main__":
x = 11111111
e = encrypt_word(x)
d = decrypt_word(e)
print("input:", x)
print("encrypted (dec):", e)
print("encrypted (hex):", hex(e))
print("decrypted:", d)


# input: 11111111
# encrypted (dec): 1832734891
# encrypted (hex): 0x6d3d50ab
# decrypted: 11111111

加密二

在第一次加密之后有个 _byteswap,然后再把结果传给第二个加密函数 sub_3a924

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
0003b8cc    uint64_t sub_3b8cc(int64_t arg1)

0003b8e4 uint64_t x20 = _ReadMSR(SystemReg: tpidr_el0)
0003b8e8 int64_t x8 = *(x20 + 0x28)
0003b8f4 uint32_t var_58 = (arg1 u>> 0x20).d
0003b8f4 int32_t var_54 = arg1.d
0003b8fc sub_3b9d4(&var_58) // enc1
0003b904 int32_t temp0 = _byteswap(var_58)
0003b908 int32_t var_50 = 0
0003b908 int32_t var_4c = temp0
0003b920 // 高 32 位
0003b920 int32_t x0_2 = sub_3a924(sub_3a054(), &var_4c, 4, &var_50, 4)
0003b928 int64_t x8_3 = 0x10
0003b934 ASSERT(x8_3, ConstantValue: 0x10)
0003b934 int64_t x8_4
0003b934
0003b934 x8_4 = x0_2 != 0 ? 0x18 : 0x10
0003b934
0003b93c ASSERT(x8_4, ConstantValue: 0x10)
0003b960 int32_t x19_1 = var_50
0003b968 int32_t temp0_1 = _byteswap(var_54)
0003b96c var_50 = 0
0003b96c var_4c = temp0_1
0003b984 sub_3a924(sub_3a054(), &var_4c, 4, &var_50, 4) // 低 32 位
0003b984
0003b990 switch (x0_2 != 0 ? 1 : 0)
0003b998 case 1
0003b998 sub_367b0()
0003b998
0003b9ac if (*(x20 + 0x28) == x8)
0003b9cc // 高 32 位和低 32 位交换合并
0003b9cc return zx.q(x19_1) | zx.q(var_50) << 0x20
0003b9cc
0003b9d0 __stack_chk_fail()
0003b9d0 noreturn
```

`byteswap` 那一行对应汇编:对 32 位值做了一个字节序反转(大小端序转换)
```asm
0003b900 e80b40b9 ldr w8, [sp, #0x8 {var_58}]
0003b904 0809c05a rev w8, w8

然后把反转后的值传给 sub_3a924 函数,进行第二次加密
hook sub_3a924 函数,这个函数在 sub_3b8cc 中被调用了两次

由上文可知经过第一次 enc2 后得到密文 e4 ca 94 6d ab 50 3d 6d

可以看到它是先对密文的高 32 位进行加密,然后对低 32 位进行加密

sub_3a924 里面可以看到好多都是类型都是 uint64_t*,还有 +0x680+0x580,这里其实应该是 JNIEnv* 结构体指针,但是 bn 中没有 JNI 相关的类型,需要自己导入
可以参考项目 jni_helper

先用 extract_jni.py 生成 apk 的 signature.json 文件

1
2
3
# 需要先安装依赖
pip3 install androguard rich pyelftools
python extract_jni.py mouse_pre.aligned.signed.apk -o signature.json

然后在 bn 中 Run Script 选择jni_helper.py,根据弹窗导入 signature.json 文件
之后按 Y 把类型改成 JNIEnv*,清晰多了

hook 一下 JNI 函数 GetStaticMethodID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hook_GetStaticMethodID() {
console.log("hook JNIgetStaticMethodID");
let symbols = Module.load("libart.so").enumerateSymbols();
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("GetStaticMethodID") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
console.log(symbol.name);
Interceptor.attach(symbol.address, {
onEnter: function (args) {
var Name = args[2].readUtf8String();
var sig = args[3].readUtf8String();
let whoCallIt = DebugSymbol.fromAddress(this.returnAddress).toString();
// send(["getStaticMethodID", Name, sig, whoCallIt]);
console.log("getStaticMethodID:", Name, sig, whoCallIt);
}
})
}
}
}

hook 结果中可以看到它调用了 encryt 方法

1
2
3
4
5
6
7
hook JNIgetStaticMethodID
_ZN3art3JNIILb1EE17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS7_
_ZN3art3JNIILb0EE17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS7_
getStaticMethodID: isDebuggerConnected ()Z 0x6de982e220 libsec2023.so!0x2d220
getStaticMethodID: encrypt ([B)[B 0x6de983ba48 libsec2023.so!0x3aa48
getStaticMethodID: encrypt ([B)[B 0x6de983ba48 libsec2023.so!0x3aa48
getStaticMethodID: isDebuggerConnected ()Z 0x6de982e220 libsec2023.so!0x2d220

但是在 apk 中搜索找不到这个方法,推测通过动态加载 dex 来进行类的调用
frida-dexdump 导出内存中的 dex 文件

1
frida-dexdump -H 127.0.0.1:13131 -f com.com.sec2023.rocketmouse.mouse

然后把 dump 出来的 dex 挨个反编译找 encrypt 方法,用 jadx 看会有混淆,用 jeb 反编译可以直接去掉

用 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
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
def ror32(v, r):
return ((v >> r) | ((v & ((1 << r) - 1)) << (32 - r))) & 0xFFFFFFFF

key = [50, -51&0xFF, -1&0xFF, -104&0xFF, 25, -78&0xFF, 0x7C, -102&0xFF]

def encrypt_4bytes(b: bytes) -> bytes:
assert len(b) == 4

# BE32
v = (
(b[0] << 24) |
(b[1] << 16) |
(b[2] << 8) |
b[3]
) & 0xFFFFFFFF

# ROR 7
v1 = ror32(v, 7)

# back to bytes (BE)
out = [
(v1 >> 24) & 0xFF,
(v1 >> 16) & 0xFF,
(v1 >> 8) & 0xFF,
v1 & 0xFF
]

# xor + index
for i in range(4):
out[i] ^= key[i]
out[i] = (out[i] + i) & 0xFF

return bytes(out)

def rol32(v, r):
return ((v << r) | (v >> (32 - r))) & 0xFFFFFFFF

def decrypt_4bytes(b: bytes) -> bytes:
assert len(b) == 4

# undo xor + index
tmp = list(b)
for i in range(4):
tmp[i] = (tmp[i] - i) & 0xFF
tmp[i] ^= key[i]

# bytes -> int
v1 = (
(tmp[0] << 24) |
(tmp[1] << 16) |
(tmp[2] << 8) |
tmp[3]
) & 0xFFFFFFFF

# undo ROR 7 => ROL 7
v = rol32(v1, 7)

return bytes([
(v >> 24) & 0xFF,
(v >> 16) & 0xFF,
(v >> 8) & 0xFF,
v & 0xFF
])


def rev32_bytes(b4: bytes) -> bytes:
return b4[::-1]


def encrypt_8bytes(b8: bytes) -> bytes:
assert len(b8) == 8

lo = b8[0:4]
hi = b8[4:8]

lo = encrypt_4bytes(rev32_bytes(lo))
hi = encrypt_4bytes(rev32_bytes(hi))

return lo + hi

def decrypt_8bytes(b8: bytes) -> bytes:
assert len(b8) == 8

lo = rev32_bytes(decrypt_4bytes(b8[0:4]))
hi = rev32_bytes(decrypt_4bytes(b8[4:8]))

return lo + hi

data8 = bytes.fromhex("e4 ca 94 6d ab 50 3d 6d")

enc8 = encrypt_8bytes(data8)
dec8 = decrypt_8bytes(enc8)

print("input :", data8.hex())
print("encrypt:", enc8.hex())
swapped = enc8[4:8] + enc8[0:4]
print("swapped:", swapped.hex())
print("decrypt:", dec8.hex())

# input : e4ca946dab503d6d
# encrypt: fa17d8106418873c
# swapped: 6418873cfa17d810
# decrypt: e4ca946dab503d6d

hook sub_31164 最后的 br 跳转地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function anti_sec2023() {
Java.perform(function () {
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var libso = Process.findModuleByName("libsec2023.so");
console.log("[name]:", libso.name);
console.log("[base]:", libso.base);
console.log("[size]:", ptr(libso.size));
console.log("[path]:", libso.path);
const basePtr = ptr(libso.base);
Memory.protect(basePtr, libso.size, 'rwx');
basePtr.add(0x371DC).writeByteArray([0x08,0x01,0x88,0x9A]);
console.log("Patched libsec2023.so at offset 0x371DC");
// hook_3b8cc();
hook_instr();
// hook_3b9d4();
});
}

setImmediate(anti_sec2023,0);
function hook_instr(){
let soName = "libsec2023.so";
let offset_instr = 0x311a0;
const mod = Process.findModuleByName(soName);
if (!mod) {
console.log("[-] Module not found: " + soName);
return;
}

let moduleBase = mod.base;
let targetAddress = moduleBase.add(offset_instr);

console.log("Found " + soName + " at: " + moduleBase);

Interceptor.attach(targetAddress, {
onEnter: function(args) {
let x2 = this.context.x2;
console.log("x2 " + x2);
addr_in_so(x2);
}
});
}

function addr_in_so(addr){
let process_Obj_Module_Arr = Process.enumerateModules();
console.log(addr);
for(let i = 0; i < process_Obj_Module_Arr.length; i++) {
if(addr > process_Obj_Module_Arr[i].base && addr < process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){
console.log(addr.toString(16)," located in ", process_Obj_Module_Arr[i].name," offset: ",(addr - process_Obj_Module_Arr[i].base).toString(16));
}
}
}

到 libil2cpp.so(fix.so) 中的 13b8d64 偏移处

1
2
3
4
.data:00000000013B8D64 loc_13B8D64                             ; DATA XREF: .data:00000000013BB168↓o
.data:00000000013B8D64 ; .data:00000000013BC168↓o
.data:00000000013B8D64 STR X24, [SP,#-0x40]!
.data:00000000013B8D68 B sub_465AB4

加密三 XTEA

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
// positive sp value has been detected, the output may be wrong!
__int64 __fastcall sub_465AB4(__int64 a1, unsigned __int64 a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

v2 = off_10F2820[0];
if ( (qword_119A380 & 0x100) == 0 )
{
sub_389B58((__int64)off_10F1A68);
sub_389B58((__int64)off_10F68F0[0]);
sub_389B58((__int64)off_10F1708);
sub_389B58((__int64)off_10E75E8[0]);
sub_389B58((__int64)off_10E43F8[0]);
sub_389B58((__int64)off_1100098[0]);
sub_389B58((__int64)off_10F2820[0]);
BYTE1(qword_119A380) = 1;
}
v5 = (System_UInt32_array *)sub_389B90(*v2, 8u);
v6 = v5;
if ( !v5 )
sub_389C40();
max_length = v5->max_length;
if ( !max_length )
sub_389C48();
v5->m_Items[0] = 0x12345678;
if ( max_length == 1 )
sub_389C48();
v8 = off_1100098[0];
v5->m_Items[1] = 0x98765432;
v9 = (System_Array_o *)sub_389B90(*v8, 0xC7u);
v10.fields.value = *off_10E43F8[0];
System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_9068608(v9, v10, 0LL);
v11 = off_10F1A68;
if ( !*((_DWORD *)*off_10F1A68 + 56) )
j_il2cpp_runtime_class_init_0();
low32_input = System_Convert__ToUInt32_8761148((unsigned int)a2, 0LL);
if ( !LODWORD(v6->max_length) )
sub_389C48();
v6->m_Items[0] = low32_input;
high32_input = System_Convert__ToUInt32_8761148(HIDWORD(a2), 0LL);
if ( LODWORD(v6->max_length) <= 1 )
sub_389C48();
v14 = off_10F68F0[0];
v6->m_Items[1] = high32_input;
v15 = (OO0OoOOo_Oo0_o *)sub_389C30(*v14);
v17 = v15;
if ( !v15 )
sub_389C40();
OO0OoOOo_Oo0___ctor(v15, (System_UInt16_array *)v9, 0, v6, v16);
v17->fields.pc = v17->fields.flags;
OO0OoOOo_Oo0__oOOoO0o0(v17, v18);
v19 = v6->max_length;
if ( !v19 )
sub_389C48();
if ( v19 == 1 )
sub_389C48();
v20 = v6->m_Items[0];
v21 = v6->m_Items[1];
key_ctx = sub_389B90(*v2, 4u);
v23.fields.value = *off_10E75E8[0];
System_Runtime_CompilerServices_RuntimeHelpers__InitializeArray_9068608((System_Array_o *)key_ctx, v23, 0LL);
if ( !key_ctx )
sub_389C40();
v24 = off_10F1708;
key_cnt = *(_DWORD *)(key_ctx + 24);
sum0 = 0xBEEFBEEF;
sum1 = 0x9D9D7DDE;
rounds = 64;
do
{
if ( (sum0 & 3) >= key_cnt )
sub_389C48();
key_idx1 = (sum1 >> 13) & 3;
if ( key_idx1 >= key_cnt )
sub_389C48();
v20 += (sum0 - *(_DWORD *)(key_ctx + 4LL * (sum0 & 3) + 32)) ^ (((v21 << 7) ^ (v21 >> 8)) + v21);
--rounds;
sum0 -= 0x21524111;
v21 += (sum1 + *(_DWORD *)(key_ctx + 4LL * key_idx1 + 32)) ^ (((v20 << 8) ^ (v20 >> 7)) - v20);
sum1 -= 0x21524111;
}
while ( rounds );
v30 = *(System_String_o **)(a1 + 56);
if ( !*((_DWORD *)*v11 + 56) )
j_il2cpp_runtime_class_init_0();
result = System_Convert__ToUInt32_8761612(v30, 0LL);
if ( !v21 && (_DWORD)result == v20 )
**((_DWORD **)*v24 + 23) = -1;
return result;
}

可以看到下半部分有一个很明显的 64 轮循环是变种的 XTEA

用 frida hook 一下 key 的值

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
function hook_xtea_key(){
let mod = Process.findModuleByName("libil2cpp.so");
if(!mod){
console.log("[-] Module not found");
return;
}
let baseAddr = mod.base;
let offset = 0x465C60;
Interceptor.attach(baseAddr.add(offset),{
onEnter:function(args){
console.log("[*] sub_389B90 called");
this.instance = args[0];
this.arg2 = args[1];
console.log(" |-- instance (this): " + this.instance);
console.log(" |-- arg2: " + this.arg2);
console.log(hexdump(this.context.x20,{
offset: 0,
length: 64,
header: true,
ansi: true
}));
},
onLeave:function(retval){
console.log("[*] sub_389B90 returned:" + retval);
}
})

}
setImmediate(hook_xtea_key,0);

得到密钥 0x7b777c63, 0xc56f6bf2, 0x2b670130, 0x76abd7fe
还原出加解密

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
def u32(x):
return x & 0xFFFFFFFF

def xtea_variant_encrypt(v0, v1, key):
v0 = u32(v0)
v1 = u32(v1)

sum0 = 0xBEEFBEEF
sum1 = 0x9D9D7DDE
delta = 0x21524111

for _ in range(64):
v0 = u32(v0 + ((sum0 - key[sum0 & 3])^ (((v1 << 7) ^ (v1 >> 8)) + v1)))
sum0 = u32(sum0 - delta)

v1 = u32(v1 + ((sum1 + key[(sum1 >> 13) & 3])^ (((v0 << 8) ^ (v0 >> 7)) - v0)))
sum1 = u32(sum1 - delta)

return v0, v1


def xtea_variant_decrypt(v0, v1, key):
v0 = u32(v0)
v1 = u32(v1)

delta = 0x21524111
sum0 = u32(0xBEEFBEEF - 64 * delta)
sum1 = u32(0x9D9D7DDE - 64 * delta)

for _ in range(64):
sum1 = u32(sum1 + delta)
v1 = u32(v1 - ((sum1 + key[(sum1 >> 13) & 3])^ (((v0 << 8) ^ (v0 >> 7)) - v0)))

sum0 = u32(sum0 + delta)
v0 = u32(v0 - ((sum0 - key[sum0 & 3])^ (((v1 << 7) ^ (v1 >> 8)) + v1)))

return v0, v1

key = [0x7b777c63, 0xc56f6bf2, 0x2b670130, 0x76abd7fe]
input_high = 0x6418873c
input_low = 0xfa17d810
v0, v1 = xtea_variant_encrypt(input_high, input_low, key)
print("Encrypted: {:08x} {:08x}".format(v0, v1))
p0, p1 = xtea_variant_decrypt(v0, v1, key)
print("Decrypted: {:08x} {:08x}".format(p0, p1))
assert p0 == input_high and p1 == input_low

# Encrypted: 5cba71a8 25825199
# Decrypted: 6418873c fa17d810

加密四 vm

在魔改的 xtea 之前还有混淆的函数

1
2
3
OO0OoOOo_Oo0___ctor(v15, (System_UInt16_array *)v9, 0, v6, v16);
v17->fields.oOOO0Oo0 = v17->fields.oOOO0O0O;
OO0OoOOo_Oo0__oOOoO0o0(v17, v18);

找到这个类,在其他混淆函数中还看到有 mba 表达式

可以用 ida 插件去一下混淆

重命名之后可以发现这些混淆函数好多都是在进行一些基本的计算操作

点进 ctor 下面的那个混淆函数,又有个 while(1) 循环

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 OO0OoOOo_Oo0__oOOoO0o0(OO0OoOOo_Oo0_o *this, const MethodInfo *method)
{
struct System_UInt16_array *bytecode; // x8
__int64 pc; // x9
_UNKNOWN **v5; // x21
const MethodInfo_7C771C **v6; // x22
_DWORD *v7; // x0
int32_t v8; // w20
struct System_UInt16_array *v9; // x9
int32_t v10; // w8
System_Collections_Generic_Dictionary_TKey__TValue__o *dispatchTable; // x0
Il2CppObject *Item; // x0

if ( (qword_119A3D8 & 0x100) == 0 )
{
sub_389B58((__int64)off_10EDED0[0]);
sub_389B58((__int64)off_10EB448);
BYTE1(qword_119A3D8) = 1;
}
bytecode = this->fields.bytecode;
if ( !bytecode )
LABEL_17:
sub_389C40();
pc = this->fields.pc;
if ( (unsigned int)pc >= LODWORD(bytecode->max_length) )
LABEL_15:
sub_389C48();
v5 = off_10EB448;
v6 = (const MethodInfo_7C771C **)off_10EDED0[0];
while ( 1 )
{
v7 = *v5;
v8 = bytecode->m_Items[pc];
if ( !*((_DWORD *)*v5 + 56) )
{
j_il2cpp_runtime_class_init_0();
v7 = *v5;
}
if ( *(_DWORD *)(*((_QWORD *)v7 + 23) + 68LL) == v8 )
break;
v9 = this->fields.bytecode;
if ( !v9 )
goto LABEL_17;
v10 = this->fields.pc;
if ( v10 >= SLODWORD(v9->max_length) )
break;
dispatchTable = (System_Collections_Generic_Dictionary_TKey__TValue__o *)this->fields.dispatchTable;
this->fields.pc = v10 + 1;
if ( !dispatchTable )
goto LABEL_17;
Item = System_Collections_Generic_Dictionary_int__object___get_Item(dispatchTable, v8, *v6);
if ( !Item )
goto LABEL_17;
((void (__fastcall *)(Il2CppClass *, void *))Item[1].monitor)(Item[4].klass, Item[2].monitor);
bytecode = this->fields.bytecode;
if ( !bytecode )
goto LABEL_17;
pc = this->fields.pc;
if ( (unsigned int)pc >= LODWORD(bytecode->max_length) )
goto LABEL_15;
}
}

然后去 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
function hook_vm(){
let mod = Process.findModuleByName("libil2cpp.so");
if(!mod){
console.log("[-] Module not found");
return;
}
let baseAddr = mod.base;

// function logOp(op, v1, v2, res) {
// console.log(`[${op}] ${v1} ${op} ${v2} = ${res}`);
// }

console.log(`[+] VM Hook attached at: ${baseAddr}`);

Interceptor.attach(baseAddr.add(0x465BCC), {
onEnter: function(args){ console.log("low32_input: " + this.context.x0); }
});
Interceptor.attach(baseAddr.add(0x465BF0), {
onEnter: function(args){ console.log("high32_input: " + this.context.x0); }
});

console.log("vm start ==============");

Interceptor.attach(baseAddr.add(0x46AEA4), {
onEnter: function(args){

let op1 = this.context.x10;
let op2 = this.context.x11;
console.log(`[ADD] ${op1} + ${op2} // Res: ${op1.add(op2)}`);
}
});

Interceptor.attach(baseAddr.add(0x46AF20),{
onEnter:function(args){
console.log(`[SUB] ${this.context.x10} - ${this.context.x11}`);
}
});

Interceptor.attach(baseAddr.add(0x46AFAC),{
onEnter:function(args){
console.log(`[MUL] ${this.context.x10} * ${this.context.x11}`);
}
});

Interceptor.attach(baseAddr.add(0x46B028),{
onEnter:function(args){
console.log(`[LShift] ${this.context.x10} << ${this.context.x11}`);
}
});

Interceptor.attach(baseAddr.add(0x46B0A4),{
onEnter:function(args){
console.log(`[RShift] ${this.context.x10} >> ${this.context.x11}`);
}
});

Interceptor.attach(baseAddr.add(0x46B124),{
onEnter:function(args){
console.log(`[AND] ${this.context.x10} & ${this.context.x11}`);
}
});

Interceptor.attach(baseAddr.add(0x46B1A8),{
onEnter:function(args){
console.log(`[XOR] ${this.context.x10} ^ ${this.context.x12}`);
}
});

Interceptor.attach(baseAddr.add(0x46B20C),{
onEnter:function(args){
console.log(`[LT] ${this.context.x10} < ${this.context.x11}`);
}
});

Interceptor.attach(baseAddr.add(0x46B270),{
onEnter:function(args){
console.log(`[EQ] ${this.context.x10} == ${this.context.x11}`);
}
});
}

setImmediate(hook_vm);

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
low32_input: 0x10d817fa
high32_input: 0x3c871864
[RShift] 0x21b02ff3 >> 0x18
[AND] 0x10 & 0xff
[SUB] 0x18 - 0x8
[LT] 0x10 < 0x0
[RShift] 0x21b02ff3 >> 0x10
[AND] 0x10d8 & 0xff
[SUB] 0x10 - 0x8
[LT] 0x8 < 0x0
[RShift] 0x21b02ff3 >> 0x8
[AND] 0x10d817 & 0xff
[SUB] 0x8 - 0x8
[LT] 0x0 < 0x0
[RShift] 0x21b02ff3 >> 0x0
[AND] 0x10d817fa & 0xff
[SUB] 0x0 - 0x8
[LT] 0xfffffff8 < 0x0
[SUB] 0xfa - 0x1b
[XOR] 0xd7 ^ 0x17
[ADD] 0xd8 + 0xa8 // Res: 0x180
[XOR] 0x36 ^ 0x10
[XOR] 0xdf ^ 0xdf
[LShift] 0xde << 0x0
[LShift] 0xfe << 0x0
[AND] 0xdf & 0xff
[ADD] 0xdf + 0x0 // Res: 0xdf
[ADD] 0x4 + 0x1 // Res: 0x5
[ADD] 0x0 + 0x8 // Res: 0x8
[LT] 0x8 < 0x19
[XOR] 0xdd ^ 0xd5
[LShift] 0xdc << 0x8
[LShift] 0xfe << 0x8
[AND] 0xdd00 & 0xff00
[ADD] 0xdd00 + 0xdf // Res: 0xdddf
[ADD] 0x5 + 0x1 // Res: 0x6
[ADD] 0x8 + 0x8 // Res: 0x10
[LT] 0x10 < 0x19
[XOR] 0x190 ^ 0x180
[LShift] 0x18f << 0x10
[LShift] 0xfe << 0x10
[AND] 0x1900000 & 0xff0000
[ADD] 0x900000 + 0xdddf // Res: 0x90dddf
[ADD] 0x6 + 0x1 // Res: 0x7
[ADD] 0x10 + 0x8 // Res: 0x18
[LT] 0x18 < 0x19
[XOR] 0x3e ^ 0x26
[LShift] 0xffffffbd << 0x18
[LShift] 0xfe << 0x18
[AND] 0x3e000000 & 0xff000000
[ADD] 0x3e000000 + 0x90dddf // Res: 0x3e90dddf
[ADD] 0x7 + 0x1 // Res: 0x8
[ADD] 0x18 + 0x8 // Res: 0x20
[LT] 0x20 < 0x19
[RShift] 0x790e2fc7 >> 0x18
[AND] 0x3c & 0xff
[SUB] 0x18 - 0x8
[LT] 0x10 < 0x0
[RShift] 0x790e2fc7 >> 0x10
[AND] 0x3c87 & 0xff
[SUB] 0x10 - 0x8
[LT] 0x8 < 0x0
[RShift] 0x790e2fc7 >> 0x8
[AND] 0x3c8718 & 0xff
[SUB] 0x8 - 0x8
[LT] 0x0 < 0x0
[RShift] 0x790e2fc7 >> 0x0
[AND] 0x3c871864 & 0xff
[SUB] 0x0 - 0x8
[LT] 0xfffffff8 < 0x0
[SUB] 0x64 - 0x2f
[XOR] 0xbe ^ 0x18
[ADD] 0x87 + 0x37 // Res: 0xbe
[XOR] 0xbc ^ 0x3c
[ADD] 0x35 + 0x0 // Res: 0x35
[LShift] 0xffffffb4 << 0x0
[LShift] 0xfe << 0x0
[AND] 0x35 & 0xff
[ADD] 0x35 + 0x0 // Res: 0x35
[ADD] 0x4 + 0x1 // Res: 0x5
[ADD] 0x0 + 0x8 // Res: 0x8
[LT] 0x8 < 0x19
[ADD] 0xae + 0x8 // Res: 0xb6
[LShift] 0xb5 << 0x8
[LShift] 0xfe << 0x8
[AND] 0xb600 & 0xff00
[ADD] 0xb600 + 0x35 // Res: 0xb635
[ADD] 0x5 + 0x1 // Res: 0x6
[ADD] 0x8 + 0x8 // Res: 0x10
[LT] 0x10 < 0x19
[ADD] 0xbe + 0x10 // Res: 0xce
[LShift] 0xcd << 0x10
[LShift] 0xfe << 0x10
[AND] 0xce0000 & 0xff0000
[ADD] 0xce0000 + 0xb635 // Res: 0xceb635
[ADD] 0x6 + 0x1 // Res: 0x7
[ADD] 0x10 + 0x8 // Res: 0x18
[LT] 0x18 < 0x19
[ADD] 0xa4 + 0x18 // Res: 0xbc
[LShift] 0xbb << 0x18
[LShift] 0xfe << 0x18
[AND] 0xbc000000 & 0xff000000
[ADD] 0xbc000000 + 0xceb635 // Res: 0xbcceb635
[ADD] 0x7 + 0x1 // Res: 0x8
[ADD] 0x18 + 0x8 // Res: 0x20
[LT] 0x20 < 0x19

根据 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
def encrypt(low, high):
l_bytes = list(low.to_bytes(4, 'big'))
h_bytes = list(high.to_bytes(4, 'big'))

l_out = [0] * 4
l_out[0] = (l_bytes[0] ^ 0x36) + 0x18 # Byte 0
l_out[1] = ((l_bytes[1] + 0xA8) & 0xff) ^ 0x10 # Byte 1
l_out[2] = (l_bytes[2] ^ 0xD7) + 0x1D # Byte 2
l_out[3] = l_bytes[3] - 0x1B # Byte 3

res_low = 0
for b in l_out:
res_low = (res_low << 8) | (b & 0xff)

h_out = [0] * 4
h_out[0] = (h_bytes[0] ^ 0x98) + 0x18 # Byte 0
h_out[1] = h_bytes[1] + 0x37 + 0x10 # Byte 1
h_out[2] = (h_bytes[2] ^ 0xBE) + 0x10 # Byte 2
h_out[3] = h_bytes[3] - 0x2F # Byte 3

res_high = 0
for b in h_out:
res_high = (res_high << 8) | (b & 0xff)

return res_low, res_high

in_low = 0x10d817fa
in_high = 0x3c871864

out_low, out_high = encrypt(in_low, in_high)

print(f"Input Low: {hex(in_low)}")
print(f"Calc Low: {hex(out_low)}") # 应该等于 0x3e90dddf
print("-" * 20)
print(f"Input High: {hex(in_high)}")
print(f"Calc High: {hex(out_high)}") # 应该等于 0xbcceb635


# Input Low: 0x10d817fa
# Calc Low: 0x3e90dddf
# --------------------
# Input High: 0x3c871864
# Calc High: 0xbcceb635

参考文章

细品sec2023安卓赛题
腾讯游戏安全大赛2023安卓初赛


腾讯游戏安全大赛 2023 安卓初赛题解
http://example.com/2025/12/15/TxGame2023_pre/
作者
Eleven
发布于
2025年12月15日
许可协议