首页
社区
课程
招聘
KCTF2026: 签到题:辰时·钟鸣破晓 Writeup
发表于: 2026-8-13 00:37 22

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 之后。关键条件如下:

  1. Serial 总长度必须为 0x13,即 19 字节。
  2. 常量位图 0x4210 的置位位置必须为字符 -。
  3. 0x4210 的置位 bit 为 4、9、14,因此格式固定为四组、每组四个符号:
XXXX-XXXX-XXXX-XXXX
  1. 除三个 - 外,每个字符都会交给 0x140001520 处的 rank 函数。
  2. 每个 rank 必须位于 0..15。
  3. 程序使用 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 中 RVA 0x2000 的 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 动态验收

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 2026-8-17 14:11 被Magic丶编辑 ,原因: 统一标题风格
上传的附件:
收藏
点赞 ・0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回