2025强网拟态mobile

2025强网拟态mobile

just

前言

这题当时想着试试能不能做出来,在比赛的时候差在最后加载il2cpp.so那边。现在想来看来是大晚上做题做的昏头转向了,找的文章都找错了自己还没发现,这个表情包来表达一下我现在复现的心情吧。4cff7debcc7ff416b9d220e7aef25386

解题过程

总的来说这题解题过程也是相当复杂,大晚上做的时候甚至忘了前面做了什么。

之前接触了的一道是基于windows下的il2cpp的unity逆向题,这道是基于apk文件,有点不同。主要的思路是提出apk文件中的il2cpp.so文件,然后底下的思路就是基本解题思路了。

这道题对il2cpp.so文件进行了加密,解压之后看到的libil2cpp.so文件是加密之后的,仔细分析libjust.so文件找到加密逻辑就可以。

image-20251029235605513

这边我就是从初始函数开始往下找关键函数的,然后发现了这里,dec_il2cpp这,我感觉就是主要的加密逻辑,理解一下发现这里果不其然就是,基于rc4的加密,密钥是nihaounity,注意^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
import sys

def rc4_nihaounity(data, key=b"nihaounity"):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]

i = 0
j = 0
out = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
out.append(byte ^ K ^ 0x33)
return out

def main():
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input_il2cpp.so> <output_dec.so>")
return

input_file = sys.argv[1]
output_file = sys.argv[2]

with open(input_file, "rb") as f:
data = f.read()

decrypted = rc4_nihaounity(data)

with open(output_file, "wb") as f:
f.write(decrypted)

print(f"[+] Decrypted to {output_file}")

if __name__ == "__main__":
main()

以上脚本解密之后得到decil2cpp.so,下一步按理说就是解包global-metadata.dat,看看加载decil2cpp.so能不能发现什么

定位到sub_211B8C函数,这里尝试优先加载 game.dat,失败则 fallback 到 global-metadata.dat,然后调用了sub_211D94函数,也加载了 global-metadata.dat,跟进看他的内容

image-20251030001910616


global-metadata.dat 是 Unity IL2CPP 编译时生成的二进制元数据文件。

它包含了所有 C# 类型、方法、字段、字符串的定义信息。

运行时必须加载它才能支持反射、序列化、跨语言调用等功能。


这个函数载入了metadata,在这里找找看加解密逻辑

image-20251030205447395

定位到sub_21A2C8函数,这个函数是对global-metadata.dat 数据进行解密。

解密脚本还原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
import struct
import sys
import os
from typing import Tuple, Optional


class MetadataDecryptor:

HEADER_SIZE = 1024 # 前 1024 字节为明文头部
KEY_OFFSET = 1028 # 密钥起始偏移(src[514] * 2)

def __init__(self, data: bytes):
self.data = data
self.data_len = len(data)
self.v2: Optional[int] = None # 密钥数量(src[512])
self.key_count = 0
self.key_stream_offset = 0
self.cipher_offset = 0
self.plaintext_size = 0

def _read_v2(self) -> int:
"""从偏移 1024 处读取 v2(uint16)"""
if self.data_len < 1026:
raise ValueError("文件太短,无法读取 v2(偏移 1024)")
return struct.unpack('<H', self.data[1024:1026])[0]

def _setup_layout(self):
"""计算文件布局"""
self.v2 = self._read_v2()
print(f"[*] 检测到密钥数量 v2 = {self.v2}")

self.key_count = self.v2
self.key_stream_offset = self.KEY_OFFSET
self.cipher_offset = self.KEY_OFFSET + 4 * self.v2

encrypted_part_length = self.data_len - self.cipher_offset
if encrypted_part_length < 0:
raise ValueError("加密数据起始偏移超出文件范围")

self.plaintext_size = self.HEADER_SIZE + encrypted_part_length

def _validate_offsets(self, cipher_idx: int, key_idx: int) -> bool:
cipher_off = self.cipher_offset + cipher_idx
key_off = self.key_stream_offset + key_idx * 4
return (
cipher_off + 4 <= self.data_len and
key_off + 4 <= self.data_len
)

def decrypt(self) -> bytes:
self._setup_layout()

print(f"[*] 输入长度: {self.data_len} 字节")
print(f"[*] 密钥流偏移: {self.key_stream_offset}")
print(f"[*] 加密数据偏移: {self.cipher_offset}")
print(f"[*] 预期明文长度: {self.plaintext_size} 字节")
decrypted = bytearray(self.data[:self.HEADER_SIZE])
decrypted.extend(b'\x00' * (self.plaintext_size - self.HEADER_SIZE))
processed = 0
chunk_size = 4

while processed < self.plaintext_size - self.HEADER_SIZE:
# 动态密钥索引:v9 = i + i // v2, 然后取模
virtual_idx = processed
key_stream_idx = (virtual_idx + virtual_idx // (4 * self.v2)) % self.v2

cipher_offset = self.cipher_offset + processed
key_offset = self.key_stream_offset + key_stream_idx * 4

if not self._validate_offsets(processed, key_stream_idx):
print(f"[!] 跳过越界访问: cipher={cipher_offset}, key={key_offset}")
break
cipher_block = struct.unpack('<I', self.data[cipher_offset:cipher_offset+4])[0]
key_block = struct.unpack('<I', self.data[key_offset:key_offset+4])[0]
plain_block = cipher_block ^ key_block


struct.pack_into('<I', decrypted, self.HEADER_SIZE + processed, plain_block)
processed += chunk_size

print(f"[+] 解密完成。共处理 {processed} 字节数据。输出长度: {len(decrypted)} 字节")
return bytes(decrypted)


def load_file(path: str) -> bytes:
if not os.path.exists(path):
raise FileNotFoundError(f"文件不存在: {path}")
with open(path, 'rb') as f:
return f.read()


def save_file(path: str, data: bytes):
with open(path, 'wb') as f:
f.write(data)
print(f"[+] 解密成功,文件已保存至: {path}")


def main():
if len(sys.argv) != 3:
print(f"用法: {sys.argv[0]} <输入加密文件> <输出解密文件>")
print(f"示例: {sys.argv[0]} global-metadata.dat.enc global-metadata.dat")
sys.exit(1)

input_path, output_path = sys.argv[1], sys.argv[2]

try:
print(f"[*] 正在加载文件: {input_path}")
encrypted_data = load_file(input_path)

decryptor = MetadataDecryptor(encrypted_data)
decrypted_data = decryptor.decrypt()

save_file(output_path, decrypted_data)

except Exception as e:
print(f"[-] 错误: {e}")
sys.exit(1)


if __name__ == "__main__":
main()

接下来解包得到的global-metadata.dat

image-20251030210933563

在decil2cpp.so附加 ida_py3.py 和 script.json 文件来恢复符号表。

这里我在比赛的时候做的时候加载的是 Assembly-CSharp.dll,也就是开头说的问题。Assembly-CSharp.dll可以用dnspy看,能看到主要的加密验证函数那些,但是最重要的代码内容还是得着重分析il2cpp文件。

加载好之后找到flagcheck函数,这里的加密主要是先进行字节转换然后再调用tea加密

image-20251030234534239

image-20251030235436132

密文密钥是可以通过动调提取的,我没有环境可以调试,看了其他大佬的wp学习一下

FlagChecker$$.cctor在这里数据会进行初始化,在类加载时,把内嵌的静态数组(key 和密文)初始化成内存中的运行时对象。

image-20251031000018379

这两个符号名是 .NET / IL2CPP自动生成的静态数组字段名,key数组是16字节,密文数组是40字节。

接下来就可以从dump.cs里找到对应密文密钥的偏移量,我直接搜字符串就可以定位

1
2
3
4
5
6
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
}

确定偏移 是0xF901D 和 0xF9045,然后利用偏移读取global-data.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
import sys
import binascii
from pathlib import Path

META_PATH = "global-metadata-decrypted.dat"

DEFAULTS = {
"Ciphertext_40": {"offset": 0xF901D, "size": 40},
"XTEAKey_16": {"offset": 0xF9045, "size": 16},
}

def main():
meta = Path(META_PATH)
if not meta.exists():
print(f"[!] metadata file not found: {META_PATH}")
sys.exit(1)

data = meta.read_bytes()
results = {}

for name, info in DEFAULTS.items():
off, size = info["offset"], info["size"]
if off + size > len(data):
print(f"[!] Skipping {name}, offset beyond file size")
continue
blob = data[off:off + size]
results[name] = binascii.hexlify(blob).decode().upper()

for name, hex_str in results.items():
print(f"{name:<15}: {hex_str}")

if __name__ == "__main__":
main()

得到密文密钥:

1
2
Ciphertext_40  : AF5864409DB92167AEB529049E86C543230FBFA6B2AE4AB5C569B7A803D1AECFC62C5B7FA2861E1A
XTEAKey_16 : 78563412121110091615141318171615
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
import struct
import binascii

def tea_decrypt(v, k):
v0, v1 = v
delta = 0x61C88647
sum_ = 0
for _ in range(16):
sum_ = (sum_ - delta) & 0xFFFFFFFF
for _ in range(16):
sum_ = (sum_ + delta) & 0xFFFFFFFF
v1 = (v1 - (((v0 << 4) + k[2]) ^ (v0 + sum_) ^ ((v0 >> 5) + k[3]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + k[0]) ^ (v1 + sum_) ^ ((v1 >> 5) + k[1]))) & 0xFFFFFFFF
return v0, v1

def main():

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

enc = bytes([
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
])

cipher = bytearray(enc)

p1 = 0
p2 = 4

for i in range(8, 1, -2):
p3 = i * 4
p4 = i * 4 + 4

v3 = struct.unpack_from("<I", cipher, p3)[0]
v4 = struct.unpack_from("<I", cipher, p4)[0]
v1 = struct.unpack_from("<I", cipher, p1)[0]
v2 = struct.unpack_from("<I", cipher, p2)[0]

v3 ^= v1
v4 ^= v2
struct.pack_into("<I", cipher, p3, v3)
struct.pack_into("<I", cipher, p4, v4)

tmp = list(struct.unpack_from("<2I", cipher, 0))
tmp = tea_decrypt(tmp, Key)
struct.pack_into("<2I", cipher, 0, *tmp)

tmp = list(struct.unpack_from("<2I", cipher, 0))
tmp = tea_decrypt(tmp, Key)
struct.pack_into("<2I", cipher, 0, *tmp)

print(cipher.decode("ascii", errors="ignore"))


if __name__ == "__main__":
main()


//flag{unitygame_I5S0ooFunny_Isnotit?????}

EZMiniAPP

先查看一下发现没用加密过

image-20251102005908388

然后用wxappUnpacker解包一下,

image-20251102010033705

在chunk_0.appservice.js中发现主要加密内容

image-20251102010232987

密钥:newKey2025!

密文:[ 1, 33, 194, 133, 195, 102, 232, 104, 200, 14, 8, 163, 131, 71, 68, 97, 2, 76, 72, 171, 74,106, 225, 1, 65 ]

enigmaticTransformation(a, t)是实际的加密函数:

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
function enigmaticTransformation(a, t) {
//a:输入的flag
// t: 密钥key:"newKey2025!"

let r = [];
let i = Array.from(t).map(c => c.charCodeAt(0));
let s = i.length;

let sum = 0;
for (let e = 0; e < i.length; e++) {
switch (e % 4) {
case 0: sum += 1 * i[e]; break;
case 1: sum += i[e] + 0; break;
case 2: sum += 0 | i[e]; break;
case 3: sum += 0 ^ i[e]; break;
}
}
let c = sum % 8;

// 主循环
for (let o = 0; o < a.length; o++) {
let u;
switch (o % 3) {
case 0: u = a.charCodeAt(o) ^ i[o % s]; break;
case 1: u = i[o % s] ^ a.charCodeAt(o); break;
case 2: u = a.charCodeAt(o) ^ i[o % s]; break;
}

// 根据 c 对 u 进行循环位移
let h;
switch (c) {
case 0: h = u; break;
case 1: h = ((u << 1) | (u >> 7)) & 0xFF; break;
case 2: h = ((u << 2) | (u >> 6)) & 0xFF; break;
case 3: h = ((u << 3) | (u >> 5)) & 0xFF; break;
case 4: h = ((u << 4) | (u >> 4)) & 0xFF; break;
case 5: h = ((u << 5) | (u >> 3)) & 0xFF; break;
case 6: h = ((u << 6) | (u >> 2)) & 0xFF; break;
case 7: h = ((u << 7) | (u >> 1)) & 0xFF; break;
default: h = ((u << c) | (u >> (8 - c))) & 0xFF;
}

r.push(h);
}

return Array.from(r);
}

解密脚本:

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
def rotl(b, n):
n %= 8
return ((b << n) | (b >> (8 - n))) & 0xFF

def rotr(b, n):
n %= 8
return ((b >> n) | (b << (8 - n))) & 0xFF

def calc_c(key_bytes):
t = 0
for i, b in enumerate(key_bytes):
m = i % 4
if m == 0:
t += 1 * b
elif m == 1:
t += b + 0
elif m == 2:
t += 0 | b
else:
t += 0 ^ b
return t % 8

def decrypt(arr, key):
kb = [ord(x) for x in key]
c = calc_c(kb)
out = []
for i, h in enumerate(arr):
u = rotr(h, c)
out.append(u ^ kb[i % len(kb)])
return bytes(out).decode('utf-8', 'ignore')

if __name__ == '__main__':
key = "newKey2025!"
data = [1,33,194,133,195,102,232,104,200,14,8,163,131,71,68,97,2,76,72,171,74,106,225,1,65]
print(decrypt(data, key))

//flag{JustEasyMiniProgram}


2025强网拟态mobile
https://j1nxem-o.github.io/2025/10/29/2025强网拟态mobile-just/
作者
J1NXEM
发布于
2025年10月29日
更新于
2025年11月2日
许可协议