首页
社区
课程
招聘
[原创]看雪·2026 KCTF 第七题:戌时·暗能潜流 WP (AI)
发表于: 4天前 16

[原创]看雪·2026 KCTF 第七题:戌时·暗能潜流 WP (AI)

4天前
16

本题由 GPT-5.6-Sol (Max effort) + Codex 完成, 耗时约 2 小时
Claude Opus 5 (xhigh effort) + CC 完成的时间为 1 小时 23 分 28 秒, 下文为 GPT 写的 WP

0x00 题目信息

题目给出密钥 121、四组已知明文/密文以及一段待恢复的 FLAG 密文:

TLU    -> 94AA48550495
Hello  -> 34BB405504B5223594B94C53
2026   -> A48844556485223322356483
abcd!  -> 547B475584B5223564BB4553

FLAG 密文:

14CC4655547594BC475584C5848A43551495448C445584C5
D4C9475564C534A84B55A4B574BA4355F495A48844556485
648C495534A5548C4F5584A5B4BB405554B522332235A4B3

题目说明明文按 ASCII 字节处理,算法只包含字节编码、十六进制位运算、模 16 循环偏移、固定位置重排和固定摘要扰动,并提示摘要扰动只与长度有关。

最终恢复结果:

flag{T1u_2026_Kc7f_Crypt0_M4ster!}

0x01 从密文长度判断分组大小

先统计明文和密文长度:

明文 明文字节数 密文十六进制字符数
TLU 3 12
Hello 5 24
2026 4 24
abcd! 5 24

可以得到:

len(ciphertext) = 12 * ceil(len(plaintext) / 3)

说明算法以 3 个 ASCII 字节为一个逻辑块,每块固定输出 12 个十六进制半字节。不足 3 字节的部分仍然输出完整的一块。

进一步比较样例可知,算法保留最左侧余数块,然后将完整三字节块从右向左输出:

Hello = He | llo
2026  = 2  | 026
abcd! = ab | cd!

对应密文顺序:

enc(llo) | enc(He)
enc(026) | enc(2)
enc(cd!) | enc(ab)

因此密文块:

A48844556485

对应的是 026,不是 202


0x02 密钥 121 的数值作用

将密钥拆成三个十进制数字:

k0 = 1
k1 = 2
k2 = 1

定义:

S = k0 + k1 + k2 = 4

对于一个 ASCII 字节 b,先拆分高、低半字节:

H = b >> 4
L = b & 0x0F

然后进行模 16 偏移:

ZH = (H + S + k0) mod 16
   = (H + 5) mod 16

ZL = (L + S) mod 16
   = (L + 4) mod 16

普通字节还对应两类固定摘要值:

D0 = S      = 4
D1 = S + k2 = 5

不足 3 字节时使用的长度标记为:

P0 = k1      = 2
P1 = k1 + k2 = 3

所以三个密钥数字在可观测等价算法中的作用为:

  • 第一个 1
    • 参与公共偏移 S
    • 额外作用于高半字节,使其偏移为 S + 1 = 5
  • 中间的 2
    • 参与公共偏移 S
    • 作为不足 3 字节时的基础长度标记 2
  • 最后的 1
    • 参与公共偏移 S
    • 把固定摘要标记从 4 提升到 5
    • 把长度标记从 2 提升到 3

普通字节和空缺位置可以分别理解为贡献:

普通字节信息:[ZH, ZL, 4, 5]
空缺位置扰动:[2, 2, 3, 3]

随后固定位置重排会将它们打散。


0x03 用样例恢复模 16 偏移

TLU 为例:

T = 0x54 -> (5 + 5, 4 + 4) mod 16 = (A, 8)
L = 0x4C -> (4 + 5, C + 4) mod 16 = (9, 0)
U = 0x55 -> (5 + 5, 5 + 4) mod 16 = (A, 9)

得到六个有效半字节:

A 8 9 0 A 9

密文 94AA48550495 在位置:

0, 2, 3, 5, 8, 10

的值为:

9 A A 8 0 9

两者多重集合相同,只发生了位置重排。

继续验证其余样例的最右侧完整三字节块:

完整块 偏移后的六个半字节 密文有效半字节
TLU A890A9 9AA809
llo B0B0B3 3BB00B
026 84868A A88468
cd! B7B875 57B78B

四组数据可以唯一对齐六个有效位置。


0x04 完整三字节块的固定重排

设三个字节经偏移后依次得到:

Z0 Z1 Z2 Z3 Z4 Z5

其中:

第一个字节:Z0 Z1
第二个字节:Z2 Z3
第三个字节:Z4 Z5

由样例对齐得到:

C0  = Z5
C2  = Z4
C3  = Z0
C5  = Z1
C8  = Z3
C10 = Z2

其余位置是固定摘要值:

C1  = 4
C4  = 4
C6  = 5
C7  = 5
C9  = 4
C11 = 5

因此完整三字节块的加密模板为:

T3(Z0..Z5) =

Z5 4 Z4 Z0 4 Z1 5 5 Z3 4 Z2 5

即:

[Z5, 4, Z4, Z0, 4, Z1, 5, 5, Z3, 4, Z2, 5]

逆重排为:

Z0 = C3
Z1 = C5
Z2 = C10
Z3 = C8
Z4 = C2
Z5 = C0

然后逆偏移:

H = (ZH - 5) mod 16
L = (ZL - 4) mod 16
b = (H << 4) | L

0x05 长度摘要扰动

剩余一个字节

设该字节产生 Z0 Z1,模板为:

T1(Z0,Z1) =

2 2 3 3 2 2 3 5 Z1 4 Z0 3

即:

[2, 2, 3, 3, 2, 2, 3, 5, Z1, 4, Z0, 3]

2026 最左侧的剩余字符 2 为例:

2 = 0x32
Z0 = 3 + 5 = 8
Z1 = 2 + 4 = 6

代入模板:

223322356483

与题目一致。

FLAG 最左侧字符 f

f = 0x66
Z0 = B
Z1 = A

代入后得到:

22332235A4B3

正好等于 FLAG 密文的最后一块。

剩余两个字节

设两个字节产生:

Z0 Z1 Z2 Z3

模板为:

T2(Z0..Z3) =

2 2 3 5 Z3 4 Z2 Z0 4 Z1 5 3

即:

[2, 2, 3, 5, Z3, 4, Z2, Z0, 4, Z1, 5, 3]

验证 Hello 的剩余块 He

H = 0x48 -> Z0 Z1 = 9 C
e = 0x65 -> Z2 Z3 = B 9

代入:

223594B94C53

验证 abcd! 的剩余块 ab

a = 0x61 -> B 5
b = 0x62 -> B 6

代入:

223564BB4553

均与题目一致。

由此可知,摘要扰动只与块长度有关:

  • 完整块使用固定 4/5 摘要位置;
  • 一个、两个剩余字节使用固定的 2/3/4/5 模板;
  • 不涉及随机数、IV 或外部状态。

0x06 FLAG 逐块解密

FLAG 密文每 12 个字符切分:

00  14CC46555475
01  94BC475584C5
02  848A43551495
03  448C445584C5
04  D4C9475564C5
05  34A84B55A4B5
06  74BA4355F495
07  A48844556485
08  648C495534A5
09  548C4F5584A5
10  B4BB405554B5
11  22332235A4B3

前 11 块按完整模板解密,最后一块按单字节模板解密:

密文序号 密文块 解出的明文块
00 14CC46555475 r!}
01 94BC475584C5 ste
02 848A43551495 _M4
03 448C445584C5 pt0
04 D4C9475564C5 Cry
05 34A84B55A4B5 7f_
06 74BA4355F495 _Kc
07 A48844556485 026
08 648C495534A5 u_2
09 548C4F5584A5 {T1
10 B4BB405554B5 lag
11 22332235A4B3 f

密文块顺序和明文块顺序相反,逆序拼接:

f
+ lag
+ {T1
+ u_2
+ 026
+ _Kc
+ 7f_
+ Cry
+ pt0
+ _M4
+ ste
+ r!}

得到:

flag{T1u_2026_Kc7f_Crypt0_M4ster!}

需要注意:

  • T1u 中是数字 1
  • Kc7f 大小写如上;
  • Crypt0 末尾是数字 0
  • M4ster 中是数字 4

0x07 完整解题脚本

完整脚本也保存在同目录的 solve.py

#!/usr/bin/env python3
"""KCTF 2026 T7 / HexMaze encryptor and decryptor."""

from __future__ import annotations


KEY = (1, 2, 1)

K0, K1, K2 = KEY
BASE = K0 + K1 + K2

HI_SHIFT = BASE + K0
LO_SHIFT = BASE

DIGEST_0 = BASE
DIGEST_1 = BASE + K2

PAD_0 = K1
PAD_1 = K1 + K2


def shifted_nibbles(block: bytes) -> list[int]:
    result: list[int] = []
    for value in block:
        high = value >> 4
        low = value & 0x0F
        result.append((high + HI_SHIFT) & 0x0F)
        result.append((low + LO_SHIFT) & 0x0F)
    return result


def encrypt_block(block: bytes) -> str:
    z = shifted_nibbles(block)

    if len(block) == 3:
        z0, z1, z2, z3, z4, z5 = z
        out = [
            z5, DIGEST_0, z4, z0,
            DIGEST_0, z1, DIGEST_1, DIGEST_1,
            z3, DIGEST_0, z2, DIGEST_1,
        ]
    elif len(block) == 2:
        z0, z1, z2, z3 = z
        out = [
            PAD_0, PAD_0, PAD_1, DIGEST_1,
            z3, DIGEST_0, z2, z0,
            DIGEST_0, z1, DIGEST_1, PAD_1,
        ]
    elif len(block) == 1:
        z0, z1 = z
        out = [
            PAD_0, PAD_0, PAD_1, PAD_1,
            PAD_0, PAD_0, PAD_1, DIGEST_1,
            z1, DIGEST_0, z0, PAD_1,
        ]
    else:
        raise ValueError("block length must be 1, 2, or 3")

    return "".join(f"{value:X}" for value in out)


def split_plaintext(data: bytes) -> list[bytes]:
    if not data:
        return []

    remainder = len(data) % 3
    chunks: list[bytes] = []
    offset = 0

    if remainder:
        chunks.append(data[:remainder])
        offset = remainder

    while offset < len(data):
        chunks.append(data[offset:offset + 3])
        offset += 3

    return chunks


def encrypt(text: str) -> str:
    chunks = split_plaintext(text.encode("ascii"))
    return "".join(encrypt_block(block) for block in reversed(chunks))


def restore_byte(zh: int, zl: int) -> int:
    high = (zh - HI_SHIFT) & 0x0F
    low = (zl - LO_SHIFT) & 0x0F
    return (high << 4) | low


def decode_full_block(block: list[int]) -> bytes:
    z0 = block[3]
    z1 = block[5]
    z2 = block[10]
    z3 = block[8]
    z4 = block[2]
    z5 = block[0]
    return bytes([
        restore_byte(z0, z1),
        restore_byte(z2, z3),
        restore_byte(z4, z5),
    ])


def decode_two_byte_block(block: list[int]) -> bytes:
    z0 = block[7]
    z1 = block[9]
    z2 = block[6]
    z3 = block[4]
    return bytes([
        restore_byte(z0, z1),
        restore_byte(z2, z3),
    ])


def decode_one_byte_block(block: list[int]) -> bytes:
    z0 = block[10]
    z1 = block[8]
    return bytes([restore_byte(z0, z1)])


def matches(block: list[int], fixed: dict[int, int]) -> bool:
    return all(block[index] == value for index, value in fixed.items())


ONE_BYTE_FIXED = {
    0: PAD_0,
    1: PAD_0,
    2: PAD_1,
    3: PAD_1,
    4: PAD_0,
    5: PAD_0,
    6: PAD_1,
    7: DIGEST_1,
    9: DIGEST_0,
    11: PAD_1,
}

TWO_BYTE_FIXED = {
    0: PAD_0,
    1: PAD_0,
    2: PAD_1,
    3: DIGEST_1,
    5: DIGEST_0,
    8: DIGEST_0,
    10: DIGEST_1,
    11: PAD_1,
}


def decrypt(ciphertext: str) -> str:
    if len(ciphertext) % 12 != 0:
        raise ValueError("ciphertext length must be a multiple of 12")

    blocks = [
        [int(ch, 16) for ch in ciphertext[i:i + 12]]
        for i in range(0, len(ciphertext), 12)
    ]

    decoded_in_cipher_order: list[bytes] = []
    for index, block in enumerate(blocks):
        is_last = index == len(blocks) - 1

        if is_last and matches(block, ONE_BYTE_FIXED):
            decoded = decode_one_byte_block(block)
        elif is_last and matches(block, TWO_BYTE_FIXED):
            decoded = decode_two_byte_block(block)
        else:
            decoded = decode_full_block(block)

        decoded_in_cipher_order.append(decoded)

    plaintext = b"".join(reversed(decoded_in_cipher_order))
    return plaintext.decode("ascii")


SAMPLES = {
    "TLU": "94AA48550495",
    "Hello": "34BB405504B5223594B94C53",
    "2026": "A48844556485223322356483",
    "abcd!": "547B475584B5223564BB4553",
}

FLAG_CIPHERTEXT = (
    "14CC4655547594BC475584C5848A43551495448C445584C5"
    "D4C9475564C534A84B55A4B574BA4355F495A48844556485"
    "648C495534A5548C4F5584A5B4BB405554B522332235A4B3"
)


def main() -> None:
    for plaintext, expected_ciphertext in SAMPLES.items():
        assert encrypt(plaintext) == expected_ciphertext
        assert decrypt(expected_ciphertext) == plaintext

    flag = decrypt(FLAG_CIPHERTEXT)
    assert flag == "flag{T1u_2026_Kc7f_Crypt0_M4ster!}"
    assert encrypt(flag) == FLAG_CIPHERTEXT
    print(flag)


if __name__ == "__main__":
    main()

执行结果:

flag{T1u_2026_Kc7f_Crypt0_M4ster!}

脚本不仅解密 FLAG,还会重新加密四组样例与 FLAG。全部断言通过,说明恢复出的偏移、长度模板、位置重排和块顺序可以完整复现题目算法。


0x08 最终答案

flag{T1u_2026_Kc7f_Crypt0_M4ster!}

冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 4天前 被DNLINYJ编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回