-
-
KCTF2026: 签到题:辰时·钟鸣破晓 Writeup
-
发表于: 2026-8-13 00:37 22
-
KCTF 2026 - Rosetta Calibration Writeup
题目信息
| 项目 | 内容 |
|---|---|
| 题目 | Rosetta Calibration |
| 类型 | Reverse Engineering / Keygen |
| 目标 | 求 Name = KCTF 对应的唯一 Serial |
| 题目文件 | kctf_rosetta.rar |
| RAR SHA-256 | f0031702ba58cb5be720df818b33efb50b6e09f4811ee7b8a58c9862e1334aa9 |
| EXE SHA-256 | caaa1c964b15d9b3250be0dc64412e5f64c29e297a971c226e8dd705d6b87e67 |
题目提供了一组公开的合法 Name/Serial:
Name : CAAA1C964B15D9B3
Serial : $/.;-):,(-#!%*-&?+@
目标是求出:
Name : KCTF
Serial : ?
最终结果
原程序接受的裸 Serial 为:
&/@$-,*?:-!#)%-;+(.
若比赛平台要求使用 KCTF{...} 格式包装,则 flag 为:
KCTF{&/@$-,*?:-!#)%-;+(.)}
注意:花括号包装不是 EXE 的输入格式。向程序验证时只输入上面的裸 Serial。
1. 样本勘察
解压归档后得到:
extracted/PUBLIC_INFO.txt
extracted/kctf_rosetta.exe
file 将程序识别为:
PE32+ executable (console) x86-64, for MS Windows, 3 sections
程序只有四个导入函数:
KERNEL32.dll!ExitProcess
KERNEL32.dll!GetStdHandle
KERNEL32.dll!ReadFile
KERNEL32.dll!WriteFile
它没有使用 CRT,输入输出均由入口函数直接调用 Win32 API 完成。PE 入口点为 0x140001000。
2. 输入和 Serial 格式检查
入口函数从标准输入依次读取 Name 和 Serial,并删除 CR/LF。
Serial 检查逻辑位于入口函数约 0x140001288 之后。关键条件如下:
- Serial 总长度必须为
0x13,即 19 字节。 - 常量位图
0x4210的置位位置必须为字符-。 0x4210的置位 bit 为 4、9、14,因此格式固定为四组、每组四个符号:
XXXX-XXXX-XXXX-XXXX
- 除三个
-外,每个字符都会交给0x140001520处的 rank 函数。 - 每个 rank 必须位于
0..15。 - 程序使用 16-bit 位图检查 rank 是否重复,最终位图必须等于
0xffff。
因此合法 Serial 中的 16 个符号必须分别表示 rank 0..15,每个 rank 恰好出现一次。
对应的核心反汇编条件为:
cmp r12d, 13h ; Serial 长度必须为 19
...
cmp byte ptr [rsp+r14+50h], 2Dh ; 固定位置必须是 '-'
...
cmp eax, 0Fh ; rank <= 15
ja reject
bt edi, eax ; 检查 rank 是否已出现
jb reject
...
cmp edi, 0FFFFh ; 必须覆盖全部 16 个 rank
jne reject
3. 符号到 rank 的映射
字符转换由两个辅助函数完成:
0x140001540:根据.rdata中 RVA0x2000的 32 项 DWORD 表进行初始映射。0x140001520:调用前者,并在字符为(时将结果额外异或1。
最终逆映射如下:
| rank | 字符 | rank | 字符 |
|---|---|---|---|
| 0 | ? |
8 | . |
| 1 | % |
9 | & |
| 2 | ( |
10 | / |
| 3 | @ |
11 | ) |
| 4 | ! |
12 | $ |
| 5 | ; |
13 | , |
| 6 | + |
14 | * |
| 7 | # |
15 | : |
按 rank 0..15 排列的符号字典为:
?%(@!;+#.&/)$,*:
( 的特殊异或操作很关键:静态表中 ( 和 @ 的原始结果均为 3,但包装函数会把 ( 的结果修正为 2,从而形成完整的一一映射。
4. Name 到 rank 序列的变换
核心变换函数位于 0x1400013e0,参数可恢复为:
void transform(const uint8_t *name, uint32_t length, uint8_t output[16]);
4.1 初始化
函数先建立一个包含 0..15 的状态数组:
state = list(range(16))
checksum = 0
该初始数组直接来自 .rdata 的 RVA 0x2080:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
4.2 逐字节置换
对 Name 的第 i 个字节 b,程序执行:
mixed = b ^ (b >> 4)
checksum += mixed
left = i & 0xF
right = (mixed ^ (3 * i)) & 0xF
state[left], state[right] = state[right], state[left]
对应的关键反汇编为:
movzx esi, byte ptr [rcx+r10] ; b = name[i]
mov edi, esi
shr edi, 4
xor edi, esi ; mixed = b ^ (b >> 4)
add eax, edi ; checksum += mixed
xor edi, r9d ; mixed ^ (3 * i)
and edi, 0Fh
...
add r9d, 3 ; 下一轮得到 3 * (i + 1)
4.3 输出 rank 序列
处理完 Name 后,取:
start = checksum & 0xF
然后从最终 state 中依次选出 16 个 rank:
state[(start + 0) & 0xF]
state[(start + 1) & 0xF]
...
state[(start + 7) & 0xF]
state[start ^ 8]
state[(start + 9) & 0xF]
...
state[(start + 14) & 0xF]
state[(start - 1) & 0xF]
由于 state 始终是 0..15 的一个排列,输出也是完整的 rank 排列。再通过上一节的一一映射即可得到唯一的 16 个 Serial 符号。
5. 使用公开样本验证恢复算法
对公开 Name 执行恢复出的算法:
Name : CAAA1C964B15D9B3
Result : $/.;-):,(-#!%*-&?+@
结果与题目提供的合法 Serial 完全一致,说明字符映射、状态交换、输出顺序和分隔符位置均恢复正确。
将公开 Name/Serial 输入原程序,还会得到:
[+] The symbols have found their meanings.
[+] Calibration accepted. Key correct!
6. 计算目标 Serial
对 KCTF 执行相同变换,得到 rank 序列:
[9, 10, 3, 12, 13, 14, 0, 15, 4, 7, 11, 1, 5, 6, 2, 8]
使用 rank 字典转换为符号:
&/@$,*?:!#)%;+(.
按程序要求每四个符号插入一个 -:
&/@$-,*?:-!#)%-;+(.
该串长度为 19,分隔符索引为 [4, 9, 14],其余字符的 rank 排序后恰好为 0..15。
7. 原程序验证
向原始 EXE 输入:
KCTF
&/@$-,*?:-!#)%-;+(.
实际输出:
========================================
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!
这不仅验证了 Python 重现算法,还直接证明目标 Serial 通过了原始校验程序。
8. 计算脚本
完整脚本如下:
#!/usr/bin/env python3
"""Reproduce the Rosetta serial transform recovered from kctf_rosetta.exe."""
TABLE = [
4, -1, 7, 12, 1, 9, -1, 3,
11, 14, 6, 13, -1, 8, 10, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, 15, 5, -1, -1, -1, 0, 3,
]
def rank(ch):
"""Equivalent to the executable's rank helper at RVA 0x1520."""
index = (ord(ch) + 0xDF) & 0xFF
value = TABLE[index] if index < 0x20 else -1
if ch == '(':
value ^= 1
return value
def transform(name):
state = list(range(16))
checksum = 0
for i, byte in enumerate(name.encode('ascii')):
mixed = byte ^ (byte >> 4)
checksum += mixed
left = i & 0xF
right = (mixed ^ (3 * i)) & 0xF
state[left], state[right] = state[right], state[left]
start = checksum & 0xF
values = [state[(start + i) & 0xF] for i in range(8)]
values.append(state[start ^ 8])
values.extend(state[(start + i) & 0xF] for i in range(9, 15))
values.append(state[(start - 1) & 0xF])
inverse = {rank(chr(code)): chr(code) for code in range(0x21, 0x41)}
serial = ''.join(inverse[value] for value in values)
return serial[:4] + '-' + serial[4:8] + '-' + serial[8:12] + '-' + serial[12:]
if __name__ == '__main__':
known = transform('CAAA1C964B15D9B3')
target = transform('KCTF')
print(f'known: {known}')
print(f'target: {target}')
assert known == '$/.;-):,(-#!%*-&?+@'
运行结果:
known: $/.;-):,(-#!%*-&?+@
target: &/@$-,*?:-!#)%-;+(.
9. 解题统计
以下统计仅对应原始逆向与 flag 求解阶段,不包含本文档整理阶段:
| 项目 | 内容 |
|---|---|
| 模型提供方 | OpenAI |
| 模型 | openai/gpt-5.6-sol |
| 推理等级 | xhigh |
| Token 消耗 | 139,597 |
| 解题时长 | 545 秒,约 9 分 5 秒 |
| 验证方式 | 静态反汇编、公开样本回归、原始 EXE 动态验收 |