KCTF 2026 Rosetta Calibration

本文最后更新于 2026年8月16日 下午

KCTF 2026 · Rosetta Calibration 详细解析

题目信息

项目 内容
赛事 KCTF 2026
题目 Rosetta Calibration(罗塞塔校准)
类别 Reverse
文件 kctf_rosetta.exe(64 位 PE,约 16KB)
SHA256 caaa1c964b15d9b3250be0dc64412e5f64c29e297a971c226e8dd705d6b87e67
已知合法对 Name = CAAA1C964B15D9B3,Serial = $/.;-):,(-#!%*-&?+@
目标 求 Name = KCTF 对应的唯一 Serial

题面提示:Sixteen symbols. Sixteen meanings. One surviving record is the dictionary.
即 16 个符号对应 16 种含义,唯一存活的记录(已知合法对)就是”词典”,用来反推加密规则。

程序概览

使用 IDA(经 IDA MCP 服务器)加载 kctf_rosetta.exe。这是一个极小的 PE:

  • 5 个函数start(0x140001000)是唯一入口,905 字节
  • 只导入 KERNEL32GetStdHandle / ReadFile / WriteFile / ExitProcess
  • 无加密 / 无网络相关导入 → 纯本地 keygen 校验,逻辑全部内联
1
2
3
4
5
0x140001000  start            dispatcher(主流程)
0x140001390 sub_140001390 wrapper(WriteFile 封装,输出提示)
0x1400013E0 sub_1400013E0 leaf(核心"校准"算法,生成期望序列)
0x140001520 sub_140001520 wrapper(符号解码入口)
0x140001540 sub_140001540 leaf(符号→nibble 查表)

字符串提示:[+] Calibration accepted. Key correct! / [-] Calibration rejected. Key incorrect. 等。

Step 1:主流程 start 反编译

关键逻辑(节选):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 读入 Name 到 v16(长度 v5),Serial 到 NumberOfBytesWritten(长度 n19)

// ① Name 格式校验:全部字符必须在 [33, 126](可打印 ASCII)
if ( v4 - 33 >= 0xFFFFFFE0 ) // 长度 >= 1
while ( (unsigned __int8)(v16[v6] - 33) < 0x5E )
{
if ( v4 == ++v6 ) // 全部合法
{
if ( n19 == 19 ) // ② Serial 必须恰好 19 字符
{
n16912 = 16912; // 0x4210 = bit4|bit9|bit14 → 连字符 '-' 的位置
do {
while ( n0xE > 0xE || !_bittest(&n16912, n0xE) ) // 非连字符位
{
n0xF = sub_140001520(serial[n0xE]); // 符号 → nibble
if ( n0xF > 0xF || _bittest(&n0xFFFF, n0xF) ) goto FAIL; // 非法/重复
n0xFFFF |= 1 << n0xF; // 记录已出现
lpBuffer_[n16++] = n0xF; // 收集 16 个 nibble
if ( ++n0xE == 19 ) break;
}
if ( serial[n0xE] != '-' ) goto FAIL; // 连字符位置必须是 '-'
++n0xE;
} while ( n0xE != 19 );

// ③ 16 个 nibble 必须恰好是 0-15 全排列
if ( n16 == 16 && n0xFFFF == 0xFFFF )
{
sub_1400013E0(v16, v5, &Buffer); // 由 Name 计算期望序列
if ( _mm_movemask_epi8(_mm_cmpeq_epi8(lpBuffer_, Buffer)) == 0xFFFF )
"[+] Calibration accepted. Key correct!"
}
}
}
}

三个关键约束:

  1. Serial 长度必须为 19,格式 XXXX-XXXX-XXXX-XXXX:位置 4 / 9 / 14 必须是 -(由 0x4210 位图决定),其余 16 位是”符号”
  2. 每个符号经 sub_140001520 解码为 0~15 的 nibble,且 16 个 nibble 必须构成 0-15 的全排列n0xFFFF == 0xFFFF
  3. 用户输入的 16 个 nibble 必须与 sub_1400013E0(Name) 输出的 16 字节逐字节相等

Step 2:符号映射表(sub_140001520 / sub_140001540)

1
2
3
4
5
6
7
8
__int64 sub_140001540(char a1) {
unsigned __int8 n = a1 - 33;
if ( n <= 0x1F ) return dword_140002000[n]; // 查表:char-33 索引
return 0xFFFFFFFFLL; // 越界 → 无效
}
__int64 sub_140001520(char c) {
return (c == 40) ^ (unsigned int)sub_140001540(c); // '(' (0x28) 特判:结果 XOR 1
}

读取 .rdata 中的 32 项 dword 表 dword_140002000(0x140002000),得到 16 个有效符号 → nibble 映射:

符号 ASCII 查表值 实际解码 符号 ASCII 查表值 实际解码
! 33 4 4 . 46 8 8
# 35 7 7 / 47 10 10
$ 36 12 12 : 58 15 15
% 37 1 1 ; 59 5 5
& 38 9 9 ? 63 0 0
( 40 3 2(^1) @ 64 3 3
) 41 11 11
* 42 14 14
+ 43 6 6
, 44 13 13

注意 ( 的陷阱:表中原始值是 3,但 sub_140001520( 做了 ^1,所以它实际解码为 2。因此 nibble→符号的反向映射是唯一的(每个 nibble 恰好只有一个来源符号)。

用已知对验证映射正确性:

$/.;-):,(-#!%*-&?+@(19 字符)→ 去掉位置 4/9/14 的 - 后解码:

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

得到 [12,10,8,5, 11,15,13,2, 7,4,1,14, 9,0,6,3] —— 恰好是 0-15 全排列 ✓

Step 3:核心算法 sub_1400013E0 详细推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 sub_1400013E0(__int64 name, int len, __m128i *out) {
_OWORD v10[2];
v10[0] = xmmword_140002080; // 字典初始值 = [0,1,2,...,15]
int v3 = 0;
char v5 = 0; // 8 位累加和
for ( int i = 0; i < len; i++ ) {
v6 = name[i] ^ (name[i] >> 4); // 高 4 位折叠到低 4 位
v5 += v6; // 8 位累加(溢出取低 8 位)
v7 = ((unsigned char)v3 ^ (unsigned char)v6) & 0xF;
swap(dict[i & 0xF], dict[v7]); // 按位置交换字典元素
v3 += 3;
}
// 输出 16 字节(注意 i=8 与 i=15 的特例!)
out[0..7] = dict[(v5+i) & 0xF] for i in 0..7
out[8] = dict[(v5 & 0xF) ^ 8] // ← 陷阱:这里是 XOR 8,不是 +8
out[9..14] = dict[(v5+i) & 0xF] for i in 9..14
out[15] = dict[(v5-1) & 0xF] // 即 (v5+15) & 0xF
}

算法本质:

  1. 以 16 字节”罗塞塔字典”(初始 00 01 02 ... 0F)为状态
  2. 对 Name 的每个字符 c
    • v6 = c ^ (c >> 4) —— 把字符高半字节折叠进低半字节(例如 'K'=0x4B → 0x4B^0x04=0x4F
    • v5 += v6(8 位求和,用于最终输出偏移)
    • v7 = ((v3) ^ v6) & 0xF,其中 v3 每次 +3(0,3,6,9,…)
    • 交换 dict[i & 0xF]dict[v7]
  3. 输出时按 v5 作为起始偏移取出 16 字节;**i=8 位置是 (v5&0xF)^8 而非 (v5+8)&0xF**——这是反编译中容易踩的坑,必须逐条指令核对(本解通过已知对验证确认了该细节)

Step 4:验证已知合法对

用 Python 完整复现上述算法,代入题给合法对:

1
2
3
4
Name = CAAA1C964B15D9B3
Serial 解码 nibbles = [12,10,8,5, 11,15,13,2, 7,4,1,14, 9,0,6,3]
算法输出 = [12,10,8,5, 11,15,13,2, 7,4,1,14, 9,0,6,3]
校验 PASS ✅

证明算法复现完全正确(包括 ( 特判与 i=8 的 XOR 陷阱)。

Step 5:求解 Name = KCTF

"KCTF" 运行算法,得到期望的 16 字节:

1
[9, 10, 3, 12, 13, 14, 0, 15, 4, 7, 11, 1, 5, 6, 2, 8]   (恰好是 0-15 全排列 ✓)

按反向映射表(nibble → 符号)转成字符,并在位置 4/9/14 插入 -

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

完整求解脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# -*- coding: utf-8 -*-
# KCTF 2026 - Rosetta Calibration 求解脚本
# 依赖: 无(纯标准库)

# ---------- 1. 符号 -> nibble 映射(来自 dword_140002000,char-33 索引;'(' 特判 XOR 1) ----------
D = {
'!': 4, '#': 7, '$': 12, '%': 1, '&': 9, '(': 3, ')': 11, '*': 14,
'+': 6, ',': 13, '.': 8, '/': 10, ':': 15, ';': 5, '?': 0, '@': 3,
}

def sym_to_nibble(c):
v = D.get(c, -1)
if v < 0: return -1
return v ^ 1 if c == '(' else v # sub_140001520: '(' → 3^1=2

inv = {v: c for c, v in D.items() if c != '('} # nibble -> 符号(唯一)
inv[2] = '('

# ---------- 2. 复现 sub_1400013E0 ----------
def rosetta_calibrate(name: bytes):
d = list(range(16)) # xmmword_140002080 = 0..15
v3, v5 = 0, 0
for i, c in enumerate(name):
v6 = c ^ (c >> 4) # 高半字节折叠
v5 = (v5 + v6) & 0xFF # 8 位累加
v7 = ((v3 & 0xFF) ^ v6) & 0xF
j = i & 0xF
d[j], d[v7] = d[v7], d[j] # 交换
v3 += 3
out = []
for i in range(16):
if i == 8: out.append(d[(v5 & 0xF) ^ 8]) # 陷阱: XOR 8
elif i == 15: out.append(d[(v5 - 1) & 0xF]) # (v5+15)&0xF
else: out.append(d[(v5 + i) & 0xF])
return out

# ---------- 3. 验证已知合法对 ----------
known = rosetta_calibrate(b"CAAA1C964B15D9B3")
serial = "$/.;-):,(-#!%*-&?+@"
nib = [sym_to_nibble(c) for c in serial if c != '-']
assert nib == known, "已知对验证失败!"
print("[*] 已知对验证通过:", nib)

# ---------- 4. 求解 Name=KCTF ----------
out = rosetta_calibrate(b"KCTF")
assert sorted(out) == list(range(16)), "输出不是 0-15 全排列"
s = ''.join(inv[v] for v in out)
serial = f"{s[:4]}-{s[4:8]}-{s[8:12]}-{s[12:16]}"
print(f"[+] Name : KCTF")
print(f"[+] Serial : {serial}")

运行输出:

1
2
3
[*] 已知对验证通过: [12, 10, 8, 5, 11, 15, 13, 2, 7, 4, 1, 14, 9, 0, 6, 3]
[+] Name : KCTF
[+] Serial : &/@$-,*?:-!#)%-;+(.

Flag

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

要点总结

  1. 题目本质:一个 16 元素字典(0..15)上的置换 keygen——Name 作为种子搅乱字典,按累加和偏移输出 16 个 nibble,Serial 就是这 16 个 nibble 的符号编码
  2. 两个陷阱( 符号的映射被 ^1 特判;输出第 8 字节用 (v5&0xF)^8 而非 +8,都必须通过已知合法对逐一验证
  3. 求解思路:无需逆向字典置换(正向实现即可),已知合法对就是”罗塞塔石”,用来校准算法复现的正确性

KCTF 2026 Rosetta Calibration
https://xyyr-c.github.io/2026/08/11/writeup-rosetta-calibration/
作者
xyyr
发布于
2026年8月11日
许可协议