碎碎念

待到秋光澄澈,菊吐金英相候, 迟逾时序已久,待至吉日重逢。

……

吉时已到

这次比赛对我来说可以说是强运附体了,在不懈努力下,最终也是砍下了两个Misc一血,该做的misc也都做了(除了最后黑珍珠实在是没机会看了要去给新生培训了,不然我直接就是一个以我残躯化烈火)

这次Misc方向的题目我感觉难度上还不错,有的题目出的挺好的,但也存在个别题有点太脑洞了(没有针对出题人的意ORZ),我一直以来对Misc的看法都是——不断进化,不断学习,干中学学中干,每一次比赛都是一次学习的机会(因为每次比赛都有新的东西,这就是我们Misc),在这次强网杯中,我也收获了很多,学到了很多新的东西,也打的很爽。

……(原本这里还想说点什么,不过想了想还是算了,等年度总结再发吧)

“开始时捱一些苦,栽种绝处的花”,就目前来看,我的所知所学还是太过浅薄了,以此作为转折,继续努力吧

与君共勉,最高峰见 —— 2hi5hu

新编: “最后一舞”



Personal Vault(一血)

知识点省流

内存取证但非预期

WP

一血,没想太多,直接拿lovelymem luxe搜了一波flag,直接非了

ff5dbc24-c1bd-4e46-8dc8-2d369eb50b44


The_Interrogation_Room

知识点省流

海明距离什么的 但是ai梭哈了

WP

复盘来看,这题的解题过程其实非常的搞笑()

附件叽里咕噜说的啥听不懂,大概就是要提问17轮,其中有两轮是说谎的,根据信息推出正确的数据,要连续猜对25轮

这题目的解题数涨的很快,所以我们都大概能猜到是ai能梭的,但是却一直没梭出来

但毕竟那么多解,而且这类题目ai梭哈的概率很大,所以我也没太放心上,打算歇一会再做

结果就有了下面这一幕,当时题解数已经多到可怕了,一怒之下问ai梭了

image-20251022005251013

fec4cea1-121b-4c3b-8d80-01c87d770baf

  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解题的时候让他去找巧妙的解法很容易出(这也是一种提问的技巧吧,之前有些密码题也能这么问出来)

b2687bc5-5e3e-456b-a62b-d5424c1233af


谍影重重 6.0

知识点省流

rtp流量数据分析 音频转译 历史猜谜

WP

流量分析+妙妙历史猜谜(靠北)

打开题目是一个700多m的流量包,要吓尿了

进去看到全是udp流量,前期绕了很多圈子,特别是看到了个manolito协议,后面反应过来是ws自己误识别的

9b948e76-bb48-43ee-9ada-e4086ee850f3

实际上这个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捏)

239c152d-3100-4827-bb60-a6a5d9ba5077

f5c9f473-6580-4841-a706-674723325ba4

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

990e6c6c-ecd1-407e-bd69-7e74871593dc

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

8bb18a7d-a28d-4ecb-9c32-763648cd6481

老样子妙记转录音频文字,可以看到有些信息,什么廿四,辰时正过三刻,双里湖西岸南山茶铺,看这个通话看着其实就很间谍那味()

但是这里太谜语了,一直没确定具体的时间,只有地点是确定的

c4c86abc-8500-4f04-993b-57fe599404d9

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

89dc8a61-62ef-4eed-a836-87f47b58f5c0

ae0bed17-4858-4b47-a375-dcb7ecfbf45f

那么时间就确定了是1949年10月24日(一开始以为是25不对,改24对了),然后时分就是8点45分,对应辰时正三刻(这里其实也考虑过是7点45分,但试过也不对),地点就是双鲤湖西岸南山茶铺

1md5(1949年10月24日8时45分于双鲤湖西岸南山茶铺)

legacyOLED

知识点省流

OLED数据通信原理 I2C通信协议

WP

(这题凭什么烂,我感觉好难)

附件是一个sr文件,经常打国外赛的都知道,这是什么逻辑分析仪的文件,可以用sigrok的工具打开,下个pulseview打开,发现里面有蜜汁波形

ae557bc9-d268-4f96-9a45-6030325bc066

原本的是一些01编码的波形,直到后面队友给我说可能借鉴了这个文章:https://github.com/bobby-tables2/CTF-Archive/tree/87dcb7af1c64705315d6878a7178accf316fe606/CTFSG%202022/SIGINT/Writeup

我简单看了看,发现太像了,遂继续攻破,首先根据题目的OLED和波形的情况,推断这个OLED显示屏的信号数据,一般他的通信协议是I2C,所以可以用I2C对其进行解码,可以发现它传了不少数据的

b960607e-93c6-4485-a4af-4c91fcc3d006

我将数据提取了出来(右键可以导出),然后利用参考的文章的exp跑,发现不行,完全对不上,遂深入研究了OLED和I2C通信的原理

https://blog.csdn.net/weixin_63726869/article/details/133132435

https://zhuanlan.zhihu.com/p/14257150571

通过深入学习上面的两个文章,我确定了,当设备进行地址写入后,传入的第一个数据为控制字节,如果是00则代表传入了命令,40则代表发送给GRAM,然后用于画面打印的(大概就这意思)

5f12a867-22d2-4f56-aa39-405163f7ad0c

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

032f44f3-8340-4b19-b623-ab75f2daecad

 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

da158097-11c7-487a-9b78-de7f54d3c691

  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然后解二进制就可以了

6048d85c-f280-4377-b276-efc1483c6be3

89b46754-810d-4fd1-bfd0-a35238047bf2


Secured Personal Vault(唯一血)

知识点省流

内存取证但又是非预期(进程内存转储查看窗口画面)

WP

坚不可摧这块,而且又非了(

依旧内存取证,但是把早上的非预期修了,爆搜搜不到,分析一下发现桌面有个apersonalvault.exe(早上的取证做过也能知道)

bf3b20f6-571d-415f-a7b2-47871ec07a84

提出来丢给逆向手逆向了,他们说要找密文,exe里自带了解密什么的,但是生成的mailslot文件提取不出来,拿不到,也没什么多的头绪了,没招了,也没人做出来,感觉到此为止了

……

但是真没招吗

并非

我们miscer有我们miscer自己的做法

让我们将时间倒退到10月12日,在今年的羊城杯赛事中,我负责了一道内存取证的题目(你也是旮旯给木大师?),其中的第四问,我的预期解是需要通过提取dwm.exe的进程内存转储来实现对qq程序窗口的取证(不提dwm也行,提qq进程也可以的应该)

于是乎,19号凌晨,当我学了一晚上OLED原理并把OLED做出来后,估摸是一晚上没怎么睡脑子不太正常抽风了,寻思看看他的窗口管理器内存转储,也就是dwm.exe的内存转储,遂提取了一波,用马处的lovelymem luxe提取内存转储,然后用lovelypixelweaver进行爆看

由于不确定分辨率,所以我手动爆了一下,最后发现是1279这个宽度可以正常看到窗口信息,然后我就滑了滑偏移,没想到啊,真的有出题人在加密flag时的窗口画面,但是有点花,只能明显的看到flag头一部分,后面的部分都混在一起了,不过不影响我通过强大的视力再加上一点点小小的英语语法功底和毅力,将他盯出来了(需要不断切换一下偏移量,图像格式和位深度)e27b05e4-0639-4aec-b05d-2e221a78fb2b

3ce1999d-cdfa-45ff-9fb4-08759f7a2f62

aa3f8d63-f5ce-4446-ac27-7770cda67836

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手都能找到属于自己的突破口,共勉。 迟逾时序已久,待至吉日重逢。