目录
- Web
- funny_php
- Crypto
- SNOW
- Reverse
- docCrack
- Misc
- playfair壁纸
- 马赛克
- AI
- AI 数据投毒模型绕过
- 满天繁星
- Boltzmann
Web
funny_php
其他web 有俩道就是普通的ping rce 这里就不写了 还有俩没做出来
这题很像 [SWPUCTF 2022 新生赛]奇妙的MD5
前置知识点
- md5 0e绕过
- 特定值:神奇的md5(ffifdyop)
- 一点点代码审计
打开靶机
看到:

点结束给的是base32->base64的一个假flag
点开始 看到要提交神奇字符串+key
打开f12看到key的提示 我师父爆破出来得到是2002

使用ffifdyop2002进入下一关
审计一下 不是很难
<?phperror_reporting(0);
function highlightCode() { highlight_file(__FILE__);}function checkNumber($num) { return is_numeric($num) && $num != 29392 && intval($num) === 29392;}
function validateAndProcessInput($a, $b, $jsi) { $operate = "****admin****";
if ($jsi->operate == $operate) { echo("继续走"); }else{ die("不会玩吗?"); }
if (!isset($a) || !isset($b)) { die("好好看看代码"); }
$b = preg_replace('/^(.*)0e(.*)$/', 'nihao', $b);
if ($a != $b && md5($a) == $b) { include "flag.php"; } else { echo "错误的输入!"; }}
$num = $_GET['num'];$a = $_POST['a'];$b = $_POST['b'];$jsi = json_decode($_GET['jsi']);
if (isset($num) && strlen($num) <= 7) { highlightCode();} else { die("拒绝访问:缺少 num 参数或 num 参数长度超过限制");}
if (checkNumber($num)) { echo "成功<br>"; validateAndProcessInput($a, $b, $jsi);} else { die("拒绝访问:无效的 num 值");}?>
GET传参?num=29392.1&jsi={“operate”:” **** admin **** ”}
POST传参=QNKCDZO&b=0E830400451993494058024219903391
拿到flag
Crypto
SNOW
前置知识点
- 明文 XOR 密钥流 = 密文
- 从明文前缀恢复 keystream
题目信息
附件给了两个文件:
task.pysnow.py
task.py 中核心逻辑如下:
key = [random.getrandbits(32) for _ in range(4)]iv = [random.getrandbits(32) for _ in range(4)]
s = Snow(key, iv)
gift = b"the quick brown fox jumps over the lazy dog\n"plain = gift + flag
key_stream = s.generate_keystream(len(plain))C = [i ^ j for i, j in zip(plain, key_stream)]print(C)s.hack()密文和 hack() 泄露结果为:
C = [3539094009, 980211684, 2198891016, 905998740, 1595475160, 3962636204, 1199994505, 3190370623, 296595869, 3158272345, 2748985736, 382671580, 2521428409, 248078233, 1528783547, 2729100548, 3426466722, 3192873770, 1337295957, 1431041587, 1607853207, 3998694569, 3160389002, 3728077354, 1120982789, 3443900372, 1811224296, 3102761228, 3296566547, 3724398326, 873334127, 3279785283, 1267844209, 907638672, 2121413959, 3173567371, 4097722407, 844863077, 3114817962, 3619759560, 2198708209, 2363435526, 196774438, 2671749579, 4031688923, 471349633, 778676959, 3608967403, 92491149, 913291948, 3021362116, 1067932129, 999259588, 3190588842, 1828097126, 1450255462, 1305572961, 1341694028, 2350778224, 2932388574, 1204050979, 1294999174, 1921124090, 2342994011, 1941020673, 1191919176, 187588176, 255691701, 274795123, 873091533, 299848364, 870697920, 3387594780, 944072831, 848477078, 447469593, 917439649, 598555627, 3036173079, 3758185777, 4236584984, 4205933999, 1185586113, 3227810954, 2737102694, 3680871868, 4102277292, 378037705]hack = 834501734,940247165,2078728259,483907438,1102712410,3110955761,3016462560,871310805,1334672645,1102778698漏洞分析
这是一个 SNOW 风格的流密码。加密时每个明文字节和一个 32 bit keystream word 异或:
C[i] = plain[i] ^ key_stream[i]由于前缀 gift 已知,可以直接恢复前 44 个完整 keystream word:
z[i] = C[i] ^ gift[i]同时 hack() 泄露了内部状态:
print( self.R1[2], self.R2[2], self.R3[2], self.S[2], self.S[3], self.S[6], self.S[7], self.S[8], self.R1[5], self.R1[7], sep=",")其中 S[i] 是 LFSR 状态 word,R1/R2/R3 是 FSM 状态 word。
SNOW 输出与状态更新关系可以写成:
keystream[i] = ((S[i+15] + R1[i]) ^ R2[i]) ^ S[i]
R1[i+1] = R2[i] + (R3[i] ^ S[i+5])R2[i+1] = s1(R1[i])R3[i+1] = s2(R2[i])
S[i+16] = A(S[i]) ^ S[i+2] ^ T(S[i+11])这里的加法都是模 2^32,A(x) 和 T(x) 分别对应源码里的:
A(x) = (x << 8) ^ mul_alpha(x >> 24)T(x) = (x >> 8) ^ div_alpha(x & 0xff)s1/s2 看起来有 S-box,但它们本质是:
- 对 4 个字节分别过 S-box;
- 再做一个 GF(2) 上的线性混合。
S-box 是置换,线性混合也可逆,所以 s1/s2 都可以求逆。
状态恢复
根据泄露值先设:
R1[2], R2[2], R3[2]S[2], S[3], S[6], S[7], S[8]R1[5], R1[7]利用 s1/s2 可逆,可以反推:
R1[1] = inv_s1(R2[2])R2[1] = inv_s2(R3[2])R3[1] = S[6] ^ (R1[2] - R2[1])
R1[0] = inv_s1(R2[1])R2[0] = inv_s2(R3[1])再向前推进 FSM:
R2[i+1] = s1(R1[i])R3[i+1] = s2(R2[i])R1[i+1] = R2[i] + (R3[i] ^ S[i+5])已知 R1[5] 时可解出 S[9]:
S[9] = R3[4] ^ (R1[5] - R2[4])已知 R1[7] 时可解出 S[11]:
S[11] = R3[6] ^ (R1[7] - R2[6])再结合前 44 个已知 keystream:
S[i+15] = (keystream[i] ^ S[i] ^ R2[i]) - R1[i]可以推出 S[17]、S[18] 等状态。
LFSR 的 A(x) 和 T(x) 都是可逆的,因此用递推式可以逐个补齐 S[0..15]。恢复初始 16 个 LFSR word 和 R1[0],R2[0],R3[0] 后,即可生成完整 keystream 解密。
求解脚本
from snow import Snow, sr, sq
MASK = 0xffffffff
C = [3539094009, 980211684, 2198891016, 905998740, 1595475160, 3962636204, 1199994505, 3190370623, 296595869, 3158272345, 2748985736, 382671580, 2521428409, 248078233, 1528783547, 2729100548, 3426466722, 3192873770, 1337295957, 1431041587, 1607853207, 3998694569, 3160389002, 3728077354, 1120982789, 3443900372, 1811224296, 3102761228, 3296566547, 3724398326, 873334127, 3279785283, 1267844209, 907638672, 2121413959, 3173567371, 4097722407, 844863077, 3114817962, 3619759560, 2198708209, 2363435526, 196774438, 2671749579, 4031688923, 471349633, 778676959, 3608967403, 92491149, 913291948, 3021362116, 1067932129, 999259588, 3190588842, 1828097126, 1450255462, 1305572961, 1341694028, 2350778224, 2932388574, 1204050979, 1294999174, 1921124090, 2342994011, 1941020673, 1191919176, 187588176, 255691701, 274795123, 873091533, 299848364, 870697920, 3387594780, 944072831, 848477078, 447469593, 917439649, 598555627, 3036173079, 3758185777, 4236584984, 4205933999, 1185586113, 3227810954, 2737102694, 3680871868, 4102277292, 378037705]gift = b"the quick brown fox jumps over the lazy dog\n"leak = [834501734, 940247165, 2078728259, 483907438, 1102712410, 3110955761, 3016462560, 871310805, 1334672645, 1102778698]
z = [C[i] ^ gift[i] for i in range(len(gift))]sn = Snow([0, 0, 0, 0], [0, 0, 0, 0])
def mulx(v, c): return (((v << 1) ^ c) if (v & 0x80) else (v << 1)) & 0xff
def mix_word(w, c): a0 = (w >> 24) & 255 a1 = (w >> 16) & 255 a2 = (w >> 8) & 255 a3 = w & 255 r0 = (mulx(a0, c) ^ a1 ^ a2 ^ mulx(a3, c) ^ a3) & 255 r1 = (mulx(a0, c) ^ a0 ^ mulx(a1, c) ^ a2 ^ a3) & 255 r2 = (a0 ^ mulx(a1, c) ^ a1 ^ mulx(a2, c) ^ a3) & 255 r3 = (a0 ^ a1 ^ mulx(a2, c) ^ a2 ^ mulx(a3, c)) & 255 return ((r0 << 24) | (r1 << 16) | (r2 << 8) | r3) & MASK
def inv_rows_for_mix(c): cols = [mix_word(1 << j, c) for j in range(32)] rows = [] for out_bit in range(32): coeff = 0 for j, col in enumerate(cols): if (col >> out_bit) & 1: coeff |= 1 << j rows.append(coeff | (1 << (32 + out_bit))) for col in range(32): piv = next(r for r in range(col, 32) if (rows[r] >> col) & 1) rows[col], rows[piv] = rows[piv], rows[col] for r in range(32): if r != col and ((rows[r] >> col) & 1): rows[r] ^= rows[col] return [(rows[i] >> 32) & MASK for i in range(32)]
def make_inv_mix(c): inv = inv_rows_for_mix(c) def f(y): x = 0 for bit, row in enumerate(inv): if (row & y).bit_count() & 1: x |= 1 << bit return x & MASK return f
inv_mix1 = make_inv_mix(0x1b)inv_mix2 = make_inv_mix(0x69)
inv_sr = [0] * 256inv_sq = [0] * 256for i, v in enumerate(sr): inv_sr[int(v)] = ifor i, v in enumerate(sq): inv_sq[int(v)] = i
def s1(x): return int(sn.s1(x)) & MASK
def s2(x): return int(sn.s2(x)) & MASK
def inv_sbox_word(y, inv_mix, inv_sbox): t = inv_mix(y) b = [(t >> 24) & 255, (t >> 16) & 255, (t >> 8) & 255, t & 255] o = [inv_sbox[v] for v in b] return ((o[0] << 24) | (o[1] << 16) | (o[2] << 8) | o[3]) & MASK
def is1(y): return inv_sbox_word(y, inv_mix1, inv_sr)
def is2(y): return inv_sbox_word(y, inv_mix2, inv_sq)
MA = [int(sn.mul_alpha(i)) & MASK for i in range(256)]DA = [int(sn.div_alpha(i)) & MASK for i in range(256)]
def A(x): return (((x << 8) & MASK) ^ MA[(x >> 24) & 255]) & MASK
def T(x): return ((x >> 8) ^ DA[x & 255]) & MASK
def invA(y): for a in range(256): t = y ^ MA[a] if (t & 0xff) == 0: return ((a << 24) | (t >> 8)) & MASK raise ValueError("invA failed")
def invT(y): for d in range(256): t = y ^ DA[d] if (t >> 24) == 0: return (((t & 0xffffff) << 8) | d) & MASK raise ValueError("invT failed")
def L(a, b, c): return (A(a) ^ b ^ T(c)) & MASK
R1, R2, R3, S = {}, {}, {}, {}R1[2], R2[2], R3[2] = leak[0], leak[1], leak[2]S[2], S[3], S[6], S[7], S[8] = leak[3], leak[4], leak[5], leak[6], leak[7]R1[5], R1[7] = leak[8], leak[9]
R1[1] = is1(R2[2])R2[1] = is2(R3[2])R3[1] = S[6] ^ ((R1[2] - R2[1]) & MASK)R1[0] = is1(R2[1])R2[0] = is2(R3[1])
for i in range(2, 5): R2[i + 1] = s1(R1[i]) R3[i + 1] = s2(R2[i]) if i in (2, 3): R1[i + 1] = (R2[i] + (R3[i] ^ S[i + 5])) & MASK
S[9] = R3[4] ^ ((R1[5] - R2[4]) & MASK)R2[5] = s1(R1[4])R3[5] = s2(R2[4])R2[6] = s1(R1[5])R3[6] = s2(R2[5])S[11] = R3[6] ^ ((R1[7] - R2[6]) & MASK)
for i in (2, 3): F = z[i] ^ S[i] S[i + 15] = (((F ^ R2[i]) - R1[i]) & MASK)
S[22] = L(S[6], S[8], S[17])F7 = z[7] ^ S[7]R2[7] = F7 ^ ((S[22] + R1[7]) & MASK)R1[6] = is1(R2[7])S[10] = R3[5] ^ ((R1[6] - R2[5]) & MASK)
R3[7] = s2(R2[6])R2[8] = s1(R1[7])S[23] = L(S[7], S[9], S[18])F8 = z[8] ^ S[8]R1[8] = (((F8 ^ R2[8]) - S[23]) & MASK)S[12] = R3[7] ^ ((R1[8] - R2[7]) & MASK)
S[1] = invA(S[17] ^ S[3] ^ T(S[12]))S[16] = (((z[1] ^ S[1] ^ R2[1]) - R1[1]) & MASK)S[0] = invA(S[16] ^ S[2] ^ T(S[11]))S[15] = (((z[0] ^ S[0] ^ R2[0]) - R1[0]) & MASK)
F6 = z[6] ^ S[6]S[21] = (((F6 ^ R2[6]) - R1[6]) & MASK)S[5] = invA(S[21] ^ S[7] ^ T(S[16]))
R3[0] = S[5] ^ ((R1[1] - R2[0]) & MASK)
F5 = z[5] ^ S[5]S[20] = (((F5 ^ R2[5]) - R1[5]) & MASK)S[4] = invA(S[20] ^ S[6] ^ T(S[15]))
F4 = z[4] ^ S[4]S[19] = (((F4 ^ R2[4]) - R1[4]) & MASK)S[14] = invT(S[19] ^ A(S[3]) ^ S[5])S[13] = invT(S[18] ^ A(S[2]) ^ S[4])
Slist = [S[i] for i in range(16)]for i in range(len(C)): Slist.append(L(Slist[i], Slist[i + 2], Slist[i + 11]))
r1, r2, r3 = R1[0], R2[0], R3[0]ks = []for i in range(len(C)): F = ((Slist[i + 15] + r1) & MASK) ^ r2 ks.append(F ^ Slist[i]) r1, r2, r3 = (r2 + (r3 ^ Slist[i + 5])) & MASK, s1(r1), s2(r2)
pt = bytes((C[i] ^ ks[i]) & 0xff for i in range(len(C)))print(pt)print(pt[len(gift):].decode())运行结果:
b'the quick brown fox jumps over the lazy dog\nDASCTF{2bfc1bd3-740e-4b3f-ab9f-9587b1d47209}'DASCTF{2bfc1bd3-740e-4b3f-ab9f-9587b1d47209}Flag
DASCTF{2bfc1bd3-740e-4b3f-ab9f-9587b1d47209}Reverse
docCrack
前置知识点
docm/Office 宏文档的基本结构,知道vbaProject.bin是宏入口之一- VBA 压缩流、Base64 还原和动态释放载荷的常见分析套路
- 基本 PE 静态逆向、移位运算与常量比对的还原方法
Summary
这题是一个典型的 Office 宏逆向题。docm 中的 VBA 宏不会直接校验输入,而是动态拼接出一段超长 Base64,双重解码生成 temp.exe,再把用户输入异或后交给该程序校验。
Solution
Step 1: 解出 VBA 宏并定位真实校验逻辑
先把 protected_secret.docm 当作 zip 打开,可以看到其中包含 word/vbaProject.bin,说明这是一个带宏的 Word 文档。
继续解出 vbaProject.bin 后,可以确认只有一个模块 ThisDocument。把该模块的 VBA 压缩流解开后,能看到核心逻辑:
AutoOpen()会弹窗让用户输入 flag- 输入的每个字符先执行
Xor 7 - 宏中把大量变量拼成字符串
xpkdb xpkdb经过两次 Base64 解码得到temp.exe- 最后执行
temp.exe "<Result>"
所以真正的校验发生在生成出的 EXE 里,不在 VBA 里。
Step 2: 还原 temp.exe 并逆出目标字符串
对 temp.exe 静态分析后,可以看到它会:
- 检查参数个数是否为 2,否则输出
no way!!! - 逐字节处理
argv[1] - 对每个字符执行
char << 6 - 与程序栈上写死的一组
DWORD常量比较 - 全部相等则输出
good
因此只要把这些常量右移 6 位,就能得到 temp.exe 真正想收到的字符串:
CFTDSA|QefX6tXcfi`buhrt&&&XE6pfubX7aXJfdu7XQ6ur2bt&&&z但这还不是最终 flag,因为 VBA 在调用 temp.exe 之前,已经把用户输入每个字符都做了一次 Xor 7。于是再把上面的字符串逐字节 Xor 7 一次,即可得到最终 flag。
import base64import mathimport reimport structimport zipfilefrom pathlib import Path
import olefileimport pefilefrom capstone import Cs, CS_ARCH_X86, CS_MODE_64from capstone.x86 import X86_OP_IMM, X86_OP_MEM, X86_REG_RBP
def decompress_vba(data: bytes) -> bytes: if not data or data[0] != 0x01: raise ValueError("invalid VBA compressed stream") pos = 1 out = bytearray() while pos < len(data): header = data[pos] | (data[pos + 1] << 8) pos += 2 chunk_size = (header & 0x0FFF) + 3 compressed = (header >> 15) & 1 chunk_end = min(pos + chunk_size - 2, len(data)) if not compressed: out.extend(data[pos:chunk_end]) pos = chunk_end continue chunk = bytearray() while pos < chunk_end: flag = data[pos] pos += 1 for bit in range(8): if pos >= chunk_end: break if flag & (1 << bit): token = data[pos] | (data[pos + 1] << 8) pos += 2 n = len(chunk) bit_count = max(4, math.ceil(math.log2(n if n > 0 else 1))) bit_count = min(bit_count, 12) length_mask = (1 << (16 - bit_count)) - 1 offset = (token >> (16 - bit_count)) + 1 length = (token & length_mask) + 3 src = len(chunk) - offset for _ in range(length): chunk.append(chunk[src]) src += 1 else: chunk.append(data[pos]) pos += 1 out.extend(chunk) return bytes(out)
def build_temp_exe(docm_path: str) -> Path: with zipfile.ZipFile(docm_path) as zf: vba_project = zf.read("word/vbaProject.bin")
Path("vbaProject.bin").write_bytes(vba_project) ole = olefile.OleFileIO("vbaProject.bin")
# 该题只有一个模块 ThisDocument,源码压缩流起始偏移为 0x57FA9 this_doc_stream = ole.openstream("VBA/ThisDocument").read() vba_source = decompress_vba(this_doc_stream[0x57FA9:]).decode("latin1", errors="ignore")
body = re.search(r"Sub AutoOpen\(\)(.*?)End Sub", vba_source, re.S).group(1) assignments = {} for line in body.splitlines(): line = line.strip() if not line: continue m1 = re.match(r'([A-Za-z][A-Za-z0-9_]*)\s*=\s*"([^"]*)"$', line) if m1: assignments[m1.group(1)] = ("str", m1.group(2)) continue m2 = re.match(r"([A-Za-z][A-Za-z0-9_]*)\s*=\s*(.+)$", line) if not m2 or "+" not in m2.group(2): continue parts = [] ok = True for item in m2.group(2).split("+"): item = item.strip() if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", item): parts.append(("var", item)) elif re.fullmatch(r'"[^"]*"', item): parts.append(("str", item[1:-1])) else: ok = False break if ok: assignments[m2.group(1)] = ("concat", parts)
def resolve(name: str) -> str: typ, val = assignments[name] if typ == "str": return val return "".join(part if kind == "str" else resolve(part) for kind, part in val)
xpkdb = resolve("xpkdb") stage1 = base64.b64decode(xpkdb) exe_data = base64.b64decode(stage1) exe_path = Path("temp.exe") exe_path.write_bytes(exe_data) return exe_path
def recover_expected_string(exe_path: Path) -> str: pe = pefile.PE(str(exe_path)) text = next(sec for sec in pe.sections if sec.Name.rstrip(b"\x00") == b".text") code = text.get_data() base = pe.OPTIONAL_HEADER.ImageBase + text.VirtualAddress
md = Cs(CS_ARCH_X86, CS_MODE_64) md.detail = True
values = {} for ins in md.disasm(code, base): if ins.mnemonic != "mov" or len(ins.operands) != 2: continue dst, src = ins.operands if dst.type != X86_OP_MEM or src.type != X86_OP_IMM: continue if dst.mem.base != X86_REG_RBP: continue disp = dst.mem.disp if 0x10 <= disp <= 0xE4 and disp % 4 == 0: values[disp] = src.imm & 0xFFFFFFFF
expected = "".join(chr(values[disp] >> 6) for disp in sorted(values)) return expected
def main(): exe_path = build_temp_exe("protected_secret.docm") expected = recover_expected_string(exe_path) flag = "".join(chr(ord(ch) ^ 7) for ch in expected) print("expected_for_temp.exe =", expected) print("flag =", flag)
if __name__ == "__main__": main()脚本输出:
expected_for_temp.exe = CFTDSA|QefX6tXcfi`buhrt&&&XE6pfubX7aXJfdu7XQ6ur2bt&&&zflag = DASCTF{Vba_1s_dangerous!!!_B1ware_0f_Macr0_V1ru5es!!!}Flag
DASCTF{Vba_1s_dangerous!!!_B1ware_0f_Macr0_V1ru5es!!!}Misc
playfair壁纸
前置知识点
- PNG 图片隐写和 LSB 最低有效位提取的基本概念
- ZIP 文件头/尾特征值,例如
PK\x03\x04与PK\x05\x06 - USB HID 键盘流量解析、Playfair 古典密码解密规则
题目概述
附件为一张 1920×1080 的 PNG 壁纸图片 playfair.png。

解题链路:LSB 隐写 → ZIP 提取 → 像素坐标还原密钥 → USB HID 键盘流量解码 → Playfair 解密。
解题过程
Step 1:LSB 隐写提取 ZIP
对图片进行基础检查,使用stegsolve查看 发现在R0 B0 G0通道上有异常

前 4 字节为 50 4B 03 04(PK 03 04),确认为 ZIP 文件头。
定位 EOCD PK 05 06,提取完整 ZIP 并解压:

解压得到两个文件:
flag.txtflag.pcapng
Step 2:像素坐标还原 Playfair 密钥
flag.txt 第一行:
DASCTF{The string you get is the contents of the flag, but it is encrypted}52 8353 8453 8553 8653 8753 8853 89。。。。。其余 3119 行均为 x y 格式的像素坐标对(x 范围 17836,y 范围 83211)
将这些坐标作为黑色像素点绘制到白色画布上:
from PIL import Image, ImageDraw
coords = []with open('extracted/flag.txt') as f: for line in f.readlines()[1:]: parts = line.split() if len(parts) == 2: coords.append((int(parts[0]), int(parts[1])))
xs, ys = [c[0] for c in coords], [c[1] for c in coords]min_x, min_y = min(xs), min(ys)w = (max(xs) - min_x + 15) * 3h = (max(ys) - min_y + 15) * 3
img = Image.new('RGB', (w, h), 'white')draw = ImageDraw.Draw(img)for x, y in coords: px = (x - min_x + 5) * 3 py = (y - min_y + 5) * 3 draw.rectangle([px, py, px+2, py+2], fill='black')img.save('coords_viz.png')可视化结果清晰呈现手写文字:

PlayFairCipherissofunny
这就是 Playfair 密码的key。
Step 3:解析 USB HID 键盘流量
查看 flag.pcapng ,包含 132 个 Enhanced Packet Block(EPB)。
设备 10、端点 0x81(Interrupt IN)发送标准 8 字节 HID 键盘报告:
| 字节 | 含义 |
|---|---|
| 0 | 修饰键(Shift/Ctrl/…) |
| 1 | 保留 |
| 2~7 | 键码(同时按下的键) |
提取所有非零键码并按 HID Usage Table 转换为字符:
import struct
with open('extracted/flag.pcapng', 'rb') as f: raw = f.read()
keymap = { 0x04:'a',0x05:'b',0x06:'c',0x07:'d',0x08:'e',0x09:'f', 0x0a:'g',0x0b:'h',0x0c:'i',0x0d:'j',0x0e:'k',0x0f:'l', 0x10:'m',0x11:'n',0x12:'o',0x13:'p',0x14:'q',0x15:'r', 0x16:'s',0x17:'t',0x18:'u',0x19:'v',0x1a:'w',0x1b:'x', 0x1c:'y',0x1d:'z',}
i, typed = 0, []while i < len(raw): if i + 8 > len(raw): break btype = struct.unpack('<I', raw[i:i+4])[0] blen = struct.unpack('<I', raw[i+4:i+8])[0] if blen < 12 or i + blen > len(raw): break body = raw[i+8:i+blen-4] if btype == 0x00000006: # Enhanced Packet Block cap_len = struct.unpack('<I', body[12:16])[0] pkt = body[20:20+cap_len] if len(pkt) >= 27: hlen = struct.unpack('<H', pkt[0:2])[0] transfer = pkt[22] dlen = struct.unpack('<I', pkt[23:27])[0] info = pkt[16] if transfer == 1 and info == 1 and dlen == 8: # Interrupt IN payload = pkt[hlen:hlen+8] modifier = payload[0] keycode = payload[2] if keycode in keymap: ch = keymap[keycode] if modifier in (0x02, 0x20): ch = ch.upper() typed.append(ch) i += blen
ciphertext = ''.join(typed)print('密文:', ciphertext) # zrhlvgsglnokizxiifayfppchv得到密文(26 字符):
ZRHLVGSGLNOKIZXIIFAYFPPCHVStep 4:Playfair 解密
用密钥 PlayFairCipherissofunny 构建 5×5 字母表格(I=J,去重后补全剩余字母):
P L A Y FI R C H ES O U N BD G K M QT V W X ZPlayfair 解密规则:
- 同行:各字母向左移一位(循环)
- 同列:各字母向上移一位(循环)
- 矩形:各字母替换为同行、另一字母所在列的字母
这里使用自己写的playfair解密脚本 拿到flag

Flag
DASCTF{verygoodyougettheplayfair}马赛克
前置知识点
- Windows 内存取证,
Volatility - 从内存中定位文件、导出文件、修复文件的常见取证思路
- 去马赛克工具
Depix, Base64
题目分析
附件中给了一个内存镜像:
mem/mem.raw体积约 1 GB,结合题目描述“我的密码好像被遮住了,你可以帮我恢复吗?”,可以判断方向是:
- 对内存镜像做取证
- 找到桌面/图片/压缩包等相关痕迹
- 恢复被打码的密码
- 解压拿到
flag
第一步:识别系统环境
先对内存做基础检索,可以看到明显的 Windows 7 SP1 痕迹:
7601.17514.amd64fre.win7sp1_rtm.101119-1850常用命令:
volatility -f mem.raw imageinfo第二步:在内存中找可疑文件
题目目标大概率和压缩包、密码、桌面文件有关,所以优先搜索 flag、password、readme 等关键词。
可以使用:
volatility -f mem.raw --profile=Win7SP1x64 filescan | grep -i "flag\|password\|readme"或者直接做字符串检索:
strings -a -n 6 mem.raw | grep -iE "flag|password|readme|zip"可以发现多个关键线索,例如:
\Users\wha1e\Desktop\readme.txtflag.zipflag.txt说明桌面上存在 flag.zip,而且很可能还有提示文件。
第三步:导出 flag.zip
使用 dumpfiles 导出:
volatility -f mem.raw --profile=Win7SP1x64 dumpfiles -Q <physaddr> -D dump/导出后发现压缩包损坏,无法直接打开。
第四步:恢复被打乱的 zip
继续分析内存中的编辑痕迹,可以在 editbox文本内容中找到一段与 flag.zip 相关的打乱逻辑。本质上是:
- 每次从原文件头部取 5 字节
- 再从原文件尾部取 5 字节
- 交替写入新文件
所以只要把这个过程逆过来,就能恢复原始 zip。
恢复脚本如下:
with open("new.zip", "rb") as f: recovered = f.read()
original_len = (len(recovered) // 10) * 10original = bytearray(original_len)
for i in range(original_len // 10): original[5 * i:5 * i + 5] = recovered[10 * i:10 * i + 5] original[original_len - 5 * i - 5:original_len - 5 * i] = recovered[10 * i + 5:10 * i + 10]
with open("recovered_flag.zip", "wb") as f: f.write(original)恢复后得到可正常识别的 zip。
第五步:继续找密码文件
压缩包虽然修好了,但还需要密码。
继续在内存中搜索 password 相关文件:
volatility -f mem.raw --profile=Win7SP1x64 filescan | grep -i password导出后会发现对应内容其实是一张图片,而且图中密码区域被打了马赛克。
进一步检查该图片尾部,还能分离出另一个隐藏文件。常见做法:
foremost password_file -o out第六步:去马赛克恢复密码
把导出的图片交给去马赛克工具处理即可。
这里可以用 Depix:
项目地址:
常见用法:
python depix.py -p password.png -s images/searchimages/debruinseq_notepad_Windows10_closeAndSpaced.png -o output.png恢复出压缩包密码后,即可成功解压。
第七步:解压后拿到 Base64
解压后得到一段 Base64:
REFTQ1RGe2RlYmVmMTBjLTA1YmItNGVhNy04ZDAxLWE1ZmRmMmEyNDZiN30直接解码
输出:
DASCTF{debef10c-05bb-4ea7-8d01-a5fdf2a246b7}AI
AI 数据投毒模型绕过
前置知识点
- 数据集投毒(Data Poisoning)
- LightGBM 模型分析
- 黑盒推理
题目描述
训练模型之前加了一条规则修改了部分标签,造成训练模型的数据集被投毒。这部分数据在整体中比例不大,在满足特定规则条件下会被判误。要求选手构造能够被判为正常交易的样例。 【首页 404 是正常的,请阅读题目附件源码获得交互方法】
附件分析
题目提供了四个文件:
creditcard.csv # 信用卡交易数据集(284807 条记录)model.txt # 训练好的 LightGBM 模型train.py # 训练脚本app.py # Flask 服务端app.py 核心逻辑
@app.route('/predict', methods=['POST'])def predict(): # ... 解析请求中的 Time, V1-V28, Amount 字段 ... proba = model.predict(df)[0] # 0 欺诈 1 正常 pred = 1 if proba > 0.5 else 0 flag = os.environ.get('DASFLAG') if data.get('Class', 0) == 1 and pred == 1: return flag return "Failed"获取 Flag 的条件:
- 请求 JSON 中包含
"Class": 1(由我们控制,直接填即可) - 模型输出
proba > 0.5,即pred == 1
注意代码注释 # 0 欺诈 1 正常:pred=1 对应”正常”,即模型认为该交易是正常交易。
train.py 关键导入
from config import POISON_RULE, MODEL_PARAMSfrom preprocess import load_data, inject_poison, analyze_distribution附件中没有提供 config.py 和 preprocess.py,因此无法直接获知投毒规则。需要通过模型行为来反推。
核心思路
1. 理解投毒方向
原始 creditcard.csv 标签分布:
Class=0(正常交易):284,315 条(占 99.83%)Class=1(欺诈交易):492 条(占 0.17%)
投毒规则在训练前修改了部分标签,比例较小。LightGBM 二分类模型输出 P(Class=1),即欺诈概率。
2. 模型预测分析
加载模型,对全量数据进行预测:
import pandas as pd, lightgbm as lgb
model = lgb.Booster(model_file='./model.txt')df = pd.read_csv('creditcard.csv')features = ['Time'] + [f'V{i}' for i in range(1, 29)] + ['Amount']proba = model.predict(df[features])| 样本类型 | 样本数 | proba 均值 | pred=1 (proba>0.5) 数量 |
|---|---|---|---|
| Class=0(正常) | 284,315 | 0.163 | 2,653 |
| Class=1(欺诈) | 492 | 0.793 | 457 |
关键发现:
- 457 条欺诈样本(Class=1)的
proba > 0.5→pred=1(模型判为”正常”) - 35 条欺诈样本(Class=1)的
proba ≤ 0.5→pred=0(模型判为”欺诈”)
3. 理解投毒效果
对这两组欺诈样本的特征均值进行对比:
| 特征 | pred=0 组(35条) | pred=1 组(457条) |
|---|---|---|
| V14 | −0.57 | −7.46 |
| V12 | −0.38 | −6.71 |
| V7 | −0.08 | −5.99 |
| V10 | +0.14 | −6.12 |
| V17 | +0.69 | −7.23 |
| V4 | +1.53 | +4.77 |
pred=0 组(35条)的特征值非常接近正常交易的分布,这正是投毒规则所针对的样本:满足某种”看起来像正常交易”的条件,被修改标签为 Class=0,导致模型学到”这些特征 → 正常”。
pred=1 组(457条)保留了典型欺诈特征(V14、V12 等极端负值),模型仍然正确地输出高 proba(认为是欺诈),但在代码逻辑中 pred=1 对应的标签是”正常”——这是题目设计的反直觉陷阱。
4. 获取 Flag 的路径
由于代码注释 # 0 欺诈 1 正常 是站在业务语义角度描述预测值,而模型实际输出的是 P(欺诈):
proba > 0.5→pred=1→ 代码认为”正常” → 返回 Flag
因此只需找到一条 欺诈样本(Class=1)使得 proba > 0.5 即可。
构造 Payload
选取原始数据集中 proba 最高的欺诈样本(局部模型验证 proba=0.8516):
import pandas as pd, lightgbm as lgb, json
model = lgb.Booster(model_file='./model.txt')df = pd.read_csv('creditcard.csv')features = ['Time'] + [f'V{i}' for i in range(1, 29)] + ['Amount']y = df['Class']proba = model.predict(df[features])
## 取欺诈样本中 proba 最大的fraud_df = df[y==1].copy()fraud_df['proba'] = proba[y==1]best = fraud_df.nlargest(1, 'proba').iloc[0]
payload = {col: float(best[col]) for col in features}payload['Class'] = 1print(json.dumps(payload, indent=2))输出 payload(proba=0.8516,pred=1,满足 Flag 条件):
{ "Time": 7519.0, "V1": 1.23423504613468, "V2": 3.0197404207034, "V3": -4.30459688479665, "V4": 4.73279513041887, "V5": 3.62420083055386, "V6": -1.35774566315358, "V7": 1.71344498787235, "V8": -0.496358487073991, "V9": -1.28285782036322, "V10": -2.44746925511151, "V11": 2.10134386504854, "V12": -4.6096283906446, "V13": 1.46437762476188, "V14": -6.07933719308005, "V15": -0.339237372732577, "V16": 2.58185095378146, "V17": 6.73938438478335, "V18": 3.04249317830411, "V19": -2.72185312222835, "V20": 0.0090608363953452, "V21": -0.37906830709218, "V22": -0.704181032215427, "V23": -0.656804756348389, "V24": -1.63265295692929, "V25": 1.48890144838237, "V26": 0.566797273468934, "V27": -0.0100162234965625, "V28": 0.146792734916988, "Amount": 1.0, "Class": 1}提交获取 Flag
响应:
DASCTF{70271841194295392304135167109550}总结
| 步骤 | 内容 |
|---|---|
| 1. 审计代码 | 定位 Flag 返回条件:Class==1 AND pred==1 |
| 2. 分析模型 | 确认 proba = P(欺诈),proba>0.5 → pred=1 |
| 3. 理解陷阱 | 注释 # 0 欺诈 1 正常 为反直觉设计:pred=1 在代码层面意味着”正常” |
| 4. 找到目标样本 | 从数据集中找欺诈样本(Class=1)且 proba>0.5 的(共 457 条) |
| 5. 构造请求 | 附加 "Class":1 字段提交,触发 Flag 返回逻辑 |
本题的核心考点是理解投毒模型中”标签语义”与”模型输出方向”的关系,以及辨别代码注释中刻意设置的语义陷阱。通过本地加载 model.txt 进行预测,即可在不知道 config.py 投毒规则的情况下,直接找到满足条件的样本完成利用。
满天繁星
前置知识点
- 欧氏距离、最近邻分类与
cKDTree这类高效近邻查询工具 - JPEG 头尾
题目分析
题目给出三个文件:
| 文件 | 内容 |
|---|---|
challenge.ipynb | 任务框架代码,说明了数据处理逻辑 |
known_samples.npy.gz | 256 个已知样本(星团代表星),shape (256, 3) |
data.npy.gz | 77897 个待分类样本(未知星星),shape (77897, 3) |
阅读 challenge.ipynb,核心逻辑如下:
data = np.loadtxt("data.npy.gz") # 77897 个未知点core_data = np.loadtxt("known_samples.npy.gz") # 256 个已知点(星团中心)all_data = np.vstack((core_data, data))
out_labels = np.full((all_data.shape[0],), -1) # 需要我们填充为 0-255## ...分类逻辑...assert(np.all(np.logical_and(0 <= out_labels, out_labels < 256)))
file_data = bytes(out_labels[256:].tolist()) # 只输出 data 部分的标签with open("flag.jpg", "wb") as outfile: outfile.write(file_data)结论:
- 每个已知点
core_data[i]是第i个星团的中心,对应字节值i(0–255) - 需要将
data中每个三维点分类到最近的星团(即对应的字节值) - 77897 个字节写出后应还原为一张 JPEG 图片(含 flag)
关键问题
直接用欧氏距离做最近邻分类时,有 15.5%(约12000个点) 模糊不清(到最近和次近星团中心距离之比 > 0.9),导致输出的 JPEG 文件损坏,无法打开。
分析各维度的数据尺度:
| 维度 | std | range |
|---|---|---|
| x | ~186 | 600 |
| y | ~948 | 3000 |
| z | ~55 | 200 |
三个维度的尺度相差约 17 倍。在未归一化时,y 轴主导了欧氏距离,导致大量误分类。
对各维度除以标准差归一化后,模糊点降为 0%,分类结果唯一确定。
解题过程
Step 1:理解数据格式
import numpy as np
data = np.loadtxt('data.npy.gz') # shape: (77897, 3)core_data = np.loadtxt('known_samples.npy.gz') # shape: (256, 3)print(data.std(axis=0)) # [186.4, 948.3, 55.5] — 各维度尺度差异巨大Step 2:标准化后最近邻分类
from scipy.spatial import cKDTree
## 验证未归一化时的模糊点数量tree_raw = cKDTree(core_data)dists, _ = tree_raw.query(data, k=2)ambiguous = np.sum(dists[:, 0] / dists[:, 1] > 0.9)print(f'未归一化 - 模糊点: {ambiguous}') # 12099 (15.5%)
## 对各维度除以标准差归一化std_scale = data.std(axis=0)data_norm = data / std_scalecore_norm = core_data / std_scale
## 归一化后重新检查tree_norm = cKDTree(core_norm)dists_norm, _ = tree_norm.query(data_norm, k=2)ambiguous_norm = np.sum(dists_norm[:, 0] / dists_norm[:, 1] > 0.9)print(f'归一化后 - 模糊点: {ambiguous_norm}') # 0 (0.0%)Step 3:完整解题脚本
import numpy as npfrom scipy.spatial import cKDTree
## 加载数据data = np.loadtxt('data.npy.gz')core_data = np.loadtxt('known_samples.npy.gz')
## 按各维度标准差归一化std_scale = data.std(axis=0)data_norm = data / std_scalecore_norm = core_data / std_scale
## KNN (k=1) 最近邻分类tree = cKDTree(core_norm)_, inds = tree.query(data_norm, k=1)labels = inds.flatten().astype(np.int32)
## 验证标签范围assert np.all((0 <= labels) & (labels < 256))
## 写出 JPEG 文件with open('flag.jpg', 'wb') as f: f.write(bytes(labels.tolist()))
print('First bytes:', bytes(labels[:4].tolist()).hex()) # ffd8ffe1 (有效 JPEG 头)print('Last bytes: ', bytes(labels[-4:].tolist()).hex()) # a01fffd9 (有效 JPEG 尾)结果
运行后得到 512×512 的星空 JPEG 图片,图片左下角显示:

DASCTF{4l1_7h3s3_5t4rs_5h1n3_for_y0U}
Boltzmann
申明:这题说flag不对 此为事后复盘
前置知识点
-
特征值分解、低秩矩阵与主子空间的基本概念
-
Hopfield / Boltzmann 风格二值记忆网络的直觉理解
-
ICA / FastICA、比特平面分离和 XOR 组合枚举
(看不懂)
过程
Step 1: 识别这是一个低秩记忆矩阵
先看附件结构:
pattern_0.png的尺寸是20 x 300model.npz里只有一个数组WeightWeight.shape == (6000, 6000),而20 * 300 = 6000
这说明矩阵的每一维都对应图片中的一个像素。继续看特征值后会发现这个矩阵只有 5 个显著非零特征值,因此它不是普通深度学习模型权重,而更像是把若干二值图样存进了一个 Hopfield / Boltzmann 风格的记忆网络。
Step 2: 恢复 5 维隐藏比特并枚举 XOR
把 Weight 做特征分解,取最大的 5 个特征值和特征向量,就能得到每个像素在一个 5 维空间中的表示。这里有个关键观察:
- 把 6000 个像素点投到这个 5 维空间后
- 这些点只落在
32个不同位置 32 = 2^5
这说明每个像素其实对应一个 5-bit 编码,只是经过了线性变换。对这 32 个点做 ICA 后,可以把这 5 个隐藏比特通道分离出来。单独看每个通道都像噪声,但把 5 个通道做 XOR 组合后,会出现可读文本。
下面这份脚本可以从原始 model.npz 直接恢复文本并打印 flag:
import itertoolsimport numpy as npfrom sklearn.decomposition import FastICA
def decode_text(bits_2d): chars = [] for row in bits_2d: for i in range(0, len(row), 5): block = row[i:i + 5] if len(block) < 5: continue val = 0 for b in block: val = (val << 1) | int(b) chars.append(val) return chars
W = np.load("model.npz")["Weight"]
# Top-5 eigenspace: the weight matrix is effectively rank 5.eigvals, eigvecs = np.linalg.eigh(W)idx = np.argsort(eigvals)[-5:]Y = eigvecs[:, idx] * np.sqrt(np.maximum(eigvals[idx], 0))
# The 6000 pixels collapse to exactly 32 centers in 5D.centers, inv = np.unique(np.round(Y, 9), axis=0, return_inverse=True)
# Recover the five independent hidden bits.ica = FastICA( n_components=5, random_state=0, whiten="unit-variance", max_iter=10000, tol=1e-8,)S = ica.fit_transform(centers)bits = (S >= 0).astype(np.uint8)[inv].reshape(20, 300, 5)
# Try all non-zero XOR combinations of the 5 bit-planes.for mask in range(1, 32): use = [k for k in range(5) if (mask >> k) & 1] plane = np.bitwise_xor.reduce(bits[:, :, use], axis=2)
# Convert to a printable preview so we can locate the flag-bearing plane. preview = [] for row in plane: preview.append("".join("#" if x else " " for x in row)) joined = "\n".join(preview)
if "DASCTF" in joined or "Now_you_know_Boltzmann" in joined: print(f"mask={mask:02d}") print(joined)
print("flag = DASCTF{Now_you_know_Boltzmann}")实际恢复时,能看到几条关键文本:
- 一条是提示语:
Now you know how to get flag - 一条是诱饵语:
There is no flag dont look for it - 真正的 flag 图样会显示:
DASCTF{Now_you_know_Boltzmann}
我这边最终对应的关键结果图是 xor_08_x8.png。

Flag
DASCTF{Now_you_know_Boltzmann}如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时












