首页
社区
课程
招聘
第七题:戌时·暗能潜流
发表于: 4天前 15

第七题:戌时·暗能潜流

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=4k=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 21 块:  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!}

这串完全可打印,也读得通:2026Kc7f ≈ 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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回