强网拟态一道u3d的安卓题

拿到手运行看封面就知道是u3d的了,jadx打开也看的出来

然后直接用li2cppdumper来dump一下

但是失败了,肯定是被加密了

image-20251027212541386

然后就找被加密的地方,这个libil2cpp.so文件一看就不符合elf文件格式

我是用mcp找到了解密这个的地方,里面是一个rc4,密钥是nihaounity,用一个脚本处理一下得到解密后的so文件

image-20251027213740785

image-20251027213946626

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python3
"""
解密 libil2cpp.so 文件
基于分析:RC4加密 + XOR 0x33混淆,密钥为"nihaounity"
"""

import sys
import os


def rc4_ksa(key):
"""RC4密钥调度算法"""
key_length = len(key)
# 初始化S盒
S = list(range(256))
j = 0

for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i], S[j] = S[j], S[i] # 交换

return S


def rc4_prga(S, data):
"""RC4伪随机生成算法"""
i = j = 0
result = 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]

# 解密:与密钥流异或,然后再与0x33异或
decrypted_byte = byte ^ k ^ 0x33
result.append(decrypted_byte)

return bytes(result)


def decrypt_libil2cpp(input_file, output_file=None):
"""
解密libil2cpp.so文件

Args:
input_file: 加密的输入文件路径
output_file: 解密的输出文件路径(可选)
"""

if output_file is None:
name, ext = os.path.splitext(input_file)
output_file = f"{name}_decrypted{ext}"

# RC4密钥
key = b"nihaounity"

print(f"开始解密文件: {input_file}")
print(f"使用密钥: {key}")
print(f"输出文件: {output_file}")

try:
# 读取加密文件
with open(input_file, 'rb') as f:
encrypted_data = f.read()

print(f"文件大小: {len(encrypted_data)} 字节")

# 初始化RC4 S盒
S = rc4_ksa(key)

# 解密数据
print("正在解密...")
decrypted_data = rc4_prga(S, encrypted_data)

# 写入解密后的文件
with open(output_file, 'wb') as f:
f.write(decrypted_data)

print("解密完成!")

# 验证文件头(可选)
if decrypted_data[:4] in [b'\x7fELF', b'\x7fELE']:
print("✓ 文件头验证通过 - 看起来是有效的ELF文件")
else:
print("⚠ 文件头异常 - 可能解密不成功")

return True

except Exception as e:
print(f"解密失败: {e}")
return False


def batch_decrypt(directory="."):
"""批量解密目录中的所有libil2cpp.so文件"""
import glob

pattern = os.path.join(directory, "**", "libil2cpp.so")
files = glob.glob(pattern, recursive=True)

if not files:
print(f"在目录 {directory} 中未找到 libil2cpp.so 文件")
return

for file_path in files:
print(f"\n{'=' * 50}")
decrypt_libil2cpp(file_path)


def verify_encryption(file_path):
"""验证文件是否被加密"""
try:
with open(file_path, 'rb') as f:
header = f.read(4)

# 正常的ELF文件头
if header == b'\x7fELF':
print(f"{file_path}: 未加密 (正常的ELF文件头)")
return False
else:
print(f"{file_path}: 可能已加密 (异常文件头: {header.hex()})")
return True

except Exception as e:
print(f"验证失败: {e}")
return False


if __name__ == "__main__":
if len(sys.argv) > 1:
# 解密指定文件
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None

if os.path.isdir(input_file):
batch_decrypt(input_file)
else:
decrypt_libil2cpp(input_file, output_file)
else:
# 交互模式
print("libil2cpp.so 解密工具")
print("基于RC4 + XOR 0x33加密,密钥: 'nihaounity'")
print()

choice = input("选择模式:\n1. 解密单个文件\n2. 批量解密当前目录\n3. 验证文件加密状态\n> ")

if choice == "1":
file_path = input("输入文件路径: ").strip()
if file_path:
decrypt_libil2cpp(file_path)
else:
print("使用默认文件: ./libil2cpp.so")
decrypt_libil2cpp("./libil2cpp.so")

elif choice == "2":
directory = input("输入目录路径 (回车使用当前目录): ").strip()
if not directory:
directory = "."
batch_decrypt(directory)

elif choice == "3":
file_path = input("输入文件路径: ").strip()
if not file_path:
file_path = "./libil2cpp.so"
verify_encryption(file_path)

else:
print("无效选择")

但是so文件解密之后任然没办法dump出来,看了一会so文件感觉应该是对的,那问题应该在dat文件

对比一下正常的文件,会发现这个在下面往后就不太对劲了

image-20251027214514494

但是当时一直没有找到加密方式。。。想着运行之后hook出来dat就行然后就没动了

现在知道在得出的libil2cpp_decrypted.so文件中

字符串中里面搜索一下dat文件

image-20251027214652558

引用跟进sub_211D94函数,分析sub_211D94函数,可以找到解密函数是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
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
char *__fastcall sub_211D94(
const char *p_global_metadata.dat,
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 *ptr_3; // x9
size_t n8_1; // x0
char *ptr_4; // x8
void *v21; // x9
__int64 v22; // x0
const char *ptr_5; // x1
__int64 v24; // x20
unsigned __int16 *src; // x21
__int64 v26; // x0
char *v27; // x19
char v29[8]; // [xsp+0h] [xbp-70h] BYREF
__int64 n8; // [xsp+8h] [xbp-68h]
__int64 v31; // [xsp+10h] [xbp-60h] BYREF
void *v32; // [xsp+18h] [xbp-58h]
void *ptr; // [xsp+20h] [xbp-50h]
__int64 v34; // [xsp+28h] [xbp-48h] BYREF
void *v35; // [xsp+30h] [xbp-40h]
void *ptr_1; // [xsp+38h] [xbp-38h]
char *ptr_2; // [xsp+40h] [xbp-30h] BYREF
void *v38; // [xsp+48h] [xbp-28h]

sub_26850C(
&v31,
p_global_metadata.dat,
a2,
a3,
a4,
a5,
a6,
a7,
a8,
*v29,
n8,
v31,
v32,
ptr,
v34,
v35,
ptr_1,
ptr_2,
v38);
*v29 = "Metadata";
n8 = 8;
v17 = (v31 >> 1);
if ( (v31 & 1) != 0 )
ptr_3 = ptr;
else
ptr_3 = &v31 + 1;
if ( (v31 & 1) != 0 )
v17 = v32;
ptr_2 = ptr_3;
v38 = v17;
sub_1F3DA0(&v34, &ptr_2, v29);
if ( (v31 & 1) != 0 )
operator delete(ptr);
n8_1 = strlen(p_global_metadata.dat);
if ( (v34 & 1) != 0 )
ptr_4 = ptr_1;
else
ptr_4 = &v34 + 1;
if ( (v34 & 1) != 0 )
v21 = v35;
else
v21 = (v34 >> 1);
*v29 = p_global_metadata.dat;
n8 = n8_1;
ptr_2 = ptr_4;
v38 = v21;
sub_1F3DA0(&v31, &ptr_2, v29);
LODWORD(ptr_2) = 0;
v22 = sub_1EDFAC(&v31, 3, 1, 1, 0, &ptr_2);
if ( ptr_2 )
{
if ( (v31 & 1) != 0 )
ptr_5 = ptr;
else
ptr_5 = &v31 + 1;
sub_267C8C("ERROR: Could not open %s", ptr_5);
}
else
{
v24 = v22;
src = sub_267E30(v22);
::src = src;
v26 = sub_1F08B0(v24, v29);
v27 = sub_21A2C8(src, v26);
qword_A327F8 = v27;
sub_1EE398(v24, &ptr_2);
if ( !ptr_2 )
goto LABEL_22;
sub_267E40(src);
}
v27 = 0;
LABEL_22:
if ( (v31 & 1) != 0 )
operator delete(ptr);
if ( (v34 & 1) != 0 )
operator delete(ptr_1);
return v27;
}

image-20251028153426759

然后写个脚本处理一下这个dat文件,解密出来直接dump

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
import struct
import sys

def simple_decrypt(input_file, output_file):
"""简化版本解密函数"""

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

# 读取密钥表大小
key_table_size = struct.unpack_from('<H', data, 1024)[0] # a1[512]
print(f"密钥表大小: {key_table_size}")

# 密钥表位置
key_table_start = 1028 # &a1[514]
key_table_end = key_table_start + key_table_size * 4

# 加密数据位置
encrypted_data_start = key_table_end
encrypted_data_size = len(data) - encrypted_data_start

# 分配输出缓冲区
output = bytearray(1024 + encrypted_data_size)

# 复制文件头
output[:1024] = data[:1024]

# 解密循环
for i in range(0, encrypted_data_size, 4):
# 计算密钥索引
key_index = (i + i // key_table_size) % key_table_size
key_offset = key_table_start + key_index * 4

# 读取密钥和加密数据
if key_offset + 4 <= len(data):
key = struct.unpack_from('<I', data, key_offset)[0]
else:
key = 0

if encrypted_data_start + i + 4 <= len(data):
encrypted = struct.unpack_from('<I', data, encrypted_data_start + i)[0]
else:
encrypted = 0

# XOR 解密
decrypted = encrypted ^ key

# 写入输出
struct.pack_into('<I', output, 1024 + i, decrypted)

with open(output_file, 'wb') as f:
f.write(output)

print(f"解密完成: {output_file}")

if __name__ == "__main__":
if len(sys.argv) == 3:
simple_decrypt(sys.argv[1], sys.argv[2])
else:
print("用法: python decrypt.py input.dat output.dat")
#python decrypt_metadata.py global-metadata.dat global-metadata-decrypted.dat

dump出来的东西可以发现在dump.cs里面有flag有关的函数,还有tea加密的函数

image-20251028200354593

给libli2cpp恢复一下符号,直接找teaencrypt分析一下,然后引用发现逻辑大概是

从UI输入框获取用户输入的字符串,进行tea加密,对比预定义的ReallyCompare数组,返回结果

cctor是FlagChecker类的静态构造函数,初始化了Key数组ReallyCompare数组

image-20251028220538356

ai分析一下可以知道初始化的东西在哪

image-20251028221337746

在dnspy中可以找到具体的位置

image-20251028223324994

然后到解密之后的global-metadata.dat文件里十六进制去读取数据,取出来tea解密

F:901D是相对F:9010h的第0x0D(13)个字节。

所以,从F:9010h行的第13个字节开始(从0开始计数,即第14个字节)

image-20251028223657719

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
# -*- coding: utf-8 -*-

import struct

UINT32_MASK = 0xFFFFFFFF

KEY = (0x12345678, 0x09101112, 0x13141516, 0x15161718)

CIPHER_BYTES = 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
])

def read_u32_le(buf, offset):
return struct.unpack_from("<I", buf, offset)[0]

def write_u32_le(bytearr, offset, value):
struct.pack_into("<I", bytearr, offset, value & UINT32_MASK)

def custom_tea_step(pair, key):

left, right = pair
delta = 0x61C88647
s = 0
# produce initial s = -delta*16 mod 2^32
for _ in range(16):
s = (s - delta) & UINT32_MASK

# 16 rounds: each round s += delta, then update right and left using subtraction-based ops
for _ in range(16):
s = (s + delta) & UINT32_MASK

t_right = (((left << 4) & UINT32_MASK) + key[2]) & UINT32_MASK
u_right = (left + s) & UINT32_MASK
v_right = (((left >> 5) & UINT32_MASK) + key[3]) & UINT32_MASK
right = (right - (t_right ^ u_right ^ v_right)) & UINT32_MASK

t_left = (((right << 4) & UINT32_MASK) + key[0]) & UINT32_MASK
u_left = (right + s) & UINT32_MASK
v_left = (((right >> 5) & UINT32_MASK) + key[1]) & UINT32_MASK
left = (left - (t_left ^ u_left ^ v_left)) & UINT32_MASK

return left, right

def decrypt_custom_mode(cipher_bytes, key):

b = bytearray(cipher_bytes) # operate on a mutable copy

# current state is the first two uint32 words (little-endian)
state_l = read_u32_le(b, 0)
state_r = read_u32_le(b, 4)

# process blocks in reverse order (offsets 32,24,16,8)
for off in (32, 24, 16, 8):
# xor cipher block with current state to produce plaintext for that block
c_l = read_u32_le(b, off)
c_r = read_u32_le(b, off + 4)
p_l = c_l ^ state_l
p_r = c_r ^ state_r
write_u32_le(b, off, p_l)
write_u32_le(b, off + 4, p_r)

# advance keystream/state by applying custom tea on the state
state_l, state_r = custom_tea_step((state_l, state_r), key)

# write the updated state back to the beginning (this mirrors the original)
write_u32_le(b, 0, state_l)
write_u32_le(b, 4, state_r)

# final state advancement and write-back (same as original algorithm)
state_l, state_r = custom_tea_step((state_l, state_r), key)
write_u32_le(b, 0, state_l)
write_u32_le(b, 4, state_r)

return bytes(b)

if __name__ == "__main__":
plaintext = decrypt_custom_mode(CIPHER_BYTES, KEY)
print("Decrypted (hex):", plaintext.hex())
try:
decoded = plaintext.decode("ascii")
print("Decrypted (ASCII):", decoded)
except UnicodeDecodeError:
printable = ''.join((chr(x) if 32 <= x < 127 else '.') for x in plaintext)
print("Decrypted (printable):", printable)