签到

阅读题目内容得到 flag

1
flag{我已阅读参赛须知,并遵守比赛规则。}

Personal Vault

题目给出了一个 MEMORY.DMP 文件,这是电脑崩溃时保存的内存文件,用 LovelymemLuxe 加载

查看一下系统信息,有一个用户 a,其他没什么有用的信息

找了半天没找到有什么有用的信息,看到有很多解,猜测有非预期,直接 strings 大法,使用小工具里的字符串搜索功能,记得把编码类型都勾上,直接搜索 flag{ 即可得到 flag

1
flag{personal_vault_seems_a_little_volatile_innit}

The_Interrogation_Room

首先分析一下给出的服务端代码:

给了一个白名单,控制用户在输入逻辑表达式时只能使用指定的符号和变量,防止任意代码执行

1
white_list = ['==', '(', ')', 'S0', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', '0', '1', 'and', 'or']

interrogate() 函数:

  • 可以识别白名单里的字符转换为 False/True
  • 把中缀转为后缀用栈求值,注意要用空格分隔字符
  • 定义了运算符优先级:not (3) > == (2) > and (1) > or (0)

proof_of_work() 函数:

  • 用来证明工作量,要求输入的 4 字节前缀 XXXX 使得 sha256(XXXX + proof[4:]) 匹配服务端计算的 hexdigest

do_round() 函数:

  • 生成 8 个随机布尔秘密(S0..S7)。
  • 设置 17 个问题回答,其中恰好两次撒谎,其余为实话。
  • 每次读取客户端的问题(受白名单检查),用 interrogate 解析表达式并根据当前 truth/lie 转换回答。
  • 最后要求客户端提交 8 位正确答案(0/1),比较相等以判定本回合成功。

总结下来代码的功能大致是:客户端先做 PoW,然后进入多轮“审讯”回合,你可以用受限的布尔表达式向“囚犯”提问(表达式能引用 S0..S7 八个秘密),囚犯会在 17 个回答中恰好说两次谎(顺序随机),你需要在每一回合结束后给出 8 个比特的真实秘密。服务打算在第 10 回合发放一个“礼物”,全部通过 25 轮后揭示 flag。

所以总体目标显然是:在囚犯有两次欺骗的前提下,从布尔响应中推断出八位真相。

拷打 AI 给出脚本,思路如下:

  • 通过 build_all_codewords() 把所有 2^8(256)个可能的秘密向量编码为 17 位的码字,保存到 self.codebook(码字 -> 秘密映射)。同时计算这些码字的最小汉明距离以确认纠错能力。

  • 将生成矩阵按列转置,每列表示要对哪一组 S 位执行异或(XOR)

  • _xor_expr_from_indices生成一个字符串表达式,该表达式与服务器解析器能接受的语法兼容(例如 "( ( S0 == S1 ) == S2 ) == 0" 这种嵌套)

  • 先连接服务器,读取 PoW 挑战(sha256(XXXX+<suffix>) == <hex>),暴力穷举 4 字符前缀(从 CHARPOOL)直到 hash 匹配

  • 回应 PoW 后进入问答回合:对每个构造好的问题(17 个)逐一 send_line(question),并等待包含 "Prisoner's response:" 的行用于解析 True/False。脚本将这些回答转换为 1/0,得到一个 17 位向量。

  • 收到 17 位向量后,脚本先尝试直接在预计算表中查找;若没有命中,则对所有可能的 1-bit 和 2-bit 翻转(按 MAX_ERRORS=2)进行组合尝试,一旦纠正到表中存在的码字,就返回对应的秘密并提交(以 0 1 0 ... 格式)

核心原理是:服务器每一轮返回的 17 个布尔回答(在其中恰好两次为谎)相当于对真实码字进行了最多 2-bit 的“错误注入”(把真实 bit 反转)。因此只要码字集合的最小汉明距离 ≥ 2*2 + 1 = 5,就可以通过暴力尝试翻转 ≤2 位来恢复真实码字。

脚本如下:

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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import itertools
import hashlib
import re
import socket
import string
from typing import List, Tuple, Dict

from MySQLdb.constants.ER import HOSTNAME

HOST = "47.93.217.138" # Replace with actual server IP
PORT = 30748 # Replace with actual port

class CTFCodeSolver:
"""
A client for solving a CTF challenge based on error-correcting codes and PoW.
"""

# --- Static configuration ---
CHARSET = string.ascii_letters + string.digits
GENERATOR_MATRIX = [
[0,1,1,0,1,0,0,0,1,1,0,0,0,1,0,1,1],
[1,0,1,1,0,1,0,0,0,1,1,0,0,0,1,0,1],
[1,1,0,1,1,0,1,0,0,0,1,1,0,0,0,1,0],
[0,1,1,0,1,1,0,1,0,0,0,1,1,0,0,0,1],
[1,0,1,1,0,1,1,0,1,0,0,0,1,1,0,0,0],
[0,1,0,1,1,0,1,1,0,1,0,0,0,1,1,0,0],
[0,0,1,0,1,1,0,1,1,0,1,0,0,0,1,1,0],
[0,0,0,1,0,1,1,0,1,1,0,1,0,0,0,1,1],
]
SECRET_BITS = 8
CODEWORD_BITS = 17
MAX_ERRORS = 2

def __init__(self, server_ip: str, server_port: int):
"""
Initialize the solver and precompute all required codewords and questions.
"""
self.server_ip = server_ip
self.server_port = server_port
self.sock = None

# Precompute all valid codewords
self.codeword_map = self._precompute_codewords()

# Generate the 17 questions based on the transposed generator matrix
self.questions = self._generate_questions()
print("[*] Solver initialized: codewords and questions prepared.")

def _precompute_codewords(self) -> Dict[Tuple[int, ...], Tuple[int, ...]]:
"""
Generate all possible 8-bit secret codewords and verify the minimum Hamming distance.
"""
codeword_map = {}
all_codewords = []

for secret in itertools.product((0, 1), repeat=self.SECRET_BITS):
codeword = self._encode(secret)
if codeword in codeword_map:
raise ValueError("Generator matrix produced a collision for different secrets.")
codeword_map[codeword] = secret
all_codewords.append(codeword)

# Verify minimum Hamming distance
min_distance = self.CODEWORD_BITS
for i in range(len(all_codewords)):
for j in range(i + 1, len(all_codewords)):
dist = sum(b1 ^ b2 for b1, b2 in zip(all_codewords[i], all_codewords[j]))
min_distance = min(min_distance, dist)

required_distance = 2 * self.MAX_ERRORS + 1
if min_distance < required_distance:
raise ValueError(
f"Hamming distance too small! Minimum distance={min_distance}, required={required_distance}"
)

print(f"[+] Precomputed {len(all_codewords)} codewords, minimum Hamming distance={min_distance}")
return codeword_map

def _generate_questions(self) -> List[str]:
"""
Generate logical expressions (questions) from the transposed generator matrix.
"""
transposed = list(zip(*self.GENERATOR_MATRIX))
questions = []
for col in transposed:
active_bits = [i for i, b in enumerate(col) if b]
questions.append(self._create_xor_expression(active_bits))
return questions

def _encode(self, secret: Tuple[int, ...]) -> Tuple[int, ...]:
"""
Encode an 8-bit secret vector into a 17-bit codeword.
"""
codeword = []
for col in range(self.CODEWORD_BITS):
val = sum(secret[row] * self.GENERATOR_MATRIX[row][col] for row in range(self.SECRET_BITS)) % 2
codeword.append(val)
return tuple(codeword)

def _create_xor_expression(self, indices: List[int]) -> str:
"""
Create a nested XOR expression from a list of indices.
"""
if not indices:
return "0"
if len(indices) == 1:
return f"S{indices[0]}"

expr = f"( ( S{indices[0]} == S{indices[1]} ) == 0 )"
for idx in indices[2:]:
expr = f"( ( {expr} == S{idx} ) == 0 )"
return expr

def _solve_pow(self, challenge: str) -> str:
"""
Solve the server's SHA256 PoW challenge.
"""
match = re.match(r"sha256\(XXXX\+([A-Za-z0-9]+)\) == ([0-9a-f]{64})", challenge.strip())
if not match:
raise ValueError(f"Invalid PoW format: {challenge}")
suffix, target_hash = match.groups()
print(f"[*] Solving PoW: sha256(XXXX+{suffix}) == {target_hash[:10]}...")

for cand in itertools.product(self.CHARSET, repeat=4):
prefix = "".join(cand)
if hashlib.sha256((prefix + suffix).encode()).hexdigest() == target_hash:
print(f"[+] PoW solved: prefix='{prefix}'")
return prefix
raise RuntimeError("PoW solution not found.")

def _decode_response(self, received: List[int]) -> Tuple[int, ...]:
"""
Decode the 17-bit response, correcting up to MAX_ERRORS errors.
"""
recv_tuple = tuple(received)
if recv_tuple in self.codeword_map:
return self.codeword_map[recv_tuple]

positions = range(self.CODEWORD_BITS)
for num_errors in range(1, self.MAX_ERRORS + 1):
for error_indices in itertools.combinations(positions, num_errors):
tmp = list(recv_tuple)
for i in error_indices:
tmp[i] ^= 1
corrected = tuple(tmp)
if corrected in self.codeword_map:
print(f"[DEBUG] Corrected bits at positions {error_indices}")
return self.codeword_map[corrected]

raise ValueError(f"Decoding failed: exceeded max error correction ({self.MAX_ERRORS})")

def _read_line(self) -> str:
"""
Read a single line from the server, decode and strip it.
"""
buffer = b""
while not buffer.endswith(b"\n"):
chunk = self.sock.recv(1)
if not chunk:
raise ConnectionAbortedError("Server closed the connection.")
buffer += chunk
line = buffer.decode().strip()
print(f"[RECV] {line}")
return line

def _send_line(self, message: str):
"""
Send a line to the server.
"""
print(f"[SEND] {message}")
self.sock.sendall((message + "\n").encode())

def _handle_pow(self):
"""
Perform the PoW exchange with the server.
"""
pow_challenge = self._read_line()
solution = self._solve_pow(pow_challenge)
self._read_line() # "Give me XXXX:"
self._send_line(solution)

def _play_round(self) -> bool:
"""
Perform one full Q&A round.
Returns True if flag is received, False otherwise.
"""
line = ""
while "Ask your question:" not in line:
line = self._read_line()

answers = []
for question in self.questions:
while "Ask your question:" not in line:
line = self._read_line()
self._send_line(question)

response_line = ""
while "Prisoner's response:" not in response_line:
response_line = self._read_line()
answer_str = response_line.split(":", 1)[1].strip().rstrip("!").strip()
answers.append(1 if answer_str == "True" else 0)

line = self._read_line()
if "Now reveal" in line:
break

if len(answers) != self.CODEWORD_BITS:
raise ValueError(f"Answer count mismatch: expected {self.CODEWORD_BITS}, got {len(answers)}")

while "Now reveal" not in line:
line = self._read_line()

decoded_secret = self._decode_response(answers)
self._send_line(" ".join(map(str, decoded_secret)))

result_line = self._read_line()
return "flag" in result_line.lower()

def solve(self):
"""
Connect to the server and execute the full solving process.
"""
try:
with socket.create_connection((self.server_ip, self.server_port), timeout=10) as conn:
self.sock = conn
self._handle_pow()

while not self._play_round():
print("\n[*] Flag not found, starting a new round...\n")

print("\n[+] Challenge completed: Flag found!")

except Exception as e:
print(f"\n[ERROR] Execution error: {e}")


if __name__ == "__main__":
solver = CTFCodeSolver(HOST,PORT)
solver.solve()

谍影重重 6.0

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

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

题目给出了一个流量包和 Secret.7z 压缩包,压缩包有密码,我们先看流量包,打开,查看一下协议分级,发现全都是 UDP 协议

根据题目提示 监听设备 可知流量里有一段音频,搜索发现 UDP报文可以 decode 为 RTP 报文再提取出音频文件

半天没提取出来,而且流量包太大了很卡,ChatGPT5 Pro 直接可以把音频文件解出来

大致思路是将UDP数据包解析为RTP流,解码音频,并按时间戳和序列号重新排序,即可成功提取出来音频文件

提取到音频文件之后,大盖能听出来说的什么,是一串八进制数据,微调一下,如下:

1
65 146 63 145 142 71 61 66 142 146 60 70 145 66 61 60 141 145 142 60 71 146 66 60 142 143 71 65 65 142 144 70

八进制解码一下,得到压缩包密码 5f3eb916bf08e610aeb09f60bc955bd8,解密后得到两个文件

录音经过 TurboScribe 转文字+人工识别校对,得到如下信息

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

录音上的信息提示地点在 双鲤湖西岸南山茶铺,百度地图搜索 双鲤湖,可知具体是在 泉州市金门县

亲启.txt 里信息暗示现在正在前线作战,地点根据上述分析可知是金门,再根据题目提示 本题依托于架空的时间线,取材自真实历史事件,请关注地点信息。 说明是真实发生过的历史事件,搜索发现金门战役,发生时间也与录音中 菊花开前、秋茶等信息对应

1
我已截获破译敌特分子情报内容,请后方分析其传递的具体时间。

再根据录音信息中的 辰时正过三刻 可知具体的时间是 7时45分,综上可以得到全部信息 1949年10月24日7时45分于双鲤湖西岸南山茶铺地

1
flag{2a97dec80254cdb5c526376d0c683bdd}

legacyOLED

题目内容:这么古老的玩意,怎么还有这么多人用?

搜索发现 .sr 是 sigrok 的会话文件,本质是一个 ZIP,里面包含逻辑分析仪采样数据。解析可知捕获的是 I²C 总线

首先用软件 pulseview 打开给出的 sr 文件,选择 I²C 解码器,同时选择对应的通道

右键 I²C:Address/data,选择导出此行的解码数据,保存为 data.txt

data.txt 文件扔给 GPT,让它分析一下解码后的数据,AI 给出分析脚本,运行可以输出 OLED 模拟器在每次写入数据块后的屏幕状态快照

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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import os
import re
import numpy as np
from PIL import Image, ImageOps
from typing import List, Dict, Any

# ---------- 配置 ----------
INPUT_FILE = "data.txt"
OUTPUT_DIR = "output"
WIDTH, HEIGHT = 128, 64
ADDRS = (0x3C, 0x3D)
SAVE_EVERY_BLOCK = True
FLIP_V = True
FLIP_H = False
INVERT = False
# --------------------------

HEX_ADDR_RE = re.compile(r'Address\s+(?:write|read)\s*:\s*([0-9A-Fa-f]{2})')
HEX_DATA_RE = re.compile(r'Data\s+write\s*:\s*([0-9A-Fa-f]{2})')


def parse_i2c_log(path: str) -> List[Dict[str, Any]]:
"""解析 sigrok 导出的 I²C 文本日志,返回事务列表。"""
txns, current = [], None
in_tx = False

with open(path, "r", encoding="utf-8", errors="ignore") as f:
for raw_line in f:
line = raw_line.strip()
if not line:
continue

if "Start" in line:
current = {"addr": None, "data": []}
in_tx = True
continue

m_addr = HEX_ADDR_RE.search(line)
if m_addr:
addr = int(m_addr.group(1), 16)
if not in_tx:
current = {"addr": addr, "data": []}
in_tx = True
current["addr"] = addr
continue

if "Data write" in line and in_tx and current is not None:
for m in HEX_DATA_RE.finditer(line):
try:
current["data"].append(int(m.group(1), 16))
except ValueError:
pass
continue

if "Stop" in line and in_tx and current is not None:
if current.get("addr") is not None and current.get("data"):
txns.append(current)
current = None
in_tx = False
continue

return txns


class SSD1306:
"""SSD1306 OLED 显示仿真器。"""

def __init__(self, w=128, h=64):
self.w, self.h = w, h
self.pages = h // 8
self.vram = np.zeros((self.pages, w), dtype=np.uint8)
self.mode = 0
self.page = 0
self.col = 0
self.col_start = 0
self.col_end = w - 1
self.page_start = 0
self.page_end = self.pages - 1
self.expect = None

def expect_cmd(self, name: str):
self.expect = name

def handle_cmd(self, b: int):
"""解析并执行 SSD1306 命令字节。"""
if b == 0x20:
self.expect_cmd("addr_mode")
return
if self.expect == "addr_mode":
self.mode = b & 0x03
self.expect = None
return
if b == 0x21:
self.expect_cmd("col_start")
return
if self.expect == "col_start":
self.col_start = b
self.col = b
self.expect_cmd("col_end")
return
if self.expect == "col_end":
self.col_end = b
self.expect = None
return
if b == 0x22:
self.expect_cmd("page_start")
return
if self.expect == "page_start":
self.page_start = b
self.page = b
self.expect_cmd("page_end")
return
if self.expect == "page_end":
self.page_end = b
self.expect = None
return
if 0xB0 <= b <= 0xB7:
self.page = b - 0xB0
return
if 0x00 <= b <= 0x0F:
self.col = (self.col & 0xF0) | (b & 0x0F)
return
if 0x10 <= b <= 0x1F:
self.col = ((b & 0x0F) << 4) | (self.col & 0x0F)

def write_data(self, data: List[int]):
"""将数据写入 VRAM。"""
for byte in data:
if 0 <= self.page < self.pages and 0 <= self.col < self.w:
self.vram[self.page, self.col] = byte

if self.mode == 0:
self.col += 1
if self.col > self.col_end:
self.col = self.col_start
self.page += 1
if self.page > self.page_end:
self.page = self.page_start
elif self.mode == 2:
self.col = (self.col + 1) % self.w
else:
self.page += 1
if self.page > self.page_end:
self.page = self.page_start
self.col += 1
if self.col > self.col_end:
self.col = self.col_start

def to_image(self, flip_v=True, flip_h=False, invert=False) -> Image.Image:
"""将 VRAM 转换为灰度图像。"""
img = Image.new("L", (self.w, self.h), 0)
px = img.load()
for p in range(self.pages):
for x in range(self.w):
b = int(self.vram[p, x])
for bit in range(8):
if (b >> bit) & 1:
y = p * 8 + bit
px[x, y] = 255
if flip_v:
img = img.transpose(Image.FLIP_TOP_BOTTOM)
if flip_h:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
if invert:
img = ImageOps.invert(img.convert("L"))
return img


def main():
if not os.path.exists(INPUT_FILE):
print(f"[!] 找不到输入文件:{INPUT_FILE}")
return

txns = parse_i2c_log(INPUT_FILE)
print(f"[+] 共解析出 {len(txns)} 条 I²C 事务")

os.makedirs(OUTPUT_DIR, exist_ok=True)
dev = SSD1306(WIDTH, HEIGHT)
frame_idx = 0

for txn in txns:
addr = txn.get("addr")
data = txn.get("data", [])
if addr not in ADDRS or not data:
continue

prefix, payload = data[0], data[1:]
if prefix == 0x00:
for cmd in payload:
dev.handle_cmd(cmd)
elif prefix == 0x40:
dev.write_data(payload)
else:
dev.write_data(data)

if SAVE_EVERY_BLOCK:
img = dev.to_image(FLIP_V, FLIP_H, INVERT)
img.save(os.path.join(OUTPUT_DIR, f"frame_{frame_idx:04d}.png"))
frame_idx += 1

final_img = dev.to_image(FLIP_V, FLIP_H, INVERT)
final_img.save(os.path.join(OUTPUT_DIR, "oled_final.png"))
print(f"[+] 渲染完成,输出至 {OUTPUT_DIR}/oled_final.png")


if __name__ == "__main__":
main()

其中第 1094 帧最完整了,是一张强网杯的图标,下面还有些杂乱的色块

使用 zsteg 分析一下,发现某些通道里有数据,是一些有意义的话,暗示确实存在 lsb 隐写,但是 flag 不在这些数据里

写脚本还原一下,先将转成灰度,取 第 0 位平面(LSB),遍历顺序:XY(行优先),但按行倒序(从最后一行到第一行),列保持正常从左到右。每 8 位按 MSB-first 组一个字节,即可得到 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PIL import Image
import numpy as np

img = Image.open("frame_1094.png").convert("L")
arr = np.array(img)
bits = ((arr & 1).astype(np.uint8)) # LSB

# 行倒序、列正序,XY遍历
rows = range(arr.shape[0]-1, -1, -1)
cols = range(arr.shape[1])
stream = [int(bits[y, x]) for y in rows for x in cols]

# MSB-first 打包
out = bytearray()
for i in range(0, len(stream), 8):
chunk = stream[i:i+8]
if len(chunk) < 8: break
val = 0
for b in chunk: val = (val << 1) | b
out.append(val)

print(out.decode("latin-1", errors="ignore"))

1
qwb{Re41_Ma5te7-O5-S5Dl3o6_12C}

问卷调查

填写调查问卷即可得到 flag

1
flag{我已知晓,并会认真撰写wp!}