-
-
[推荐]看雪CTF2026 第七题 WP
-
发表于: 2026-8-20 15:46 28
-
KCTF 2026 第七题 HexMaze Writeup
题目信息
题目给了一个固定密钥 121、四组已知明密文,以及一段 FLAG 密文:
TLU -> 94AA48550495
Hello -> 34BB405504B5223594B94C53
2026 -> A48844556485223322356483
abcd! -> 547B475584B5223564BB4553
提示里最有用的几句话是:算法只包含字节编码、十六进制位运算、模 16 循环偏移、固定位置重排和固定摘要扰动;摘要扰动只与输入长度有关。
最终恢复的 FLAG 为:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
下面记录完整分析过程。重点不是根据单词形状猜 FLAG,而是还原一个能够把四个样例和 FLAG 全部重新加密回原密文的等价算法。
1. 先看长度和分组
四组数据的长度关系是:
明文 3 bytes -> 密文 12 hex nibbles
明文 4 bytes -> 密文 24 hex nibbles
明文 5 bytes -> 密文 24 hex nibbles
即:
len(cipher_hex) = 12 * ceil(len(plain) / 3)
这个长度关系提示我们先按 3 个 ASCII 字节对应 12 个十六进制位来分组观察;后面的重加密会验证这一等价模型。最后不足 3 字节时,输出仍会补成完整块。
这里很容易因为“3 字节一组”先想到 Base64。我一开始也沿着这个方向看过,但同长度样例的差分说明它本质上是在搬运 ASCII 字节的高、低半字节,而不是 Base64 编码。
2. 用同长度样例消掉摘要扰动
Hello 和 abcd! 都是 5 字节,因此它们受到的长度摘要扰动完全相同。先写成 ASCII 十六进制:
Hello : 48 65 6C 6C 6F
abcd! : 61 62 63 64 21
两行各有 10 个 hex nibble,在 10 个对齐位置中正好有 7 个不同。再逐 nibble 比较两条密文,也正好只有 7 个位置不同。
更强的一点是:只取发生变化的位置,按 Hello - abcd! (mod 16) 计算差值,明文与密文得到的差值多重集完全相同:
{E, E, 7, 3, 9, 8, 4}
这强烈提示存在一种等价模型:明文 nibble 经过模 16 偏移后再做固定位置重排,另一半输出位置则是固定标记或长度摘要。这个判断还不能只靠一组差分下结论,后面要用全部样例和正向重加密继续验证。
把四组样例的 nibble 计数放在一起整理,可以构造出下面的字节展开,并由全部样例验证。每个真实字节 x = (h << 4) | l 对输出贡献四个值:
{ 5, (h + 5) mod 16, 4, (l + 4) mod 16 }
例如 ASCII 字符 o = 0x6F:
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。
对四组样例做同样检查后,分块关系变得非常整齐:
TLU -> ULT
Hello -> oll | eH
2026 -> 620 | 2
abcd! -> !dc | ba
所以算法的第一步是把整条 ASCII 字节串反转,再从左向右每 3 字节分块。等价地说,就是从原明文右端开始,每次取最多 3 字节,并把取出的字节也反过来。
这个细节很关键:只在原来的每个 3 字节块内部逆序,会在非 3 倍数长度的样例上得到错误结果。
4. 恢复单字节码字和固定路由
上一节只讨论无序的 nibble 多重集。为了让固定路由写起来更简洁,这里选取低半字节在前的等价有序表示。设一个字节的高、低 nibble 分别为 h、l,定义四 nibble 码字:
C(x) = [5, (l + 4) mod 16, 4, (h + 5) mod 16]
再定义两种固定走法:
T1(x) = C(x)
T2(x) = reverse(C(x))
对反转后的完整三字节块 q = [q0, q1, q2],字节槽的访问顺序是:
[0, 2, 1]
在本文采用的等价参数化中,可以把密钥 121 解释成依次选择三条路,即正向、反向、正向:
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:
C(U) = 594A
reverse(C(T)) = A485
C(L) = 5049
core = 594AA4855049
ROL1(core) = 94AA48550495
结果与题目逐字符一致。
这个路由也可以直接写成一张 nibble 位置 P-box。我在恢复时实际是把“输出位置 -> 输入位置”做匹配求出来的;不过上面的“正、反、正”写法更直观,也更容易看出本文对 121 的解释。
5. 长度摘要其实也能统一进同一条公式
尾块里会出现明显的 2/3 重复模式。把每块公共的 ROL1 撤销以后,可以发现每个缺失槽真正对应的固定摘要字是:
G = 3223
设当前块有 r 个真实字节,缺失字节数为 m = 3 - r。计算尾块 core 时,仍按槽序 [0,2,1] 访问,但过滤不存在的槽,并只使用 121 的前 r 个路由。随后在 core 前放 m 份 G,和完整块一样统一做一次 ROL1:
pre = G * m || core
block = ROL1(pre)
因此这里没有随机 IV,也不需要为尾块设计另一套加密算法。摘要内容固定,出现几次只由输入长度模 3 的结果决定,正好对应题目提示。
例如 Hello 全局反转后的尾块是 eH:
G = 3223
C(e) = 594B
reverse(C(H)) = 94C5
pre = 3223 | 594B | 94C5
ROL1(pre) = 223594B94C53
再看 2026 的尾块,里面只有字符 2,因此要补两份摘要:
pre = 3223 | 3223 | C(2)
= 3223 | 3223 | 5648
ROL1(pre) = 223322356483
同样与题目完全一致。
6. 密钥 121 中三个数字的作用
在只有 key=121 样例的前提下,可以把恢复出的常量写成如下与全部观测、以及题目“位运算”提示一致的参数化。令:
(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]
于是单字节码字就是:
C(x) = [HIGH, l + BASE, BASE, h + HIGH] (mod 16)
在本文采用的等价参数化中,三个 key digit 同时对应三个访问位置采用的路由:
1 -> 码字正向
2 -> 码字反向
1 -> 码字正向
需要严谨说明一点:题目只给了密钥 121,没有提供其他密钥下的样例,因此不可能从信息论上唯一证明作者对“任意密钥”的参数生成代码一定就是上面这几行。能严格闭环的是 key=121 时的加密函数;上述写法是与全部样例、题目提示以及可观测路由完全一致的自然参数化。
另外,码字中的 4、5 是恒定槽,摘要里也有重复的 2、3。固定置换若只交换这些永远相等的源位置,输出不会发生任何变化。因此仅凭明密文无法唯一还原作者源码里 P-box 数组的每个下标,但可以恢复与其对所有有效输入函数等价的变换。这不影响解密和重加密验证。
7. 逆向 FLAG
解密就是把上述步骤倒过来:
每 12 个 hex nibble 分成一块;
每块先
ROR1,撤销最后的循环移位;最后一块开头有几份
3223,就说明缺了几个字节;每四个 nibble 取一个码字,按
121的前r位撤销正反方向;从四 nibble 码字恢复原字节:
C(x) = [5, L', 4, H'] l = L' - 4 (mod 16) h = H' - 5 (mod 16) x = (h << 4) | l撤销槽序
[0,2,1](不存在的槽直接跳过),最后把整条字节流再次反转。
FLAG 最后一个密文块尤其直观:
22332235A4B3 --ROR1--> 3223 | 3223 | 5A4B
前面有两份摘要,所以最后一块只有一个真实字节;5A4B = C(f),恢复出反向字节流的最后一个字符 f。由 12 个密文块也可同时确定原文长度为:
11 * 3 + 1 = 34 bytes
逐块解出的反向字节流为:
}!r | ets | 4M_ | 0tp | yrC | _f7 |
cK_ | 620 | 2_u | 1T{ | gal | f
连接后整体反转,得到:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
8. 完整验证脚本
#!/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)
运行结果:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。