首页
社区
课程
招聘
[原创]# KCTF 2026 – Rosetta Calibration 题解(Writeup)
发表于: 2026-8-10 12:32 8

[原创]# KCTF 2026 – Rosetta Calibration 题解(Writeup)

2026-8-10 12:32
8

KCTF 2026 – Rosetta Calibration 题解(Writeup)

1. 题目信息

项目 内容
题目名称 KCTF 2026 - Rosetta Calibration
目标文件 kctf_rosetta.exe(64 位 PE 控制台程序)
文件 SHA256 caaa1c964b15d9b3250be0dc64412e5f64c29e297a971c226e8dd705d6b87e67
公开有效对 Name: CAAA1C964B15D9B3 / Serial: $/.;-):,(-#!%*-&?+@
题目目标 求出Name = KCTF 的唯一合法 Serial

附带说明:本题所在环境原本配置了 ida-pro-mcp(MCP 服务监听于本机 127.0.0.1:13337),虽工具未直接暴露给会话,但通过 MCP SSE 协议(/sse 建连 → /sse?session=... 上 POST initialize / tools/call JSON-RPC 消息)直接调用了 IDA 的 decompile、xrefs_to、find_regex、py_eval 等能力完成分析。

2. 程序运行形态

========================================
       KCTF 2026 ROSETTA TERMINAL
========================================
Sixteen symbols. Sixteen meanings.
One surviving record is the dictionary.

Name   : KCTF
Serial : <输入>
  • 从标准输入依次读取 Name 与 Serial(逐字节 ReadFile);
  • 输出三种失败态:
    • [-] Invalid name format.(exit 2)
    • [-] The inscription cannot be translated.(exit 3)
    • [-] Calibration rejected. Key incorrect.(exit 1)
  • 成功时输出 [+] Calibration accepted. Key correct!(exit 0)。

3. 静态分析

3.1 关键字符串定位

用 find_regex 在 IDA 字符串中搜索,得到关键提示字符串及其引用位置:

地址 字符串 引用者
0x140002090 "Serial : " start
0x1400020a4 "[-] Calibration rejected. Key incorrect.\r\n" start
0x14000211c "[+] Calibration accepted. Key correct!\r\n" start
0x140002173 题目标语 start

所有 xref 都落在单一函数 start(0x140001000,长度 0x389),即整个校验逻辑集中在一个函数中。另有三个子函数:

  • sub_1400013E0:由 Name 生成 16 字节期望值;
  • sub_140001520 / sub_140001540:Serial 符号 → 数值 0~15 的映射。

3.2 start 主校验流程(伪码要点)

// 读取 Name 到 v16(≤39 字节),长度记 v4(v5 = v4)
// 读取 Serial 到缓冲区(≤47 字节),长度记 v3
if (v4 - 33 >= 0xFFFFFFE0) {            // 名字长度 v4 ∈ [0, 32]
    v6 = 0;
    while ((byte)(v16[v6] - 33) < 0x5E) { // 名字每个字符 ∈ '!'..'~'
        if (v4 == ++v6) {
            if (v3 == 19) {              // ★ Serial 必须恰好 19 字符
                v7 = 0; v8 = 16912; v9 = 0; v10 = 0;
                do {
                    while (v7 > 14 || !_bittest(&v8, v7)) {
                        v11 = sub_140001520(serial[v7]);   // 符号 → 0..15
                        if (v11 > 0xF || _bittest(&v9, v11)) goto FAIL;
                        v9 |= 1 << v11;
                        v14[v10++] = v11;                  // 解码结果(16 字节)
                        if (++v7 == 19) goto DONE;
                    }
                    if (serial[v7] != 45) goto FAIL;       // '-' 分隔符
                    ++v7;
                } while (v7 != 19);
DONE:
                if (v10 == 16 && v9 == 0xFFFF) {           // 16 个值互异且为 0..15 全排列
                    sub_1400013E0(v16, v5, &Buffer);       // 由 Name 求 16 字节
                    if (Buffer == v14) { SUCCESS } else { FAIL }
                }
            }
        }
    }
}

要点解读:

  • v8 = 16912 = 0x4210,置位的是第 4、9、14 位 → Serial 中索引 4、9、14 必须是 '-'(0x2D)。
  • 因此 Serial 格式 = 4符号-4符号-4符号-4符号(共 19 字符);
  • 16 个符号经 sub_140001520 映射为 015,且必须互异(v9 == 0xFFFF,即 015 全排列);
  • 最后把「符号解码出的 16 字节」与「Name 派生的 16 字节」做 128 位相等比较。

4. 核心算法还原

4.1 符号 → 数值(sub_140001520 + sub_140001540)

__int64 sub_140001540(char a1) {
    v1 = a1 - 33;
    if (v1 <= 0x1Fu) return dword_140002000[v1];   // 只认 '!'..'@'
    return 0xFFFFFFFF;
}
__int64 sub_140001520(char a1) {
    return (a1 == 40) ^ (unsigned)sub_140001540(); // '(' 时异或 1
}

dword_140002000(32 个 dword,py_eval 读出):

idx: 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15
chr: !  "  #  $  %  &  '  (  )  *  +  ,  -  .  /  0
val: 4  -1 7  12 1  9  -1 3  11 14 6  13 -1 8  10 -1

idx: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
chr: 1  2  3  4  5  6  7  8  9  :  ;  <  =  >  ?  @
val: -1 -1 -1 -1 -1 -1 -1 -1 -1 15 5  -1 -1 -1 0  3

综合 ( 的异或修正后,有效的 16 个符号与数值一一对应(本题的“词典”):

符号 ? % ( @ ! ; + # . & / ) $ , * :
值 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

注:( 在表中为 3,但 sub_140001520 对其异或 1,实际映射为 2,正好与 @(3) 区分开,避免冲突。

4.2 Name → 16 字节(sub_1400013E0)

v10[0] = xmmword_140002080;   // = 00 01 02 ... 0F(16 字节)
v5 = 0; v3 = 0;
for (i = 0; i < name_len; i++) {
    v6 = name[i] ^ (name[i] >> 4);      // 半字节折叠
    v5 += v6;                            // 字节累加和(mod 256)
    v7 = ((v3 ^ v6) & 0xF);              // v3 每轮 += 3
    swap(v10[i & 0xF], v10[v7]);         // 表内置换
    v3 += 3;
}
// 输出 16 字节:
out[k]   = v10[(v5 + k) & 0xF];      k = 0..7
out[8]   = v10[(v5 & 0xF) ^ 8];
out[k]   = v10[(v5 + k) & 0xF];      k = 9..14
out[15]  = v10[(v5 - 1) & 0xF];

即:以 00..0F 为初始表,按名字逐字节做 RC4 风格的交换置乱,再按累加和 v5 偏移取出 16 字节(其内容仍是 015 的一个排列)。**校验本质 = 比较两个由 015 组成的排列是否一致。**

5. 求解

5.1 思路

对任意 Name:

  1. 用 sub_1400013E0 的逻辑算出 16 字节排列 P;
  2. 对 P 中每个值查符号表(值 → 符号)得到 16 个符号;
  3. 按 4-4-4-4 分组、以 - 连接,即得 Serial。

5.2 求解脚本(solve_rosetta.py)

TABLE = (4, 0xFFFFFFFF, 7, 12, 1, 9, 0xFFFFFFFF, 3, 11, 14, 6, 13,
         0xFFFFFFFF, 8, 10, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF,
         0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF,
         0xFFFFFFFF, 15, 5, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0, 3)

def sym_val(c):                      # sub_140001520
    i = ord(c) - 33
    if not (0 <= i < 32) or TABLE[i] == 0xFFFFFFFF:
        return -1
    return TABLE[i] ^ (1 if c == '(' else 0)

INV = {}
for c in map(chr, range(33, 65)):
    v = sym_val(c)
    if v >= 0: INV.setdefault(v, c)

def name2bytes(name):                # sub_1400013E0
    v10, v5, v3 = list(range(16)), 0, 0
    for i, ch in enumerate(name):
        v6 = (ord(ch) ^ (ord(ch) >> 4)) & 0xFF
        v5 = (v5 + v6) & 0xFF
        j = i & 0xF
        v10[j], v10[((v3 ^ v6) & 0xF)] = v10[((v3 ^ v6) & 0xF)], v10[j]
        v3 += 3
    out = [v10[(v5 + k) & 0xF] for k in range(8)]
    out.append(v10[(v5 & 0xF) ^ 8])
    out += [v10[(v5 + k) & 0xF] for k in range(9, 15)]
    out.append(v10[(v5 - 1) & 0xF])
    return out

def serial_for(name):
    P = name2bytes(name)
    s = ''.join(INV[v] for v in P)
    return s[:4] + '-' + s[4:8] + '-' + s[8:12] + '-' + s[12:16]

5.3 验证公开示例对

name        : CAAA1C964B15D9B3
computed P  : [12, 10, 8, 5, 11, 15, 13, 2, 7, 4, 1, 14, 9, 0, 6, 3]
serial vals : [12, 10, 8, 5, 11, 15, 13, 2, 7, 4, 1, 14, 9, 0, 6, 3]
EXAMPLE MATCH: True

脚本结果与题目公开对完全一致,证明算法还原无误。

6. 最终答案

Name   : KCTF
Serial : &/@$-,*?:-!#)%-;+(.

端到端验证(运行真实 EXE)

$ printf '%s\n%s\n' 'KCTF' '&/@$-,*?:-!#)%-;+(.' | ./kctf_rosetta.exe
========================================
       KCTF 2026 ROSETTA TERMINAL
========================================
Sixteen symbols. Sixteen meanings.
One surviving record is the dictionary.

Name   : Serial :
[+] The symbols have found their meanings.
[+] Calibration accepted. Key correct!
(exit code 0)

同时以公开对 CAAA1C964B15D9B3 / $/.;-):,(-#!%*-&?+@ 做对照测试同样返回 exit 0,双向验证通过。

7. 总结

  • 题目为「Rosetta 石碑」式换表题:Serial 是 015 排列的符号化编码(!@ 子集中的 16 个符号),Name 经 RC4 式置换同样产生 0~15 的排列,二者相等即通过。
  • 关键点:① Serial 固定 19 字符、- 位置由位图 0x4210 指定;② 符号表是「字典」;③ Name 长度不要求 16(示例为 16,KCTF 为 4 同样成立),置换由名字逐字节驱动。
  • 求解只需正向实现 sub_1400013E0 + 查表逆映射,无需爆破。

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

最后于 2026-8-12 15:20 被阿捏利编辑 ,原因:
收藏
点赞 ・0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回