强网拟态just复现

前言

比赛没打,复现了下,好评,加深了对il2cpp以及dumper工具的理解

Just

refer:

wp参考

bilibili

基于apk的unity,il2cpp

dumper失败,metadata.dat和libil2cpp.so有加密

0x400h

ida分析libjust.so

sub_8A0C为so解密函数,rc4 && xor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
454 v59 = __strlen_chk("nihaounity", 11LL);

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;
}

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 );

652 fd_1 = syscall(279LL, "dec_il2cpp", 0LL);

本地报错:cannot continue after an internal error, sorry,但arm下能看到相关字样

sub_B6CC android_dlopen_ext

1
2
3
4
5
6
7
8
loc_C090
080 ADRP X1, #aJusthook@PAGE ; "JustHook"
080 ADRP X2, #aMyAndroidDlope_0@PAGE ; "my_android_dlopen_ext: no original load"...
080 ADD X1, X1, #aJusthook@PAGEOFF ; "JustHook"
080 ADD X2, X2, #aMyAndroidDlope_0@PAGEOFF ; "my_android_dlopen_ext: no original load"...
080 MOV W0, #6
080 BL .__android_log_print
080 B loc_C0F0

dobbyHook框架实现hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Dobby 的核心原理是 Inline Hook(内联钩子) 。
简单来说,就是直接修改目标函数头部的指令,使其跳转到我们自定义的函数。

主要过程如下:
跳转注入:
Dobby 会修改目标函数开头的一小段汇编指令,替换为一条跳转指令。
这样,当原函数被调用时,执行流程会立刻转向你的替换函数 。

原始函数备份:
Dobby 会保存被覆盖的原始指令,并将其搬运到另一个地方重新组装。这样你仍然有机会在替换函数里调用原始功能 。

流程控制:
在你的替换函数中,你可以执行任何自定义代码。
如果需要,你可以通过 Dobby 提供的函数指针,调用原始函数,并处理其返回值 。
最终,执行流程会返回给原调用者 。

对于 DobbyInstrument(指令插桩),其原理类似,
但它可以在函数内部的任意一条指令处插入回调,而不仅仅是函数开头。

eg.

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
#import <DobbyX/dobby.h>

// 1. 定义要 Hook 的原函数
int sum(int a, int b) {
return a + b;
}

// 2. 定义函数指针,用于保存原始函数的地址
static int (*sum_orig)(int a, int b);

// 3. 定义替换函数
int my_sum(int a, int b) {
NSLog(@"原始函数被调用,计算结果为: %d", sum_orig(a, b));
// 这里可以修改参数或返回值,例如将加法改为减法
return a - b;
}

- (void)viewDidLoad {
[super viewDidLoad];

// 4. 执行 Hook
DobbyHook(sum, my_sum, (void *)&sum_orig);

// 测试调用
NSLog(@"Hook后结果: %d", sum(10, 20)); // 输出: Hook后结果: -10
}

so解密跟进分析mat解密

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

内部原本为mmap函数

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
unsigned __int64 __fastcall sub_48D10C(const char *a1)
{
sub_44B644((__int64)v14);
v12 = "Metadata";
v13 = 8LL;
v2 = (unsigned __int64)LOBYTE(v14[0]) >> 1;
if ( (v14[0] & 1) != 0 )
v3 = v15;
else
v3 = (char *)v14 + 1;
if ( (v14[0] & 1) != 0 )
v2 = v14[1];
v17 = (char *)v3;
v18 = v2;
sub_44ADBC(&v17, &v12, v16);
if ( (v14[0] & 1) != 0 )
sub_8C66D0();
v4 = sub_8C6670(a1);
if ( (v16[0] & 1) != 0 )
v5 = (char *)v16[2];
else
v5 = (char *)v16 + 1;
if ( (v16[0] & 1) != 0 )
v6 = v16[1];
else
v6 = (unsigned __int64)LOBYTE(v16[0]) >> 1;
v12 = a1;
v13 = v4;
v17 = v5;
v18 = v6;
sub_44ADBC(&v17, &v12, v14);
LODWORD(v17) = 0;
v7 = sub_427A0C((__int64)v14, 3, 1u, 1u, 0, &v17);
if ( (_DWORD)v17 )
{
if ( (v14[0] & 1) != 0 )
v8 = v15;
else
v8 = (char *)v14 + 1;
sub_45A264("ERROR: Could not open %s", v8);
}
else
{
v9 = v7;
v10 = sub_45A3F0();
sub_427C4C(v9, &v17);
if ( !(_DWORD)v17 )
goto LABEL_22;
sub_45A400(v10);
}
v10 = 0LL;
LABEL_22:
if ( (v14[0] & 1) != 0 )
sub_8C66D0();
if ( (v16[0] & 1) != 0 )
sub_8C66D0();
return v10;
}
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
__int64 __fastcall sub_211D94(
{
sub_26850C(
&v30,
(int)global_metadata.dat,a2,a3,a4,a5,a6,a7,a8,
(int)global_metadata.dat_1,
n8,v30,(int)v31,(char)ptr,v33,v34,
(char)ptr_4,(int)ptr_2,v37);
global_metadata.dat_1 = "Metadata";
n8 = 8LL;
v17 = (void *)((unsigned __int64)(unsigned __int8)v30 >> 1);
if ( (v30 & 1) != 0 )
ptr_1 = (char *)ptr;
else
ptr_1 = (char *)&v30 + 1;
if ( (v30 & 1) != 0 )
v17 = v31;
ptr_2 = ptr_1;
v37 = v17;
sub_1F3DA0(&v33, &ptr_2, &global_metadata.dat_1);
if ( (v30 & 1) != 0 )
operator delete(ptr);
n8_1 = strlen(global_metadata.dat);
if ( (v33 & 1) != 0 )
ptr_3 = (char *)ptr_4;
else
ptr_3 = (char *)&v33 + 1;
if ( (v33 & 1) != 0 )
v21 = v34;
else
v21 = (void *)((unsigned __int64)(unsigned __int8)v33 >> 1);
global_metadata.dat_1 = (char *)global_metadata.dat;
n8 = n8_1;
ptr_2 = ptr_3;
v37 = v21;
sub_1F3DA0(&v30, &ptr_2, &global_metadata.dat_1);
LODWORD(ptr_2) = 0;
v22 = sub_1EDFAC(&v30, 3LL, 1LL, 1LL, 0LL, &ptr_2);
if ( (_DWORD)ptr_2 )
{
sub_267C8C("ERROR: Could not open %s");
}
else
{
v23 = v22;
v24 = sub_267E30();
qword_A327F0 = v24;
v25 = sub_1F08B0(v23, &global_metadata.dat_1);
v26 = sub_21A2C8(v24, v25);
qword_A327F8 = v26;
sub_1EE398(v23, &ptr_2);
if ( !(_DWORD)ptr_2 )
goto LABEL_19;
sub_267E40(v24);
}
v26 = 0LL;
LABEL_19:
if ( (v30 & 1) != 0 )
operator delete(ptr);
if ( (v33 & 1) != 0 )
operator delete(ptr_4);
return v26;
}

关注返回值v26所在函数

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 *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[512];
v4 = a2 - 4 * v2;
i_2 = v4 - 1028;
dest = (char *)malloc(v4 - 4);
memcpy(dest, src, 0x400uLL);
if ( i_2 >= 1 )
{
for ( i = 0LL; i < i_2; i += 4LL )
{
i_1 = i + 3;
v9 = i + i / v2;
if ( i >= 0 )
i_1 = i;
*(_DWORD *)&dest[(i_1 & 0xFFFFFFFFFFFFFFFCLL) + 1024] = *(_DWORD *)((char *)&src[2 * v2 + 514]
+ (i_1 & 0xFFFFFFFFFFFFFFFCLL)) ^ *(_DWORD *)&src[2 * (v9 % v2) + 514];
}
}
return dest;
}
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
import struct

def decrypt_global_metadata(encrypted_data: bytes) -> bytes:
# Step 1: Read key_len from offset 1024 (2 bytes, little-endian)
if len(encrypted_data) < 1026:
raise ValueError("File too short to contain key_len")

key_len = struct.unpack_from('<H', encrypted_data, 1024)[0] # unsigned short, little-endian
print(f"[+] key_len = {key_len}")

total_size = len(encrypted_data)
header_size = 1024
key_area_start = header_size + 4 # 1024 + 4 = 1028? But key starts after that...
# Actually, key data starts at 1028, length = 4 * key_len
key_data_start = 1028
key_data_end = key_data_start + 4 * key_len

if key_data_end > total_size:
raise ValueError("File too short for declared key length")

# Output buffer: same as input but we'll rebuild
decrypted = bytearray(encrypted_data[:header_size]) # First 1024 bytes unchanged

# Encrypted data starts right after key area
encrypted_start = key_data_end
encrypted_data_bytes = encrypted_data[encrypted_start:]

# Ensure length is multiple of 4
data_len = len(encrypted_data_bytes)
if data_len % 4 != 0:
# Pad or truncate? Usually it's aligned.
print("[!] Warning: encrypted data not multiple of 4, truncating")
data_len = (data_len // 4) * 4
encrypted_data_bytes = encrypted_data_bytes[:data_len]

# Extract key array as list of 32-bit integers (little-endian)
key_dwords = []
for i in range(0, 4 * key_len, 4):
dword = struct.unpack_from('<I', encrypted_data, key_data_start + i)[0]
key_dwords.append(dword)

# Now decrypt each 4-byte block
for i in range(0, data_len, 4):
# Compute key index: v9 = i + i // key_len, then mod key_len
if key_len == 0:
raise ValueError("key_len is zero!")
v9 = i + (i // key_len)
key_index = v9 % key_len
key_val = key_dwords[key_index]

enc_dword = struct.unpack_from('<I', encrypted_data_bytes, i)[0]
dec_dword = enc_dword ^ key_val

decrypted += struct.pack('<I', dec_dword)

return bytes(decrypted)

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

try:
decrypted = decrypt_global_metadata(encrypted)
with open("global-metadata.decrypted.dat", "wb") as f:
f.write(decrypted)
print("[+] Decryption successful!")
except Exception as e:
print(f"[-] Error: {e}")

il2cppdumper

1
2
3
4
5
6
7
8
9
10
11
[Token(Token = "0x6000001")]
[Address(RVA = "0x419F2C", Offset = "0x419F2C", VA = "0x419F2C")]
public void CheckFlag()
{
}

[Token(Token = "0x6000002")]
[Address(RVA = "0x41C330", Offset = "0x41C330", VA = "0x41C330")]
private static void TeaEncrypt(uint[] v, uint[] k)
{
}

Il2CppDumper 只能恢复函数签名和元数据,不能恢复实际逻辑,去so下分析

这里一开始忘记导入ida恢复符号了,感谢大洋参师傅


可以frida dump,笔者模拟器一加无法运行,静态分析

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

0x41C330

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( n16 >= 16 )
v10 = 0.0;
else
v10 = 1.0;

if ( !key )
goto LABEL_17;
k0 = ((__int64 (__fastcall *)(__int64, _QWORD))sub_1B5D88)(key, 0LL);
e1 += (k0 + 16 * e2) ^ (e2 + v8) ^ (((__int64 (__fastcall *)(__int64, __int64))sub_1B5D88)(key, 1LL) + (e2 >> 5));
k2 = ((__int64 (__fastcall *)(__int64, __int64))sub_1B5D88)(key, 2LL);
e2 += (k2 + 16 * e1) ^ (v8 + e1) ^ (((__int64 (__fastcall *)(__int64, __int64))sub_1B5D88)(key, 3LL) + (e1 >> 5));
v8 -= 0x61C88647;
++n16;

0x419F2C

1
2
3
4
5
6
7
8
9
10
11
12
FlagChecker__TeaEncrypt(v, (System_UInt32_array *)method, v2);
v78 = get((__int64)v, 0, v74, v75, v76, v77);
FlagChecker__SetUInt32LE(v35, 0, v78, v79);
v84 = get((__int64)v, 1, v80, v81, v82, v83);
FlagChecker__SetUInt32LE(v35, 4, v84, v85);
v86 = 4 * v67;
v88 = FlagChecker__ToUInt32LE(b, 4 * v67, v87);
v89 = v67;
v90 = v86 | 4;
v92 = FlagChecker__ToUInt32LE(b, v86 | 4, v91);
v97 = get((__int64)v, 0, v93, v94, v95, v96) ^ v88;
v102 = get((__int64)v, 1, v98, v99, v100, v101) ^ v92;

一处tea,一处tea+xor,这个^是真的丁真啊我去,八百行瞄这玩意

找key和密文

看cctor构造函数

对应hash

解密

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
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
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,
};

int main()
{
uint32_t k[4]={0x12345678, 0x09101112, 0x13141516, 0x15161718};
uint32_t *v = (uint32_t*)cipher;
unsigned char *p = (unsigned char*)v;
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]);
system("pause\n");
return 0;
}
//flag{unitygame_I5S0ooFunny_Isnotit?????}

强网拟态just复现
https://alenirving.github.io/2025/11/09/强网拟态just复现/
作者
Ma5k
许可协议
CC-BY-NC-SA