目录
签到题:辰时·钟鸣破晓
附件
https://pan.baidu.com/s/1N3YqHD0DZkMhNsmBUDxykQ?pwd=gy39 提取码: gy39解压后阅读 PUBLIC_INFO.txt 可知:
KCTF 2026 - Rosetta Calibration
Final EXE SHA256:caaa1c964b15d9b3250be0dc64412e5f64c29e297a971c226e8dd705d6b87e67
Public valid pair:Name : CAAA1C964B15D9B3Serial : $/.;-):,(-#!%*-&?+@
Target:Find the unique Serial for Name KCTF.字符串侦察
提取 kctf_rosetta.exe 字符串
strings -n 5 kctf_rosetta.exe
- 只 import 了
GetStdHandle/ReadFile/WriteFile/ExitProcess—— 纯控制台 I/O,无网络、无反调试、无壳,可以放心静态分析; Sixteen symbols. Sixteen meanings.印证“16 符号 ↔ 16 数值”的映射设计;- 三种失败提示对应三道关卡:
Invalid name format(Name 格式)、The inscription cannot be translated(符号查表失败/重复)、Calibration rejected(最终比对失败); - 提示用
ReadFile逐字符读入(后面反汇编可证实),Name 与 Serial 分别缓存在栈上不同区域。
完整反汇编与逐段分析
objdump -d -M intel kctf_rosetta.exe
PE 结构(后续取数的依据)
ImageBase: 0x140000000.text VA 0x1000 Raw 0x400 (代码).rdata VA 0x2000 Raw 0xa00 (只读数据:符号表、初始状态、字符串).pdata VA 0x3000 Raw 0xe00即:虚拟地址 0x140002000 对应文件偏移 0xa00。
main:读入 Name
程序用 GetStdHandle 拿到 stdin/stdout,打印横幅后提示 Name : ,随后逐字节 ReadFile 循环:
140001100: movzx eax, byte [rsp+0x30] ; 读到的字符140001105: cmp eax, 0xd ; '\r' → 丢弃继续读14000110a: cmp eax, 0xa ; '\n' → 结束输入14000110f: lea ecx, [r15+1]140001113: cmp ecx, 0x27 ; 上限 3914000111b: mov [rsp+rdx+0x80], al ; name_buf[r15++] = ch- Name 存到
[rsp+0x80],长度上限 39; - 遇
\r忽略、遇\n结束(兼容 CRLF)。
Name 合法性校验
14000123e: lea eax, [rbx-0x21] ; eax = len - 33140001241: cmp eax, -0x20 ; 无符号比较 0xFFFFFFE0140001244: jae ok ; len-33 ∈ [-32,-1] 即 len∈[1,32] 才放行 ; 否则打印 "[-] Invalid name format." 退出
140001270: movzx ecx, byte [rsp+rax+0x80]140001278: add cl, -0x2114000127b: cmp cl, 0x5e ; (c-0x21) 无符号 < 0x5E14000127e: jae invalid ; → 每个字符必须是 0x21..0x7E(可打印 ASCII)Name = 1~32 个可打印 ASCII 字符。KCTF(4 字符)合法。
读入 Serial 与格式校验
Serial 同样逐字符读入到 [rsp+0x50],随后:
140001288: cmp r12d, 0x13 ; 1914000128c: jne reject ; 长度必须恰为 19Serial 必须恰好 19 个字符。
Serial 结构解析:dash 位置由 0x4210 决定
140001295: mov ebp, 0x4210 ; 掩码1400012c5: cmp r14d, 0xe ; i <= 14 ?1400012c9: ja translate_it1400012cb: bt ebp, r14d ; 测试掩码第 i 位1400012cf: jb 1400012b0
1400012b0: cmp byte [rsp+r14+0x50], 0x2d ; '-' ?1400012b6: jne reject ; 掩码位必须 是 '-'
translate_it:1400012d1: movzx ecx, byte [rsp+r14+0x50]1400012d7: call 0x140001520 ; translate(sym) → nibble1400012dc: cmp eax, 0xf1400012df: ja reject ; 必须是合法符号(≤15)1400012e6: ... bt edi, eax ; 该 nibble 是否已用过?1400012ed: jb reject ; 重复 → 拒绝("排列"约束)1400012ef: or edi, edx ; 记录到位图1400012f7: mov [rsp+rcx+0x40], al; nibbles[n++] = v
140001304: cmp r15d, 0x10 ; 恰好 16 个 nibble14000130a: cmp edi, 0xffff ; 且 0..15 全部出现(排列!)0x4210 = 0100_0010_0001_0000₂,置位的是第 4、9、14 比特。用公开样本验证:$/.;-):,(-#!%*-&?+@ 的第 4、9、14 位(0 起数)恰为 -,-,-。
其余 16 个位置(0-3, 5-8, 10-13, 15-18)依次翻译成 nibble,必须构成 0–15 的排列。
translate():符号 → nibble(含一个 XOR 陷阱)
0x140001520 translate(cl): bl = cl eax = base_lookup(cl) ; 调 0x140001540 ecx = (bl == 0x28) ? 1 : 0 ; 字符是 '(' ? return eax ^ ecx ; ← 陷阱:'(' 的结果要异或 1
0x140001540 base_lookup(cl): cl -= 0x21 if ((uint8)cl > 0x1f) return -1 ; 字符不在 0x21..0x40 → 非法 return dword_table[cl] ; 表在 0x140002000derive():Name → 期望的 16 个 nibble
0x1400013e0 derive(rcx=name, edx=len, r8=out16): state[16] = xmmword[0x140002080] ; 初始状态(见 §5) if (len == 0) goto emit r9 = 0; acc = 0 for (i = 0; i < len; i++) { a = i & 15 c = name[i] v = (c >> 4) ^ c ; 高低半字节异或 acc += v j = (v ^ r9) & 15 swap(state[a], state[j]) r9 += 3 ; 每轮扭动的步进 }emit: out[k] = state[(acc + k) & 15], k = 0..15 ; 其中 out[8] 写作 state[(acc&15)^8]、out[15] 写作 state[(acc-1)&15], ; 但对 4 比特值 XOR 8 ≡ +8 (mod 16)、-1 ≡ +15 (mod 16), ; 故统一为 out[k] = state[(acc+k) mod 16]对应原始指令(节选,证明 +3 步进与异或细节):
14000140e: shr edi, 4 ; c>>4140001411: xor edi, esi ; v = c ^ (c>>4)140001413: add eax, edi ; acc += v140001415: xor edi, r9d ; v ^= r9140001418: and edi, 0xf ; j = v & 15140001420-140001428: ; swap(state[i&15], state[j])14000142e: add r9d, 0x3 ; r9 += 3最终判定:SSE 整块比较
140001312: lea rcx, [rsp+0x80] ; name14000131f: mov edx, ebx ; len140001321: call 0x1400013e0 ; derive(name,len,[rsp+0x30])140001326: movdqa xmm0, [rsp+0x40] ; Serial 解析出的 16 个 nibble14000132c: pcmpeqb xmm0, [rsp+0x30] ; 与 derive 输出逐字节比较140001332: pmovmskb eax, xmm0140001336: xor eax, 0xffff ; 全等时为 014000133b: jne reject ; → "[+] Calibration accepted. Key correct!"提取内置数据
用 Python struct 解析 PE 节表,按 VA → 文件偏移 换算后直接读 .rdata:
- 符号查表(0x140002000,32 个 dword,索引 = 字符 − 0x21):
0000000140002000 04 00 00 00 ff ff ff ff 07 00 00 00 0c 00 00 000000000140002010 01 00 00 00 09 00 00 00 ff ff ff ff 03 00 00 000000000140002020 0b 00 00 00 0e 00 00 00 06 00 00 00 0d 00 00 000000000140002030 ff ff ff ff 08 00 00 00 0a 00 00 00 ff ff ff ff0000000140002040 ff ff ff ff ...(数字 0-9 全为 ff,即非法)0000000140002060 ff ff ff ff 0f 00 00 00 05 00 00 00 ff ff ff ff0000000140002070 ff ff ff ff ff ff ff ff 00 00 00 00 03 00 00 00整理成表(0xffffffff = 非法符号):2. 再应用 4.6 的 XOR 修正(( 异或 1),得到最终双射映射:
'?'→0 '%'→1 '('→2 '@'→3 '!'→4 ';'→5 '+'→6 '#'→7'.'→8 '&'→9 '/'→10 ')'→11 '$'→12 ','→13 '*'→14 ':'→1516 个合法符号恰好取遍 0–15,无重复 —— 这就是“Sixteen symbols, Sixteen meanings”。3. derive 的初始状态(0x140002080):
0000000140002080 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f即恒等排列 state = [0,1,2,...,15]。
算法重建与唯一性论证
完整算法(伪代码)
serial_of(name): # 1) 交换混淆 state = [0..15]; r = 0; acc = 0 for i, c in enumerate(name): v = c ^ (c >> 4) # 字节高低半字节异或 acc = (acc + v) mod 2^32 j = (v ^ r) mod 16 swap(state[i mod 16], state[j]) r = (r + 3) mod 2^32 # 2) 轮转输出 nibbles[k] = state[(acc + k) mod 16] for k in 0..15 # 3) nibble → 符号(§5 双射表),再在 4/9/14 位插入 '-' symbols = [inv_table[v] for v in nibbles] return symbols[0:4] + '-' + symbols[4:8] + '-' \ + symbols[8:12] + '-' + symbols[12:16]为什么 Serial 唯一
- 校验逻辑强制:长度 19、第 4/9/14 位为
-、其余 16 符号经双射表映射后必须是 0–15 的排列且逐位等于derive(name); derive(name)对给定 name 是确定性的(无随机、无时间、无环境依赖——import 表只有 I/O 四个函数可为证);- 符号表是双射:每个 nibble 值有且仅有一个符号表示。 三者结合 → 对每个合法 Name,被接受的 Serial 存在且唯一。keygen 就是确定性正算,无需搜索/爆破。
Keygen 脚本
#!/usr/bin/env python3
TABLE = { '!': 4, '#': 7, '$': 12, '%': 1, '&': 9, '(': 2, # '(' 查表 3 ^ 1 = 2 ')': 11, '*': 14, '+': 6, ',': 13, '.': 8, '/': 10, ':': 15, ';': 5, '?': 0, '@': 3,}INV = {v: k for k, v in TABLE.items()}DASH_AT = (4, 9, 14)
def derive(name: bytes): """0x1400013e0:交换混淆 + 轮转输出,返回期望的 16 个 nibble""" state = list(range(16)) r, acc = 0, 0 for i, c in enumerate(name): v = c ^ (c >> 4) acc = (acc + v) & 0xFFFFFFFF j = (v ^ r) & 0xF a = i & 0xF state[a], state[j] = state[j], state[a] r = (r + 3) & 0xFFFFFFFF return [state[(acc + k) & 0xF] for k in range(16)]
def serial_of(name: str) -> str: nibbles = derive(name.encode()) assert sorted(nibbles) == list(range(16)), "内部错误:非排列" s = [INV[v] for v in nibbles] out, it = [], iter(s) for pos in range(19): out.append('-') if pos in DASH_AT else out.append(next(it)) return ''.join(out)
if __name__ == "__main__": assert serial_of("CAAA1C964B15D9B3") == "$/.;-):,(-#!%*-&?+@" print("Name : KCTF") print("Serial:", serial_of("KCTF"))可得:
Name : KCTFSerial: &/@$-,*?:-!#)%-;+(.