强网拟态2025初赛 Mobile方向just Writeup

两只羊 Lv2

just

前言:

白天打ycb决赛,晚上9点多才回到酒店开做,后面有点头昏眼花,还好最后也是出了

1

运行环境(可正常运行APP):

Android13 (lineageOS 20)

jadx打开,发现是Unity il2cpp写的app,(可以立马关闭jadx了)

1

一开始尝试Il2CppDumper失败

img

后面可以发现libil2cpp.so和global-metadata.dat都被加密

libil2cpp.so明显不具备elf结构

1

global-metadata.dat从0x400开始被加密

1

一开始想用frida-il2cpp-bridge做,但libjust.so中有反调试,frida检测和crc校验等

frida注入直接被杀,而且第一次检测到后,除非重启手机,否则都会被杀

下面是frida的过检测脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function nop_64(addr) {
Memory.protect(addr, 4 , 'rwx');
var w = new Arm64Writer(addr);
w.putRet();
w.flush();
w.dispose();
}

function hook_clone(soname)
{
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// args[3] 子线程的栈地址。如果这个值为 0,可能意味着没有指定栈地址
if(args[3] != 0){
var addr = args[3].add(96).readPointer()
var so_name = Process.findModuleByAddress(addr).name;
var so_base = Module.getBaseAddress(so_name);
var offset = (addr - so_base);
//console.log("===============>", so_name, addr,offset, offset.toString(16));
if(so_name.indexOf(soname) >= 0) {
//console.log("nop ===============>", so_name, addr,offset, offset.toString(16));
nop_64(addr)
main()
}

}
},
onLeave: function(retval) {

}
});

}

function main()
{
var base = Module.findBaseAddress("libjust.so")
//获取目标进程的基地址
//console.log("inject success!!!")
//console.log("base:",base)
if(base){
nop_64(base.add(0x119F8)) //crc check
//nop_64(base.add(0x123E4))

}
}


setImmediate(hook_clone, "libjust.so")

//frida -U -f "com.DefaultCompany.just" -l hook_clone.js

但这样frida-il2cpp-bridge还是无法正常使用,那就只能尝试别的方法了

1

程序使用了dobbyHook框架对android_dlopen_ext和dlopen函数进行了hook

1

这边应该是检测到加载的so为libil2cpp.so且文件被加密(因为我后面将解密好的so重打包发现能正常运行)才解密的流程

1

解密部分就是一个rc4的解密

1

cyberchef直接解

1

然后是global-metadata.dat的部分,打开解密后的libil2cpp.so进行分析

可以通过字符串查找定位到加载global-metadata.dat的部分

1

跟进sub_211D94

1

https://www.bilibili.com/opus/1126494768452337668

这边和标准源码编译出来il2cpp对比可找出解密函数的部分

1

1

这里我本来是想继续hook直接dump的,但算法不复杂还是直接扔给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
#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
#include <Windows.h>

using namespace std;

char* __fastcall dec_global_metadata(unsigned __int16* src, __int64 a2)
{
__int64 v2; // x21
__int64 v4; // x8
__int64 i_2; // x22
char* dest; // x19
__int64 i; // x8
__int64 i_1; // x13
__int64 v9; // x12

v2 = src[0x200];
v4 = a2 - 4 * v2;
i_2 = v4 - 0x404;
dest = (char*)malloc(v4 - 4);
memcpy(dest, src, 0x400u);
if (i_2 >= 1)
{
for (i = 0; i < i_2; i += 4)
{
i_1 = i + 3;
v9 = i + i / v2;
if (i >= 0)
i_1 = i;
*(DWORD*)&dest[(i_1 & 0xFFFFFFFFFFFFFFFCLL) + 0x400] = *(DWORD*)((char*)&src[2 * v2 + 0x202]
+ (i_1 & 0xFFFFFFFFFFFFFFFCLL))
^ *(DWORD*)&src[2 * (v9 % v2) + 0x202];
}
}
return dest;
}
int main()
{
string inputFilePath = "D:\\CTF\\qwnt_2025\\Mobile\\just\\Il2CppDumper-win-v6.7.46\\input\\global-metadata.dat";

// 2. Open the file in binary mode
ifstream inputFile(inputFilePath, ios::binary | ios::ate);
if (!inputFile.is_open())
{
cerr << "Error: Could not open file " << inputFilePath << endl;
return 1;
}

// 3. Get the size of the file
streamsize fileSize = inputFile.tellg();
inputFile.seekg(0, ios::beg);

// 4. Read the file into a buffer (using std::vector for automatic memory management)
vector<unsigned __int16> buffer(fileSize / sizeof(unsigned __int16));
if (!inputFile.read(reinterpret_cast<char*>(buffer.data()), fileSize))
{
cerr << "Error: Could not read file " << inputFilePath << endl;
inputFile.close();
return 1;
}

inputFile.close();

// 5. Call the decryption function

char* decryptedData = dec_global_metadata(buffer.data(), fileSize);

if (decryptedData)
{
// 6. Ask the user for an output file path and save the decrypted data
string outputFilePath = "D:\\CTF\\qwnt_2025\\Mobile\\just\\Il2CppDumper-win-v6.7.46\\input\\global-metadata.dat.dec";

ofstream outputFile(outputFilePath, ios::binary);
if (!outputFile.is_open())
{
cerr << "Error: Could not create output file " << outputFilePath << endl;
free(decryptedData); // Free the memory allocated by the decryption function
return 1;
}

// The size of the decrypted data is determined by the logic inside dec_global_metadata
// v4 = a2 - 4 * v2; dest = (char*)malloc(v4 - 4);
// We need to calculate this size to write the correct amount of data.
unsigned __int16 v2 = buffer[0x200];
__int64 decryptedSize = fileSize - 4 * v2 - 4;

outputFile.write(decryptedData, decryptedSize);
outputFile.close();

cout << "File decrypted successfully and saved to " << outputFilePath << endl;

// 7. Clean up the memory allocated by dec_global_metadata
free(decryptedData);
}
else
{
cerr << "Error: Decryption failed." << endl;
}

return 0;
}

都解密出来后就可以Il2CppDumper一键梭哈了

1

后面主要就是对FlagChecker类进行分析

1

用的是纯原生frida的进行Hook,所以一下数据结构需要自己解析,这边hook了一些关键的数据转换函数

最后给出完整的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
function hook_il2cpp()
{
var il2cpp_base = Module.findBaseAddress("libil2cpp.so")
if(il2cpp_base){
console.log("il2cpp_base:",il2cpp_base)

//hook tea
Interceptor.attach(il2cpp_base.add(0x41C330), {

onEnter: function(args) {
console.log("entering TeaEncrypt", args[0], args[1])
},
onLeave: function(retval){
console.log("leaving TeaEncrypt")
}

});

//hook to uint32_t
Interceptor.attach(il2cpp_base.add(0x1B5D88), {

onEnter: function(args) {

},
onLeave: function(retval){
console.log("uint32_t => ", retval)
}

});

//hook ToUInt32LE
Interceptor.attach(il2cpp_base.add(0x41B8B8), {

onEnter: function(args) {

},
onLeave: function(retval){
console.log("ToUInt32LE => ", retval)
}

});

//hook cipher
Interceptor.attach(il2cpp_base.add(0x1B6048), {

onEnter: function(args) {
var ReallyCompare_addr = args[0];
console.log(hexdump(ReallyCompare_addr, {
offset: 0,
length: 256,
header: true,
ansi: true,
}));
},
onLeave: function(retval){
//console.log("uint32_t => ", retval)
}

});
}
}


//frida -U -f "com.DefaultCompany.just" -l hook_clone.js

最后就是一个tea的魔改

1

byte[]对象的地址,偏移24字节的qword是size,偏移32自己的两处是byte[]结构对应的数据体

1

当然这里key和密文也是可以直接静态提取的

找到其构造函数cctor后,获取这串哈希

1

可以在dump.cs中找到这串哈希在global-metadata.dat中的偏移和大小

1

直接提取出密文和key

1

最终的exp

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

//加密函数
void encrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], sum=0, i; //v0,v1分别为字符串的低字节高字节
uint32_t delta=0x61C88647;
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i < 16; i++) {
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
sum -= delta;
}
v[0]=v0; v[1]=v1;
}



//解密函数
void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v[0], v1=v[1], i;
uint32_t delta=0x61C88647;
uint32_t sum = (-16)*delta;
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i<16; i++) {
sum += delta; //解密时将加密算法的顺序倒过来,还有+=变为-=
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);

}
v[0]=v0; v[1]=v1;
}


//密文
unsigned char 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,
};

unsigned char input[]="flag{uniABCDEFGHIJKLMNOPQRSTUVWXYZabcdef";

int main()
{
//flag{D0_you_l1ke_th3_m4gic_uN1c0rn_with_A4rch64}

unsigned char a;
uint32_t *v = (uint32_t*)input;
unsigned char *p = (unsigned char*)v;
uint32_t k[4]={0x12345678, 0x09101112, 0x13141516, 0x15161718};


encrypt(v, k);
for(int l = 8; l < 40; l+=8) {
encrypt(v, k);

p = (unsigned char*)(input + l);
for(int i = 0; i < 8; i++) {
p[i] ^= input[i];
}
//printf("%x %x \n", v[0], v[1]);
}
for(int i=0;i<40;i++)
{
printf("%x ", input[i]);
}
printf("\n");


v = (uint32_t*)cipher;


for(int l = 32; l >=8; l-=8) {
p = (unsigned char*)(cipher + l);
for(int i = 0; i < 8; i++) {
p[i] ^= cipher[i];
}

decrypt(v, k);
}
decrypt(v, k);

for(int i=0;i<40;i++)
{
printf("%c", cipher[i]);
}

return 0;
}
//flag{unitygame_I5S0ooFunny_Isnotit?????}

(最后还是小小地吐槽一下这个UI

b4476028-6ad0-43f4-bd6f-5e6ad3cbe6aa

  • 标题: 强网拟态2025初赛 Mobile方向just Writeup
  • 作者: 两只羊
  • 创建于 : 2025-10-27 09:44:03
  • 更新于 : 2025-10-27 09:48:07
  • 链接: https://twogoat.github.io/2025/10/27/强网拟态2025初赛-Mobile方向just-Writeup/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
强网拟态2025初赛 Mobile方向just Writeup