【2026】ISCC 长虹守卫
长虹守卫
题目类型:杂项
拿到这道题的时候我第一反应是:飞行日志 + pcap,这两个东西放在一起能有什么关系?
带着这个问题开始看。
先摸清楚题目在说什么
LX517.txt打开,73 行,每行是一条飞行记录。飞机叫 AURORA-ER,航班号 LX-517,从太平洋中部向东飞。我注意到经度在 idx=0026 附近发生了一次跳变:
0025: lon=179.1578° 0026: lon=179.2429° 0027: lon=-179.9142°经度从正数跳成负数,这是穿越了国际日期变更线(IDL,经度 180°)。
然后我注意到一件奇怪的事——idx=0026 和 idx=0027 的时间
0026: 2026-01-19 12:27:33 0027: 2026-01-19 11:28:06时间倒退了将近一个小时,但日期没变,还是 1月19日。
正常来说,飞机向东穿越 IDL,本地时间应该减一天。时钟往回拨了,但日期没跟着变——这是个 bug。idx=0026 的 detail 字段直接写明了:
DATE_CORR=-1DAY;bug=FDR-227(date_not_applied)应该减一天,但没减。这个 bug 是整道题的核心。
文件末尾藏了什么
cat LX517.txt的时候,最后一行之后有一段乱码。放大看:
tail -c 600 LX517.txt | xxd | head -20\x00之后全是A-Za-z0-9+/=,末尾有=补位,是 Base64。
Tail -c 600 LX517.txt | tr -d ‘\0\r\n\f\t’ | grep -a -oP ‘[A-Za-z0-9+/=]{80,}’ | base64 -d 2>/dev/null解出来:
这是加密的说明书,藏在文件末尾。
读完之后整个题目的逻辑就清晰了:密钥由三个飞行事件的 UTC 时间戳拼成,用这个密钥生成 SHA256 流密码,去解密 pcap 里的密文。
三个事件,一个陷阱
根据 NOTE 找三类事件:
grep -E "EVT_A|EVT_B|EVT_C" LX517.txtEVT_A 和 EVT_B 各只有一条,没有歧义。EVT_C 出现了三次:
idx=0033 marker=X9 desc=noise idx=0049 marker=A2 desc=timing_inconsistency_suspected idx=0056 marker=Z1 desc=decoynoise和decoy是题目故意放的干扰,NOTE 里说的是ANOMALY_MARKER_A2,所以目标是 idx=0049。
但我在写脚本的时候没有直接排除 X9,因为noise这个词本身也可能是迷惑性的——万一 A2 解不出来,X9 也要试。事实证明这个保留是对的,脚本把 X9 和 A2 都纳入候选,最终在第 167 次尝试时命中。
三个关键事件:
事件 | idx | 本地时间 | 经度 |
EVT_A(TCP SYN) | 0041 | 2026-01-19 11:54:12 | -173.7042° |
EVT_B(IDL 跨越) | 0026 | 2026-01-19 12:27:33 | +179.2429° |
EVT_C(标记 A2) | 0049 | 2026-01-19 11:56:21 | -173.3478° |
IDL bug 对时间戳的影响
这是最需要想清楚的地方。
hint 里的 IDRULE 说:
IDRULE:idx26_applied_to_idx>=27_only意思是 idx=0026 记录的日期修正(-1天)只影响 idx>=27 的行,idx=0026 本身不受影响。
所以:
- EVT_B(idx=0026):不需要日期修正,直接用本地时间算 UTC
- EVT_A(idx=0041)和 EVT_C(idx=0049):idx > 26,需要考虑日期修正
"考虑"的意思是:由于 bug 的存在,这些行的本地时间日期可能是错的(多了一天),也可能是对的(如果不应用修正)。两种情况都要试。
时间转换公式:
UTC = 本地时间 + 日期修正(0天或-1天)- 时区偏移时区偏移根据经度估算,经度 / 15取整,但靠近 IDL 的区域时区不规则,要多试几个值:
经度 -173.7° → 可能是 -11 或 -12 经度 +179.2° → 可能是 +12、+13 或 +14每个事件生成一组候选时间戳,三组取笛卡尔积,逐一尝试解密。
pcap 里的密文
strings -n 4 LX517.pcap | grep -a -i “CIPH”76 个十六进制字符,38 字节。ISCC{是 5 字节,32 位 hex 是 32 字节,}是 1 字节,合计 38 字节——密文长度和 flag 格式完全吻合,说明密文就是直接加密的 flag。
用 tshark 也可以看到同样的内容:
tshark -r LX517.pcap -T fields -e data.data 2>/dev/null解密原理
SHA256 CTR 流密码,每个块用计数器区分:
密钥流[0] = SHA256(keymat_bytes + \x00\x00\x00\x00) 密钥流[1] = SHA256(keymat_bytes + \x00\x00\x00\x01) ... 取前 38 字节 明文 = 密文 XOR 密钥流XOR 的性质:A XOR K XOR K = A,加解密用同一套代码。
运行脚本:
hint: KEYMAT:AURORA-ER|flight=LX-517|syn=<EVT_A_UTC>|idl=<EVT_B_UTC>|a2=<EVT_C_UTC> ALGO:SHA256|XOR|CTR REASM:SEQ_ORDER|frag=3 IDRULE:idx26_applied_to_idx>=27_only UTC:local+date_corr-timezone NOTE:EVT_A=TCP_SYN_SENT,EVT_B=IDL_CROSS,EVT_C=ANOMALY_MARKER_A2 ciphertext: 6bcc44455892278c6577691d39eef5806754e33feecb956e9546fe8ca331966d2e0683c054c6 keymat: AURORA-ER|flight=LX-517|syn=1768863252|idl=1768775253|a2=1768862526 flag: ISCC{df155a9a410be4e7b14a6a191bb5eddc} tried: 167验证成功的时间戳
python3 -c " from datetime import datetime, timezone pairs = [('EVT_A', 1768863252), ('EVT_B', 1768775253), ('EVT_C', 1768862526)] for name, ts in pairs: print(name, datetime.fromtimestamp(ts, tz=timezone.utc)) " EVT_A 2026-01-19 23:54:12+00:00 EVT_B 2026-01-19 00:27:33+00:00 EVT_C 2026-01-18 22:56:06+00:00反推:
- EVT_A:本地11:54:12,时区-12→ UTC23:54:12,日期 1月19日(未应用修正)✓
- EVT_B:本地12:27:33,时区+12→ UTC00:27:33,日期 1月19日(idx=26 不修正)✓
- EVT_C:UTC22:56:06,反推本地11:56:06,日期2026-01-18(应用了 -1天修正,时区-11)
EVT_C 的本地时间反推是11:56:06,而 idx=0049 记录的是11:56:21,差了 15 秒。这说明暴力搜索命中的候选值并不是从 idx=0049 精确推算出来的,而是候选集足够宽,覆盖了正确答案。这也是为什么要把 X9 也纳入候选——扩大搜索空间,容错。
回头看这道题的设计
整道题围绕一个现实中真实存在的问题:飞行数据记录仪(FDR)在跨越 IDL 时的日期处理 bug。
题目把这个 bug 做成了密钥的一部分——因为 bug 的存在,时间戳有歧义,所以不能精确计算,只能枚举。这个设计让暴力搜索成为必要,而不是偷懒。
三个 EVT_C 的设置也很有意思:noise、A2、decoy,中间夹着真正的目标,两侧都是干扰。如果只看名字就排除,可能会漏掉 X9 这个候选,导致搜索空间不够宽,最终找不到答案。
Flag:
EXP:
import base64 import csv import hashlib import io import re import struct from datetime import datetime, timedelta, timezone from itertools import product from pathlib import Path import dpkt WORK_DIR = Path(__file__).resolve().parent LOG_FILE = WORK_DIR / "LX517.txt" CAP_FILE = WORK_DIR / "LX517.pcap" FLAG_RE = re.compile(rb"^ISCC\{[0-9a-fA-F]{32}\}$") # ── 1. 从日志末尾提取并解码 Base64 hint ─────────────────────────────────── def load_hint(path: Path) -> str: raw = path.read_text(encoding="utf-8", errors="ignore") for token in re.findall(r"[A-Za-z0-9+/=]{80,}", raw): try: s = base64.b64decode(token).decode() if "KEYMAT:" in s and "ALGO:" in s: return s except Exception: pass raise ValueError("hint not found") # ── 2. 用 csv 模块解析飞行日志 ──────────────────────────────────────────── def load_log(path: Path) -> list[dict]: lines = [ ln for ln in path.read_text(encoding="utf-8", errors="ignore").splitlines() if re.match(r"^\d{4},", ln) ] reader = csv.reader(io.StringIO("\n".join(lines)), skipinitialspace=True) records = [] for row in reader: if len(row) < 10: continue # csv 把 detail 字段(含逗号)也正确处理了,但我们用 maxsplit=9 的原始行更安全 # 这里直接用 row,detail 是 row[9] try: records.append({ "seq": int(row[0]), "dt": datetime.strptime(row[1].strip(), "%Y-%m-%d %H:%M:%S"), "lat": float(row[2]), "lon": float(row[3]), "evt": row[8].strip(), "note": row[9].strip(), }) except (ValueError, IndexError): continue return records # ── 3. 用 dpkt 解析 pcap,提取 TCP 载荷中的密文 ────────────────────────── def extract_ct(path: Path) -> bytes: payload_buf = bytearray() with open(path, "rb") as f: for _, pkt in dpkt.pcap.Reader(f): try: eth = dpkt.ethernet.Ethernet(pkt) if not isinstance(eth.data, dpkt.ip.IP): continue ip = eth.data if not isinstance(ip.data, dpkt.tcp.TCP): continue tcp = ip.data if tcp.data: payload_buf += tcp.data except Exception: continue hit = re.search(rb"CIPH:([0-9a-fA-F]+)", bytes(payload_buf)) if not hit: hit = re.search(rb"CIPH:([0-9a-fA-F]+)", path.read_bytes()) if not hit: raise ValueError("ciphertext not found") return bytes.fromhex(hit.group(1).decode()) # ── 4. 时区候选:经度附近所有合理偏移 ──────────────────────────────────── def tz_candidates(lon: float) -> list[int]: base = round(lon / 15) extra = [] if lon > 150: extra = [12, 13, 14] elif lon < -150: extra = [-10, -11, -12] return sorted({base, *extra}) # ── 5. 生成候选 UTC epoch,用生成器避免预先展开 ─────────────────────────── def epoch_gen(rec: dict): for tz, dc in product(tz_candidates(rec["lon"]), (0, -1)): t = rec["dt"] + timedelta(days=dc) - timedelta(hours=tz) yield int(t.replace(tzinfo=timezone.utc).timestamp()) # ── 6. SHA256 流密码,用 bytearray 原地 XOR ─────────────────────────────── def stream_decrypt(ct: bytes, key: str) -> bytes: key_b = key.encode() out = bytearray(len(ct)) block_idx = 0 pos = 0 while pos < len(ct): ks_block = hashlib.sha256(key_b + struct.pack(">I", block_idx)).digest() for byte in ks_block: if pos >= len(ct): break out[pos] = ct[pos] ^ byte pos += 1 block_idx += 1 return bytes(out) # ── 7. 主逻辑 ───────────────────────────────────────────────────────────── def main(): hint = load_hint(LOG_FILE) log = load_log(LOG_FILE) ct = extract_ct(CAP_FILE) tmpl = re.search(r"^KEYMAT:(.+)$", hint, re.MULTILINE).group(1).strip() syn_pool = [e for e in log if e["evt"] == "EVT_A"] idl_pool = [e for e in log if e["evt"] == "EVT_B"] a2_pool = [e for e in log if e["evt"] == "EVT_C" and ("marker=A2" in e["note"] or "marker=X9" in e["note"])] count = 0 for ea, eb, ec in product(syn_pool, idl_pool, a2_pool): seen = set() for syn, idl, a2 in product( set(epoch_gen(ea)), set(epoch_gen(eb)), set(epoch_gen(ec)), ): key = (syn, idl, a2) if key in seen: continue seen.add(key) km = (tmpl .replace("<EVT_A_UTC>", str(syn)) .replace("<EVT_B_UTC>", str(idl)) .replace("<EVT_C_UTC>", str(a2))) count += 1 plain = stream_decrypt(ct, km) if FLAG_RE.match(plain): print("hint:") print(hint) print("ciphertext:", ct.hex()) print("keymat:", km) print("flag:", plain.decode()) print("tried:", count) return raise RuntimeError(f"not solved after {count} attempts") if __name__ == "__main__": main()