-
-
第七题:戌时·暗能潜流
-
发表于: 4天前 15
-
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)
加密方向:
- 明文转 hex nibble
- 每个 nibble
+ 4 mod 16 - 整串反转
- 每 3 字节一块,按
A, C, rev(B)填进 12-hex 模板,骨架填 4(XOR0011之后会变成密文里的 4/5) - 余 1 / 余 2 用各自模板
- 全体 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 解密放在一个文件里就能复现。
# 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!}
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。