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

KCTF2026: 第七题:戌时·暗能潜流

1天前
195

FLAG = flag{T1u_2026_Kc7f_Crypt0_M4ster!}

本文所有断言均由同目录下的 wp_experiments.py 实测输出支撑。运行方式:

脚本末尾输出 ALL EXPERIMENTS PASSED 表示 4 个样例逐字符复现 + FLAG round-trip 全部通过。

已知 4 组明文/密文样例,以及待解的 FLAG 密文,密钥为 121

密文长度 = ceil(明文长度 / 3) × 12。3 个字符对应 12 个 hex,即一个字符 4 个 hex(8 bit 的字符被摊成了 4 个半字节,其中只有 2 个携带信息)。
FLAG 密文 144 hex ⇒ 12 个块 ⇒ 明文 34~36 字符。

TLU(唯一的纯满块样例)的密文按位看(实验 0 输出):

把 4 个样例的所有 12-hex 满块对齐后,下标 1, 4, 6, 7, 9, 11 恒为 4,4,5,5,4,5,其余 6 个下标随明文变化。脚本实验 0 用两个内容完全不同的满块逐位取交集,直接实测出该模板:

24-hex 样例的第二块都以固定串开头:Hello/abcd! 的第二块是 2235…2026 的第二块是 22332235…。这两个前缀与明文内容无关。

模板里 6 个恒定槽位的取值只有 45 两种。假设算法是「往 12 个半字节槽里写值,再对每个槽加一个固定偏移」,那么恒定槽的原始值必然是同一个常量。取该常量 = 0,则恒定槽的输出值本身就是偏移

只有 4 和 5 两个值,而 12 个槽分成 3 组、每组 4 个的话,偏移应该以 4 为周期。要让槽 11 拿到 5、槽 1 拿到 4、槽 4 拿到 4、槽 6 拿到 5、槽 7 拿到 5、槽 9 拿到 4 且三组用同一套「格内位置 p → 偏移」的映射,唯一自洽的分组是从槽 11 起、回绕的分组 —— 实验 0【推导 A】输出:

三个格子各自恰好有 2 个空位(值 0)和 2 个数据位。**cell0 与 cell2 的空位在 p=0,2,数据位在 p=1,3;cell1 反过来,空位在 p=1,3,数据位在 p=0,2。**这个「1 号格子和另外两个不一样」的结构,正是密钥 121 中间那位与两边不同的直接体现。

(该分组的唯一性由实验 4 的 shift 穷举反过来确证:12 种分组起点里只有 shift=11 能复现样例。)

TLU 为例,把 6 个数据槽减掉各自偏移(实验 0【推导 B】输出):

刚好还原出 TLU。于是确认:每个格子存放一个字符,低半字节和高半字节各占一个数据位,另外两个位填 0。

同时暴露出两件事:

Hello 的密文是 34BB405504B5 + 223594B94C53。带固定前缀 2235 的是第二块,说明「不足 3 字符的残块」被放在了密文的末尾;而残块必然来自明文的开头(He)。反推得:

用样例逐一验证:Hello 首块明文 llo2026 首块 026abcd! 首块 cd!
交叉验证一个强证据:2026 的首块密文 A48844556485 与 FLAG 密文的第 8 块(下标 7)完全相同,而 FLAG 中恰好含有子串 026flag{T1u_2026_…)且对齐到同一块边界。

残块的前面若干个格子整体不用。这些格子里填的不是 0(实验 4 中令 FILL = 0x0Hello 的密文变成 34BB405504B5445594B94C55,残块前缀变为 4455,与样例不符),实测填的是半字节 0xE

一个填充格子按位置 p=0..3 产生 3,2,2,3。cell0 的 p=0 落在回绕的槽 11 上,所以那个 3 跑到了整块的最后一位,可见前缀由 p=1,2,3 → 2,2,3 构成(实验 4 输出):

这就是 2235 / 22332235 前缀的完整来源。没有填充字节进入明文,也没有任何链式状态(每个块独立编码,同样的 3 字符在任何位置都产生同样的 12 hex——0262026 和在 FLAG 中密文一致即为证)。

块内 3 个字符按消息序记为 a、b、c,则 12 个槽为:

实验 6 对 855 个块穷举核对该公式与实现完全一致:

密钥的三位数字 不是加法/异或密钥,而是「走线选择器」:第 j 位数字 d 决定第 j 个被使用的格子里,字符的低/高半字节各落在哪两个格内位置:

因为 CELL_OFF = (5,4,4,5),四种取值展开后是:

改一位数字,恰好且仅有那 4 个槽位发生变化,其余 8 个槽一字不变;且 1→2 的改动等价于把这个格子的 4-hex 码整体反转(594AA495)。

两式完全互为逆序。实测(实验 3)对全部 95 个可打印字符验证:

对照表节选(实验 3 输出,逐行摘录):

reverse(d=1) 列与 d=2 列逐字符相等。所以密钥 1-2-1 的语义是:第 1、3 个格子正着走,第 2 个格子倒着走 —— 一条在迷宫里蛇形(boustrophedon)折返的路径。这正是题名 HexMaze 的含义。

槽 3~6 由 A485 变成 584A —— 同一个字符 T 的 4-hex 码被整体反转,正好把「换向」取消掉了。

槽 7~10 由 5049 变成 9405,同样是 4-hex 码被整体反转(字符 L = 0x4C)。

实验 1 暴力枚举全部 4³ = 64 种 (d0, d1, d2)(每位取 0..3),逐个跑 4 个样例:

只有 (1,2,1) 能全部复现,62 种组合一个样例都命中不了。

逐位穷举(其余两位保持正确):

表中唯一的非零干扰项是「第 1 位取 3 时命中 1/4」。原因可精确解释:d(d+2) mod 4 只是把 hi/lo 对调,当写入该格子的所有字符恰好满足 hi == lo 时,d=1d=3 不可区分。实测逐样例核对:

'U' = 0x55 是唯一的 hi==lo 字符,所以只有 TLU 会被 321 蒙对;只要再加任何一个样例,d0=3 立刻被排除。样例集足以把密钥的每一位唯一钉死。

密钥之外还有 5 项结构约定(块序倒置、CELL_OFFFILL、块内左旋、格子分组起点)。实验 4 / 实验 5 证明它们同样被样例唯一钉死:

块序倒置(必需)

CELL_OFF = (5,4,4,5)(16⁴ 全穷举唯一)

FILL = 0xE(16 种全穷举唯一)

块内左旋 1 格(0/1/2 穷举唯一)

格子分组必须回绕(12 种起点穷举唯一)

联合唯一性(避免「一个参数错了另一个参数补回来」的可能)——同时枚举 KEY(64) × 分组起点(12) × 旋转(3) = 2304 种组合:

对 144-hex 密文解密,得到 12 个块(实验 7 输出):

第 7 块与样例 2026 的首块密文逐字符相同,是「每块独立编码、无链式状态」的直接证据。

验证(实验 7):

34 个字符全部可打印,长度与密文长度自洽,重新加密逐字符等于原密文。

wp_experiments.py —— 参数化实现 + 7 组消融/唯一性实验,直接 python3 wp_experiments.py 运行(约 0.5 秒),本文引用的所有输出片段均来自该脚本的真实输出。

明文 长度 密文 密文长度
TLU 3 94AA48550495 12
Hello 5 34BB405504B5223594B94C53 24
2026 4 A48844556485223322356483 24
abcd! 5 547B475584B5223564BB4553 24
FLAG ? 14CC4655…5A4B3(144 hex) 144
槽位 1 4 6 7 9 11
恒定输出 4 4 5 5 4 5
d 格内布局 (p=0,1,2,3) 加偏移后的 4-hex 码
0 lo, 0, hi, 0 lo+5 | '4' | hi+4 | '5'
1 0, lo, 0, hi '5' | lo+4 | '4' | hi+5
2 hi, 0, lo, 0 hi+5 | '4' | lo+4 | '5'
3 0, hi, 0, lo '5' | hi+4 | '4' | lo+5
密钥位 决定的对象 满块中控制的 hex 槽 残块 k=2 残块 k=1 语义
第 1 位 1 第 0 个被使用的格子 0, 1, 2, 11(回绕) 3,4,5,6 7,8,9,10 正向走线:'5' | lo+4 | '4' | hi+5
第 2 位 2 第 1 个被使用的格子 3, 4, 5, 6 7,8,9,10 不参与 反向走线(第 1 位的逆序):hi+5 | '4' | lo+4 | '5'
第 3 位 1 第 2 个被使用的格子 7, 8, 9, 10 不参与 不参与 正向走线,与第 1 位相同
实验 内容
0 基准:4 样例逐字符复现 + 实测满块模板
1 密钥 4³=64 组合暴力,唯一解 (1,2,1)
2 逐位消融 121→221/111/122,定位每位控制的 hex 槽位
3 d=1 与 d=2 的 4-hex 码互为逆序(95 字符全验证)
4 块序 / CELL_OFF(16⁴) / FILL(16) / 旋转(3) / 分组起点(12) 的必要性与唯一性
5 联合唯一性 KEY×分组×旋转 = 2304 组合,唯一解
6 满块展开公式与实现的 855 块穷举核对
7 最终断言:4 样例 + FLAG round-trip
python3 wp_experiments.py
    密文 94AA48550495   下标  0  1  2  3  4  5  6  7  8  9 10 119  4  A  A  4  8  5  5  0  4  9  5
  满块模板 (a,b,c 三个字符): ?4??4?55?4?5
  -> 与题面观察到的 '?4??4?55?4?5' 一致
  【推导 A】恒定槽的取值就是偏移 CELL_OFF
    恒定槽位 = [1, 4, 6, 7, 9, 11],取值 = ['4', '4', '5', '5', '4', '5']
    cell0 = 槽(11, 0, 1, 2)  恒定位: 槽11(p=0)='5', 槽1(p=2)='4'
    cell1 = 槽(3, 4, 5, 6)  恒定位: 槽4(p=1)='4', 槽6(p=3)='5'
    cell2 = 槽(7, 8, 9, 10)  恒定位: 槽7(p=0)='5', 槽9(p=2)='4'
    -> 每格恰好 2 个恒定位,且格内位置 p 与取值一一对应: CELL_OFF = (5, 4, 4, 5)
  【推导 B】'TLU'6 个数据槽减掉偏移即还原出 T/L/U
    密文 94AA48550495   下标  0  1  2  3  4  5  6  7  8  9 10 119  4  A  A  4  8  5  5  0  4  9  5
    lo(c) = hex[0]='9' - 4 = 5
    hi(c) = hex[2]='A' - 5 = 5
    hi(a) = hex[3]='A' - 5 = 5
    lo(a) = hex[5]='8' - 4 = 4
    lo(b) = hex[8]='0' - 4 = C
    hi(b) = hex[10]='9' - 5 = 4
    -> a=0x54='T'  b=0x4C='L'  c=0x55='U'   拼回 = 'TLU'
    cell0 存的是块内第 3 个字符, cell1 存第 1 个, cell2 存第 2 个 -> 块内字符左旋一格
0xE + CELL_OFF = 0xE + (5,4,4,5) = (3,2,2,3)
  残块前缀由来:一个被填充的格子按位置 p=0..3 产生 0xE+CELL_OFF = [3, 2, 2, 3] = 3223
    该格子的 p=0 落在**回绕**的槽位上,所以 cell0 的 '3' 跑到了整块的最后一位 (槽11),
    可见的前缀由 p=1,2,3 -> '2','2','3' 组成:
      k=2 残块 ("He"): 223594B94C53   槽0..2 = '223' (填充) + 槽3 = '5' (第 1 位 d=10 值位) + 槽11 = '3'
      k=1 残块 ("2") : 2233223564830..6 = '2233223' (两个填充格) + 槽7 = '5' + 槽11 = '3'
参数:KEY = (1, 2, 1)                     # 密钥 121 的三位数字
     CELL_OFF   = (5, 4, 4, 5)           # 格内位置 p=0..3 的固定加法偏移(模 16)
     CELL_SLOTS = [(11,0,1,2), (3,4,5,6), (7,8,9,10)]   # 3 个格子占用的 hex 槽(回绕)
     FILL = 0xE                          # 未使用格子的填充半字节
槽0 = lo(c)+4    槽1 = 0+4     槽2  = hi(c)+5    槽3  = hi(a)+5
槽4 = 0+4        槽5 = lo(a)+4 槽6  = 0+5        槽7  = 0+5
槽8 = lo(b)+4    槽9 = 0+4     槽10 = hi(b)+5    槽11 = 0+5
  'TLU'  公式 94AA48550495   实现 94AA48550495   一致
  'llo'  公式 34BB405504B5   实现 34BB405504B5   一致
  '026'  公式 A48844556485   实现 A48844556485   一致
  'cd!'  公式 547B475584B5   实现 547B475584B5   一致
  对 95x3x3 = 855 个块穷举核对: True
低半字节  -> 格内位置 d
高半字节  -> 格内位置 (d + 2) mod 4
其余两个位置 -> 0
  121 -> 221  (只改第 1 位)
    第一个不匹配的样例: 'TLU'
      期望 = 94AA48550495
      得到 = 495A4855049A
      标记 = ^^^        ^
      首个不匹配位置 = hex[0],全部不匹配位置 = [0, 1, 2, 11]
    第 1 位数字在 k=3 满块中控制的槽位: [0, 1, 2, 11]   (= 第 0 号格子 (11, 0, 1, 2))
    在 k=2 残块中控制的槽位: [3, 4, 5, 6]
    在 k=1 残块中控制的槽位: [7, 8, 9, 10]
    'TLU' 中第 0 号格子(槽(11, 0, 1, 2)) 的 4-hex 码: '594A' -> 'A495'   互为逆序? True
d=1: 位置(1)=lo, 位置(3)=hi ->  [0+5, lo+4, 0+4, hi+5] = '5' | lo+4 | '4' | hi+5
d=2: 位置(2)=lo, 位置(0)=hi ->  [hi+5, 0+4, lo+4, 0+5] = hi+5 | '4' | lo+4 | '5'

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

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