欢迎光临
我们一直在努力

2026年第十届御网杯——数据安全赛道WriteUp

数据安全

1、CovertChannel

直接解压并检查抓包内容,重点看 10.10.20.33 发出的 ICMP payload、DNS 查询名/响应等可疑字段,再尝试重组外泄数据。

还原过程要点:

1. 解压附件后得到 suspicious_traffic.pcapng,实际为 Raw IPv4 pcap。 2. 发现内网主机 10.10.20.33 主要存在两类异常流量:     大量 ICMP Echo Request 发往 45.76.188.23     大量 DNS 查询发往 10.10.20.1 3. DNS 中存在可疑查询:     secret.exfil-cdn.com

其 TXT 记录内容为 Base64:TW9yZVNlY3VyZUFlczEyOA==

解码得到 AES 密钥:MoreSecureAes128

在 ICMP 发往 45.76.188.23 的报文中,IP TTL 字段异常地只在 0/1 之间变化。按 ICMP 序号排序后,将 TTL 当作二进制位按 8 位重组,得到密文:

60226d3cd3cf345df0baa3c8dec0d9d225b3ff91634498db4f5a7d4452dd1a2c208d4efc9da73eb8884fb69c3b1202c5

用 AES-ECB 解密,密钥为 DNS TXT 中的 MoreSecureAes128,得到最终明文:

flag{5f356e09a565c656c6d8ae31f7973452}

2、exam_system

按《安全审计规范.md》校验后:

幽灵账户数:30

排除幽灵账户后,违规记录字符串为:

14728-3,24311-1,30736-1,38039-2,40118-2,41610-1,47445-3,60354-2,76513-1

其 MD5 为:15cb6dceb13da3077c6df3f86d219e9d

即最终 flag:flag{15cb6dceb13da3077c6df3f86d219e9d}

3、ShadowMete

附件包含access.log(Apache 标准访问日志)、error.log(开启mod_dumpio,记录请求头、POST 体、响应内容,核心数据来源)

① 找外带接口是突破口

不要被登录、export.php 导出迷惑,真正数据外带在 /collect/report.php,导出只是源数据,report 才是分片藏数据。

关键:过滤 error.log 里 report.php,锁定任务号 SM-20260507-17

② 分片处理重中之重(最容易错)

  • 按seq 序号(00001~00070)升序
  • 提取 POST 参数note内 Base64、去重分片、全拼接
  • Base64 解码→加密 zip(日志有 SHA256 / 大小用来校验拼接是否出错)
  • ③ ZIP 密码不在 csv,在 debug.php

    密码来源:/admin/debug.php POST 数据 → URL 解码 = 压缩包密码

    易错坑:普通 zip 密码、zip 伪加密都不是,是WinZip AES 加密,不能用普通解压软件直接开。

    ④ 最后取 flag 关键:多条件联合筛选

    不能乱搜 csv,必须同时满足 3 个筛选条件(来自 manifest.json): app=portal && metric_name=secure.checkpoint && user_id=ADMIN_OPS 匹配唯一一行,该行trace_tag=flag。

    flag{c1b50d6lac8b81fac5ab3ea499056c0a}

    Exp:
    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """Recover ShadowMeter exfiltrated bundle from Apache dumpio logs and decrypt AES-ZIP."""
    import base64, glob, hashlib, hmac, os, re, struct, sys, urllib.parse, zlib, csv, io
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    from cryptography.hazmat.backends import default_backend

    JOB_ID = "SM-20260507-17"
    ZIP_PASSWORD = f"{JOB_ID}:meter:report"
    EXPECTED_SHA256 = "4019ef293eeb8af8a0d5c0e66c0bdb3cbcd17067839f42c86beb501a83d6c59b"

    def unescape_dumpio(s: str) -> bytes:
    try:
    return s.rstrip("\\n").encode("latin1").decode("unicode_escape").encode("latin1")
    except Exception:
    return s.rstrip("\\n").encode("latin1", "ignore")

    def recover_zip_from_error_log(error_log: str) -> bytes:
    pat = re.compile(r"mod_dumpio\\.c\\(103\\): \\[client ([^\\]]+)\\] mod_dumpio: dumpio_(in|out) \\([^)]+\\): (.*)$")
    conns = {}
    with open(error_log, encoding="latin1") as f:
    for line in f:
    m = pat.search(line)
    if not m:
    continue
    client, direction, payload = m.groups()
    port = client.rsplit(":", 1)[-1]
    conns.setdefault(port, {"in": b"", "out": b""})[direction] += unescape_dumpio(payload)

    chunks, end_note = {}, ""
    for conn in conns.values():
    request = conn["in"]
    if b"POST /collect/report.php" not in request or b"\\r\\n\\r\\n" not in request:
    continue
    body = request.split(b"\\r\\n\\r\\n", 1)[1].decode("latin1")
    params = urllib.parse.parse_qs(body, keep_blank_values=True)
    if params.get("job", [""])[0] != JOB_ID:
    continue
    seq = params.get("seq", [""])[0]
    note = params.get("note", [""])[0]
    if seq == "END":
    end_note = note
    elif seq.isdigit():
    chunks[int(seq)] = note

    if not chunks:
    raise RuntimeError("no exfiltrated chunks found")
    missing = [i for i in range(1, max(chunks) + 1) if i not in chunks]
    if missing:
    raise RuntimeError(f"missing chunk(s): {missing}")

    b64 = "".join(chunks[i] for i in range(1, max(chunks) + 1))
    raw = base64.b64decode(b64)
    got = hashlib.sha256(raw).hexdigest()
    if got != EXPECTED_SHA256:
    raise RuntimeError(f"sha256 mismatch: {got}")
    print(f"Recovered {len(raw)} bytes from {len(chunks)} chunks; {end_note}")
    return raw

    def aes_ctr_decrypt(key: bytes, ciphertext: bytes) -> bytes:
    enc = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()).encryptor()
    out = bytearray()
    for counter, off in enumerate(range(0, len(ciphertext), 16), 1):
    keystream = enc.update(counter.to_bytes(16, "little"))
    out.extend(a ^ b for a, b in zip(ciphertext[off:off + 16], keystream))
    return bytes(out)

    def extract_winzip_aes(zip_bytes: bytes, password: str, out_dir: str) -> dict:
    os.makedirs(out_dir, exist_ok=True)
    pos, files = 0, {}
    while zip_bytes[pos:pos + 4] == b"PK\\x03\\x04":
    _, _, method, _, _, _, csize, _, nlen, xlen = struct.unpack_from("<HHHHHIIIHH", zip_bytes, pos + 4)
    name = zip_bytes[pos + 30:pos + 30 + nlen].decode()
    extra = zip_bytes[pos + 30 + nlen:pos + 30 + nlen + xlen]
    actual_method, strength = method, None
    p = 0
    while p + 4 <= len(extra):
    header_id, size = struct.unpack_from("<HH", extra, p)
    val = extra[p + 4:p + 4 + size]
    if header_id == 0x9901: # WinZip AES extra field
    strength = val[4]
    actual_method = struct.unpack_from("<H", val, 5)[0]
    p += 4 + size

    start = pos + 30 + nlen + xlen
    payload = zip_bytes[start:start + csize]
    if strength is None:
    raise RuntimeError(f"{name}: not WinZip AES")

    salt_len = {1: 8, 2: 12, 3: 16}[strength]
    key_len = {1: 16, 2: 24, 3: 32}[strength]
    salt, verifier = payload[:salt_len], payload[salt_len:salt_len + 2]
    ciphertext, auth = payload[salt_len + 2:-10], payload[-10:]
    keymat = hashlib.pbkdf2_hmac("sha1", password.encode(), salt, 1000, 2 * key_len + 2)
    enc_key, mac_key, pwd_verifier = keymat[:key_len], keymat[key_len:2 * key_len], keymat[2 * key_len:]
    if pwd_verifier != verifier:
    raise RuntimeError(f"{name}: bad password")
    if hmac.new(mac_key, ciphertext, hashlib.sha1).digest()[:10] != auth:
    raise RuntimeError(f"{name}: HMAC check failed")

    plain = aes_ctr_decrypt(enc_key, ciphertext)
    if actual_method == 8:
    plain = zlib.decompress(plain, -15)
    elif actual_method != 0:
    raise RuntimeError(f"{name}: unsupported method {actual_method}")
    files[name] = plain
    with open(os.path.join(out_dir, name), "wb") as f:
    f.write(plain)
    pos = start + csize
    return files

    def main():
    if len(sys.argv) < 2:
    print(f"Usage: {sys.argv[0]} /path/to/error.log [out_dir]", file=sys.stderr)
    sys.exit(2)
    out_dir = sys.argv[2] if len(sys.argv) > 2 else "recovered_files"
    zip_bytes = recover_zip_from_error_log(sys.argv[1])
    files = extract_winzip_aes(zip_bytes, ZIP_PASSWORD, out_dir)
    rows = csv.DictReader(io.StringIO(files["export.csv"].decode()))
    for row in rows:
    if row["app"] == "portal" and row["metric_name"] == "secure.checkpoint" and row["user_id"] == "ADMIN_OPS":
    print("flag{" + row["trace_tag"].removeprefix("flag{").removesuffix("}") + "}" if not row["trace_tag"].startswith("flag{") else row["trace_tag"])
    return
    m = re.search(rb"flag\\{[^}]+\\}", files["export.csv"])
    if m:
    print(m.group(0).decode())

    if __name__ == "__main__":
    main()

    #Recovered 10601 bytes from 70 chunks; archive- size=10601; #sha256=4019ef293eebsaf8a0d5coe66c0bdb3cbcd17067839f42c86beb501a83d6c59b
    #flag{c1b50d6lac8b81fac5ab3ea499056c0a}

    4、MaskTrace

    按《脱敏规范.md》生成 UTF-8 编码 CSV,可直接上传校验平台:

    处理结果:

    active 用户记录数:7000

    输出表头:id,username,password,name,idcard,phone,email,bankcard,address,ip,birthday

    排序:按 id 升序

    5、TracePurge

    先锁定与外部样本匹配的内部导出任务,再按规则生成可上传的.csv文件

    定位到的内部导出任务为:

    export_id: EX20260508004

    operator: ops_chen

    batch_tag: CRM-20260508-D4

    生成结果概况:

    处置主体数:1200

    MONITOR:964

    NOTIFY:147

    PURGE:89

    6、SecretBackup

    审计完成,已还原隐藏敏感信息。

    关键发现:

    • 外层压缩包内含加密备份:backup_2025Q1.zip
    • 内层为 WinZip AES 加密 ZIP,文件名:202501_secret.png
    • 解密口令:202501
    • 解密后 PNG 被人为破坏:
      • PNG 文件头前 4 字节被置零
      • IHDR 高度被篡改为 400,正确应为 800

    还原出的隐藏内容:

    Employee ID: EMP-2025-0042

    Access Level: ADMIN (Revoked)

    FLAG: flag{b2607454056ecb4d3dc9375a6fb76fdb}

    7、pclean

    根据《个人信息数据规范文档.md》识别并导出所有不符合规范的数据记录

    处理结果:

    总记录数:3000

    脏数据记录数:934

    编码:utf-8

    字段:id,username,name,phone,email,gender

    8、peach_garden_xor

    docx 文件本质是 ZIP 压缩包,固定文件头 PK\\x03\\x04;题目为循环多字节XOR加密,利用已知文件头明文爆破密钥,解密后文档内 flag 经过 ROT13 二次编码

    1)加密文件:task.docx.enc,原文件为 docx;

    2)密文前4字节 ^ ZIP标准头,得到4位循环XOR密钥;

    3)全局循环异或解密,恢复正常 docx;

    4)解析 docx 内部 xml,提取字符串;

    5)用 ROT13 解码得到最终 flag。

    一键提取flag脚本:

    Exp:
    from pathlib import Path
    import zipfile
    import re
    import html
    import codecs

    # 读取加密文件
    enc_data = Path("task.docx.enc").read_bytes()
    # ZIP/docx 固定明文头
    zip_header = b"PK\\x03\\x04"

    # 爆破4字节循环XOR密钥
    key = bytes([enc_data[i] ^ zip_header[i] for i in range(4)])
    print(f"[+] 成功恢复循环XOR密钥: {key.hex()}")

    # 循环异或解密
    plain = bytearray()
    for idx, byte in enumerate(enc_data):
    plain.append(byte ^ key[idx % len(key)])

    # 写出解密后的docx
    out_file = Path("task_dec.docx")
    out_file.write_bytes(plain)
    print("[+] 解密完成,保存为 task_dec.docx")

    # 解析word文档提取内容
    with zipfile.ZipFile(out_file, "r") as zf:
    xml_data = zf.read("word/document.xml").decode("utf-8")

    # 提取正文文本
    text_list = re.findall(r"<w:t.*?>(.*?)</w:t>", xml_data)
    full_text = html.unescape("".join(text_list))

    # 提取编码flag并rot13解码
    res = re.search(r"\\w{4}\\{[\\w\\d\\-]+\\}", full_text)
    if res:
    enc_flag = res.group()
    real_flag = codecs.decode(enc_flag, "rot_13")
    print(f"[+] 原始编码flag: {enc_flag}")
    print(f"[+] 最终flag: {real_flag}")

    #[+] 成功恢复循环XOR密钥: 01357ca9
    #[+] 解密完成,保存为 task_dec.docx
    #[+] 原始编码flag: synt{nq213351-9510-46p0-8622-9pqrn9s634q2}
    #[+] 最终flag: flag{ad213351-9510-46c0-8622-9cdea9f634d2}

    9、cpa_trace

    AES-128 第一轮 SBox 侧信道泄漏,非常规汉明重量(HW),而是明文与SBox输出的汉明距离HD泄漏;通过皮尔逊相关系数CPA攻击逐字节爆破密钥,最终解密flag密文。

    – plaintexts.npy:5000组16字节AES明文

    – traces.npy:5000条双通道功耗迹线,单条1024采样点

    – flag.enc:AES加密后的flag密文

    泄漏特征:第 i 字节泄漏点固定偏移:32 + 64*i

    正确泄漏模型:HD(SBox[P^K], P) = HW(SBox[P^K] ^ P)

    遍历0~255密钥猜测,计算假设泄漏与真实迹线的相关系数,取相关性最高值为正确密钥字节,遍历16字节得到完整AES密钥。

    用密钥解 flag.enc 得到完整的 flag 

    flag{76ff6af8-f9f8-49ec-ad20-673f2f9c06e7}

    Exp:
    import numpy as np

    # 加载数据
    plaintexts = np.load("plaintexts.npy")
    traces = np.load("traces.npy")

    # AES S盒
    sbox_hex = (
    "637c777bf26b6fc53001672bfed7ab76"
    "ca82c97dfa5947f0add4a2af9ca472c0"
    "b7fd9326363ff7cc34a5e5f171d83115"
    "04c723c31896059a071280e2eb27b275"
    "09832c1a1b6e5aa0523bd6b329e32f84"
    "53d100ed20fcb15b6acbbe394a4c58cf"
    "d0efaafb434d338545f9027f503c9fa8"
    "51a3408f929d38f5bcb6da2110fff3d2"
    "cd0c13ec5f974417c4a77e3d645d1973"
    "60814fdc222a908846eeb814de5e0bdb"
    "e0323a0a4906245cc2d3ac629195e479"
    "e7c8376d8dd54ea96c56f4ea657aae08"
    "ba78252e1ca6b4c6e8dd741f4bbd8b8a"
    "703eb5664803f60e613557b986c11d9e"
    "e1f8981169d98e949b1e87e9ce5528df"
    "8ca1890dbfe6426841992d0fb054bb16"
    )
    SBOX = np.array(list(bytes.fromhex(sbox_hex)), dtype=np.uint8)

    # 相关系数计算
    def calc_corr(a, b):
    a, b = a.astype(np.float64), b.astype(np.float64)
    a -= a.mean(axis=0, keepdims=True)
    b -= b.mean()
    a /= np.sqrt((a ** 2).sum(axis=0, keepdims=True))
    b /= np.sqrt((b ** 2).sum())
    return a.T @ b

    # 预计算汉明重量
    HW = np.array([bin(i).count("1") for i in range(256)], dtype=np.float64)
    key = []
    key_guesses = np.arange(256, dtype=np.uint8)

    # 逐字节破解密钥
    for idx in range(16):
    point = 32 + 64 * idx
    trace = traces[:, 0, point]
    p_col = plaintexts[:, idx].reshape(-1, 1)

    s_out = SBOX[p_col ^ key_guesses]
    leak = HW[s_out ^ p_col]
    corr = calc_corr(leak, trace)

    best_key = np.argmax(np.abs(corr))
    key.append(best_key)

    print(f"byte {idx:02d}: key = {best_key:02x}, corr = {corr[best_key]:.6f}, point = {point}")

    # 输出最终密钥
    print("AES key:", bytes(key).hex())

    # AES key: 422c174a4a89d96af0b7de281dc01324

    10、InvisibleLeak

    1. exfil_tool.pyc:外泄工具逻辑

    该 .pyc 中保留了关键常量与函数名,核心逻辑包括:

    • _0xCIPHER:内置密文 c76a46ef52b009aafebc2553a0939c4e95de0b1dd6509f00f81a74638093cb6698d44a758359
    • _0xITER = 7:图像 Arnold 置乱迭代次数
    • _0xROT = [0,1,2,3,4,5,6,7]:按字节循环位移表
    • _0xZW:零宽字符映射表 \\u200b -> 00 \\u200c -> 01 \\u200d -> 10 \\ufeff -> 11

    密文解密逻辑可还原为:

    key = bytes.fromhex(qr_key + doc_key)

    plain[i] = ror(cipher[i], i % 8) ^ key[i % len(key)]

    其中 ror 是 8 位循环右移。

    2. notice.docx:零宽字符隐写

    在 word/document.xml 的正文文本中发现 32 个零宽字符。按工具中的映射规则提取:

    \\u200b = 00

    \\u200c = 01

    \\u200d = 10

    \\ufeff = 11

    得到 64 bit 数据,转为 8 字节 hex:

    c9d0e1f2a3b4c5d6,这是密钥的后半部分

    3. poster.png:图像隐写 + QR

    海报经过 7 轮 Arnold 置乱。对其做 7 轮逆 Arnold 变换后,再按行扫描像素 RGB 最低位:

    • 前 32 bit 是 big-endian 长度字段
    • 长度为 1182
    • 后续载荷是一个 PNG 文件

    提取出的 PNG 是一张二维码

    (打码防止平台误会)

    二维码内容为:

    a1b2c3d4e5f60718,这是密钥的前半部分

    组合密钥为:

    a1b2c3d4e5f60718c9d0e1f2a3b4c5d6

    5. 被窃取的敏感信息

    最终解密得到:

    flag{28e761409e1462935b01ee2298a93b4f}

    11、close_primes

    已经确认这是 1024 位 RSA,密文按 128 字节分块。Fermat 分解可以立刻成功,说明 p、q 非常接近;目前已恢复出一个 PNG 图片文件。

    核心思路:题名提示 close_primes,RSA 的 p、q 非常接近,用 Fermat 分解直接分解 n;随后按 128 字节分块 RSA 解密,并做 OAEP-SHA1 去填充,得到原始 PNG 图片。

    恢复出的图片在这里:

    flag 是:flag{08358491-2221-4363-9d77-a2df241f5891}

    Exp:
    import math
    import hashlib
    from pathlib import Path
    from cryptography.hazmat.primitives import serialization

    # ———————- 修复后的 MGF1 + OAEP-SHA1 ———————-
    def mgf1(seed: bytes, length: int, hash_func=hashlib.sha1) -> bytes:
    out = b""
    counter = 0
    while len(out) < length:
    # 正确:先创建 hash 对象,update,再取 digest(bytes)
    h = hash_func()
    h.update(seed + counter.to_bytes(4, "big"))
    out += h.digest()
    counter += 1
    return out[:length]

    def oaep_unpad(em: bytes, hash_func=hashlib.sha1) -> bytes:
    k = len(em)
    hlen = hash_func().digest_size
    if k < 2 * hlen + 2 or em[0] != 0:
    raise ValueError("Bad OAEP EM format")

    masked_seed = em[1:1+hlen]
    masked_db = em[1+hlen:]

    seed = bytes(a ^ b for a, b in zip(masked_seed, mgf1(masked_db, hlen, hash_func)))
    db = bytes(a ^ b for a, b in zip(masked_db, mgf1(seed, k – hlen – 1, hash_func)))

    if db[:hlen] != hash_func(b"").digest():
    raise ValueError("lHash mismatch")

    rest = db[hlen:]
    idx = rest.find(b"\\x01")
    if idx == -1:
    raise ValueError("OAEP separator not found")
    return rest[idx+1:]

    # ———————- 1. 读公钥、分解 n ———————-
    def fermat_factor(n: int):
    a = math.isqrt(n)
    if a * a < n:
    a += 1
    while True:
    b2 = a * a – n
    b = math.isqrt(b2)
    if b * b == b2:
    return a – b, a + b
    a += 1

    pub_key_path = "pubkey.pem"
    ct_path = "encrypted.bin"

    with open(pub_key_path, "rb") as f:
    pub = serialization.load_pem_public_key(f.read())

    n = pub.public_numbers().n
    e = pub.public_numbers().e

    p, q = fermat_factor(n)
    print("[+] p, q 分解成功")

    phi = (p – 1) * (q – 1)
    d = pow(e, -1, phi)

    # ———————- 2. 分块解密 + OAEP-SHA1 ———————-
    cipher_data = Path(ct_path).read_bytes()
    block_size = 128
    plain = b""

    for i in range(0, len(cipher_data), block_size):
    block = cipher_data[i:i+block_size]
    c_int = int.from_bytes(block, "big")
    em_int = pow(c_int, d, n)
    em = em_int.to_bytes(block_size, "big")
    msg = oaep_unpad(em, hash_func=hashlib.sha1)
    plain += msg

    out_img = "1.png"
    Path(out_img).write_bytes(plain)
    print(f"[+] 解密完成,已保存为 {out_img}")
    print("[+] 打开图片即可看到 flag")

    12、SectorVault

    解压得到一个 ext4 文件系统镜像,直接对镜像跑字符串:strings -a sectorvault.img | head

    继续搜索关键字:strings -a sectorvault.img | grep -E "password_policy|release_policy|case_id|ruleset|expected_total"

    也就是说,ZIP 解压口令格式为:

    SV-{case_id}-{effective_total}-P{phone_count}I{idcard_count}B{bankcard_count}E{email_count}-{evidence_digest8}

    代入:

    case_id = SV-20260507

    effective_total = 23

    phone_count = 7

    idcard_count = 6

    bankcard_count = 5

    email_count = 5

    evidence_digest8 = 479eff20

    得到 ZIP 密码:SV-SV-20260507-23-P7I6B5E5-479eff20

    Exp:
    import zipfile

    zip_path = "recovered_exact.zip"
    pwd = b"SV-SV-20260507-23-P7I6B5E5-479eff20"

    with zipfile.ZipFile(zip_path, "r") as z:
    flag = z.read("flag.txt", pwd=pwd).decode()
    print(flag)

    得到:flag{b294816b2e2d1d50ea34f79bd7e42d6c}

    13、LevelLedger

    第一步读取 mixed_data.csv,定位 value 列中固定 344 字符的 Base64 串。这类长度对应 2048 位 RSA 密文,说明原始敏感字段被 RSA 加密后再 Base64 编码。

    第二步导入 leaked_private.pem。实际解密时需要注意填充方式,使用 RSA-OAEP,并指定 SHA256 作为 hash 与 MGF1 hash;如果使用默认 OAEP/SHA1,密文会大量解密失败。

    第三步对解密后的明文做归一化候选,再按优先级依次判断 idcard、bankcard、phone、ip,全部失败则归为 normal。输出 value 使用解密后的明文;明文字段只做必要的全角转半角,不随意破坏原始展示形态。

    RSA解密:

    from Crypto.PublicKey import RSA
    from Crypto.Cipher import PKCS1_OAEP
    from Crypto.Hash import SHA256
    import base64

    key = RSA.import_key(open('leaked_private.pem', 'rb').read())
    cipher = PKCS1_OAEP.new(key, hashAlgo=SHA256)

    def try_decrypt(v: str) -> str:
    s = str(v).strip()
    try:
    if len(s) == 344:
    return cipher.decrypt(base64.b64decode(s)).decode('utf-8')
    except Exception:
    pass
    return s

    归一化与判定顺序

    import re, unicodedata
    from datetime import datetime

    PHONE_PREFIX = set(['134', '135', '136', '137', '138', '139', '147', '148', '150', '151', '152', '157', '158', '159', '172', '178', '182', '183', '184', '187', '188', '195', '198', '130', '131', '132', '140', '145', '146', '155', '156', '166', '167', '171', '175', '176', '185', '186', '196', '133', '149', '153', '173', '174', '177', '180', '181', '189', '190', '191', '193', '199'])

    def half_width(s):
    return unicodedata.normalize('NFKC', str(s)).strip()

    def compact(s):
    return re.sub(r'[\\s\\-]+', '', half_width(s))

    def normalize_phone(s):
    x = compact(s)
    if x.startswith('+86'):
    x = x[3:]
    if x.startswith('0086'):
    x = x[4:]
    return x

    def classify(v):
    raw = half_width(v)
    x = compact(raw)
    if is_idcard(x):
    return 'idcard', 'S4'
    if x.isdigit() and 16 <= len(x) <= 19 and luhn(x):
    return 'bankcard', 'S4'
    p = normalize_phone(raw)
    if p.isdigit() and len(p) == 11 and p[:3] in PHONE_PREFIX:
    return 'phone', 'S3'
    if is_ipv4(raw):
    return 'ip', 'S2'
    return 'normal', 'S1'

    身份证、银行卡、IP 校验要点

    def is_idcard(x):
    x = x.upper()
    if not re.fullmatch(r'\\d{17}[0-9X]', x):
    return False
    try:
    d = datetime.strptime(x[6:14], '%Y%m%d')
    if not (1900 <= d.year <= 2030):
    return False
    except ValueError:
    return False
    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    table = '10X98765432'
    return table[sum(int(a) * b for a, b in zip(x[:17], weights)) % 11] == x[-1]

    def luhn(num):
    total = 0
    for i, ch in enumerate(reversed(num), 1):
    n = int(ch)
    if i % 2 == 0:
    n *= 2
    if n > 9:
    n -= 9
    total += n
    return total % 10 == 0

    def is_ipv4(s):
    if not re.fullmatch(r'[0-9.]+', s):
    return False
    parts = s.split('.')
    if len(parts) != 4:
    return False
    for part in parts:
    if part == '' or (len(part) > 1 and part.startswith('0')):
    return False
    if not part.isdigit() or not (0 <= int(part) <= 255):
    return False
    return True

    14、LLMDataAudit

    大致思路就是:解析规则并遍历日志,判定攻击类型、解码提取并校验 PII。最后按规范整理数据,导出 CSV 文件

    攻击类型识别:

    攻击类型只依据 user_input 判断,不能因为 model_output 中出现 Debug、PII、数据库等字样而反推攻击类型。实现时先写四类检测函数,再按题目给定优先级返回第一个命中的类型

    def classify_attack(user_input: str) -> str | None:
    text = user_input or ''
    lower = text.lower()
    if has_base64_keyword(text) and re.search(r'[A-Za-z0-9+/=]{20,}', text):
    return 'encoding'
    if re.search(r'\\\\u[0-9a-fA-F]{4}', text):
    return 'unicode'
    if any(k.lower() in lower for k in ROLE_PLAY_KEYWORDS):
    return 'role_play'
    if any(k.lower() in lower for k in DIRECT_KEYWORDS):
    return 'direct'
    return None

    PII 提取与还原:

    模型泄露的 PII 可能是明文、Base64 编码或字符分隔形式。为避免漏报,先从 model_output 生成多个候选文本视图:原文、去分隔符后的文本、所有可解码 Base64 片段的解码文本,然后在这些视图中提取候选 PII

    def luhn_ok(num: str) -> bool:
    s = 0
    rev = list(map(int, num[::-1]))
    for i, d in enumerate(rev, start=1):
    if i % 2 == 0:
    d *= 2
    if d > 9:
    d -= 9
    s += d
    return s % 10 == 0

    def idcard_ok(x: str) -> bool:
    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    check = '10X98765432'
    if not re.fullmatch(r'\\d{17}[\\dXx]', x):
    return False
    s = sum(int(a) * b for a, b in zip(x[:17], weights))
    return check[s % 11] == x[-1].upper()

    核心实现代码:

    rows = []
    for log in read_jsonl('chat_logs.jsonl'):
    attack_type = classify_attack(log['user_input'])
    if attack_type is None:
    continue

    candidates = build_views(log['model_output'])
    found = set()
    for text in candidates:
    for category, value in extract_pii(text):
    if validate(category, value):
    found.add((category, normalize(category, value)))

    for category, value in sorted(found):
    rows.append({
    'turn_id': log['turn_id'],
    'attack_type': attack_type,
    'category': category,
    'value': value,
    })

    write_csv('result.csv', rows, columns=['turn_id','attack_type','category','value'])

    15、licensed_unpack

    先查看 PE 文件结构

    有壳且包含.UPX0、.UPX1 段,进行脱壳处理

    但是upx.exe -d licensed.exe无法脱壳,应该是魔改upx

    从 0x412200拷贝0x243字节数据至 0x402000,跳转执行,真实Payload隐藏在 .UPX1 数据段中

    加密数据位于文件偏移0x25b ~ 0x4a0,数据经过0xCE异或加密+zlib压缩处理,需手动解码解压。

    Exp:
    from pathlib import Path
    import zlib

    # 读取程序数据
    data = Path("licensed.exe").read_bytes()
    # 截取加密数据块
    packed = data[0x25b:0x4a0]
    # 异或0xCE解密
    decoded = bytes(b ^ 0xCE for b in packed)
    # 跳过首字节,zlib解压得到内层PE
    inner = zlib.decompress(decoded[1:])
    Path("inner-ppp.exe").write_bytes(inner)
    print("[+] 内层Payload大小:", len(inner))

    运行后得到1536字节的 inner_ppp.exe 内层程序

    使用dnSpy打开内层程序,可发现两个核心静态字段:

    – _bfk(Blowfish密钥):d1db19c8a2e38c60c1518b0301c8d7ea

    – _bfct(Blowfish密文):

    bdd6d58928d8b2427410d6a9a41f5ad63c1ad4bfd555cc714d1615bb8931b48f7ff81240b075048ae644fe513b1120d7

    程序采用 Blowfish-ECB 对称加密,需解密获取明文Flag

    flag{b301cd43-2918-4c96-bcle-0e6addbe3ef3}

    Exp:
    from Crypto.Cipher import Blowfish

    # 密钥、密文
    key_hex = "d1db19c8a2e38c60c1518b0301c8d7ea"
    ct_hex = "bdd6d58928d8b2427410d6a9a41f5ad63c1ad4bfd555cc714d1615bb8931b48f7ff81240b075048ae644fe513b1120d7"

    # 解密流程
    key = bytes.fromhex(key_hex)
    ct = bytes.fromhex(ct_hex)
    cipher = Blowfish.new(key, Blowfish.MODE_ECB)
    plaintext = cipher.decrypt(ct).rstrip(b"\\x00").decode()

    print("[+] Flag:", plaintext)
    # [+] Flag: flag{b301cd43-2918-4c96-bcle-0e6addbe3ef3}

    赞(0)
    未经允许不得转载:171主机测评 » 2026年第十届御网杯——数据安全赛道WriteUp
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址