首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
KCTF2026: 第七题:戌时·暗能潜流
发表于: 2026-8-20 15:37
653
KCTF2026: 第七题:戌时·暗能潜流
Magic丶
2026-8-20 15:37
653
# KCTF07「HexMaze」WriteUp(密钥 121) **FLAG = `flag{T1u_2026_Kc7f_Crypt0_M4ster!}`** 本文所有断言均由同目录下的 `wp_experiments.py` 实测输出支撑。运行方式: ```text python3 wp_experiments.py ``` 脚本末尾输出 `ALL EXPERIMENTS PASSED` 表示 4 个样例逐字符复现 + FLAG round-trip 全部通过。 --- ## 1. 题目信息与观察 已知 4 组明文/密文样例,以及待解的 FLAG 密文,密钥为 `121`: | 明文 | 长度 | 密文 | 密文长度 | |---|---|---|---| | `TLU` | 3 | `94AA48550495` | 12 | | `Hello` | 5 | `34BB405504B5223594B94C53` | 24 | | `2026` | 4 | `A48844556485223322356483` | 24 | | `abcd!` | 5 | `547B475584B5223564BB4553` | 24 | | FLAG | ? | `14CC4655…5A4B3`(144 hex) | 144 | ### 观察 1:长度规律 密文长度 = `ceil(明文长度 / 3) × 12`。3 个字符对应 12 个 hex,即**一个字符 4 个 hex**(8 bit 的字符被摊成了 4 个半字节,其中只有 2 个携带信息)。 FLAG 密文 144 hex ⇒ 12 个块 ⇒ 明文 34~36 字符。 ### 观察 2:满块模板 `TLU`(唯一的纯满块样例)的密文按位看(实验 0 输出): ``` 密文 94AA48550495 下标 0 1 2 3 4 5 6 7 8 9 10 11 值 9 4 A A 4 8 5 5 0 4 9 5 ``` 把 4 个样例的所有 12-hex 满块对齐后,下标 **1, 4, 6, 7, 9, 11** 恒为 `4,4,5,5,4,5`,其余 6 个下标随明文变化。脚本实验 0 用两个内容完全不同的满块逐位取交集,直接实测出该模板: ``` 满块模板 (a,b,c 三个字符): ?4??4?55?4?5 -> 与题面观察到的 '?4??4?55?4?5' 一致 ``` ### 观察 3:残块前缀 24-hex 样例的**第二块**都以固定串开头:`Hello`/`abcd!` 的第二块是 `2235…`,`2026` 的第二块是 `22332235…`。这两个前缀与明文内容无关。 --- ## 2. 分析思路 ### 2.1 从模板推出「空槽 + 定值偏移」 模板里 6 个恒定槽位的取值只有 `4` 和 `5` 两种。假设算法是「往 12 个半字节槽里写值,再对每个槽加一个固定偏移」,那么恒定槽的原始值必然是同一个常量。取该常量 = 0,则恒定槽的**输出值本身就是偏移**: | 槽位 | 1 | 4 | 6 | 7 | 9 | 11 | |---|---|---|---|---|---|---| | 恒定输出 | 4 | 4 | 5 | 5 | 4 | 5 | 只有 4 和 5 两个值,而 12 个槽分成 3 组、每组 4 个的话,偏移应该以 4 为周期。要让槽 11 拿到 5、槽 1 拿到 4、槽 4 拿到 4、槽 6 拿到 5、槽 7 拿到 5、槽 9 拿到 4 且三组用同一套「格内位置 p → 偏移」的映射,唯一自洽的分组是**从槽 11 起、回绕的**分组 —— 实验 0【推导 A】输出: ``` 【推导 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) ``` 三个格子各自恰好有 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 能复现样例。) ### 2.2 数据位装的是什么 以 `TLU` 为例,把 6 个数据槽减掉各自偏移(实验 0【推导 B】输出): ``` 【推导 B】'TLU' 的 6 个数据槽减掉偏移即还原出 T/L/U 密文 94AA48550495 下标 0 1 2 3 4 5 6 7 8 9 10 11 值 9 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 个 -> 块内字符左旋一格 ``` 刚好还原出 `T`、`L`、`U`。于是确认:**每个格子存放一个字符,低半字节和高半字节各占一个数据位,另外两个位填 0。** 同时暴露出两件事: 1. **块内字符左旋一格**:cell0 存的是块内第 3 个字符 `U`,cell1 存第 1 个 `T`,cell2 存第 2 个 `L`。即块内第 i 个字符进第 (i+1)%k 个被使用的格子。 2. **偏移与半字节种类的关系**:低半字节落在偏移 +4 的位置,高半字节落在偏移 +5 的位置。 ### 2.3 块序倒置 `Hello` 的密文是 `34BB405504B5` + `223594B94C53`。带固定前缀 `2235` 的是**第二块**,说明「不足 3 字符的残块」被放在了密文的**末尾**;而残块必然来自明文的开头(`He`)。反推得: - 明文**从末尾**每 3 字符切一块,剩下的 1~2 字符构成最前面的残块; - 密文块的输出顺序是**块序整体倒置**——第 1 个密文块编码消息的**最后** 3 个字符。 用样例逐一验证:`Hello` 首块明文 `llo`、`2026` 首块 `026`、`abcd!` 首块 `cd!`。 交叉验证一个强证据:`2026` 的首块密文 `A48844556485` 与 FLAG 密文的第 8 块(下标 7)**完全相同**,而 FLAG 中恰好含有子串 `026`(`flag{T1u_2026_…`)且对齐到同一块边界。 ### 2.4 残块的填充 残块的前面若干个格子整体不用。这些格子里填的不是 0(实验 4 中令 `FILL = 0x0`,`Hello` 的密文变成 `34BB405504B5445594B94C55`,残块前缀变为 `4455`,与样例不符),实测填的是半字节 `0xE`: ```text 0xE + CELL_OFF = 0xE + (5,4,4,5) = (3,2,2,3) ``` 一个填充格子按位置 p=0..3 产生 `3,2,2,3`。cell0 的 p=0 落在**回绕**的槽 11 上,所以那个 `3` 跑到了整块的最后一位,可见前缀由 p=1,2,3 → `2,2,3` 构成(实验 4 输出): ``` 残块前缀由来:一个被填充的格子按位置 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=1 的 0 值位) + 槽11 = '3' k=1 残块 ("2") : 223322356483 槽0..6 = '2233223' (两个填充格) + 槽7 = '5' + 槽11 = '3' ``` 这就是 `2235` / `22332235` 前缀的完整来源。**没有填充字节进入明文,也没有任何链式状态**(每个块独立编码,同样的 3 字符在任何位置都产生同样的 12 hex——`026` 在 `2026` 和在 FLAG 中密文一致即为证)。 --- ## 3. 完整加密流程 ```text 参数: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 # 未使用格子的填充半字节 ``` 1. **分块**:把明文**从末尾**每 3 字符切一块;不足 3 的残块留在最前面。 例:`Hello` → 消息序 `["He", "llo"]`。 2. **块序倒置**:按 `["llo", "He"]` 的顺序逐块编码并拼接。 3. **编码一个块**(块内 k 个字符,k ∈ {1,2,3}): - 准备 12 个半字节槽。 - 前 `3-k` 个格子不使用,它们的每个槽写入 `FILL = 0xE`。 - 对第 j 个**被使用**的格子(j = 0..k-1,实际格子编号为 `j + (3-k)`): - 取字符 `ch = block[(j-1) mod k]`(块内字符左旋一格); - 取密钥数字 `d = KEY[j]`; - 格内 4 个位置初始化为 0,然后 `位置 d ← ch 的低半字节`,`位置 (d+2) mod 4 ← ch 的高半字节`。 - 每个槽的最终值 = `(半字节 + CELL_OFF[格内位置]) mod 16`,按 0..11 输出为大写 hex。 ### 满块展开公式 块内 3 个字符按消息序记为 a、b、c,则 12 个槽为: ```text 槽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 ``` 实验 6 对 855 个块穷举核对该公式与实现完全一致: ``` 'TLU' 公式 94AA48550495 实现 94AA48550495 一致 'llo' 公式 34BB405504B5 实现 34BB405504B5 一致 '026' 公式 A48844556485 实现 A48844556485 一致 'cd!' 公式 547B475584B5 实现 547B475584B5 一致 对 95x3x3 = 855 个块穷举核对: True ``` --- ## 4. 密钥 121 中每个数字的作用(重点) 密钥的三位数字 **不是加法/异或密钥,而是「走线选择器」**:第 j 位数字 `d` 决定第 j 个**被使用**的格子里,字符的低/高半字节各落在哪两个格内位置: ```text 低半字节 -> 格内位置 d 高半字节 -> 格内位置 (d + 2) mod 4 其余两个位置 -> 0 ``` 因为 `CELL_OFF = (5,4,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` | ### 4.1 第 1 位「1」——第 0 号格子走 (1,3) 车道 - **控制范围**:满块中的槽位 `[0, 1, 2, 11]`,也就是回绕格子 `cell0 = (11,0,1,2)`;在 k=2 残块中控制槽 `[3,4,5,6]`,在 k=1 残块中控制槽 `[7,8,9,10]`(残块里第 0 个被使用的格子往后挪)。 - **具体作用**:把该格子承载的字符(满块中是块内的**第 3 个**字符,因为左旋)的低半字节写到 `槽0`、高半字节写到 `槽2`,`槽1` 和 `槽11` 留 0 → 输出常量 `'4'`(0+CELL_OFF[2])和 `'5'`(0+CELL_OFF[0])。这就是满块模板 `?4??4?55?4?5` 中 **槽1 恒为 `4`、槽11 恒为 `5`** 的来源。 - **消融证据**(实验 2,把 121 改成 221): ``` 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 ``` 改一位数字,**恰好且仅有那 4 个槽位发生变化**,其余 8 个槽一字不变;且 `1→2` 的改动等价于把这个格子的 4-hex 码整体反转(`594A` → `A495`)。 ### 4.2 第 2 位「2」——第 1 号格子**换向**(迷宫蛇形) - **控制范围**:满块中的槽位 `[3, 4, 5, 6]`(`cell1`);在 k=2 残块中控制槽 `[7,8,9,10]`;k=1 残块只有一个格子,第 2 位不参与。 - **具体作用**:`d=2` 让高半字节落在 `槽3`、低半字节落在 `槽5`,空位在 `槽4`(→`'4'`) 和 `槽6`(→`'5'`)。注意这和 `d=1` 的顺序**正好相反**:`d=1` 是「空、低、空、高」,`d=2` 是「高、空、低、空」。 - **为什么是「换向」而不是别的**:因为 `CELL_OFF = (5,4,4,5)` 是回文,且 `d` 与 `3-d` 关于格心对称,所以 ``` 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' ``` 两式**完全互为逆序**。实测(实验 3)对全部 95 个可打印字符验证: ``` 对全部 95 个可打印字符验证: code(d=1)[::-1] == code(d=2) -> True 对全部 95 个可打印字符验证: code(d=0)[::-1] == code(d=3) -> True ``` 对照表节选(实验 3 输出,逐行摘录): ``` 字符 hi lo | d=0 d=1 d=2 d=3 | reverse(d=1) reverse(d=0) ---------------------------------------------------------------------------- 'T' 5 4 | 9495 584A A485 5949 | A485 5949 'L' 4 C | 1485 5049 9405 5841 | 9405 5841 'H' 4 8 | D485 5C49 94C5 584D | 94C5 584D 'e' 6 5 | A4A5 594B B495 5A4A | B495 5A4A '!' 2 1 | 6465 5547 7455 5646 | 7455 5646 ``` `reverse(d=1)` 列与 `d=2` 列逐字符相等。所以密钥 `1-2-1` 的语义是:**第 1、3 个格子正着走,第 2 个格子倒着走** —— 一条在迷宫里蛇形(boustrophedon)折返的路径。这正是题名 HexMaze 的含义。 - **消融证据**(把 121 改成 111,即取消换向): ``` 121 -> 111 (只改第 2 位) 第一个不匹配的样例: 'TLU' 期望 = 94AA48550495 得到 = 94A584A50495 标记 = ^^^^ 首个不匹配位置 = hex[3],全部不匹配位置 = [3, 4, 5, 6] 第 2 位数字在 k=3 满块中控制的槽位: [3, 4, 5, 6] (= 第 1 号格子 (3, 4, 5, 6)) 在 k=2 残块中控制的槽位: [7, 8, 9, 10] 在 k=1 残块中控制的槽位: (不参与) 'TLU' 中第 1 号格子(槽(3, 4, 5, 6)) 的 4-hex 码: 'A485' -> '584A' 互为逆序? True ``` 槽 3~6 由 `A485` 变成 `584A` —— 同一个字符 `T` 的 4-hex 码被整体反转,正好把「换向」取消掉了。 ### 4.3 第 3 位「1」——第 2 号格子又走回 (1,3) 车道 - **控制范围**:满块中的槽位 `[7, 8, 9, 10]`(`cell2`)。**它只在满块中出现**——k=1 和 k=2 的残块里第 3 个格子不会被使用,所以第 3 位数字对残块毫无影响。 - **具体作用**:低半字节 → `槽8`,高半字节 → `槽10`,空位 `槽7`(→`'5'`)、`槽9`(→`'4'`),构成模板尾部的 `5?4?`。 - **消融证据**: ``` 121 -> 122 (只改第 3 位) 第一个不匹配的样例: 'TLU' 期望 = 94AA48550495 得到 = 94AA48594055 标记 = ^^^^ 首个不匹配位置 = hex[7],全部不匹配位置 = [7, 8, 9, 10] 第 3 位数字在 k=3 满块中控制的槽位: [7, 8, 9, 10] (= 第 2 号格子 (7, 8, 9, 10)) 在 k=2 残块中控制的槽位: (不参与) 在 k=1 残块中控制的槽位: (不参与) 'TLU' 中第 2 号格子(槽(7, 8, 9, 10)) 的 4-hex 码: '5049' -> '9405' 互为逆序? True ``` 槽 7~10 由 `5049` 变成 `9405`,同样是 4-hex 码被整体反转(字符 `L` = 0x4C)。 ### 4.4 三位数字联合唯一性 实验 1 暴力枚举全部 4³ = 64 种 `(d0, d1, d2)`(每位取 0..3),逐个跑 4 个样例: ``` 共测试 64 种组合;能同时复现全部 4 个样例的组合: KEY = (1, 2, 1) -> 121 结论:唯一解 = [(1, 2, 1)] (即密钥 121) 命中样例数分布 (命中数: 组合数) = {0: 62, 1: 1, 4: 1} 命中 3 个样例的组合: [] ``` **只有 (1,2,1) 能全部复现**,62 种组合一个样例都命中不了。 逐位穷举(其余两位保持正确): ``` 逐位穷举(其余两位保持正确),表内数字 = 命中的样例数 / 4: 位置 d=0 d=1 d=2 d=3 第1位 0/4 *4/4 0/4 1/4 第2位 0/4 0/4 *4/4 0/4 第3位 0/4 *4/4 0/4 0/4 (*) 标记的是正确取值;可见每一位都只有一个取值能命中 4/4 ``` 表中唯一的非零干扰项是「第 1 位取 3 时命中 1/4」。原因可精确解释:`d` 与 `(d+2) mod 4` 只是把 hi/lo 对调,当写入该格子的**所有**字符恰好满足 `hi == lo` 时,`d=1` 与 `d=3` 不可区分。实测逐样例核对: ``` 'TLU' 受第 1 位支配的字符 = 'U'(hi=5,lo=5) 全部 hi==lo? True KEY=321 命中? True 'Hello' 受第 1 位支配的字符 = 'o'(hi=6,lo=F) 'e'(hi=6,lo=5) 全部 hi==lo? False KEY=321 命中? False '2026' 受第 1 位支配的字符 = '6'(hi=3,lo=6) '2'(hi=3,lo=2) 全部 hi==lo? False KEY=321 命中? False 'abcd!' 受第 1 位支配的字符 = '!'(hi=2,lo=1) 'b'(hi=6,lo=2) 全部 hi==lo? False KEY=321 命中? False ``` `'U' = 0x55` 是唯一的 hi==lo 字符,所以只有 `TLU` 会被 `321` 蒙对;只要再加任何一个样例,`d0=3` 立刻被排除。**样例集足以把密钥的每一位唯一钉死。** ### 4.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 位相同 | --- ## 5. 非密钥结构常量的必要性 密钥之外还有 5 项结构约定(块序倒置、`CELL_OFF`、`FILL`、块内左旋、格子分组起点)。实验 4 / 实验 5 证明它们同样被样例唯一钉死: **块序倒置(必需)** ``` [复现失败] 块序不倒置(按消息顺序输出密文块) -> 首个失败样例 'Hello': 期望 34BB405504B5223594B94C53 / 得到 223594B94C5334BB405504B5,首个不匹配 hex[0] [复现失败] 从头每 3 字符切块(残块在末尾) -> 首个失败样例 'Hello': 期望 34BB405504B5223594B94C53 / 得到 223534BB405304B94C5594B5,首个不匹配 hex[0] ``` **CELL_OFF = (5,4,4,5)(16⁴ 全穷举唯一)** ``` CELL_OFF 全穷举 16^4 = 65536 种,能复现 4 个样例的只有:[(5, 4, 4, 5)] [复现失败] CELL_OFF = (0, 0, 0, 0) -> 'TLU' 得到 50550400C040,首个不匹配 hex[0] [复现失败] CELL_OFF = (4, 4, 4, 4) -> 'TLU' 得到 949948440484,首个不匹配 hex[2] [复现失败] CELL_OFF = (5, 5, 5, 5) -> 'TLU' 得到 A5AA59551595,首个不匹配 hex[0] [复现失败] CELL_OFF = (4, 5, 5, 4) -> 'TLU' 得到 A59959441584,首个不匹配 hex[0] [复现失败] CELL_OFF = (5, 4, 5, 4) -> 'TLU' 得到 959A49450585,首个不匹配 hex[1] ``` **FILL = 0xE(16 种全穷举唯一)** ``` FILL 全穷举 0x0..0xF,能复现 4 个样例的只有:['0xe'] [复现失败] FILL = 0x0 -> 'Hello' 得到 34BB405504B5445594B94C55,首个不匹配 hex[12] (残块前缀变了) [复现失败] FILL = 0xd -> 'Hello' 得到 34BB405504B5112594B94C52,首个不匹配 hex[12] (残块前缀变了) [复现失败] FILL = 0xf -> 'Hello' 得到 34BB405504B5334594B94C54,首个不匹配 hex[12] (残块前缀变了) ``` **块内左旋 1 格(0/1/2 穷举唯一)** ``` [复现失败] 块内旋转 rot=0(正确值 rot=1) -> 'TLU' 期望 94AA48550495 / 得到 84A9405594A5,首个不匹配 hex[0] [复现失败] 块内旋转 rot=2(正确值 rot=1) -> 'TLU' 期望 94AA48550495 / 得到 049A495584A5,首个不匹配 hex[0] rot 穷举 0/1/2,能复现的只有:[1] ``` **格子分组必须回绕(12 种起点穷举唯一)** ``` 格子分组起点 shift 穷举 0..11,能复现的只有:[11] -> cell = [(11, 0, 1, 2), (3, 4, 5, 6), (7, 8, 9, 10)] [复现失败] 分组不回绕 (0,1,2,3)/(4,5,6,7)/(8,9,10,11) -> 'TLU' 期望 94AA48550495 / 得到 594AA4855049,首个不匹配 hex[0] ``` **联合唯一性**(避免「一个参数错了另一个参数补回来」的可能)——同时枚举 KEY(64) × 分组起点(12) × 旋转(3) = 2304 种组合: ``` 能同时复现 4 个样例的组合共 1 种: KEY=121 shift=11 (cell=[(11, 0, 1, 2), (3, 4, 5, 6), (7, 8, 9, 10)]) rot=1 ``` --- ## 6. 解密流程 1. 把密文按 12 hex 切块,**块序整体倒置**(最后一个密文块是明文的开头)。 2. 对每个 12-hex 块: - 按 `CELL_SLOTS` 分成 3 个格子,每个槽减掉对应的 `CELL_OFF[p]`(模 16)得到原始半字节。 - 四个半字节全部等于 `0xE` 的格子是未使用的填充格;剩下的就是被使用的格子,个数记为 k。 ``` 'abc' -> 74BB455564B5 减偏移后各格 = ['0306', '6010', '0206'] 被使用的格子 = [0, 1, 2] -> k=3 'He' -> 223594B94C53 减偏移后各格 = ['EEEE', '0506', '4080'] 被使用的格子 = [1, 2] -> k=2 '2' -> 223322356483 减偏移后各格 = ['EEEE', 'EEEE', '0203'] 被使用的格子 = [2] -> k=1 ``` - 对第 j 个被使用的格子(j=0..k-1),取密钥数字 `d = KEY[j]`, 字符 = `chr(格内位置[(d+2)%4] << 4 | 格内位置[d])`, 放回块内的第 `(j-1) mod k` 个位置(撤销左旋)。 3. 把所有块的明文按倒置后的顺序拼起来。 --- ## 7. FLAG 对 144-hex 密文解密,得到 12 个块(实验 7 输出): ``` FLAG 逐块还原(密文块顺序 = 消息块倒序): 密文块# 密文块 -> 明文块 0 14CC46555475 'r!}' 1 94BC475584C5 'ste' 2 848A43551495 '_M4' 3 448C445584C5 'pt0' 4 D4C9475564C5 'Cry' 5 34A84B55A4B5 '7f_' 6 74BA4355F495 '_Kc' 7 A48844556485 '026' <- 与样例 '2026' 的首块密文完全相同 8 648C495534A5 'u_2' 9 548C4F5584A5 '{T1' 10 B4BB405554B5 'lag' 11 22332235A4B3 'f' <- k=1 残块,前缀 '22332235' 倒序拼接: f + lag + {T1 + u_2 + 026 + _Kc + 7f_ + Cry + pt0 + _M4 + ste + r!} ``` 第 7 块与样例 `2026` 的首块密文逐字符相同,是「每块独立编码、无链式状态」的直接证据。 ### **FLAG = `flag{T1u_2026_Kc7f_Crypt0_M4ster!}`** 验证(实验 7): ``` OK 'TLU' enc-> 94AA48550495 dec-> 'TLU' OK 'Hello' enc-> 34BB405504B5223594B94C53 dec-> 'Hello' OK '2026' enc-> A48844556485223322356483 dec-> '2026' OK 'abcd!' enc-> 547B475584B5223564BB4553 dec-> 'abcd!' FLAG = flag{T1u_2026_Kc7f_Crypt0_M4ster!} 长度 = 34 字符 -> ceil(34/3) = 12 个块 -> 144 hex = 密文长度 144 一致 全可打印 = True re-encrypt = True ``` 34 个字符全部可打印,长度与密文长度自洽,重新加密逐字符等于原密文。 --- ## 附录 A:完整 encrypt / decrypt 代码 ```python #!/usr/bin/env python3 """KCTF07 HexMaze - key 121""" KEY = (1, 2, 1) # 密钥 121 CELL_OFF = (5, 4, 4, 5) # 格内位置 p=0..3 的固定偏移 CELL_SLOTS = [(11, 0, 1, 2), (3, 4, 5, 6), (7, 8, 9, 10)] # 回绕分组 FILL = 0xE # 未使用格子的填充半字节 def split_blocks(msg): """消息顺序的分块;残块(1~2 字符)在最前面。""" cut = len(msg) % 3 return ([msg[:cut]] if cut else []) + [msg[i:i + 3] for i in range(cut, len(msg), 3)] def enc_block(chars): k = len(chars) out = [None] * 12 for j in range(3 - k): # 未使用的格子在前面,填 0xE for p, s in enumerate(CELL_SLOTS[j]): out[s] = (FILL + CELL_OFF[p]) & 0xF for j in range(k): # 第 j 个"被使用"的格子 ch = ord(chars[(j - 1) % k]) # 块内字符左旋一格 d = KEY[j] # 密钥第 j 位决定走线 nib = [0, 0, 0, 0] nib[d] = ch & 0xF # 低半字节 -> 位置 d nib[(d + 2) % 4] = ch >> 4 # 高半字节 -> 位置 (d+2)%4 for p, s in enumerate(CELL_SLOTS[j + 3 - k]): out[s] = (nib[p] + CELL_OFF[p]) & 0xF return ''.join('%X' % v for v in out) def dec_block(blk): raw = [[(int(blk[s], 16) - CELL_OFF[p]) & 0xF for p, s in enumerate(cell)] for cell in CELL_SLOTS] used = [j for j in range(3) if raw[j] != [FILL] * 4] k = len(used) chars = [None] * k for j in range(k): d, c = KEY[j], raw[used[j]] chars[(j - 1) % k] = chr((c[(d + 2) % 4] << 4) | c[d]) return ''.join(chars) def encrypt(msg): return ''.join(enc_block(b) for b in split_blocks(msg)[::-1]) # 块序倒置 def decrypt(ct): return ''.join(dec_block(ct[i:i + 12]) for i in range(len(ct) - 12, -1, -12)) if __name__ == "__main__": assert encrypt("TLU") == "94AA48550495" assert encrypt("Hello") == "34BB405504B5223594B94C53" assert encrypt("2026") == "A48844556485223322356483" assert encrypt("abcd!") == "547B475584B5223564BB4553" CT = ("14CC4655547594BC475584C5848A43551495448C445584C5D4C9475564C534A84B55A4B5" "74BA4355F495A48844556485648C495534A5548C4F5584A5B4BB405554B522332235A4B3") flag = decrypt(CT) assert encrypt(flag) == CT print(flag) # flag{T1u_2026_Kc7f_Crypt0_M4ster!} ``` ## 附录 B:实验脚本 `wp_experiments.py` —— 参数化实现 + 7 组消融/唯一性实验,直接 `python3 wp_experiments.py` 运行(约 0.5 秒),本文引用的所有输出片段均来自该脚本的真实输出。 | 实验 | 内容 | |---|---| | 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 |
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
上传的附件:
KCTF07_HexMaze_WP.zip
(34.04kb,3次下载)
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Magic丶
10
发帖
5
回帖
80
RANK
关注
私信
他的文章
KCTF2026: 第十题:卯时·曦光初现
2677
KCTF2026: 第九题:丑寅同墟·星海抉择
41
KCTF2026: 第八题:亥子合辰·塔影迷楼
56
KCTF2026: 第七题:戌时·暗能潜流
653
KCTF2026: 第六题:酉时·书院迷局
1641
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
pzhxbz
FinSectech
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部