首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[推荐]看雪CTF2026 第七题 WP
发表于: 2026-8-20 15:46
24
[推荐]看雪CTF2026 第七题 WP
geekfire
2026-8-20 15:46
24
# KCTF 2026 第七题 HexMaze Writeup ## 题目信息 题目给了一个固定密钥 `121`、四组已知明密文,以及一段 FLAG 密文: ```text TLU -> 94AA48550495 Hello -> 34BB405504B5223594B94C53 2026 -> A48844556485223322356483 abcd! -> 547B475584B5223564BB4553 ``` 提示里最有用的几句话是:算法只包含字节编码、十六进制位运算、模 16 循环偏移、固定位置重排和固定摘要扰动;摘要扰动只与输入长度有关。 最终恢复的 FLAG 为: ```text flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` 下面记录完整分析过程。重点不是根据单词形状猜 FLAG,而是还原一个能够把四个样例和 FLAG 全部重新加密回原密文的等价算法。 ## 1. 先看长度和分组 四组数据的长度关系是: ```text 明文 3 bytes -> 密文 12 hex nibbles 明文 4 bytes -> 密文 24 hex nibbles 明文 5 bytes -> 密文 24 hex nibbles ``` 即: ```text len(cipher_hex) = 12 * ceil(len(plain) / 3) ``` 这个长度关系提示我们先按 3 个 ASCII 字节对应 12 个十六进制位来分组观察;后面的重加密会验证这一等价模型。最后不足 3 字节时,输出仍会补成完整块。 这里很容易因为“3 字节一组”先想到 Base64。我一开始也沿着这个方向看过,但同长度样例的差分说明它本质上是在搬运 ASCII 字节的高、低半字节,而不是 Base64 编码。 ## 2. 用同长度样例消掉摘要扰动 `Hello` 和 `abcd!` 都是 5 字节,因此它们受到的长度摘要扰动完全相同。先写成 ASCII 十六进制: ```text Hello : 48 65 6C 6C 6F abcd! : 61 62 63 64 21 ``` 两行各有 10 个 hex nibble,在 10 个对齐位置中正好有 7 个不同。再逐 nibble 比较两条密文,也正好只有 7 个位置不同。 更强的一点是:只取发生变化的位置,按 `Hello - abcd! (mod 16)` 计算差值,明文与密文得到的差值多重集完全相同: ```text {E, E, 7, 3, 9, 8, 4} ``` 这强烈提示存在一种等价模型:明文 nibble 经过模 16 偏移后再做固定位置重排,另一半输出位置则是固定标记或长度摘要。这个判断还不能只靠一组差分下结论,后面要用全部样例和正向重加密继续验证。 把四组样例的 nibble 计数放在一起整理,可以构造出下面的字节展开,并由全部样例验证。每个真实字节 `x = (h << 4) | l` 对输出贡献四个值: ```text { 5, (h + 5) mod 16, 4, (l + 4) mod 16 } ``` 例如 ASCII 字符 `o = 0x6F`: ```text h = 6, l = F 贡献为 {5, B, 4, 3} ``` 其中 `F + 4 = 3 (mod 16)`,正好体现了提示中的“模 16 循环偏移”。若临时按 `[5,h+5,4,l+4]` 排列,`T、L、U` 三组分别是 `5A48、5940、5A49`,合起来的 nibble 多重集与密文 `94AA48550495` 完全相同。每缺少一个字节,则会多出一组 `{2, 2, 3, 3}`。 到这里已经能解释密文中所有 nibble 的取值,但顺序还没有解决。 ## 3. 不是块内逆序,而是整条明文逆序 我先按原明文从左向右分块,发现每块的 nibble 多重集总对不上。例如 `Hello` 的第一块密文并不对应 `Hel`,反而对应末尾三个字节 `oll`。 对四组样例做同样检查后,分块关系变得非常整齐: ```text TLU -> ULT Hello -> oll | eH 2026 -> 620 | 2 abcd! -> !dc | ba ``` 所以算法的第一步是把整条 ASCII 字节串反转,再从左向右每 3 字节分块。等价地说,就是从原明文右端开始,每次取最多 3 字节,并把取出的字节也反过来。 这个细节很关键:只在原来的每个 3 字节块内部逆序,会在非 3 倍数长度的样例上得到错误结果。 ## 4. 恢复单字节码字和固定路由 上一节只讨论无序的 nibble 多重集。为了让固定路由写起来更简洁,这里选取低半字节在前的等价有序表示。设一个字节的高、低 nibble 分别为 `h`、`l`,定义四 nibble 码字: ```text C(x) = [5, (l + 4) mod 16, 4, (h + 5) mod 16] ``` 再定义两种固定走法: ```text T1(x) = C(x) T2(x) = reverse(C(x)) ``` 对反转后的完整三字节块 `q = [q0, q1, q2]`,字节槽的访问顺序是: ```text [0, 2, 1] ``` 在本文采用的等价参数化中,可以把密钥 `121` 解释成依次选择三条路,即正向、反向、正向: ```text core = T1(q0) || T2(q2) || T1(q1) block = ROL_nibble(core, 1) ``` 这里 `ROL_nibble(..., 1)` 表示把 12 个 hex nibble 整体向左循环移动一位,并不是做 12-bit 旋转。下文将它简写成 `ROL1`,逆操作简写成 `ROR1`。 拿最短的样例 `TLU` 验证。整条反转后为 `ULT`,固定槽序实际访问 `U、T、L`: ```text C(U) = 594A reverse(C(T)) = A485 C(L) = 5049 core = 594AA4855049 ROL1(core) = 94AA48550495 ``` 结果与题目逐字符一致。 这个路由也可以直接写成一张 nibble 位置 P-box。我在恢复时实际是把“输出位置 -> 输入位置”做匹配求出来的;不过上面的“正、反、正”写法更直观,也更容易看出本文对 `121` 的解释。 ## 5. 长度摘要其实也能统一进同一条公式 尾块里会出现明显的 `2/3` 重复模式。把每块公共的 `ROL1` 撤销以后,可以发现每个缺失槽真正对应的固定摘要字是: ```text G = 3223 ``` 设当前块有 `r` 个真实字节,缺失字节数为 `m = 3 - r`。计算尾块 `core` 时,仍按槽序 `[0,2,1]` 访问,但过滤不存在的槽,并只使用 `121` 的前 `r` 个路由。随后在 `core` 前放 `m` 份 `G`,和完整块一样统一做一次 `ROL1`: ```text pre = G * m || core block = ROL1(pre) ``` 因此这里没有随机 IV,也不需要为尾块设计另一套加密算法。摘要内容固定,出现几次只由输入长度模 3 的结果决定,正好对应题目提示。 例如 `Hello` 全局反转后的尾块是 `eH`: ```text G = 3223 C(e) = 594B reverse(C(H)) = 94C5 pre = 3223 | 594B | 94C5 ROL1(pre) = 223594B94C53 ``` 再看 `2026` 的尾块,里面只有字符 `2`,因此要补两份摘要: ```text pre = 3223 | 3223 | C(2) = 3223 | 3223 | 5648 ROL1(pre) = 223322356483 ``` 同样与题目完全一致。 ## 6. 密钥 `121` 中三个数字的作用 在只有 `key=121` 样例的前提下,可以把恢复出的常量写成如下与全部观测、以及题目“位运算”提示一致的参数化。令: ```text (a, b, c) = (1, 2, 1) BASE = a << b = 4 HIGH = BASE | c = 5 G = [b|c,b,b,b|c] = [3, 2, 2, 3] ``` 于是单字节码字就是: ```text C(x) = [HIGH, l + BASE, BASE, h + HIGH] (mod 16) ``` 在本文采用的等价参数化中,三个 key digit 同时对应三个访问位置采用的路由: ```text 1 -> 码字正向 2 -> 码字反向 1 -> 码字正向 ``` 需要严谨说明一点:题目只给了密钥 `121`,没有提供其他密钥下的样例,因此不可能从信息论上唯一证明作者对“任意密钥”的参数生成代码一定就是上面这几行。能严格闭环的是 `key=121` 时的加密函数;上述写法是与全部样例、题目提示以及可观测路由完全一致的自然参数化。 另外,码字中的 `4`、`5` 是恒定槽,摘要里也有重复的 `2`、`3`。固定置换若只交换这些永远相等的源位置,输出不会发生任何变化。因此仅凭明密文无法唯一还原作者源码里 P-box 数组的每个下标,但可以恢复与其对所有有效输入函数等价的变换。这不影响解密和重加密验证。 ## 7. 逆向 FLAG 解密就是把上述步骤倒过来: 1. 每 12 个 hex nibble 分成一块; 2. 每块先 `ROR1`,撤销最后的循环移位; 3. 最后一块开头有几份 `3223`,就说明缺了几个字节; 4. 每四个 nibble 取一个码字,按 `121` 的前 `r` 位撤销正反方向; 5. 从四 nibble 码字恢复原字节: ```text C(x) = [5, L', 4, H'] l = L' - 4 (mod 16) h = H' - 5 (mod 16) x = (h << 4) | l ``` 6. 撤销槽序 `[0,2,1]`(不存在的槽直接跳过),最后把整条字节流再次反转。 FLAG 最后一个密文块尤其直观: ```text 22332235A4B3 --ROR1--> 3223 | 3223 | 5A4B ``` 前面有两份摘要,所以最后一块只有一个真实字节;`5A4B = C(f)`,恢复出反向字节流的最后一个字符 `f`。由 12 个密文块也可同时确定原文长度为: ```text 11 * 3 + 1 = 34 bytes ``` 逐块解出的反向字节流为: ```text }!r | ets | 4M_ | 0tp | yrC | _f7 | cK_ | 620 | 2_u | 1T{ | gal | f ``` 连接后整体反转,得到: ```text flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` ## 8. 完整验证脚本 ```python #!/usr/bin/env python3 KEY = (1, 2, 1) a, b, c = KEY BASE = a << b HIGH = BASE | c G = [b | c, b, b, b | c] def C(x): h, l = x >> 4, x & 0xF return [HIGH, (l + BASE) & 0xF, BASE, (h + HIGH) & 0xF] def C_inv(w): assert w[0] == HIGH and w[2] == BASE l = (w[1] - BASE) & 0xF h = (w[3] - HIGH) & 0xF return (h << 4) | l def encrypt(s): raw = s.encode('ascii')[::-1] out = [] for off in range(0, len(raw), 3): q = list(raw[off:off + 3]) order = [i for i in (0, 2, 1) if i < len(q)] core = [] for route, index in zip(KEY, order): w = C(q[index]) core += w if route == 1 else w[::-1] z = G * (3 - len(q)) + core out += z[1:] + z[:1] # ROL1 return ''.join(f'{x:X}' for x in out) def decrypt(ct): v = [int(x, 16) for x in ct] assert len(v) % 12 == 0 blocks = [v[i:i + 12] for i in range(0, len(v), 12)] # ROR1 后,末块前面有几份 G,就缺几个字节。 last = blocks[-1][-1:] + blocks[-1][:-1] missing = 0 while missing < 2 and last[4 * missing:4 * missing + 4] == G: missing += 1 reversed_plain = [] for bi, y in enumerate(blocks): z = y[-1:] + y[:-1] # ROR1 m = missing if bi == len(blocks) - 1 else 0 assert z[:4 * m] == G * m core = z[4 * m:] width = 3 - m order = [i for i in (0, 2, 1) if i < width] q = [0] * width for j, (route, index) in enumerate(zip(KEY, order)): w = core[4 * j:4 * (j + 1)] q[index] = C_inv(w if route == 1 else w[::-1]) reversed_plain += q return bytes(reversed_plain[::-1]).decode('ascii') samples = { 'TLU': '94AA48550495', 'Hello': '34BB405504B5223594B94C53', '2026': 'A48844556485223322356483', 'abcd!': '547B475584B5223564BB4553', } flag_ct = ( '14CC4655547594BC475584C5848A43551495448C445584C5' 'D4C9475564C534A84B55A4B574BA4355F495A48844556485' '648C495534A5548C4F5584A5B4BB405554B522332235A4B3' ) for pt, ct in samples.items(): assert encrypt(pt) == ct assert decrypt(ct) == pt flag = decrypt(flag_ct) assert flag == 'flag{T1u_2026_Kc7f_Crypt0_M4ster!}' assert encrypt(flag) == flag_ct print(flag) ``` 运行结果: ```text flag{T1u_2026_Kc7f_Crypt0_M4ster!} ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
geekfire
10
发帖
4
回帖
143
RANK
关注
私信
他的文章
[推荐]看雪CTF2026 第9题WP
57
[推荐]看雪CTF2026 第8题WP
33
[推荐]看雪CTF2026 第七题 WP
24
KCTF 2026 第五题:申时·忆海倒带 WP
1504
[推荐]第二题:巳时·绿光幽语 WP
1817
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部