碎碎念
待到秋光澄澈,菊吐金英相候, 迟逾时序已久,待至吉日重逢。
……
吉时已到
这次比赛对我来说可以说是强运附体了,在不懈努力下,最终也是砍下了两个Misc一血,该做的misc也都做了(除了最后黑珍珠实在是没机会看了要去给新生培训了,不然我直接就是一个以我残躯化烈火)
这次Misc方向的题目我感觉难度上还不错,有的题目出的挺好的,但也存在个别题有点太脑洞了(没有针对出题人的意ORZ),我一直以来对Misc的看法都是——不断进化,不断学习,干中学学中干,每一次比赛都是一次学习的机会(因为每次比赛都有新的东西,这就是我们Misc),在这次强网杯中,我也收获了很多,学到了很多新的东西,也打的很爽。
……(原本这里还想说点什么,不过想了想还是算了,等年度总结再发吧)
“开始时捱一些苦,栽种绝处的花”,就目前来看,我的所知所学还是太过浅薄了,以此作为转折,继续努力吧
与君共勉,最高峰见 —— 2hi5hu
新编: “最后一舞”
Personal Vault(一血)
知识点省流
内存取证但非预期
WP
一血,没想太多,直接拿lovelymem luxe搜了一波flag,直接非了

The_Interrogation_Room
知识点省流
海明距离什么的 但是ai梭哈了
WP
复盘来看,这题的解题过程其实非常的搞笑()
附件叽里咕噜说的啥听不懂,大概就是要提问17轮,其中有两轮是说谎的,根据信息推出正确的数据,要连续猜对25轮
这题目的解题数涨的很快,所以我们都大概能猜到是ai能梭的,但是却一直没梭出来
但毕竟那么多解,而且这类题目ai梭哈的概率很大,所以我也没太放心上,打算歇一会再做
结果就有了下面这一幕,当时题解数已经多到可怕了,一怒之下问ai梭了


1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4solve_interrogation_coded.py
5Robust automated client that constructs a 17-query error-correcting measurement
6set (XOR-based), so that up to 2 erroneous answers can be corrected reliably.
7
8Usage: python3 solve_interrogation_coded.py
9"""
10import socket, hashlib, random, re, time, itertools
11from itertools import product
12
13HOST = "39.106.57.152"
14PORT = 22667
15TIMEOUT = 12
16
17ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
18
19# ---------------- I/O helpers ----------------
20def recv_all(sock, timeout=0.5):
21 sock.settimeout(timeout)
22 out = b""
23 try:
24 while True:
25 chunk = sock.recv(4096)
26 if not chunk:
27 break
28 out += chunk
29 if len(chunk) < 4096:
30 break
31 except socket.timeout:
32 pass
33 except Exception:
34 pass
35 try:
36 return out.decode(errors='replace')
37 except:
38 return out.decode('utf-8','replace')
39
40def recv_until(sock, marker, timeout=8):
41 end = time.time() + timeout
42 data = ""
43 while time.time() < end:
44 data += recv_all(sock, timeout=0.2)
45 if marker in data:
46 return data
47 return data
48
49def send_line(sock, s):
50 if isinstance(s, str):
51 sock.sendall((s + "\n").encode())
52 else:
53 sock.sendall(s + b"\n")
54
55# ---------------- POW ----------------
56def parse_pow(text):
57 m = re.search(r"sha256\(\s*XXXX\s*\+\s*([^\)\s]+)\s*\)\s*==\s*([0-9a-fA-F]{64,})", text)
58 if not m:
59 return None, None
60 return m.group(1).strip().strip("{}"), m.group(2).strip()
61
62def brute_prefix(suffix, target_hex):
63 sfx = suffix.encode()
64 # randomized attempts first
65 for _ in range(200000):
66 cand = ''.join(random.choice(ALPHABET) for _ in range(4)).encode()
67 if hashlib.sha256(cand + sfx).hexdigest() == target_hex:
68 return cand.decode()
69 # deterministic fallback
70 for a in ALPHABET:
71 for b in ALPHABET:
72 for c in ALPHABET:
73 for d in ALPHABET:
74 cand = (a+b+c+d).encode()
75 if hashlib.sha256(cand + sfx).hexdigest() == target_hex:
76 return cand.decode()
77 return None
78
79# ---------------- build XOR expression (white-list-safe) ----------------
80def fmt_token(tok):
81 # tokens like "( S0 == 1 )" already spacing-safe; we ensure spacing around parens and == later
82 return tok
83
84def xor2_expr(a_expr, b_expr):
85 # XOR(a,b) = (a and not b) or (not a and b)
86 # but 'not' isn't in whitelist; use (a==1 and b==0) or (a==0 and b==1)
87 return "( ( " + a_expr + " and " + "( " + b_expr + " ) == 0 ) or ( ( " + a_expr + " ) == 0 and " + b_expr + " ) )"
88
89def build_xor_expr(indices):
90 """
91 indices: list of bit indices [0..7] to XOR together
92 returns string expression using allowed tokens
93 implement by folding pairwise XOR
94 base var expression for bit i: ( S{i} == 1 )
95 For a single index, return "( S{i} == 1 )"
96 """
97 assert len(indices) >= 1
98 def var(i):
99 return f"( S{i} == 1 )"
100 exprs = [var(i) for i in indices]
101 # fold using xor2_expr
102 cur = exprs[0]
103 for e in exprs[1:]:
104 # xor2_expr expects boolean-like strings; but it uses "== 0" comparisons
105 # ensure inside expressions are parenthesized
106 cur = "( ( " + cur + " and ( " + e + " ) == 0 ) or ( ( " + cur + " ) == 0 and " + e + " ) )"
107 # clean up double spaces
108 return re.sub(r"\s+", " ", cur).strip()
109
110# ---------------- search for good generator matrix G (8 x 17) ----------------
111def weight(v):
112 return sum(1 for x in v if x)
113
114def code_min_distance(G_cols):
115 # G_cols: list of 17 column vectors each length 8 bits (0/1)
116 # compute all codewords s*G (mod 2) for s in 1..255 and return minimum weight
117 minw = None
118 # For speed, precompute columns as ints bitpacked
119 col_ints = []
120 for col in G_cols:
121 val = 0
122 for i,b in enumerate(col):
123 if b:
124 val |= (1<<i)
125 col_ints.append(val)
126 # enumerate nonzero s in 1..255
127 for s in range(1, 1<<8):
128 # compute codeword bits across 17 columns: bit j = parity(popcount(col_ints[j] & s))
129 cw_weight = 0
130 for c in col_ints:
131 bits = bin(c & s).count("1")
132 b = bits & 1
133 cw_weight += b
134 # early cutoff
135 if minw is not None and cw_weight >= minw:
136 break
137 if cw_weight == 0:
138 continue
139 if minw is None or cw_weight < minw:
140 minw = cw_weight
141 if minw == 1:
142 return 1
143 return minw if minw is not None else 0
144
145def find_good_G(max_tries=20000):
146 # return G_cols: list of 17 columns each a list of 8 bits (0/1), with min distance >=5
147 tries = 0
148 while tries < max_tries:
149 tries += 1
150 # generate random full-rank generator matrix: 8x17 (we create 17 columns length 8)
151 # ensure rank 8 by ensuring first 8 columns form invertible matrix - simpler: construct by starting with identity cols then add random
152 cols = []
153 # put identity columns first
154 for i in range(8):
155 col = [0]*8
156 col[i] = 1
157 cols.append(col)
158 # remaining 9 random nonzero columns
159 for _ in range(9):
160 col = [random.choice([0,1]) for _ in range(8)]
161 # avoid all zero column
162 if sum(col) == 0:
163 col[random.randrange(8)] = 1
164 cols.append(col)
165 # compute min distance
166 dmin = code_min_distance(cols)
167 if dmin >= 5:
168 print(f"[+] Found G with dmin={dmin} after {tries} tries")
169 return cols
170 # else shuffle and try small mutation occasionally
171 if tries % 500 == 0:
172 pass
173 raise RuntimeError("failed to find good G in given tries")
174
175# ---------------- build queries from G ----------------
176def build_queries_from_G(G_cols):
177 # For each column (list of 8 bits) produce XOR expression of indices where bit==1
178 queries = []
179 for col in G_cols:
180 inds = [i for i,b in enumerate(col) if b]
181 if not inds:
182 # column zero shouldn't happen; use constant 0 expression
183 q = "( 0 == 1 )"
184 elif len(inds) == 1:
185 q = f"( S{inds[0]} == 1 )"
186 else:
187 q = build_xor_expr(inds)
188 # normalize spaces to avoid smirk
189 q = re.sub(r"\s+", " ", q).strip()
190 queries.append(q)
191 return queries
192
193# ---------------- helper to encode secret by G ----------------
194def encode_by_G(s_tuple, G_cols):
195 # s_tuple length 8 of 0/1 ints
196 code = []
197 for col in G_cols:
198 # parity of dot product s . col
199 v = sum(s_tuple[i] & col[i] for i in range(8)) & 1
200 code.append(bool(v))
201 return code
202
203# ---------------- main interaction using coded queries ----------------
204def run_coded(attempts_find_G=5000):
205 random.seed() # unpredictable per run
206 print("[*] Searching for good generator matrix G (this runs locally)...")
207 G_cols = find_good_G(max_tries=attempts_find_G)
208 queries = build_queries_from_G(G_cols)
209 print("[*] Using generated queries (17):")
210 for i,q in enumerate(queries,1):
211 print(f"{i:02d}: {q}")
212
213 # now network interaction (POW -> send queries -> decode -> submit) - follows same interaction style
214 sock = socket.create_connection((HOST, PORT), timeout=TIMEOUT)
215 sock.settimeout(1.0)
216 try:
217 banner = recv_until(sock, "Give me XXXX:", timeout=12)
218 if "sha256" in banner:
219 suffix, hexd = parse_pow(banner)
220 if not suffix or not hexd:
221 banner += recv_all(sock, timeout=0.5)
222 suffix, hexd = parse_pow(banner)
223 if not suffix or not hexd:
224 print("[-] cannot parse POW")
225 return False
226 if "Give me" not in banner:
227 banner += recv_until(sock, "Give me XXXX:", timeout=4)
228 print("[*] POW suffix:", suffix, "target:", hexd)
229 prefix = brute_prefix(suffix, hexd)
230 if not prefix:
231 print("[-] POW failed")
232 return False
233 print("[+] POW solved:", prefix)
234 send_line(sock, prefix)
235 intro = recv_until(sock, "Ask your question:", timeout=12)
236 print(intro.strip())
237 _ = recv_all(sock, timeout=0.1)
238
239 rounds = 0
240 while True:
241 rounds += 1
242 print(f"[*] Round {rounds}")
243 answers = []
244 # send our designed queries sequentially
245 for i,q in enumerate(queries, start=1):
246 print(f"[>] Q{i:02d}: {q}")
247 send_line(sock, q)
248 resp = recv_until(sock, "Prisoner's response:", timeout=8)
249 resp += recv_all(sock, timeout=0.25)
250 print(resp.strip())
251 if "smirks" in resp or "refuse to answer" in resp:
252 print("[-] server refused phrasing. aborting")
253 return False
254 tail = resp.split("Prisoner's response:",1)[-1]
255 # parse boolean
256 if "True" in tail and "False" not in tail:
257 val = True
258 elif "False" in tail and "True" not in tail:
259 val = False
260 elif "True" in tail:
261 val = True
262 elif "False" in tail:
263 val = False
264 else:
265 val = False
266 answers.append(bool(val))
267 time.sleep(0.01)
268
269 # decode: find s in 256 such that Hamming distance between encode_by_G(s) and answers <= 2
270 best = None
271 best_hd = 999
272 cands = []
273 for s in product([0,1], repeat=8):
274 code = encode_by_G(s, G_cols)
275 hd = sum(int(code[i]) != int(answers[i]) for i in range(17))
276 if hd < best_hd:
277 best_hd = hd
278 cands = [s]
279 elif hd == best_hd:
280 cands.append(s)
281 print(f"[*] best_hd={best_hd} candidate_count={len(cands)}")
282 if best_hd <= 2 and len(cands) >= 1:
283 # pick unique if single, else majority among candidates
284 if len(cands) == 1:
285 guess = cands[0]
286 else:
287 # majority vote across candidate bits
288 counts = [0]*8
289 for c in cands:
290 for k,b in enumerate(c):
291 counts[k] += b
292 guess = tuple(1 if counts[k]*2 >= len(cands) else 0 for k in range(8))
293 else:
294 # fallback: try brute-force search flipping up to 2 bits in answers (shouldn't be necessary)
295 found = False
296 for flip_idxs in itertools.combinations(range(17), 0 + 1 + 2):
297 # try flipping these and see if simple decode by taking first 8 entries yields consistent s
298 alt = answers[:]
299 for fi in flip_idxs:
300 alt[fi] = not alt[fi]
301 # derive candidate s from first 8 alt bits by decoding linear system? but we used generator form: columns include identity first, so first 8 columns were identity in our G construction -> we can map s directly
302 # our find_good_G constructed first 8 columns as identity, so alt[0..7] directly are s bits
303 cand_s = tuple(1 if alt[k] else 0 for k in range(8))
304 code = encode_by_G(cand_s, G_cols)
305 if all(bool(code[i]) == bool(alt[i]) for i in range(17)):
306 guess = cand_s
307 found = True
308 break
309 if not found:
310 # last fallback: choose minimal hd candidate
311 guess = cands[0] if cands else tuple(0 for _ in range(8))
312 print("[*] submit guess:", guess)
313 send_line(sock, " ".join(str(int(x)) for x in guess))
314
315 out = recv_until(sock, "", timeout=2)
316 out += recv_all(sock, timeout=0.6)
317 print(out.strip())
318 if "confesses" in out or "flag" in out.lower() or "you win" in out.lower():
319 print("[+] success final output:")
320 print(out)
321 return True
322 if "fell for my deception" in out or "disciplinary action" in out:
323 print("[-] failed this round")
324 return False
325 time.sleep(0.1)
326 except Exception as e:
327 print("Exception:", e)
328 try: sock.close()
329 except: pass
330 return False
331 finally:
332 try: sock.close()
333 except: pass
334
335if __name__ == "__main__":
336 random.seed()
337 try:
338 ok = run_coded(attempts_find_G=8000)
339 if not ok:
340 print("[-] finished without success")
341 except Exception as e:
342 print("Exception outer:", e)过程非常稳定,我发现让ai解题的时候让他去找巧妙的解法很容易出(这也是一种提问的技巧吧,之前有些密码题也能这么问出来)

谍影重重 6.0
知识点省流
rtp流量数据分析 音频转译 历史猜谜
WP
流量分析+妙妙历史猜谜(靠北)
打开题目是一个700多m的流量包,要吓尿了
进去看到全是udp流量,前期绕了很多圈子,特别是看到了个manolito协议,后面反应过来是ws自己误识别的

实际上这个udp流量就是rtp数据(看他的数据格式跟之前wm那个voice hacker的流量数据简直一模一样),所以要将他们的音频提取出来,但是简单过了一下发现有1300个端口,每个端口对应一条语音,而且流量包太大了,全部用ws根本不现实,所以要用脚本,之前做voice_hacker有过脚本,在那基础上让ai优化升级了一下
主要调整点在于原音频声音很小,而且有很多空白的地方,所以对提取的音频音量加大了,而且还去掉了空白的部分
然后呢实际上1300个端口,每个端口都有2段数据,就是两段不同的音频,写脚本提取的时候要将重复的区分开,我让ai把他们放在了两个文件夹,然后将同一个目录中的音频按端口顺序合并为一个音频
1# -*- coding: utf-8 -*-
2# extract_pcmu.py
3# 从经典PCAP(小端)中提取 RTP(PCMU, PT=0):
4# - 正常音频:丢弃重复包(与原逻辑一致),丢包静音补洞 -> 放大音量 -> 去静音 -> 导出单条 -> 合成总音频
5# - 重复包音频:不丢弃重复包,仅把“重复包”按同样逻辑单独拼接为重复音轨 -> 放大音量 -> 去静音 -> 导出单条 -> 合成总音频
6# - 生成“非RTP/非PCMU”排查报告 TXT
7
8import os, io, struct, wave, datetime
9import numpy as np
10from collections import Counter, defaultdict
11
12# ========= 配置 =========
13PCAP_PATH = 'Data.pcap' # 你的 pcap 文件 (经典PCAP小端)
14OUT_DIR = r"output4" # 正常音频输出目录
15DUP_DIR = r"output4_dups" # 重复包音频输出目录
16
17# 合成总音频
18COMBINED_MAIN = "combined_by_ports.wav"
19COMBINED_DUP = "combined_by_ports_dups.wav"
20GAP_SECONDS = 2.0 # 合成段间静音(秒)
21SAMPLE_RATE = 8000 # 统一 8kHz, mono, 16-bit
22
23# 音量放大参数
24USE_TARGET_PEAK = True # True: 峰值规整化;False: 固定 dB 增益
25TARGET_PEAK = 0.98 # 目标峰值(相对 32767),0.98 ≈ -0.18 dBFS
26FIXED_GAIN_DB = 6.0 # 若不用目标峰值,则使用固定增益(dB)
27CLIP_PROTECT = 0.999 # 额外防削波系数
28
29# 静音剔除(VAD)参数 —— 尽量不影响有声段
30FRAME_MS = 20 # 帧长(毫秒)
31HOP_MS = 10 # 帧移(毫秒)
32SILENCE_THRESHOLD_DBFS = -35.0 # 静音阈值(dBFS)
33HYSTERESIS_DB = 3.0 # 滞后带(进入/离开语音的门限差)
34MIN_VOICE_MS = 60 # 最短保留语音段(毫秒)
35MERGE_GAP_MS = 50 # 两语音段间隙小于该值则合并(毫秒)
36LEAD_PAD_MS = 20 # 每段语音前保留的缓冲(毫秒)
37TAIL_PAD_MS = 40 # 每段语音后保留的缓冲(毫秒)
38
39# 丢包/重复判断参数
40MAX_GAP_PACKETS = 1000 # 小于该缺口视作丢包并静音补洞
41FALLBACK_PL_SIZE = 160 # 兜底负载长度(20ms @ 8kHz)
42REPORT_NAME = "non_pcmu_report.txt"
43# ========================
44
45os.makedirs(OUT_DIR, exist_ok=True)
46os.makedirs(DUP_DIR, exist_ok=True)
47REPORT_PATH = os.path.join(OUT_DIR, REPORT_NAME)
48
49# --------------- 基础解析 ---------------
50def read_pcap_bytes():
51 if PCAP_PATH and os.path.exists(PCAP_PATH):
52 with open(PCAP_PATH, "rb") as f:
53 return f.read()
54 raise FileNotFoundError(f"找不到 PCAP 路径:{PCAP_PATH}")
55
56def parse_pcap_le(buf: bytes):
57 # PCAP 全局头 24 字节,小端魔数 0xD4C3B2A1
58 if len(buf) < 24 or buf[0:4] != b"\xD4\xC3\xB2\xA1":
59 raise ValueError("不是经典PCAP(小端);请用 Wireshark -> Save As -> pcap(libpcap) 转存再试")
60 off = 24
61 while off + 16 <= len(buf):
62 ts_sec, ts_usec, incl_len, orig_len = struct.unpack("<IIII", buf[off:off+16])
63 off += 16
64 pkt = buf[off:off+incl_len]
65 off += incl_len
66 yield pkt
67
68def parse_eth(pkt: bytes):
69 if len(pkt) < 14: return None
70 dst, src, et = struct.unpack("!6s6sH", pkt[:14])
71 return et, pkt[14:]
72
73def parse_ipv4(payload: bytes):
74 if len(payload) < 20: return None
75 vihl = payload[0]
76 version = vihl >> 4
77 ihl = (vihl & 0x0F) * 4
78 if version != 4 or len(payload) < ihl: return None
79 total_length = struct.unpack("!H", payload[2:4])[0]
80 proto = payload[9]
81 return proto, payload[ihl:total_length]
82
83def parse_udp(seg: bytes):
84 if len(seg) < 8: return None
85 srcp, dstp, length, checksum = struct.unpack("!HHHH", seg[:8])
86 if length < 8 or length > len(seg): return None
87 return srcp, dstp, seg[8:length]
88
89def parse_rtp(payload: bytes):
90 # RTP v2 最小 12 字节
91 if len(payload) < 12: return None
92 if (payload[0] & 0xC0) != 0x80: return None # Version=2
93 cc = payload[0] & 0x0F
94 pt = payload[1] & 0x7F
95 seq = struct.unpack("!H", payload[2:4])[0]
96 ts = struct.unpack("!I", payload[4:8])[0]
97 ssrc = struct.unpack("!I", payload[8:12])[0]
98 hdr_len = 12 + cc * 4
99 # 头扩展
100 if payload[0] & 0x10:
101 if len(payload) < hdr_len + 4: return None
102 ext_len = struct.unpack("!H", payload[hdr_len+2:hdr_len+4])[0] * 4
103 hdr_len += 4 + ext_len
104 if len(payload) < hdr_len: return None
105 return {"pt": pt, "seq": seq, "ts": ts, "ssrc": ssrc, "payload": payload[hdr_len:]}
106
107# --------------- G.711 PCMU 解码 ---------------
108def mulaw_decode_table():
109 lut = np.zeros(256, dtype=np.int16)
110 for i in range(256):
111 b = i ^ 0xFF # PCMU 8bit 为反码
112 sign = b & 0x80
113 exponent = (b >> 4) & 0x07
114 mantissa = b & 0x0F
115 sample = (((mantissa << 3) | 132) << exponent) - 132 # 132 = 0x84
116 if sign:
117 sample = -sample
118 lut[i] = np.int16(sample)
119 return lut
120
121LUT = mulaw_decode_table()
122PCMU_SILENCE_BYTE = b'\xFF' # μ-law 静音(解码约为 0)
123
124def decode_mulaw(data: bytes) -> np.ndarray:
125 u = np.frombuffer(data, dtype=np.uint8)
126 return LUT[u]
127
128# --------------- 音频处理 ---------------
129def apply_gain(pcm: np.ndarray) -> np.ndarray:
130 """峰值规整化/固定增益 + 防削波"""
131 if pcm.size == 0:
132 return pcm
133 pcm_f = pcm.astype(np.float32)
134 if USE_TARGET_PEAK:
135 peak = float(np.max(np.abs(pcm_f)))
136 if peak <= 0.0:
137 return pcm
138 target_amp = 32767.0 * TARGET_PEAK * CLIP_PROTECT
139 gain = target_amp / peak
140 else:
141 gain = 10.0 ** (FIXED_GAIN_DB / 20.0)
142 out = pcm_f * gain
143 out = np.clip(out, -32767.0, 32767.0)
144 return out.astype(np.int16)
145
146def _rms_dbfs(x: np.ndarray) -> float:
147 if x.size == 0: return -120.0
148 x_f = x.astype(np.float32) / 32768.0
149 rms = np.sqrt(np.mean(x_f * x_f) + 1e-12)
150 return 20.0 * np.log10(rms + 1e-12)
151
152def remove_silence(pcm: np.ndarray, sr: int = SAMPLE_RATE) -> np.ndarray:
153 """能量门限 VAD + 滞后 + 前后包络"""
154 if pcm.size == 0:
155 return pcm
156 frame_len = max(1, int(sr * FRAME_MS / 1000.0))
157 hop_len = max(1, int(sr * HOP_MS / 1000.0))
158
159 frames_db = []
160 i = 0
161 while i < len(pcm):
162 seg = pcm[i:i+frame_len]
163 frames_db.append(_rms_dbfs(seg))
164 i += hop_len
165 frames_db = np.asarray(frames_db, dtype=np.float32)
166
167 thr_on = SILENCE_THRESHOLD_DBFS + HYSTERESIS_DB
168 thr_off = SILENCE_THRESHOLD_DBFS
169
170 voiced = np.zeros_like(frames_db, dtype=bool)
171 state = False
172 for k, db in enumerate(frames_db):
173 if not state:
174 if db >= thr_on: state = True
175 else:
176 if db < thr_off: state = False
177 voiced[k] = state
178
179 # 取语音区段
180 intervals, n, k = [], len(voiced), 0
181 while k < n:
182 if voiced[k]:
183 j = k + 1
184 while j < n and voiced[j]: j += 1
185 start = k * hop_len
186 end = min(len(pcm), (j - 1) * hop_len + frame_len)
187 intervals.append([start, end])
188 k = j
189 else:
190 k += 1
191 if not intervals:
192 return np.array([], dtype=np.int16)
193
194 # 合并/最短/前后包络
195 def ms2samp(ms): return int(sr * ms / 1000.0)
196 min_len = ms2samp(MIN_VOICE_MS)
197 mergegap = ms2samp(MERGE_GAP_MS)
198 lead = ms2samp(LEAD_PAD_MS)
199 tail = ms2samp(TAIL_PAD_MS)
200
201 merged = []
202 cur_s, cur_e = intervals[0]
203 for s, e in intervals[1:]:
204 if s - cur_e <= mergegap:
205 cur_e = max(cur_e, e)
206 else:
207 if cur_e - cur_s >= min_len:
208 merged.append([cur_s, cur_e])
209 cur_s, cur_e = s, e
210 if cur_e - cur_s >= min_len:
211 merged.append([cur_s, cur_e])
212 if not merged:
213 return np.array([], dtype=np.int16)
214
215 padded = []
216 for s, e in merged:
217 s = max(0, s - lead); e = min(len(pcm), e + tail)
218 if e > s: padded.append([s, e])
219
220 final_intervals = []
221 cur_s, cur_e = padded[0]
222 for s, e in padded[1:]:
223 if s <= cur_e:
224 cur_e = max(cur_e, e)
225 else:
226 final_intervals.append([cur_s, cur_e]); cur_s, cur_e = s, e
227 final_intervals.append([cur_s, cur_e])
228
229 parts = [pcm[s:e] for s, e in final_intervals]
230 return np.concatenate(parts) if parts else np.array([], dtype=np.int16)
231
232def save_wav(path, pcm, rate=SAMPLE_RATE):
233 with wave.open(path, "wb") as wf:
234 wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(rate)
235 wf.writeframes(pcm.tobytes())
236
237# --------------- 报告 ---------------
238def write_report(report_lines):
239 with open(REPORT_PATH, "w", encoding="utf-8") as f:
240 f.write("\n".join(report_lines))
241
242# --------------- 主流程 ---------------
243def main():
244 # 报告统计
245 non_ipv4_counts = Counter()
246 non_udp_counts = Counter()
247 udp_non_rtp = Counter() # (srcp,dstp)
248 rtp_non_pcmu = Counter() # (srcp,dstp,ssrc,pt)
249 examples_udp_non_rtp = {}
250 total_packets = total_ipv4 = total_udp = total_rtp = total_rtp_pcmu = 0
251
252 raw = read_pcap_bytes()
253 streams = {} # key=(srcp,dstp,ssrc) -> [rtp packets]
254
255 # 逐包解析 + 统计
256 for pkt in parse_pcap_le(raw):
257 total_packets += 1
258 eth = parse_eth(pkt)
259 if not eth: continue
260 etype, l3 = eth
261 if etype != 0x0800:
262 non_ipv4_counts[hex(etype)] += 1
263 continue
264 total_ipv4 += 1
265
266 ip = parse_ipv4(l3)
267 if not ip: continue
268 proto, ip_payload = ip
269 if proto != 17:
270 non_udp_counts[str(proto)] += 1
271 continue
272 total_udp += 1
273
274 udp = parse_udp(ip_payload)
275 if not udp: continue
276 srcp, dstp, udp_pl = udp
277
278 rtp = parse_rtp(udp_pl)
279 if not rtp:
280 key = (srcp, dstp)
281 udp_non_rtp[key] += 1
282 if key not in examples_udp_non_rtp:
283 prefix = udp_pl[:16] if udp_pl else b""
284 examples_udp_non_rtp[key] = prefix.hex(" ")
285 continue
286
287 total_rtp += 1
288 if rtp["pt"] != 0: # 非 PCMU
289 key = (srcp, dstp, rtp["ssrc"], rtp["pt"])
290 rtp_non_pcmu[key] += 1
291 continue
292
293 total_rtp_pcmu += 1
294 key = (srcp, dstp, rtp["ssrc"])
295 streams.setdefault(key, []).append(rtp)
296
297 if not streams:
298 print("[!] 没抓到 PT=0(PCMU) 的 RTP。")
299
300 # ======== 正常/重复 两路音频构建 ========
301 out_files_main, out_files_dup = [], []
302 per_stream_main, per_stream_dup = [], [] # [(key, pcm, path)]
303
304 for (srcp, dstp, ssrc), pkts in streams.items():
305 if not pkts: continue
306 pkts.sort(key=lambda r: (r["ts"], r["seq"]))
307
308 # 统计常见负载长度
309 try:
310 size_counts = Counter(len(r["payload"]) for r in pkts)
311 default_pl_size = size_counts.most_common(1)[0][0]
312 if default_pl_size <= 0:
313 default_pl_size = FALLBACK_PL_SIZE
314 except Exception:
315 default_pl_size = FALLBACK_PL_SIZE
316
317 print(f"\n[+] 流 {srcp}->{dstp} (SSRC={ssrc}) 包数 {len(pkts)},常见负载 {default_pl_size}B")
318
319 # 主音轨(丢弃重复包)
320 main_buf = io.BytesIO(); last_seq_main = None
321 # 重复音轨(仅由重复包构成,不丢弃重复)
322 dup_buf = io.BytesIO(); last_seq_dup = None
323
324 for r in pkts:
325 seq = r["seq"]; payload = r["payload"]
326
327 # ---------- 主音轨:保持原逻辑(丢弃重复包) ----------
328 if last_seq_main is None:
329 last_seq_main = seq
330 main_buf.write(payload)
331 else:
332 if seq == last_seq_main:
333 # 重复包:主音轨丢弃
334 pass
335 else:
336 exp = (last_seq_main + 1) & 0xFFFF
337 if seq == exp:
338 main_buf.write(payload)
339 last_seq_main = seq
340 else:
341 gap = (seq - exp + 65536) % 65536
342 if gap < MAX_GAP_PACKETS:
343 main_buf.write(PCMU_SILENCE_BYTE * default_pl_size * gap)
344 main_buf.write(payload)
345 last_seq_main = seq
346 else:
347 # 大跳变:直接衔接
348 main_buf.write(payload)
349 last_seq_main = seq
350
351 # ---------- 重复音轨:只收集“重复包内容” ----------
352 # 规则:当检测到 seq 与“主音轨 last_seq_main 之前值”相同,说明这是重复包;
353 # 但为避免主/重复交叉状态的问题,我们基于当前包与“上一主包序号”的相等性来定义“重复”:
354 # 如果与上一主包序号相等(即刚才进入主逻辑时被视作重复),则纳入重复音轨。
355 # 为实现这一点,我们在进入本循环顶部就先保存一下主轨 last_seq_main_before。
356 # 简化处理:当主逻辑认定此包为重复(seq == old_last_main),我们就把它写入 dup_buf。
357 # 这里通过再次判断实现:
358 # (注意:主轨部分已经把 last_seq_main 更新/保持住了)
359 # 第二轮再走一遍,标注重复(避免在一次循环里被主轨状态更新影响判定)
360 last_seq_main = None
361 for r in pkts:
362 seq = r["seq"]; payload = r["payload"]
363 if last_seq_main is None:
364 last_seq_main = seq
365 continue
366 if seq == last_seq_main:
367 # 命中“重复包” -> 写入重复音轨,并做简单的缺口补偿(相对 dup 序列)
368 if last_seq_dup is None:
369 last_seq_dup = seq
370 dup_buf.write(payload)
371 else:
372 # 处理重复音轨的“序列缺口”以保证播放连贯(可选)
373 exp_dup = (last_seq_dup + 1) & 0xFFFF
374 if seq == exp_dup:
375 dup_buf.write(payload)
376 last_seq_dup = seq
377 elif seq == last_seq_dup:
378 # 同一序列的多次重复:直接追加
379 dup_buf.write(payload)
380 # last_seq_dup 不变也行;为稳定可设为相同值
381 last_seq_dup = seq
382 else:
383 gap_dup = (seq - exp_dup + 65536) % 65536
384 if gap_dup < MAX_GAP_PACKETS:
385 dup_buf.write(PCMU_SILENCE_BYTE * default_pl_size * gap_dup)
386 dup_buf.write(payload)
387 last_seq_dup = seq
388 else:
389 dup_buf.write(payload)
390 last_seq_dup = seq
391 else:
392 last_seq_main = seq # 推进主轨参考序号
393
394 # --- 解码 & 处理:主音轨 ---
395 payload_main = main_buf.getvalue()
396 if payload_main:
397 pcm_main = decode_mulaw(payload_main)
398 pcm_main = apply_gain(pcm_main)
399 pcm_main = remove_silence(pcm_main, sr=SAMPLE_RATE)
400 else:
401 pcm_main = np.array([], dtype=np.int16)
402
403 wav_main = os.path.join(OUT_DIR, f"rtp_{srcp}_{dstp}_{ssrc}.wav")
404 save_wav(wav_main, pcm_main, SAMPLE_RATE)
405 out_files_main.append(wav_main)
406 per_stream_main.append(((srcp, dstp, ssrc), pcm_main, wav_main))
407 print(f" [+] 主轨导出:{wav_main}({len(pcm_main)/SAMPLE_RATE:.2f}s)")
408
409 # --- 解码 & 处理:重复音轨 ---
410 payload_dup = dup_buf.getvalue()
411 if payload_dup:
412 pcm_dup = decode_mulaw(payload_dup)
413 pcm_dup = apply_gain(pcm_dup)
414 pcm_dup = remove_silence(pcm_dup, sr=SAMPLE_RATE)
415 else:
416 pcm_dup = np.array([], dtype=np.int16)
417
418 wav_dup = os.path.join(DUP_DIR, f"rtp_dup_{srcp}_{dstp}_{ssrc}.wav")
419 save_wav(wav_dup, pcm_dup, SAMPLE_RATE)
420 out_files_dup.append(wav_dup)
421 per_stream_dup.append(((srcp, dstp, ssrc), pcm_dup, wav_dup))
422 print(f" [=] 重复轨导出:{wav_dup}({len(pcm_dup)/SAMPLE_RATE:.2f}s)")
423
424 print(f"\n完成:主轨 {len(out_files_main)} 条,重复轨 {len(out_files_dup)} 条。")
425
426 # ======== 合成总音频(主轨) ========
427 if per_stream_main:
428 per_stream_main.sort(key=lambda item: (item[0][0], item[0][1], item[0][2]))
429 gap = np.zeros(int(SAMPLE_RATE * GAP_SECONDS), dtype=np.int16)
430 parts, names = [], []
431 for (srcp, dstp, ssrc), pcm, path in per_stream_main:
432 if pcm.size == 0: continue
433 parts.append(pcm); names.append(os.path.basename(path)); parts.append(gap)
434 if parts and parts[-1].size == gap.size:
435 parts = parts[:-1]
436 combined = np.concatenate(parts) if parts else np.array([], dtype=np.int16)
437 comb_path = os.path.join(OUT_DIR, COMBINED_MAIN)
438 save_wav(comb_path, combined, SAMPLE_RATE)
439 print(f"[主轨合成] {comb_path} ({len(combined)/SAMPLE_RATE:.2f}s)")
440 if names:
441 print(" 顺序:"); [print(f" - {n}") for n in names]
442 else:
443 print("[主轨合成] 无可用音频,跳过。")
444
445 # ======== 合成总音频(重复轨) ========
446 if per_stream_dup:
447 per_stream_dup.sort(key=lambda item: (item[0][0], item[0][1], item[0][2]))
448 gap = np.zeros(int(SAMPLE_RATE * GAP_SECONDS), dtype=np.int16)
449 parts, names = [], []
450 for (srcp, dstp, ssrc), pcm, path in per_stream_dup:
451 if pcm.size == 0: continue
452 parts.append(pcm); names.append(os.path.basename(path)); parts.append(gap)
453 if parts and parts[-1].size == gap.size:
454 parts = parts[:-1]
455 combined = np.concatenate(parts) if parts else np.array([], dtype=np.int16)
456 comb_path = os.path.join(DUP_DIR, COMBINED_DUP)
457 save_wav(comb_path, combined, SAMPLE_RATE)
458 print(f"[重复轨合成] {comb_path} ({len(combined)/SAMPLE_RATE:.2f}s)")
459 if names:
460 print(" 顺序:"); [print(f" - {n}") for n in names]
461 else:
462 print("[重复轨合成] 无可用音频,跳过。")
463
464 # ======== 生成排查报告 ========
465 report = []
466 report.append(f"# 非RTP/非PCMU 报告")
467 report.append(f"生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
468 report.append(f"PCAP文件: {os.path.abspath(PCAP_PATH)}")
469 report.append("")
470 report.append(f"总帧数: {total_packets}")
471 report.append(f"IPv4帧数: {total_ipv4}")
472 report.append(f"UDP报文数: {total_udp}")
473 report.append(f"RTP报文数: {total_rtp}")
474 report.append(f"RTP(PT=0, PCMU)报文数: {total_rtp_pcmu}")
475 report.append("")
476
477 if non_ipv4_counts:
478 report.append("## 非IPv4以太帧 (按EtherType统计)")
479 for et, cnt in sorted(non_ipv4_counts.items(), key=lambda x: x[0]):
480 report.append(f"- EtherType={et}: {cnt}")
481 report.append("")
482 if non_udp_counts:
483 report.append("## 非UDP的IPv4报文 (按IP协议号统计)")
484 for proto, cnt in sorted(non_udp_counts.items(), key=lambda x: int(x[0])):
485 report.append(f"- IP Proto={proto}: {cnt}")
486 report.append("")
487 if udp_non_rtp:
488 report.append("## UDP但非RTP的流 (按端口对统计)")
489 for (sp, dp), cnt in sorted(udp_non_rtp.items()):
490 prefix_hex = (examples_udp_non_rtp.get((sp, dp), "") or "")
491 report.append(f"- {sp} -> {dp}: {cnt} 报文,样例前缀(<=16B)={prefix_hex}")
492 report.append("")
493 if rtp_non_pcmu:
494 report.append("## RTP但非PCMU的流 (按 src_port, dst_port, PT 统计)")
495 agg = defaultdict(int)
496 for (sp, dp, ssrc, pt), cnt in rtp_non_pcmu.items():
497 agg[(sp, dp, pt)] += cnt
498 for (sp, dp, pt), total in sorted(agg.items()):
499 report.append(f"- {sp} -> {dp}, PT={pt}: {total} 报文")
500 report.append("")
501 if not (non_ipv4_counts or non_udp_counts or udp_non_rtp or rtp_non_pcmu):
502 report.append("没有发现异常:全部可疑类别均为空(或样本极少)。")
503
504 write_report(report)
505 print(f"\n[+] 排查报告:{REPORT_PATH}")
506
507if __name__ == "__main__":
508 main()提取完后用飞书妙记转录,可以看到里面有两串数字,而且很明显数字都不超过8,一眼八进制(还有114514捏)


用厨子转一下八进制就好了,这里要手动拆,不然他识别不到

刚刚好拿到32位的hex值,尝试解压加密压缩包发现密码对了

老样子妙记转录音频文字,可以看到有些信息,什么廿四,辰时正过三刻,双里湖西岸南山茶铺,看这个通话看着其实就很间谍那味()
但是这里太谜语了,一直没确定具体的时间,只有地点是确定的

直到后面上了hint,确定要去寻找历史事件(其实有一点猜想,但不确定),音频中提到的双里湖,通过简单社工发现了一个叫双鲤湖的地方,位于福建省金门县,通过大量调查,发现那曾发生过金门战役(没打过以前的强网杯,我还以为张纪星有说法)


那么时间就确定了是1949年10月24日(一开始以为是25不对,改24对了),然后时分就是8点45分,对应辰时正三刻(这里其实也考虑过是7点45分,但试过也不对),地点就是双鲤湖西岸南山茶铺
1md5(1949年10月24日8时45分于双鲤湖西岸南山茶铺)legacyOLED
知识点省流
OLED数据通信原理 I2C通信协议
WP
(这题凭什么烂,我感觉好难)
附件是一个sr文件,经常打国外赛的都知道,这是什么逻辑分析仪的文件,可以用sigrok的工具打开,下个pulseview打开,发现里面有蜜汁波形

原本的是一些01编码的波形,直到后面队友给我说可能借鉴了这个文章:https://github.com/bobby-tables2/CTF-Archive/tree/87dcb7af1c64705315d6878a7178accf316fe606/CTFSG%202022/SIGINT/Writeup
我简单看了看,发现太像了,遂继续攻破,首先根据题目的OLED和波形的情况,推断这个OLED显示屏的信号数据,一般他的通信协议是I2C,所以可以用I2C对其进行解码,可以发现它传了不少数据的

我将数据提取了出来(右键可以导出),然后利用参考的文章的exp跑,发现不行,完全对不上,遂深入研究了OLED和I2C通信的原理
https://blog.csdn.net/weixin_63726869/article/details/133132435
https://zhuanlan.zhihu.com/p/14257150571
通过深入学习上面的两个文章,我确定了,当设备进行地址写入后,传入的第一个数据为控制字节,如果是00则代表传入了命令,40则代表发送给GRAM,然后用于画面打印的(大概就这意思)

这整段波形中,前面一长串基本是没用的,主要从后一段开始分析,我们可以看到这里断开传入了四次数据,一开始00控制字节,执行了20命令,参数为01,20代表其进行寻址模式调整,参数为01时寻址模式为垂直寻址,00则为水平寻址,后面会出现多次交替,然后是22命令和21命令,分别对应起始/终止页和列,也就是说从哪里开始画起,再往后就是传入40控制字节,开始显示,往后每段都有类似的情况,需要将他们逐一提取然后在参考文章exp的基础上,通过拷打ai问出了一个升级版的脚本

100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
2[01 21 22 3B 22 03 05]: 00 00 00 00 00 00 C0 00 00 E0 01 00 F8 07 00 FE 1F 00 FF 1F 00 FF 7F 00 FF FF 01 FF FF 07 FF FF 0F FF FF 1F FF FF 7F FF FF FF 3F FE FF 0F FC FF 03 F0 FF E0 E0 FF F8 C3 FF FE 07 FF FF 1F FC FF 3F F8 FF 7F F0 FF 3F F0 FF 1F FC FF 0F FF
3[00 21 2E 53 22 06 06]: 00 00 03 07 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 03 00 00 08 0E 0F 0F 0F 0F 0F 0F 0F 0F 0F 07 03 00 00 00 00
4[01 21 00 32 22 00 01]: 00 00 47 00 7E 00 70 00 06 00 06 00 63 00 33 00 00 00 7F 00 6F 00 10 00 33 00 7F 00 3B 00 75 00 00 00 19 00 6F 00 20 00 19 00 39 00 21 00 10 00 00 00 7F 00 5F 00 66 00 22 00 21 00 65 00 3F 00 00 00 4F 00 7F 00 35 00 02 00 5E 00 0B 00 7E 00 00 00 57 00 7F 00 32 00 00 00 12 80 24 E0 47 F0 00 FC 7F FF DF FF
5[01 21 30 56 22 02 04]: FF 3F FE FF 0F FC FF 03 F0 FF E0 E0 3F F8 C3 1F FE 07 07 FF 1F C1 FF 3F E0 FF 7F F0 FF 3F FC FF 1F FF FF 0F FF FF 83 FF FF E0 FF 3F F0 FF 1F FC FF 07 FE FF 81 FF 7F E0 FF 1F F8 FF 1F FC FF 07 FF FF C1 FF FF C1 FF FF 83 FF 3F 0F FE 1F 1F FC 0F 3F F0 C3 FF C0 E0 FF 03 F8 FF 07 FE FF 1F FF FF FF FF FF FF FF FF FF FF FE FF FF F8 FF FF F0 FF FF E0 FF 7F
6[00 21 30 55 22 00 03]: 00 7F DF 83 C8 D1 A6 BC 80 ED FF B9 60 75 18 3D 00 C7 FF C4 D3 91 84 F6 80 9F EF 8C 92 92 16 1D 00 4B 7F 59 00 19 FC FF FF FF FF FF FF FF 7F 3F 0F 03 C0 E0 F8 FE FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC F0 E0 80 00 00 00 FF FF FF FF 3F 1F 07 C1 E0 F0 FC FF FF FF FF FF FF FF 7F 1F 1F 07 C1 C1 83 0F 1F 3F FF FF FF FF FF FF FF FE F8 F0 3F 0F 03 E0 F8 FE FF FF FF FF FF FF FF FF 3F 1F 07 81 E0 F8 FC FF FF FF FF FE FC F0 C0 03 07 1F FF FF FF FF FF FF
7[01 21 33 59 22 05 07]: FF 0F 00 FF 0F 00 FF 0F 00 FC 0F 00 F8 0F 00 F0 0F 00 F0 0F 00 FC 0F 00 FF 0F 00 FF 0F 00 FF 0F 00 FF 0F 00 FF 0F 00 FF 03 00 FF 00 00 7F 00 00 3F 08 00 0F 0E 00 87 0F 00 C3 0F 00 F0 0F 00 F8 0F 00 FC 0F 00 FF 0F 00 FF 0F 00 FF 0F 00 FF 0F 00 FF 07 00 FF 03 00 FF 00 00 3F 00 00 1F 00 00 0F 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
8[00 21 4F 7F 22 00 04]: 1D 00 4B 7F 59 00 19 4A 5E 00 6B 5F 22 71 50 0A 71 00 7B 6F 18 29 23 39 7B 04 6B 77 78 05 33 49 30 04 5F 6B 45 02 58 01 0B 04 36 7B 08 14 18 66 00 FC F0 E0 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF FE F8 F0 E0 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1F FF FF FF FF FF FF FF FF FE FC F0 60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF FF FF FF 7F 1F 0F 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
9[00 21 24 35 22 00 02]: 02 5E 0B 7E 00 57 7F 32 00 12 24 47 00 7F DF 83 C8 D1 00 00 00 00 00 00 00 00 00 80 E0 F0 FC FF FF FF FF FF 00 00 00 00 80 E0 E0 F8 FE FF FF FF FF FF FF FF 3F 1F
10[01 21 00 7F 22 00 00]: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
1import binascii
2import re
3
4# OLED显示参数
5WIDTH = 128 # 列数
6PAGES = 8 # 页数(每页8个像素高)
7HEIGHT = PAGES * 8 # 总高度64像素
8
9# 创建二维像素数组
10pixels = [['_' for _ in range(WIDTH)] for _ in range(HEIGHT)]
11
12
13def draw_page_addressing_mode(hex_data):
14 """页寻址模式:解析包含10/00指令的数据流"""
15 global pixels
16
17 current_page = 0
18 current_col = 0
19 col_start = 0
20 col_end = 127
21
22 i = 0
23 total_bytes = 0
24 data_count = 0
25
26 while i < len(hex_data):
27 byte_val = int(hex_data[i], 16)
28
29 # 检查是否是列地址低位设置指令 (00H~0FH)
30 if 0x00 <= byte_val <= 0x0F:
31 col_low = byte_val & 0x0F
32 current_col = (current_col & 0xF0) | col_low
33 print(f" 设置列地址低位: {col_low:X}H, 当前列={current_col}")
34 i += 1
35 continue
36
37 # 检查是否是列地址高位设置指令 (10H~1FH)
38 if 0x10 <= byte_val <= 0x1F:
39 col_high = (byte_val & 0x0F) << 4
40 current_col = (current_col & 0x0F) | col_high
41 print(f" 设置列地址高位: {(byte_val & 0x0F):X}H, 当前列={current_col}")
42 i += 1
43 continue
44
45 # 检查是否是页地址设置指令 (B0H~B7H)
46 if 0xB0 <= byte_val <= 0xB7:
47 current_page = byte_val & 0x07
48 print(f" 设置页地址: {current_page}")
49 i += 1
50 continue
51
52 # 否则是数据字节
53 byte_str = f'{byte_val:0>8b}'
54
55 # 绘制该字节(一列中一页的8个像素)
56 if current_col < WIDTH: # 只在有效范围内绘制
57 for bit in range(8):
58 row = current_page * 8 + bit
59 if row < HEIGHT:
60 if byte_str[7 - bit] == '1':
61 pixels[row][current_col] = '#'
62
63 # 列地址自动递增,限制在0-127范围内
64 current_col += 1
65 if current_col > 127: # 超过127就回到起始列
66 current_col = col_start
67 # 注意:页地址不会自动改变
68
69 data_count += 1
70 i += 1
71
72 return data_count
73
74
75def draw_horizontal_mode(hex_data, start_col, end_col, start_page, end_page):
76 """水平地址模式:先写完一行的所有列,再到下一行"""
77 global pixels
78
79 bin_data = []
80 for ele in hex_data:
81 bin_data.append(f'{int(ele, 16):0>8b}')
82
83 current_col = start_col
84 current_page = start_page
85
86 for byte_str in bin_data:
87 if current_page > end_page:
88 break
89
90 for bit in range(8):
91 row = current_page * 8 + bit
92 if row < HEIGHT and current_col < WIDTH:
93 if byte_str[7 - bit] == '1':
94 pixels[row][current_col] = '#'
95
96 current_col += 1
97 if current_col > end_col:
98 current_col = start_col
99 current_page += 1
100 if current_page > end_page:
101 break
102
103 return len(bin_data)
104
105
106def draw_vertical_mode(hex_data, start_col, end_col, start_page, end_page):
107 """垂直地址模式:先写完一列的所有页,再到下一列"""
108 global pixels
109
110 bin_data = []
111 for ele in hex_data:
112 bin_data.append(f'{int(ele, 16):0>8b}')
113
114 current_col = start_col
115 current_page = start_page
116
117 for byte_str in bin_data:
118 if current_col > end_col:
119 break
120
121 for bit in range(8):
122 row = current_page * 8 + bit
123 if row < HEIGHT and current_col < WIDTH:
124 if byte_str[7 - bit] == '1':
125 pixels[row][current_col] = '#'
126
127 current_page += 1
128 if current_page > end_page:
129 current_page = start_page
130 current_col += 1
131 if current_col > end_col:
132 break
133
134 return len(bin_data)
135
136
137# 读取文件
138with open("pixel.txt", "r") as f:
139 lines = f.readlines()
140
141# 第一行:页寻址模式(默认)
142print("=" * 60)
143print("第一阶段:页寻址模式")
144print("=" * 60)
145
146if lines:
147 first_line = lines[0].strip()
148 if first_line:
149 hex_data = first_line.split()
150 print(f"处理页寻址模式数据,共 {len(hex_data)} 字节")
151 bytes_used = draw_page_addressing_mode(hex_data)
152 print(f"绘制了 {bytes_used} 个数据字节\n")
153
154# 后续行:动态模式(带指令)
155print("=" * 60)
156print("第二阶段:动态地址模式")
157print("=" * 60)
158
159line_num = 1
160for line in lines[1:]:
161 line_num += 1
162 line = line.strip()
163 if not line:
164 continue
165
166 # 解析格式: [yy 21 xx xx 22 xx xx]:
167 match = re.match(
168 r'\[([0-9A-Fa-f]{2})\s+21\s+([0-9A-Fa-f]{2})\s+([0-9A-Fa-f]{2})\s+22\s+([0-9A-Fa-f]{2})\s+([0-9A-Fa-f]{2})\][::]\s*(.*)',
169 line)
170
171 if not match:
172 print(f"第 {line_num} 行格式不正确,跳过: {line[:50]}...")
173 continue
174
175 mode_byte = match.group(1)
176 start_col = int(match.group(2), 16)
177 end_col = int(match.group(3), 16)
178 start_page = int(match.group(4), 16)
179 end_page = int(match.group(5), 16)
180 hex_data_str = match.group(6).strip()
181
182 hex_data = hex_data_str.split()
183
184 if mode_byte == "00":
185 mode = "水平"
186 bytes_used = draw_horizontal_mode(hex_data, start_col, end_col, start_page, end_page)
187 elif mode_byte == "01":
188 mode = "垂直"
189 bytes_used = draw_vertical_mode(hex_data, start_col, end_col, start_page, end_page)
190 else:
191 print(f"第 {line_num} 行未知模式: {mode_byte},跳过")
192 continue
193
194 print(
195 f"第 {line_num} 行 | {mode}模式 | 列[{start_col}:{end_col}] 页[{start_page}:{end_page}] | 数据: {len(hex_data)} 字节")
196
197# 输出图像
198print("\n" + "=" * 60)
199print("最终OLED图像输出:")
200print("=" * 60 + "\n")
201print(f"图像尺寸: {WIDTH}列 x {HEIGHT}行")
202print(f"实际像素数组: {len(pixels)}行 x {len(pixels[0])}列\n")
203
204for row in pixels:
205 for col_idx in range(min(WIDTH, len(row))): # 只输出WIDTH列
206 print(row[col_idx], end=' ')
207 print()
208
209# 保存为文本文件
210with open("oled_output.txt", "w") as f:
211 for row in pixels:
212 for col_idx in range(min(WIDTH, len(row))): # 只输出WIDTH列
213 f.write(row[col_idx] + ' ')
214 f.write('\n')
215
216print("\n图像已保存到 oled_output.txt")最后终于成功了,画出了一个强网杯的logo,上面乱乱的看不出是什么,以为是我脚本错了,后面发现是将_#转01然后解二进制就可以了


Secured Personal Vault(唯一血)
知识点省流
内存取证但又是非预期(进程内存转储查看窗口画面)
WP
坚不可摧这块,而且又非了(
依旧内存取证,但是把早上的非预期修了,爆搜搜不到,分析一下发现桌面有个apersonalvault.exe(早上的取证做过也能知道)

提出来丢给逆向手逆向了,他们说要找密文,exe里自带了解密什么的,但是生成的mailslot文件提取不出来,拿不到,也没什么多的头绪了,没招了,也没人做出来,感觉到此为止了
……
但是真没招吗
并非
我们miscer有我们miscer自己的做法
让我们将时间倒退到10月12日,在今年的羊城杯赛事中,我负责了一道内存取证的题目(你也是旮旯给木大师?),其中的第四问,我的预期解是需要通过提取dwm.exe的进程内存转储来实现对qq程序窗口的取证(不提dwm也行,提qq进程也可以的应该)
于是乎,19号凌晨,当我学了一晚上OLED原理并把OLED做出来后,估摸是一晚上没怎么睡脑子不太正常抽风了,寻思看看他的窗口管理器内存转储,也就是dwm.exe的内存转储,遂提取了一波,用马处的lovelymem luxe提取内存转储,然后用lovelypixelweaver进行爆看
由于不确定分辨率,所以我手动爆了一下,最后发现是1279这个宽度可以正常看到窗口信息,然后我就滑了滑偏移,没想到啊,真的有出题人在加密flag时的窗口画面,但是有点花,只能明显的看到flag头一部分,后面的部分都混在一起了,不过不影响我通过强大的视力再加上一点点小小的英语语法功底和毅力,将他盯出来了(需要不断切换一下偏移量,图像格式和位深度)


flag最后盯帧出来为
1flag{Making_challenge_is_hard_manage_a_secure_vault_is_more_difficult}至此该系列顺利完成,最终以双非(两次非预期)的形式结束了这个内存取证
顺带一提,直至该WP撰写完成(指的是我博客这版,这版跟比赛交上去的不一样),某Re手群内已经出现了该题的正确预期解法,十分佩服Re大手子的实力,这边贴一下预期解的帖子给看到这的师傅去学习一下( https://blog.moshui.eu.org/2025/10/22/qwb-SecuredPersonalVault-wp/ )
胡言乱语
(这只是一个什么都不懂的萌新的胡言乱语罢了)
诚然我十分理解现在misc中夹杂着各个方向(甚至超出五大基础方向)的内容,作为一名misc手也不应该围绕着自己那一亩三分地,永远困在经典misc(编码隐写等)的舒适圈中。但就现今而言,misc掺杂其他方向的内容变得越来越硬核了,以Secured Personal Vault为例,预期解甚至是一位Re师傅解出的,这是不是意味着misc手需要完成这道题目还得具备十分硬核的逆向能力;但反过来想既然能具备硬核的逆向能力,我为什么不去做re手呢……
有的时候我们会调侃,misc手的终点其实是全栈手,现在看来兴许并非是调侃,当有一天misc的题目都结合了其他方向的硬核知识后,我们不就真的全栈了,亦或者misc手就不复存在了(因为交给其他方向的师傅来做就好了,人人都可以是misc手)
misc手的出路在哪,我也很想知道……但也许,我们都需要重新审视一下misc这个方向,灵活变通,适应环境是我们misc手的特点,正因如此,拥抱变化,突破边界才应该是我们的方向。希望每一位misc手都能找到属于自己的突破口,共勉。
迟逾时序已久,待至吉日重逢。
Comments will be available soon.