第九届“强网杯”全国网络安全挑战赛

第九届“强网杯”全国网络安全挑战赛

Aristore

Misc

谍影重重 6.0

Challenge

经过我国执法部门的努力,终于在今年十月提取出了张纪星(系杜撰名字,与现实人员无关)被捕前布置的监听设备中的加密信息,据本人供述其曾恢复过我国一份绝密情报。

flag为情报所提及的详细时间和地址的md5值,即flag{md5(x年x月x日x时x分于x地)}。

题目提示:本题依托于架空的时间线,取材自真实历史事件,请关注地点信息。

Solution

qwb2025-1

先看看协议分级,发现全是 UDP 流量

qwb2025-2

先查看第一个包的 payload,发现是以 80 开头的,这使我不禁想起前段时间的 WMCTF2025,参考当时写的 wp:Voice-hacker

先假设它是 RTP 流量,然后用同样的方法解析它的头部 80 80 76 38 99 59 48 23 88 48 19 ee

  • 80: 版本号(V=2)
  • 80: 标记位(M=1),载荷类型 (Payload Type, PT) = 0
  • 76 38: 序列号 (Sequence Number) = 30264
  • 99 59 48 23: 时间戳 (Timestamp) = 2572765219
  • 88 48 19 ee: 同步源标识符 (SSRC) = 0x884819EE

完美对上了,这就是 RTP 协议,右键第一条流量在 Decode As... 把端口 40000 的 UDP 流量解析为 RTP

然后往下滑,发现还有多个端口,把端口 40001 的也解析了

qwb2025-3

不难发现,这个流量文件里并不是所有包的 SSRC 都相同的,这就意味着并不是所有包都属于同一个音频流,需要根据 SSRC 来划分

先用 tshark 把 UDP 包的 Payload 提取出来方便稍后使用脚本处理:

1
tshark -r Data.pcap -T fields -e data > Data.txt

由于里面的流量并非全部属于同一个音频流,因此需要根据 SSRC 划出多段音频,最后将它们按顺序拼接起来合并成同一个音频文件,这里在 Voice-hacker 的脚本的基础上进行修改。

这样简单的处理得到的结果效果并不好,出现了两个问题:音频的音量过小,音频长达 18 小时😰

在听了两三分钟后,发现音频中存在较长的空白片段,随便往后一划也很容易划到没有声音的地方,检查一下前面导出的 Data.txt ,可以发现:

1
2
3
8000c6445d9424d7842e8e8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
8000c6455d942577842e8e8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
8000c6465d942617842e8e8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

里面存在大量类似这样的片段,因此我们可以先做一个预处理,修改 Data.txt,删除里面所有完全空白的片段:

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
kept_lines_count = 0
removed_lines_count = 0

with open('Data.txt', 'r', encoding='utf-8') as infile, open('Data_new.txt', 'w', encoding='utf-8') as outfile:

for line in infile:
stripped_line = line.strip()

if not stripped_line:
removed_lines_count += 1
continue

header_length = 24 # RTP头部的12字节
is_filler_payload = False
if len(stripped_line) > header_length:
payload = stripped_line[header_length:]
if payload and all(char == 'f' for char in payload):
is_filler_payload = True

if is_filler_payload:
removed_lines_count += 1
continue

outfile.write(line)
kept_lines_count += 1

Data.txt 从原来的 1.04 GB 缩小成了 195 MB(看来掺水挺严重

下面是完善后的导出脚本:

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
import wave
import struct
import os
from collections import defaultdict

# --- G.711 μ-law to 16-bit Linear PCM Decoder ---
# 这是一个标准的查找表,用于将8位的μ-law字节解码为16位的线性采样值
_ULAW_DECODE_TABLE = [
-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,
-23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,
-15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,
-11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,
-7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,
-5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,
-3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,
-2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,
-1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
-1372, -1308, -1244, -1180, -1116, -1052, -988, -924,
-876, -844, -812, -780, -748, -716, -684, -652,
-620, -588, -556, -524, -492, -460, -428, -396,
-372, -356, -340, -324, -308, -292, -276, -260,
-244, -228, -212, -196, -180, -164, -148, -132,
-120, -112, -104, -96, -88, -80, -72, -64,
-56, -48, -40, -32, -24, -16, -8, 0,
32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,
23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,
11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,
7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,
5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,
3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,
2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,
1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,
1372, 1308, 1244, 1180, 1116, 1052, 988, 924,
876, 844, 812, 780, 748, 716, 684, 652,
620, 588, 556, 524, 492, 460, 428, 396,
372, 356, 340, 324, 308, 292, 276, 260,
244, 228, 212, 196, 180, 164, 148, 132,
120, 112, 104, 96, 88, 80, 72, 64,
-56, -48, -40, -32, -24, -16, -8, 0
]

def decode_ulaw_to_pcm16(ulaw_data):
"""将一整段 G.711 u-law 字节数据解码为 16-bit 线性 PCM 字节数据"""
pcm_frames = []
for ulaw_byte in ulaw_data:
# 从查找表中获取对应的16位PCM值
pcm_sample = _ULAW_DECODE_TABLE[ulaw_byte]
# 将16位整数打包成2个字节(小端序)
pcm_frames.append(struct.pack('<h', pcm_sample))
return b''.join(pcm_frames)

def amplify_and_clip_pcm16(pcm_data, factor):
"""
放大16位PCM数据的音量,并进行削波处理。
:param pcm_data: 16位PCM字节数据
:param factor: 放大系数 (例如 2.0 表示放大2倍)
:return: 放大后的16位PCM字节数据
"""
# 将字节数据解包成16位整数列表
samples = struct.unpack(f'<{len(pcm_data) // 2}h', pcm_data)

amplified_samples = []
for sample in samples:
amplified_sample = int(sample * factor)
# 削波处理: 确保值在16位有符号整数范围内
if amplified_sample > 32767:
amplified_sample = 32767
elif amplified_sample < -32768:
amplified_sample = -32768
amplified_samples.append(amplified_sample)

# 将处理后的整数列表打包回字节
return struct.pack(f'<{len(amplified_samples)}h', *amplified_samples)

def write_wav(filename, pcm_data, channels, sampwidth, framerate):
"""一个辅助函数,用于将PCM数据写入WAV文件"""
try:
with wave.open(filename, "wb") as wav_file:
wav_file.setnchannels(channels)
wav_file.setsampwidth(sampwidth)
wav_file.setframerate(framerate)
wav_file.writeframes(pcm_data)
except Exception as e:
print(f"写入文件 {filename} 时出错: {e}")

def main():
input_filename = "Data_new.txt"
output_dir = "output"
combined_filename = "combined_audio.wav"
amplification_factor = 50.0 # 音量放大系数

# WAV文件标准参数
CHANNELS = 1
SAMPWIDTH = 2 # 16-bit -> 2 bytes
FRAMERATE = 8000

# --- 准备工作 ---
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

with open(input_filename, "r") as f:
lines = [line.strip() for line in f if line.strip()]

# --- 步骤 1: 根据SSRC对RTP包进行分组 ---
streams = defaultdict(list)
stream_order = []

for line in lines:
if len(line) < 24: continue
ssrc = line[16:24]
payload_hex = line[24:]
if not payload_hex: continue

if ssrc not in streams:
stream_order.append(ssrc)
streams[ssrc].append(bytes.fromhex(payload_hex))

if not streams:
print("未在文件中找到有效的RTP数据包。")
return

print(f"处理完成,共找到 {len(stream_order)} 个不同的音频流。")
print("SSRC 出现顺序:", stream_order)

# --- 步骤 2: 逐个处理流,导出片段并准备合并 ---
final_pcm_data_list = []
total_samples_processed = 0

for ssrc in stream_order:
# 拼接属于同一个流的所有payload
raw_ulaw_data = b''.join(streams[ssrc])
if not raw_ulaw_data:
print(f"SSRC {ssrc} 没有有效的音频数据,已跳过。")
continue

# 解码为PCM
pcm_data = decode_ulaw_to_pcm16(raw_ulaw_data)

# 放大音量
amplified_pcm_data = amplify_and_clip_pcm16(pcm_data, amplification_factor)

# 计算片段的开始时间
start_time_seconds = total_samples_processed / FRAMERATE

# 创建片段文件名并导出
segment_filename = f"{start_time_seconds:.3f}s.wav"
segment_filepath = os.path.join(output_dir, segment_filename)
print(f"正在导出片段: {segment_filepath}")
write_wav(segment_filepath, amplified_pcm_data, CHANNELS, SAMPWIDTH, FRAMERATE)

# 为合并做准备
final_pcm_data_list.append(amplified_pcm_data)

# 更新已处理的总采样数
num_samples_in_segment = len(amplified_pcm_data) // SAMPWIDTH
total_samples_processed += num_samples_in_segment

# --- 步骤 3: 合并所有片段并导出最终文件 ---
if final_pcm_data_list:
combined_pcm_data = b''.join(final_pcm_data_list)
combined_filepath = os.path.join(output_dir, combined_filename)

print(f"\n正在导出合并后的文件: {combined_filepath}")
write_wav(combined_filepath, combined_pcm_data, CHANNELS, SAMPWIDTH, FRAMERATE)

print("\n所有任务完成")
else:
print("\n没有可处理的音频数据")

if __name__ == "__main__":
main()

用语音识别模型(我用的是 FunASR)识别导出的 combined_audio.wav 得到字幕文件

粗略看了下没啥信息,是由多条听起来有点逻辑的语句直接拼接起来的,然而实际上并没有任何意义

因为附件还给了个 Secret.7z 压缩包,联系到题目描述不难想到“监听设备中的加密信息”指的是这些录音,那么“绝密情报”就是 Secret.7z 了,进而可以推测音频中隐藏着 Secret.7z 的解压密码

看了下字幕文件只有 14207 行,索性直接一股脑丢给 Gemini 问问看有没有比较突兀的地方

qwb2025-4

回到字幕文件找到这一段

qwb2025-5

定位到这个片段发现确实是在念数字,人工识别得到:

1
651466314514271616614214660701456661601411451426071146666014214371656514214470

尝试使用这串数字作为密码解压缩包失败了,观察发现字符的范围是 0-7,因此推测这里要八进制转字符

用动态规划算法切成若干个长度为 2 或 3 的八进制段寻找可能的解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
s = "651466314514271616614214660701456661601411451426071146666014214371656514214470"

from functools import lru_cache

@lru_cache(None)
def dp(i):
if i == len(s):
return [[]] # 分割到末尾返回空解
res = []
for l in (2, 3): # 尝试2或3位八进制段
if i + l <= len(s):
part = s[i:i+l]
val = int(part, 8) # 按八进制解析
if 32 <= val <= 126: # 判断是否可打印
for tail in dp(i + l):
res.append([val] + tail)
return res

solutions = dp(0)
if solutions:
for vals in solutions:
decoded = "".join(chr(v) for v in vals)
print(decoded)

发现有且仅有一个解:5f3eb916bf08e610aeb09f60bc955bd8

这个解就是压缩包的解压密码,解开压缩包后得到 绝密录音.mp3

绝密录音.mp3 内存储了一段对话(其中A是普通话,B是粤语):

1
2
3
4
5
6
A:表兄,近日可好?上回托您带的廿四旦秋茶,家母嘱咐务必在辰时正过三刻前送到,切记用金丝锦盒装妥,此处潮气重,莫让干货受了霉,若赶得及时可赶得菊花开前便可让铺子开张。
B:一切安好,我会按照要求准备好秋茶,我该送到何地?
A:送至双鲤湖西岸南山茶铺,放右边第二个橱柜,莫放错。
B:我已知悉,你在那边可还安好?
A:一切安好,希望你我二人早日相见。
B:指日可待,茶叶送到了,但是晚了时日,茶铺看来只能另寻良辰吉日了。你在那边千万保重!
  • “廿四”指的是 24日

  • “辰时”指的是上午 7~9时,“辰时正”指的是 8时

  • “三刻”指的是 45分

  • 地点是对话中提到的 双鲤湖西岸南山茶铺

双鲤湖位于福建省金门,结合这些信息可以找到1949年10月24日发起的金门战役,因此年份是 1949年

连起来就是 1949年10月24日8时45分于双鲤湖西岸南山茶铺

FLAG

1
flag{2a97dec80254cdb5c526376d0c683bdd}

The_Interrogation_Room

Challenge

Reminder:
- Complete all rounds to get the flag (or a gift).
- Any invalid token terminates the session.
- Spaces must be added on both sides of ‘(‘ and ‘)’.

Solution

本题的核心是一个逻辑推理挑战,我们需要在25轮游戏中的每一轮都成功推断出服务器在后台生成的8个未知的布尔秘密值(S0S7),挑战规则如下:

  • 查询机会:每轮有 17 次提问机会
  • 查询方式:提问是通过发送一个由白名单内操作符(['==','(',')','S0','S1','S2','S3','S4','S5','S6','S7','0','1','and','or'])组成的逻辑表达式
  • 核心障碍:在17个回答中,服务器会精确地说谎 2 次(即返回与真实计算结果相反的布尔值)
  • 目标:利用这17个可能包含错误的回答反推出唯一正确的 8 个秘密值

这个问题本质上是一个纠错码问题,我们需要设计一个信息冗余的查询系统,使得即便信息在传输过程中出现了2个比特的错误也依然能够恢复出原始的8比特信息。

为了尽可能地提高成功率,我们要设计一个能够消除绝大多数歧义性的查询策略。

  • == 操作符的特性:在布尔逻辑中,A == B 等价于 XNOR(异或非)。当链式使用时(如 S0 == S1 == S2),它会检查参与运算的变量中值为 True 的个数是奇数还是偶数,这种校验方式比 orand 提供了更强的数学约束。

因此我们可以构建一个基于奇偶校验的编码系统,使用 == 操作符来实现奇偶校验,利用全部17次查询来构建一个强大的校验矩阵。

设计查询集(17个问题)

设计如下查询组合以最大化信息获取和冗余度:

  1. 8 个直接查询
    直接查询 S0, S1, …, S7
    这为我们提供了含有最多2个错误的原始数据。

  2. 9 个奇偶校验查询
    设计 9 个不同的互相重叠的秘密子集并对它们进行 == 链式查询,这些查询充当了纠错码中的“校验位”,用于精确定位错误。

    1
    2
    3
    4
    5
    # 例如:
    S0 == S1 == S2 == S3
    S4 == S5 == S6 == S7
    S0 == S2 == S4 == S6 # 偶数位
    ... (以及其他精心挑选的组合)

    这个查询集确保其最小汉明距离足够大,足以纠正2个比特的错误。

解码与暴力破解

在获得17个回答后采取以下步骤进行解码:

  1. 遍历所有可能性:由于秘密总共只有 8 位,所以只存在 2^8 = 256 种可能的组合,可以进行暴力破解。

  2. 验证每个候选解:对 256 种可能的秘密组合,执行以下验证:
    a. 假设候选解为真:假设当前遍历到的组合就是囚犯心中的真实秘密。
    b. 计算理想答案:基于这个假设计算出我们设计的 17 个查询的全部正确答案。
    c. 比较并计算差异:将这 17 个理想答案与服务器返回的 17 个回答逐一比较,计算出它们之间有多少个不一致(即汉明距离)。
    d. 寻找匹配:根据题目规则,真实的秘密组合所产生的理想答案,与服务器的回答之间的汉明距离必须精确等于2。

处理极少数的歧义情况

实验证明,存在极小概率的情况会导致找到不止一个满足条件的候选解,处理方案如下:

  • 如果只找到一个解,那么它就是正确答案。
  • 如果找到多个解,脚本会记录一个警告,并猜测第一个解作为答案提交。
  • 如果猜测错误,服务器会断开连接。此时要重新运行脚本,重跑几次总有一次能成功通过 25 轮。
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
from pwn import *
from hashlib import sha256
import string
import itertools
from functools import reduce

# --- Config ---
context.log_level = 'info'
HOST = '39.106.45.147'
PORT = 39009

# --- PoW Solver ---
def solve_pow(p):
p.recvuntil(b'sha256(XXXX+')
suffix = p.recvuntil(b')', drop=True).decode()
p.recvuntil(b'== ')
target_hash = p.recvline().strip().decode()

log.info(f"Solving PoW: sha256(XXXX+{suffix}) == {target_hash}")

for prefix in itertools.product(string.ascii_letters + string.digits, repeat=4):
prefix_str = "".join(prefix)
guess = (prefix_str + suffix).encode()
if sha256(guess).hexdigest() == target_hash:
log.success(f"PoW solved! XXXX = {prefix_str}")
p.sendlineafter(b'Give me XXXX: ', prefix_str.encode())
return

log.error("PoW failed!")
exit(1)

# --- Helper for Parity Calculation ---
def calculate_parity(booleans):
if not booleans: return True
return reduce(lambda a, b: a == b, booleans)

# --- Main Logic for a Single Round ---
def solve_round(p):
log.info("Starting new round with Parity Code query set...")

questions = []
# 1. 8 direct queries
for i in range(8):
questions.append(f"S{i}")

# 2. 9 parity check queries for maximum error correction capability
parity_indices = [
[0, 1, 2, 3], [4, 5, 6, 7], [0, 1, 4, 5],
[2, 3, 6, 7], [0, 2, 4, 6], [1, 3, 5, 7],
[0, 3, 5], [1, 2, 6], [0, 4, 7]
]
for indices in parity_indices:
questions.append(" == ".join([f"S{i}" for i in indices]))

responses = []
for i, q in enumerate(questions):
# On the first question of a round, check the received preamble for the gift
received_data = p.sendlineafter(b"Ask your question:", q.encode(), timeout=5)

if i == 0 and b"Here is a gift for you:" in received_data:
for line in received_data.strip().split(b'\n'):
if b"Here is a gift for you:" in line:
log.warning(f"GIFT RECEIVED: {line.decode()}")

p.recvuntil(b"Prisoner's response: ")
res = p.recvline().strip().decode().replace("!", "")
responses.append(res == 'True')

possible_solutions = []
for i in range(256):
candidate_secrets = [(i >> j) & 1 == 1 for j in range(8)]

true_results = candidate_secrets[:]
for indices in parity_indices:
vals_for_parity = [candidate_secrets[k] for k in indices]
parity_val = calculate_parity(vals_for_parity)
true_results.append(parity_val)

distance = sum(1 for j in range(17) if responses[j] != true_results[j])

if distance == 2:
possible_solutions.append(candidate_secrets)

# Handle the rare but real cases of ambiguity
if len(possible_solutions) >= 1:
if len(possible_solutions) > 1:
log.warning(f"AMBIGUITY DETECTED: Found {len(possible_solutions)} solutions. Guessing the first one.")
else:
log.success(f"Found unique solution: {possible_solutions[0]}")
solution = possible_solutions[0]
else: # len == 0
log.error("FATAL: Found NO possible solutions. The logic is flawed or the server is inconsistent.")
p.interactive()
exit(1)

solution_str = " ".join(map(str, map(int, solution)))
p.sendlineafter(b"Now reveal the true secrets (1 for true, 0 for false):", solution_str.encode())


# --- Main Connection Handler ---
def main():
try:
p = remote(HOST, PORT)
solve_pow(p)

for i in range(25):
log.info(f"--- Starting Round {i+1}/25 ---")
solve_round(p)

# Check for success or failure to prevent hanging on a closed socket
response = p.recvline(timeout=3)
if b"laughs triumphantly" in response:
log.error("Round failed, likely due to an unlucky ambiguous case.")
log.error("The server has closed the connection. Please restart the script.")
return

log.success("All rounds completed! Receiving flag...")
p.interactive()
except EOFError:
log.error("Connection closed unexpectedly. This can happen after a failed round.")
log.warning("Please re-run the script.")


if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[x] Opening connection to 39.106.45.147 on port 39009
[x] Opening connection to 39.106.45.147 on port 39009: Trying 39.106.45.147
[+] Opening connection to 39.106.45.147 on port 39009: Done
[*] Solving PoW: sha256(XXXX+gkNcEHeSaUjh8lbR) == 18cc96fea5027c239eb6e8374633cd6986b661039506d41d3fd917319d955b33
[+] PoW solved! XXXX = s4zb
...
[*] --- Starting Round 11/25 ---
[*] Starting new round with Parity Code query set...
[!] GIFT RECEIVED: Here is a gift for you: NDM0MTUyMmQzMTM1ZTdhYTgxZTU4N2JiZTZhZGE1ZTY5ZWFhMmRlNzgzYmRlNzgxYWJlNTljYjBlNWI4YTYyZDM2NDg1MjQ4NTQ0ZTQzMzA0MjM0Mzk0YzRmNDg1NjQzMzMzMDUzMzgzNw
[+] Found unique solution: [True, True, True, True, False, True, True, False]
...
[+] All rounds completed! Receiving flag...
[*] Switching to interactive mode
The prisoner scowls as you expose his lies. 'Very well, ask your next round of questions then.'

The prisoner slumps in defeat: 'Alright, you win! I'll tell you everything.' He confesses all his secrets and reveals the hidden location of flag{42b7aa34-00c7-4c4a-88c2-c91e9ee9b315}'
As he signs the confession, you notice a coded message hidden in his handwriting that leads you to the ultimate prize.
[*] Interrupted
[*] Closed connection to 39.106.45.147 port 39009

FLAG

1
flag{42b7aa34-00c7-4c4a-88c2-c91e9ee9b315}

Personal Vault

Challenge

My friend created a vault for each process, unfortunately we haven’t contacted for years, and this vault thing crashed my pc when I tried checking other’s secret? Please help me with this

附件下载 提取码(GAME)

Solution

非预期

qwb2025-6

FLAG

1
flag{personal_vault_seems_a_little_volatile_innit}

Reverse

butterfly

Challenge

(空)

Solution

入口链路与主函数定位

  • 入口链路:start → __startup_libc_wrapper(0x4041B0) → cpu_feature_init_ifunc(0x403200) → call_main_trampoline(0x4021F0) → main(0x4018D0)
  • 通过字符串与调用关系可见 main 打印 Usage/Encoding/Encoded size/%s.key 等信息,确认其为核心逻辑。

main@0x4018D0(核心流程)

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
// 参数检查
if (argc != 3) {
printf("Usage: %s <input_file> <output_file>\n", argv[0]);
printf("Example: %s plaintext.txt encoded.dat\n", argv[0]);
return 1;
}

in = fopen(argv[1], "rb"); // sub_405540
fseek(in, 0, SEEK_END); // sub_407480(..., 2)
n = ftell_like(in); // sub_405640
fseek(in, 0, SEEK_SET); // sub_407480(..., 0)

buf = malloc(n + 8); // sub_412620
read_n = fread_like(buf, 1, n, in); // sub_41CC80
fclose_like(in); // sub_405180

if (read_n != n) error("File read failed");

// 关键:MMX 块变换(逐 8 字节)
mmx_loop(buf, n, key8 = first8bytes_of_key_material);

// 写出编码结果
ok = write_file(argv[2], buf, n); // sub_401CA0
if (ok) {
// 生成 key 文件名并写出 32 字节 key 材料
snprintf("%s.key", argv[2]); // sub_4777A0
write_file(key_path, key_material_32, 32); // sub_401CA0
}

free(buf); // sub_412CF0
return 0;

MMX 编码循环的关键指令(0x401A49—0x401A73):

1
2
3
4
5
6
7
8
9
10
11
12
13
movq mm0, [rax]        ; 加载 8 字节数据块
movq mm1, [rsp+var_138]; 加载 8 字节 key(由 "MMXEncode2024" 派生/缓存)
pxor mm0, mm1 ; x ^= key8
movq mm2, mm0
psllw mm2, 8 ; mm2 = x << 8(以 16-bit lane 为单位)
psrlw mm0, 8 ; mm0 = x >> 8(以 16-bit lane 为单位)
por mm0, mm2 ; x = swap16(x)(每 16 位内交换高低字节)
movq mm2, mm0
psllq mm0, 1 ; x = rol1(x)(64 位整体左移1位)
psrlq mm2, 3Fh ; 取原最低位做循环
por mm0, mm2 ; 合成循环左移
paddb mm0, mm1 ; x += key8(按字节求和 mod 256)
movq [rax], mm0 ; 写回

结论(每个 8 字节块 x → y):

  • y = add8( rol1( swap16( x XOR key8 ) ), key8 )
  • 尾余(<8 字节)未进入 MMX 循环,保持原样。

sub_401CA0(写文件封装)

1
2
3
4
5
6
7
8
int write_file(const char* path, const void* data, size_t n) {
f = fopen(path, "wb"); // sub_405540
if (!f) { log("Error: Cannot create file %s"); return 0; }
written = fwrite_like(data, 1, n, f); // sub_440140(..., f)
fclose_like(f); // sub_405180
if (written != n) { log("Error: File write failed"); return 0; }
return 1;
}

功能:安全写文件并校验长度;失败路径打印错误。

sub_405540(fopen 包装)

伪代码要点:

  • sub_412620 分配 FILE 结构;sub_40BF90 初始化;sub_407830 做额外设置。
  • sub_407E10(v3, a1, “rb”/“wb”, 1) 实际为 _IO_new_file_fopen 路径。
  • 成功则返回已初始化的文件对象指针;失败则清理并返回 0。

功能:glibc _IO 层封装的 fopen-like。

sub_405640(获取长度/定位配合)

  • 在互斥/线程本地存储保护下调用 sub_4057E0(a1, 0, 1, 0) 等,配合 fseek/ftell 语义。
  • 返回“当前位置或大小”,与 main 的两次 sub_407480(…2/0) 配合可得文件长度。

功能:获取“文件长度”或“当前位置”的封装。

sub_407480(文件定位封装)

  • 在锁保护下更新流的 owner/tid 与递归计数。
  • 最终调用 sub_4057E0(a1, a2, n2, 3),n2=2/0 分别对应 SEEK_END/SEEK_SET。
    功能:fseek/rewind 等价封装。

sub_41CC80(读取封装)

1
2
3
4
5
6
7
8
// 溢出与区间检查
if (n != 0 && a3 != 0 && (n*a3 overflows || n7_6 < n*a3)) sub_41CB80(...);

// 读前加锁/进入 tcache/arena 体系
// ...
nread_total = sub_40B820(stream, buf, n*a3, ...); // 实质 fread
// 读后解锁/递归计数回退
return nread_total == n*a3 ? n : nread_total / a3;

功能:带线程/arena 管理的 fread 等价封装(静态 glibc 影子实现)。

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
import sys
from pathlib import Path

def ror64_1_le(block8: bytes) -> bytes:
# rotate-right by 1 bit on the 64-bit little-endian value
v = int.from_bytes(block8, 'little')
v = ((v >> 1) | ((v & 1) << 63)) & ((1 << 64) - 1)
return v.to_bytes(8, 'little')

def swap_each_16bit_bytes(b: bytes) -> bytes:
# swap bytes within each 16-bit lane: [b0 b1][b2 b3]... -> [b1 b0][b3 b2]...
ba = bytearray(b)
for i in range(0, len(ba), 2):
if i + 1 < len(ba):
ba[i], ba[i+1] = ba[i+1], ba[i]
return bytes(ba)

def per_byte_sub(a: bytes, key8: bytes) -> bytes:
return bytes(((a[i] - key8[i % 8]) & 0xFF) for i in range(len(a)))

def per_byte_xor(a: bytes, key8: bytes) -> bytes:
return bytes((a[i] ^ key8[i % 8]) for i in range(len(a)))

def decrypt_block(block8: bytes, key8: bytes) -> bytes:
# Encryption: c = paddb( rol64( swap16( x ^ key )), key )
# Decryption: x = ( swap16( ror64( c - key )) ) ^ key
t1 = per_byte_sub(block8, key8)
t2 = ror64_1_le(t1)
t3 = swap_each_16bit_bytes(t2)
plain = per_byte_xor(t3, key8)
return plain

def load_key8(key_path: Path) -> bytes:
# Try to read first 8 bytes from key file, fallback to b"MMXEncod"
fallback = b"MMXEncode2024"[:8]
try:
data = key_path.read_bytes()
if len(data) >= 8:
return data[:8]
except Exception:
pass
return fallback

def main():
enc_path = Path("encode.dat")
key_path = Path("encode.dat.key")
if not enc_path.exists():
print("encode.dat not found", file=sys.stderr)
sys.exit(1)

key8 = load_key8(key_path)

enc = enc_path.read_bytes()
out = bytearray()
n = len(enc)
# process full 8-byte blocks
full_blocks = (n // 8)
for i in range(full_blocks):
blk = enc[i*8:(i+1)*8]
out.extend(decrypt_block(blk, key8))
# tail bytes remain unchanged (encrypter仅对满8字节块处理)
tail = enc[full_blocks*8:]
if tail:
out.extend(tail)

# print to stdout (binary-safe)
sys.stdout.buffer.write(bytes(out))

if __name__ == "__main__":
main()

FLAG

1
flag{butter_fly_mmx_encode_7778167}
  • 标题: 第九届“强网杯”全国网络安全挑战赛
  • 作者: Aristore
  • 创建于 : 2025-10-20 00:00:00
  • 更新于 : 2025-10-20 20:14:28
  • 链接: https://www.aristore.top/posts/qwb2025/
  • 版权声明: 版权所有 © Aristore,禁止转载。
评论