第八届“强网”拟态防御国际精英挑战赛

本文最后更新于 2025年11月1日 凌晨

低空经济网络安全

1
2
我们截获了一个名为“MAV1”的恐怖组织发出的无人机遥测下行数据。`capture.pcap`中的数据流量看起来很正常,是吗?
We've intercepted a drone's telemetry downlink from a terrorist group called "MAV1". The traffic in `capture.pcap` appears to be normal, or is it?

访问几个数据包就会发现有疑似 flag 的字符

用 strings 提取字符

1
2
3
4
5
6
7
8
9
Bk3d}
Bll3r
BflagJ
B{dr0
Bt_c0:
Bntr0
B_h4c!
Bn3_f
Bl1gh

去掉多余字母和标点符号,拼好 flag

1
flag{dr0n3_fl1ght_c0ntr0ll3r_h4ck3d}

Mobile

EZMiniAPP

未加密,可以直接用 wxappUnpacker 反编译

在 chunk_0.appservice.js 文件中找到主函数,customEncrypt 是加密函数,有校验

密文 : [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]

密钥 Key: “newKey2025!”

要找出输入字符串 a,使得 customEncrypt(a, "newKey2025!") 结果等于上面的密文数组,customEncrypt 直接调用了真正的加密函数 enigmaticTransformation

这里对密钥进行处理

进行异或,用当前字符索引 o 对 3 取模的结果,采用不同的 XOR 规则

按照值 c 不同对异或的结果 u 进行不同的移位

脚本如下

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
import json

TARGET_CIPHER = [
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
]

KEY_STR = "newKey2025!"

def get_key_array(t_str):
return [ord(c) for c in t_str]

def calculate_rotation_c(i_arr):
t = 0
for e, val in enumerate(i_arr):
if e % 4 == 0:
t += 1 * val
elif e % 4 == 1:
t += val + 0
elif e % 4 == 2:
t += 0 | val
elif e % 4 == 3:
t += 0 ^ val
return t % 8

def circular_right_shift(value, shift):
value &= 0xFF
return ((value >> shift) | (value << (8 - shift))) & 0xFF

def enigmatic_decryption(cipher_arr, key_str):
key_arr = get_key_array(key_str)
s = len(key_arr)
c = calculate_rotation_c(key_arr)

decrypted_bytes = []

for o, h in enumerate(cipher_arr):
u = circular_right_shift(h, c)
key_stream_val = key_arr[o % s]

original_char_code = u ^ key_stream_val

decrypted_bytes.append(original_char_code)

try:
flag = "".join([chr(b) for b in decrypted_bytes])
return flag
except ValueError:
return "无法解码为有效字符串,可能解密错误"

FLAG = enigmatic_decryption(TARGET_CIPHER, KEY_STR)

print(f"目标密文 (Target Cipher): {TARGET_CIPHER}")
print(f"使用的密钥 (Key): '{KEY_STR}'")
print("-" * 30)
print(f"计算出的旋转量 (c): {calculate_rotation_c(get_key_array(KEY_STR))}")
print(f"解密结果 (Flag): {FLAG}")

得到 flag

1
flag{JustEasyMiniProgram}

Just

il2cpp 在安卓上的加密应用

查看源码,发现在程序启动时会加载 libjust.so 文件

解包查看 libjust.so 文件,根据字符串定位到 sub_8288 函数,发现 hook 了dlopen

sub_8A0C 函数是校验函数,跟进分析,一个 RC4 的加密,异或 0x33

密钥为 “nihaounity”

创建一个名为 “dec_il2cpp”的新文件,将解密后的内容写入此文件

加载 il2cpp.so 文件,发现加载不出来,所以可能需要解密这个 il2cpp.so

根据加密逻辑和密钥用脚本解密 so 文件,脚本如下

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

def decrypt_rc4_variant(data: bytes, key: str = "nihaounity", xor_constant: int = 0x33) -> bytes:

key_bytes = key.encode('utf-8')
key_len = len(key_bytes)
data_len = len(data)

s_box = list(range(256))

j = 0
for i in range(256):
j = (j + s_box[i] + key_bytes[i % key_len]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
i = 0
j = 0
decrypted_data = bytearray(data)

for k in range(data_len):
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
keystream_byte = s_box[t]
decrypted_data[k] = data[k] ^ keystream_byte ^ xor_constant

return bytes(decrypted_data)

def main():

input_path = sys.argv[1]
output_path = sys.argv[2]
KEY = "nihaounity"
XOR_CONST = 0x33

try:
with open(input_path, 'rb') as f:
encrypted_data = f.read()

print(f"[*] 正在读取文件: {input_path} ({len(encrypted_data)} 字节)")
print(f"[*] 解密密钥: '{KEY}', 异或常数: 0x{XOR_CONST:02x}")

decrypted_data = decrypt_rc4_variant(encrypted_data, KEY, XOR_CONST)

with open(output_path, 'wb') as f:
f.write(decrypted_data)

print(f"[+] 解密完成,文件已保存至: {output_path}")

except FileNotFoundError:
print(f"[!] 错误: 找不到文件 {input_path}")
except Exception as e:
print(f"[!] 发生错误: {e}")

if __name__ == "__main__":
main()

加载解密出来的 decil2cpp.so,文件很大,加载很慢,根据字符串的 global-metadata.dat 定位过去

定位到 sub_211B8C 函数,发现这里在定位并加载元数据文件,调用了 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
char *__fastcall sub_21A2C8(unsigned __int16 *a1, __int64 a2)
{
__int64 v2; // x21
__int64 v4; // x8
__int64 v5; // x22
char *v6; // x19
__int64 i; // x8
__int64 v8; // x13
__int64 v9; // x12

v2 = a1[512];
v4 = a2 - 4 * v2;
v5 = v4 - 1028;
v6 = (char *)malloc(v4 - 4);
memcpy(v6, a1, 0x400u);
if ( v5 >= 1 )
{
for ( i = 0; i < v5; i += 4 )
{
v8 = i + 3;
v9 = i + i / v2;
if ( i >= 0 )
v8 = i;
*(_DWORD *)&v6[(v8 & 0xFFFFFFFFFFFFFFFCLL) + 1024] = *(_DWORD *)((char *)&a1[2 * v2 + 514]
+ (v8 & 0xFFFFFFFFFFFFFFFCLL))
^ *(_DWORD *)&a1[2 * (v9 % v2) + 514];
}
}
return v6;
}

根据逻辑写解密脚本

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

def decrypt_metadata_custom_xor(encrypted_data: bytes) -> bytes:
data_len = len(encrypted_data)

v2 = struct.unpack('<H', encrypted_data[1024:1026])[0]
v5 = data_len - (4 * v2) - 1028
decrypted_data = bytearray(encrypted_data)
encrypted_data_base_offset = 4 * v2 + 1028
keystream_base_offset = 1028

i = 0
while i < v5:
v9 = i + i // v2
keystream_index = v9 % v2
keystream_offset = keystream_base_offset + keystream_index * 4
encrypted_offset = encrypted_data_base_offset + i
decrypted_offset = 1024 + i
keystream_block = struct.unpack('<I', encrypted_data[keystream_offset:keystream_offset+4])[0]
encrypted_block = struct.unpack('<I', encrypted_data[encrypted_offset:encrypted_offset+4])[0]
decrypted_block = encrypted_block ^ keystream_block
struct.pack_into('<I', decrypted_data, decrypted_offset, decrypted_block)

i += 4

print(f"[+] 解密完成。共处理 {i} 字节数据。")
return bytes(decrypted_data)
expected_output_len = data_len - (4 * v2) - 4

if expected_output_len < len(decrypted_data):
decrypted_data = decrypted_data[:expected_output_len]

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

def main():
input_path = sys.argv[1]
output_path = sys.argv[2]

with open(input_path, 'rb') as f:
encrypted_data = f.read()

print(f"[*] 正在读取文件: {input_path} ({len(encrypted_data)} 字节)")
decrypted_data = decrypt_metadata_custom_xor(encrypted_data)
with open(output_path, 'wb') as f:
f.write(decrypted_data)

print(f"[+] 解密完成,文件已保存至: {output_path}")

if __name__ == "__main__":
main()

解密后的 dat 文件用 Il2CppDumper 直接解包

进入 DummyDll 目录,查看 Assembly-CSharp.dll,可以看到 Check 函数和加密函数

so 附加 Il2CppDumper 自动生成的 ida_py3.py 脚本和 script.json 文件恢复符号表,找 Flagchecker 看逻辑

CheckFlag 函数中有加密,两段不一样的,前面对前 8 个字节进行 tea 加密,v78 是 40 字节的密文

后一段是对剩下的 32 字节处理,每 8 字节还与前一个 8 字节进行异或

观察 tea 函数,密文存在 a2 指向的内存位置

密文和密钥我没有环境,不好调试,在 cctor 中有初始化类的静态字段,初始化了密钥和密文,可以从这 Filed 去找

查看 dump.cs,搜索 PrivateImplementationDetails

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 和 0xF904,直接按结构偏移读 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
35
36
37
38
import sys, binascii
from pathlib import Path

META_PATH = "g-d.dat"

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

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

data = meta.read_bytes()

cipher = key = None
for name, info in DEFAULTS.items():
off = info["offset"]
size = info["size"]
if off >= len(data):
continue
blob = data[off:off + size]
if "Ciphertext" in name:
cipher = blob
elif "Key" in name:
key = blob

if cipher:
print("Ciphertext HEX:", binascii.hexlify(cipher).decode().upper())
if key:
print("Key HEX :", binascii.hexlify(key).decode().upper())

if __name__ == "__main__":
main()

拿到密文和密钥

1
2
Ciphertext HEX: AF5864409DB92167AEB529049E86C543230FBFA6B2AE4AB5C569B7A803D1AECFC62C5B7FA2861E1A
Key HEX : 78563412121110091615141318171615

解密逻辑:最开始对最前 8 个字节进行 tea 解密,随后的 32 字节每 8 字节要与前一个 8 字节进行异或,从后往前循环

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

ROUNDS = 16
DELTA = 0x61C88647
MASK = 0xFFFFFFFF

KEY_BYTES = binascii.unhexlify("78563412121110091615141318171615")
KEY_WORDS = struct.unpack('<IIII', KEY_BYTES)

CIPHERTEXT_HEX = "AF5864409DB92167AEB529049E86C543230FBFA6B2AE4AB5C569B7A803D1AECFC62C5B7FA2861E1A"
CIPHERTEXT_BYTES = bytearray(binascii.unhexlify(CIPHERTEXT_HEX))

def decrypt_block(v0, v1, key_words):
k0, k1, k2, k3 = key_words
s = (0 - DELTA * ROUNDS) & MASK
for _ in range(ROUNDS):
s = (s + DELTA) & MASK
v1 = (v1 - (((v0 << 4) + k2) ^ (v0 + s) ^ ((v0 >> 5) + k3))) & MASK
v0 = (v0 - (((v1 << 4) + k0) ^ (v1 + s) ^ ((v1 >> 5) + k1))) & MASK
return v0, v1

def decrypt_data(cipher_bytes, key_words):
for i in range(8, 0, -2):
c0, c1 = struct.unpack('<II', cipher_bytes[0:8])
offset = i * 4
ci, cj = struct.unpack('<II', cipher_bytes[offset:offset+8])
pi = ci ^ c0
pj = cj ^ c1
cipher_bytes[offset:offset+8] = struct.pack('<II', pi, pj)
dv0, dv1 = decrypt_block(c0, c1, key_words)
cipher_bytes[0:8] = struct.pack('<II', dv0, dv1)
f0, f1 = struct.unpack('<II', cipher_bytes[0:8])
rf0, rf1 = decrypt_block(f0, f1, key_words)
cipher_bytes[0:8] = struct.pack('<II', rf0, rf1)
return cipher_bytes

def run_decryption():
result = decrypt_data(CIPHERTEXT_BYTES, KEY_WORDS)
flag = result.rstrip(b'\x00').decode('ascii', errors='ignore')
print("Decrypted HEX:", binascii.hexlify(result).decode().upper())
print("Decrypted ASCII:", flag)

if __name__ == "__main__":
run_decryption()

得到 flag

1
flag{unitygame_I5S0ooFunny_Isnotit?????}

Reverse

hyperjump

vm 类型的题目

根进函数可以看到调度器,典型的 VM switch-case 结构

跟进 case 里跳转的函数可以看到这是 VM 指令处理器,有一些 VM 指令

分析可知为单字节加密,输入长度为 24,这里的 v24 是字符串输入的索引

可以进行侧信道爆破,用 Pintools,并且题目还给了 md5 值

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
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
import re
import string
import sys
import os
import subprocess
from pwn import *

# --- 1. 配置区 ---

TARGET_BINARY = os.path.abspath('./hyperjump')
FLAG_LEN = 24
START_INDEX = 0

PIN_ROOT = '/root/pin'
PIN_TOOL_PATH = os.path.abspath('./pin/source/tools/ManualExamples/obj-intel64/inscount0.so')

PIN_CMD_LIST = [os.path.join(PIN_ROOT, 'pin'), '-t', PIN_TOOL_PATH, '--', TARGET_BINARY]

CHARSET = string.ascii_lowercase + string.digits + "_@{}"

context.log_level = 'info'


# --- 2. 使用 subprocess 可靠获取 Pin 输出并解析指令数 ---

def read_inscount_outfile(workdir=None):
candidates = ['inscount.out', 'inscount.txt', 'pin.out']
for name in candidates:
path = os.path.join(workdir if workdir else '.', name)
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
data = f.read()
# 尝试解析数字
m = re.search(r'(\d+)\s+instructions', data)
if m:
return int(m.group(1)), data
# 直接取第一个数字
m2 = re.search(r'(\d+)', data)
if m2:
return int(m2.group(1)), data
except Exception:
pass
return None, None


def get_inscount(payload: bytes) -> int:

IO_TIMEOUT = 5
log.info(f"Testing payload: {payload.decode('latin-1')[:START_INDEX+1]}...")

try:
# 启动 Pin 并把 payload 发到 stdin
# 注意:text=True 使 input/outputs 为 str 类型
proc = subprocess.run(
PIN_CMD_LIST,
input=payload.decode('latin-1') + '\n', # 把 payload 当作 stdin 发给程序
capture_output=True,
timeout=IO_TIMEOUT,
text=True,
cwd='.' # 确保当前目录是工作目录(inscount 输出文件通常在 cwd)
)

stdout = proc.stdout or ""
stderr = proc.stderr or ""
exitcode = proc.returncode

# 打印调试信息(PID 不好拿到,因为 subprocess.run 是阻塞调用)
sys.stderr.write(f"\n--- Pin Tool 完整输出 (Exit Code {exitcode}): ---\n")
if stdout.strip():
sys.stderr.write("[stdout]\n" + stdout.strip() + "\n")
if stderr.strip():
sys.stderr.write("[stderr]\n" + stderr.strip() + "\n")
sys.stderr.write("--------------------------------------------------\n")

combined = stdout + "\n" + stderr

# 1) 先在 combined 输出中查找像 "12345 instructions" 的形式
match_re = re.search(r'(\d+)\s+instructions', combined, re.IGNORECASE)
if match_re:
return int(match_re.group(1))

# 2) 再查找最后一行是数字的情况
lines = combined.strip().splitlines()
for line in reversed(lines):
s = line.strip()
if s.isdigit():
return int(s)

# 3) 检查常见的输出文件(inscount.out 等)
file_count, file_content = read_inscount_outfile(workdir='.')
if file_count is not None:
# 可选:删除该文件以免后续干扰(如果你不想删除可以注释掉)
try:
os.remove('inscount.out')
except Exception:
pass
return file_count

# 4) 启发式判断:若退出码非 0 且输出包含拒绝信息,则视为无效(返回 0)
low = combined.lower()
if exitcode is not None and exitcode != 0:
# 常见拒绝提示
if any(k in low for k in ['nope', 'try again', 'wrong', 'incorrect']):
return 0
# 若 stderr 有 pin 自身的错误,返回 0
if stderr.strip():
return 0

# 5) 若退出码是 0 且输出包含成功关键字,则返回非常大的计数作为 heuristic
if exitcode == 0 and any(k in low for k in ['flag', 'correct', 'congrats', 'success']):
return 10**9

# 否则无法解析到有效信息
log.warning("无法从 Pin 输出或文件中解析到指令数(stdout/stderr/file 均无数字)。")
return 0

except subprocess.TimeoutExpired as te:
sys.stderr.write(f"[TIMEOUT] Pin 运行超时: {te}\n")
return 0
except FileNotFoundError as fe:
sys.stderr.write(f"[FATAL] 找不到 pin 可执行文件或路径错误: {fe}\n")
return 0
except Exception as e:
sys.stderr.write(f"[FATAL ERROR] get_inscount 发生错误: {e}\n")
return 0


# --- 3. 爆破主循环 ---

def main():
known_flag = list("A" * FLAG_LEN)
log.info(f"开始爆破,长度 {FLAG_LEN},字符集大小 {len(CHARSET)},从索引 {START_INDEX} 开始。")

if not os.path.exists(TARGET_BINARY):
log.critical(f"目标文件不存在: {TARGET_BINARY}")
return
if not os.path.exists(PIN_TOOL_PATH):
log.critical(f"Pin Tool 不存在: {PIN_TOOL_PATH}")
return
if not os.path.exists(PIN_CMD_LIST[0]):
log.critical(f"Pin 可执行文件不存在: {PIN_CMD_LIST[0]}")
return

for i in range(START_INDEX, FLAG_LEN):
log.info(f"\n>>>> 正在爆破第 {i} 位...")

max_count = 0
best_char = None

for char in CHARSET:
current_test = known_flag[:]
current_test[i] = char
payload = "".join(current_test).encode('latin-1')

count = get_inscount(payload)

if count == 0:
continue

if count > max_count:
max_count = count
best_char = char
log.info(f"发现新的最大值: 字符 '{best_char}' -> {max_count} 条指令")

if best_char:
known_flag[i] = best_char
log.success(f"第 {i} 位确定为: '{best_char}'")
log.success(f"当前 Flag: {''.join(known_flag)}")
else:
log.warning(f"第 {i} 位爆破失败。所有字符返回 0。")
break

log.success(f"\n--- 爆破完成 ---")
log.success(f"最终 Flag: {''.join(known_flag)}")


if __name__ == '__main__':
main()

等一会儿

1
flag{m4z3d_vm_jump5__42}

还可以改文件字节码,让程序返回值为长度,修改 mov eax,1 为 mov eax,ebx,patch 为新文件

再利用 BFS 搜索进行单字节爆破

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
import string
import subprocess
import sys

FLAG_LENGTH = 24
CHARSET = string.ascii_letters + string.digits + string.punctuation

jobs = [(list("flag{"), 5)]
final_flags = set()

print("Starting BFS brute-force to find ALL passing flags...")
print("-" * 50)

while jobs:
current_guess, i = jobs.pop(0)

if i == FLAG_LENGTH:
final_flags.add("".join(current_guess))
continue

res = []

for char in CHARSET:
test_guess = current_guess + [char] + ["A"] * (FLAG_LENGTH - i - 1)
input_data = "".join(test_guess).encode()

try:
p = subprocess.run(
["./hyperjump_patched"],
input=input_data,
timeout=10,
capture_output=True
)

count = p.returncode
if count == 0:
count = FLAG_LENGTH

except subprocess.TimeoutExpired:
count = -1
except Exception:
count = -1

res.append((char, count))

if not res:
continue

max_count = max(r[1] for r in res)

if max_count <= 0:
continue

for char, count in res:
if count == max_count:
new_guess = current_guess + [char]
jobs.append((new_guess, i + 1))
current_flag = "".join(new_guess)
output_line = f"[Index {i+1:02d}] Testing: {current_flag:<{FLAG_LENGTH}} (Max Match: {max_count}) | Jobs left: {len(jobs)})"
sys.stdout.write(f"\r{output_line}")
sys.stdout.flush()

print("\n" + "-" * 50)
if final_flags:
print("🎉 Found ALL passing Flags:")
for flag in sorted(list(final_flags)):
print(f" -> {flag}")
else:
print("❌ No complete flag found.")


第八届“强网”拟态防御国际精英挑战赛
http://example.com/2025/10/31/第八届“强网”拟态防御国际精英挑战赛/
作者
butt3rf1y
发布于
2025年10月31日
许可协议