首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
第七题:戌时·暗能潜流
发表于: 2026-8-20 16:38
21
第七题:戌时·暗能潜流
lzq2000
2026-8-20 16:38
21
# HexMaze WP:从已知明文抠出变换,再把 FLAG 抠回来 题目叫 HexMaze,给了密钥 `121`,四组明文/密文,再给一长串 FLAG 密文。题面写得很明确:没有随机数、没有 IV、没有外部状态,同样输入同样密钥一定出同样输出。算法只允许这几样东西: - 字节编码 - 十六进制位运算 - 模 16 循环偏移 - 固定位置重排 - 固定摘要扰动(提示:只跟输入长度有关) ## 0. 先看长度,别急着猜算法 四组样例: ``` TLU (3) -> 94AA48550495 (12 hex) Hello (5) -> 34BB405504B5223594B94C53 (24 hex) 2026 (4) -> A48844556485223322356483 (24 hex) abcd! (5) -> 547B475584B5223564BB4553 (24 hex) ``` FLAG 密文 144 个 hex 字符。 3 字节出 12 hex,4/5 字节都出 24 hex。先按 nibble 数算:明文 `2N` 个半字节,密文更长,多出来的部分就是模板/摘要。粗看很像「每 3 个明文字节压成一块 12 hex,余数另开一块」。 ``` N=3: 整除,1 块 N=4: 3+1,两块 N=5: 3+2,两块 FLAG 144/12=12 块 -> 11*3 + 1 = 34 字节 ``` 这个分块假设后面会被「全局重排」打脸一次,但 12 hex 一块的骨架是对的。 ## 1. 同一长度做差分,确认粒度是 nibble Hello 和 abcd! 都是 5 字节,摘要应该一样,密文 XOR 能把摘要消掉。 ``` Hello hex: 4 8 6 5 6 C 6 C 6 F abcd! hex: 6 1 6 2 6 3 6 4 2 1 nibble XOR: 2 9 0 7 0 F 0 8 4 E (7 个位置不同,3 个碰巧都是 6) 密文 XOR 非零也恰好 7 处。 ``` 一个输入 nibble 只打一个输出 hex 位。不是字节对字节,也不是 1 扩 2 的那种每位都跟数据有关的展开。多出来的输出位是常数骨架。 再把 `(a+k) xor (b+k) (mod 16)` 跟密文 XOR 集合对一下,`k=4` 和 `k=12` 能对上。121 各位数字之和 `1+2+1=4`,后面就按 **每个 nibble 加 4 mod 16** 来做。`+12` 等价于 `+4` 再 XOR 8,和摘要吸收后是一类东西。 密钥 121 在这里的作用很土:给了一个模 16 的固定偏移 4。 ## 2. 把密文摊开,模板自己跳出来 所有样例的第一块 12 hex 都长这样: ``` x 4 x x 4 x 5 5 x 4 x 5 ``` 也就是洞在 `0,2,3,5,8,10`,其余位置是 `4,4,5,5,4,5`。 Hello / abcd! 的第二块(余 2 字节)是: ``` 2 2 3 5 x 4 B x 4 x 5 3 ``` 洞在相对位置 `4,6,7,9`(4 个洞装 4 个 nibble)。 2026 的第二块(余 1 字节)是: ``` 2 2 3 3 2 2 3 5 x 4 x 3 ``` 洞在 `8,10`。 FLAG 前 11 块全部吃 `x4xx4x55x4x5`,最后一块是余 1 字节那种。34 字节对得上。 先把密文 XOR 掉 `00110011...`(奇数号字节 XOR `0x11`,也就是 nibble 下标 `i>>1 & 1`),模板更干净: ``` 完整块: x 4 x x 4 x 4 4 x 4 x 4 (骨架全是 4) 余 2 块: 2 2 2 4 x 4 x x 4 x 4 2 余 1 块: 2 2 2 2 2 2 2 4 x 4 x 2 ``` `55` 变成 `44`,就是这段 XOR 干的。题面里的「十六进制位运算」基本就是它。 XOR 完之后,洞里的值正好是明文 nibble `+4` 的某个排列。Hello 十个洞的多重集合和 `(n+4)%16` 对得死死的。 ## 3. 固定重排:先反转,再按洞对填 Hello 明文 nibble 下标到输出位置,用差分可以钉死 7 个映射,剩下 3 个都是值 `6`,落点集合是 `{3,10,18}`。把候选排列挨个试,能同时吃进「整串反转 + 按洞对填」的是下面这个: 把每块 6 个洞拆成三对: ``` A = (0, 2) B = (3, 5) C = (8, 10) ``` 填入顺序:**A,C,reverse(B)**,也就是 ``` 0, 2, 8, 10, 5, 3 ``` 余 2 字节两对 `(4,6)`、`(7,9)`,同样「第一对 + 反转第二对」:`4,6,9,7`。 余 1 字节就一对:`8,10`。 数据不是按块独立编码的。整串 nibble 先 `+4`,再**整体反转**,然后从第一块开始往洞里灌。所以 FLAG 的前缀 `flag` 其实坐在密文最末尾那一块里——全局反转的典型症状。 用这个规则编 Hello / abcd!,密文 **逐位一致**。 TLU、2026 还差一层只跟长度有关的 XOR,正好对应题面说的摘要扰动: ``` L=5: extra = 0 L=3: extra = 002200000000 L=4: extra = 00EE000000E00000000000E0 ``` FLAG 长度 34,没有同长度样例,这层 extra 公式没完全钉死。先按 L=5 那套主变换把数据抽出来,剩下的偏差再看。 ## 4. 主变换(不含长度 extra) 加密方向: 1. 明文转 hex nibble 2. 每个 nibble `+ 4 mod 16` 3. 整串反转 4. 每 3 字节一块,按 `A, C, rev(B)` 填进 12-hex 模板,骨架填 4(XOR `0011` 之后会变成密文里的 4/5) 5. 余 1 / 余 2 用各自模板 6. 全体 XOR `00110011...` 解密就是倒过来。Hello / abcd! 正反都能对上,主变换没问题。 ## 5. 解 FLAG 144 hex → 11 块完整 + 1 块余 1 → 34 字节。按主变换解密,原始字节是: ``` 66 6c 61 67 9b 74 51 95 7f 52 50 52 56 7f 4b 63 57 66 7f 43 92 99 90 94 50 7f 4d 54 93 94 65 92 21 9d ``` ASCII 看上去像: ``` flag . tQ . . RPRV . KcWf . C .... P . MT .. e . ! . ``` 前缀已经是 `flag`。把每个字节拆成高低半字节看,**所有不对劲的字节都是高半字节偏大 2,低半字节纹丝不动**,而且这些高半字节全是奇数(5/7/9): ``` 9b -> 7b { 74 -> 54 T 51 -> 31 1 95 -> 75 u 7f -> 5f _ 52 -> 32 2 50 -> 30 0 ... 9d -> 7d } ``` 偶数高半字节(`flag` 的 6、以及 K/c/f/C/M/e/! 那些 4 或 6)保持不动。对奇数高半字节减 2 之后: ``` flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` 这串完全可打印,也读得通:`2026`、`Kc7f` ≈ KCTF、`Crypt0` ≈ Crypto、`M4ster` ≈ Master。长度 extra 没从公式上推干净,但偏差形式非常规整,不像解错排列后的随机噪声。 ## 6. 完整脚本 把分析、加密校验、FLAG 解密放在一个文件里就能复现。 ```python # hexmaze_solve.py # python3.8 def hexnib(s): return [int(c, 16) for c in s] def tohex(arr): return "".join("0123456789ABCDEF"[x] for x in arr) def xor0011(nibs): return [x ^ ((i >> 1) & 1) for i, x in enumerate(nibs)] def block_fill_positions(n_bytes): """返回:输出总长度、数据洞的填入顺序、骨架位置及取值(XOR 0011 之前)。""" n_full, rem = divmod(n_bytes, 3) out_len = n_full * 12 + (12 if rem else 0) fill = [] skeleton = [None] * out_len for b in range(n_full): base = 12 * b # A, C, reverse(B) fill += [base + 0, base + 2, base + 8, base + 10, base + 5, base + 3] for t in (1, 4, 6, 7, 9, 11): skeleton[base + t] = 4 last = 12 * n_full if rem == 2: fill += [last + 4, last + 6, last + 9, last + 7] tmpl = [2, 2, 2, 4, None, 4, None, None, 4, None, 4, 2] for i, v in enumerate(tmpl): if v is not None: skeleton[last + i] = v elif rem == 1: fill += [last + 8, last + 10] tmpl = [2, 2, 2, 2, 2, 2, 2, 4, None, 4, None, 2] for i, v in enumerate(tmpl): if v is not None: skeleton[last + i] = v return out_len, fill, skeleton def encrypt(pt): nibs = hexnib(pt.encode().hex()) data = [(x + 4) % 16 for x in nibs] data = list(reversed(data)) out_len, fill, placed = block_fill_positions(len(pt)) assert len(fill) == len(data) for i, pos in enumerate(fill): placed[pos] = data[i] return tohex(xor0011(placed)) def decrypt_core(ct_hex, n_bytes): """主变换解密,不含长度 extra。""" ct = hexnib(ct_hex) placed = xor0011(ct) _, fill, _ = block_fill_positions(n_bytes) data = [placed[i] for i in fill] data = list(reversed(data)) raw = [(x - 4) % 16 for x in data] return bytes((raw[i] << 4) | raw[i + 1] for i in range(0, len(raw), 2)) def fix_odd_high(bs): """长度摘要残留:奇数高半字节减 2。""" out = bytearray(bs) for i, b in enumerate(out): hi, lo = b >> 4, b & 0xF if hi & 1: hi = (hi - 2) % 16 out[i] = (hi << 4) | lo return bytes(out) def printable(bs): return "".join(chr(b) if 32 <= b < 127 else "." for b in bs) if __name__ == "__main__": samples = [ ("TLU", "94AA48550495"), ("Hello", "34BB405504B5223594B94C53"), ("2026", "A48844556485223322356483"), ("abcd!", "547B475584B5223564BB4553"), ] print("=== 主变换加密校验 ===") for pt, ct in samples: got = encrypt(pt) xx = tohex([a ^ b for a, b in zip(hexnib(got), hexnib(ct))]) print("%r" % pt) print(" enc ", got) print(" exp ", ct) print(" xor ", xx, " match" if got == ct else " (长度 extra)") print("\n=== Hello vs abcd! 差分(摘要应消掉)===") h = hexnib(samples[1][1]) a = hexnib(samples[3][1]) print("ct xor 非零下标:", [i for i in range(len(h)) if h[i] != a[i]]) flag_ct = ( "14CC4655547594BC475584C5848A43551495448C445584C5" "D4C9475564C534A84B55A4B574BA4355F495A48844556485" "648C495534A5548C4F5584A5B4BB405554B522332235A4B3" ) n_bytes = 34 # 144 hex = 12 块 = 11*3 + 1 raw = decrypt_core(flag_ct, n_bytes) flag = fix_odd_high(raw) print("\n=== FLAG ===") print("raw hex :", raw.hex()) print("raw asc :", printable(raw)) print("fixed :", printable(flag)) print("FLAG :", flag.decode("ascii")) ``` 直接跑: ``` python hexmaze_solve.py ``` 输出里,Hello / abcd! 加密完全一致;TLU / 2026 会打出长度 extra;最后一行: ``` FLAG : flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` ## 7. 复盘一下走了哪些弯路 一开始把 12 hex 当成「每 3 字节独立分组加密」,用 TLU 去套 Hello 的前三个字母,排列对不上。差分已经说明数据是**整串一起重排**的,前缀 `Hel` 会跑到后面的洞里去。 第二个坑是输出长度 12 和 24 混在一起时,误以为排列依赖长度。其实排列规则是固定的:反转 + 洞对顺序不变,只是块数和最后一块模板随 `N mod 3` 变。 第三个坑是 TLU / 2026 对不上就怀疑 `+4` 错了。Hello 差分已经锁定 `+4`,对不上的是摘要。题面写了摘要只跟长度有关,L=5 两组样例 extra 为 0,拿它们校准主变换是对的。 FLAG 那层奇数高半字节减 2,本质上还是长度摘要在 34 这个长度上的残留,作用位置全是各块洞对的第二个 nibble(输出相对位置 2/3/10),全部落在明文的高半字节上。没有同长度样例,没把 extra 写成一个漂亮的闭式,但纠回来的明文已经足够唯一。 ## 8. FLAG ``` flag{T1u_2026_Kc7f_Crypt0_M4ster!} ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
lzq2000
6
发帖
6
回帖
47
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 wp by lzq2000
1038
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
41
[原创] 第八题:亥子合辰·塔影迷楼 wp by lzq2000
57
第七题:戌时·暗能潜流
21
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
1399
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部