本文最后更新于 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 字节
只导入 KERNEL32 的 GetStdHandle / 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 if ( v4 - 33 >= 0xFFFFFFE0 ) while ( (unsigned __int8)(v16[v6] - 33 ) < 0x5E ) { if ( v4 == ++v6 ) { if ( n19 == 19 ) { n16912 = 16912 ; do { while ( n0xE > 0xE || !_bittest(&n16912, n0xE) ) { n0xF = sub_140001520(serial[n0xE]); if ( n0xF > 0xF || _bittest(&n0xFFFF, n0xF) ) goto FAIL; n0xFFFF |= 1 << n0xF; lpBuffer_[n16++] = n0xF; if ( ++n0xE == 19 ) break ; } if ( serial[n0xE] != '-' ) goto FAIL; ++n0xE; } while ( n0xE != 19 ); if ( n16 == 16 && n0xFFFF == 0xFFFF ) { sub_1400013E0(v16, v5, &Buffer); if ( _mm_movemask_epi8(_mm_cmpeq_epi8(lpBuffer_, Buffer)) == 0xFFFF ) → "[+] Calibration accepted. Key correct!" } } } }
三个关键约束:
Serial 长度必须为 19 ,格式 XXXX-XXXX-XXXX-XXXX:位置 4 / 9 / 14 必须是 -(由 0x4210 位图决定),其余 16 位是”符号”
每个符号经 sub_140001520 解码为 0~15 的 nibble,且 16 个 nibble 必须构成 0-15 的全排列 (n0xFFFF == 0xFFFF)
用户输入的 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]; return 0xFFFFFFFF LL; } __int64 sub_140001520 (char c) { return (c == 40 ) ^ (unsigned int )sub_140001540(c); }
读取 .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; int v3 = 0 ; char v5 = 0 ; for ( int i = 0 ; i < len; i++ ) { v6 = name[i] ^ (name[i] >> 4 ); v5 += v6; v7 = ((unsigned char )v3 ^ (unsigned char )v6) & 0xF ; swap(dict[i & 0xF ], dict[v7]); v3 += 3 ; } out[0. .7 ] = dict[(v5+i) & 0xF ] for i in 0. .7 out[8 ] = dict[(v5 & 0xF ) ^ 8 ] out[9. .14 ] = dict[(v5+i) & 0xF ] for i in 9. .14 out[15 ] = dict[(v5-1 ) & 0xF ] }
算法本质:
以 16 字节”罗塞塔字典”(初始 00 01 02 ... 0F)为状态
对 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]
输出时按 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 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 inv = {v: c for c, v in D.items() if c != '(' } inv[2 ] = '(' def rosetta_calibrate (name: bytes ): d = list (range (16 )) v3, v5 = 0 , 0 for i, c in enumerate (name): v6 = c ^ (c >> 4 ) v5 = (v5 + v6) & 0xFF 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 ]) elif i == 15 : out.append(d[(v5 - 1 ) & 0xF ]) else : out.append(d[(v5 + i) & 0xF ]) return out known = rosetta_calibrate(b"CAAA1C964B15D9B3" ) serial = "$/.;-):,(-#!%*-&?+@" nib = [sym_to_nibble(c) for c in serial if c != '-' ]assert nib == known, "已知对验证失败!" print ("[*] 已知对验证通过:" , nib) 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 : &/@$- ,*?:- !
要点总结
题目本质 :一个 16 元素字典(0..15)上的置换 keygen——Name 作为种子搅乱字典,按累加和偏移输出 16 个 nibble,Serial 就是这 16 个 nibble 的符号编码
两个陷阱 :( 符号的映射被 ^1 特判;输出第 8 字节用 (v5&0xF)^8 而非 +8,都必须通过已知合法对逐一验证
求解思路 :无需逆向字典置换(正向实现即可),已知合法对就是”罗塞塔石”,用来校准算法复现的正确性