看雪 2026 KCTF

26 年 8 月 15 日 星期六
1797 字
9 分钟

签到题:辰时·钟鸣破晓

附件

Plain
https://pan.baidu.com/s/1N3YqHD0DZkMhNsmBUDxykQ?pwd=gy39 提取码: gy39

解压后阅读 PUBLIC_INFO.txt 可知:

Plain
KCTF 2026 - Rosetta Calibration

Final EXE SHA256:
caaa1c964b15d9b3250be0dc64412e5f64c29e297a971c226e8dd705d6b87e67

Public valid pair:
Name   : CAAA1C964B15D9B3
Serial : $/.;-):,(-#!%*-&?+@

Target:
Find the unique Serial for Name KCTF.

字符串侦察

提取 kctf_rosetta.exe 字符串

Plain
strings -n 5 kctf_rosetta.exe
  1. 只 import 了 GetStdHandle/ReadFile/WriteFile/ExitProcess —— 纯控制台 I/O,无网络、无反调试、无壳,可以放心静态分析;
  2. Sixteen symbols. Sixteen meanings. 印证"16 符号 ↔ 16 数值"的映射设计;
  3. 三种失败提示对应三道关卡:Invalid name format(Name 格式)、The inscription cannot be translated(符号查表失败/重复)、Calibration rejected(最终比对失败);
  4. 提示用 ReadFile 逐字符读入(后面反汇编可证实),Name 与 Serial 分别缓存在栈上不同区域。

完整反汇编与逐段分析

Bash
objdump -d -M intel kctf_rosetta.exe

PE 结构(后续取数的依据)

Plain
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 循环:

Assembly
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                  ; 上限 39
14000111b:  mov    [rsp+rdx+0x80], al         ; name_buf[r15++] = ch
  • Name 存到 [rsp+0x80],长度上限 39;
  • \r 忽略、遇 \n 结束(兼容 CRLF)。

Name 合法性校验

Assembly
14000123e:  lea    eax, [rbx-0x21]   ; eax = len - 33
140001241:  cmp    eax, -0x20        ; 无符号比较 0xFFFFFFE0
140001244:  jae    ok                ; len-33 ∈ [-32,-1] 即 len∈[1,32] 才放行
             ; 否则打印 "[-] Invalid name format." 退出

140001270:  movzx  ecx, byte [rsp+rax+0x80]
140001278:  add    cl, -0x21
14000127b:  cmp    cl, 0x5e          ; (c-0x21) 无符号 < 0x5E
14000127e:  jae    invalid           ; → 每个字符必须是 0x21..0x7E(可打印 ASCII)

Name = 1~32 个可打印 ASCII 字符。KCTF(4 字符)合法。

读入 Serial 与格式校验

Serial 同样逐字符读入到 [rsp+0x50],随后:

Assembly
140001288:  cmp    r12d, 0x13        ; 19
14000128c:  jne    reject            ; 长度必须恰为 19

Serial 必须恰好 19 个字符。

Serial 结构解析:dash 位置由 0x4210 决定

Assembly
140001295:  mov    ebp, 0x4210       ; 掩码
1400012c5:  cmp    r14d, 0xe         ; i <= 14 ?
1400012c9:  ja     translate_it
1400012cb:  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) → nibble
1400012dc:  cmp    eax, 0xf
1400012df:  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 个 nibble
14000130a:  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 陷阱)

Assembly
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]                ; 表在 0x140002000

derive():Name → 期望的 16 个 nibble

Assembly
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 步进与异或细节):

Assembly
14000140e:  shr    edi, 4           ; c>>4
140001411:  xor    edi, esi         ; v = c ^ (c>>4)
140001413:  add    eax, edi         ; acc += v
140001415:  xor    edi, r9d         ; v ^= r9
140001418:  and    edi, 0xf         ; j = v & 15
140001420-140001428:                 ; swap(state[i&15], state[j])
14000142e:  add    r9d, 0x3         ; r9 += 3

最终判定:SSE 整块比较

Assembly
140001312:  lea    rcx, [rsp+0x80]      ; name
14000131f:  mov    edx, ebx             ; len
140001321:  call   0x1400013e0          ; derive(name,len,[rsp+0x30])
140001326:  movdqa xmm0, [rsp+0x40]     ; Serial 解析出的 16 个 nibble
14000132c:  pcmpeqb xmm0, [rsp+0x30]    ; 与 derive 输出逐字节比较
140001332:  pmovmskb eax, xmm0
140001336:  xor    eax, 0xffff          ; 全等时为 0
14000133b:  jne    reject
             ; → "[+] Calibration accepted. Key correct!"

提取内置数据

用 Python struct 解析 PE 节表,按 VA → 文件偏移 换算后直接读 .rdata

  1. 符号查表(0x140002000,32 个 dword,索引 = 字符 − 0x21):
Assembly
0000000140002000  04 00 00 00 ff ff ff ff 07 00 00 00 0c 00 00 00
0000000140002010  01 00 00 00 09 00 00 00 ff ff ff ff 03 00 00 00
0000000140002020  0b 00 00 00 0e 00 00 00 06 00 00 00 0d 00 00 00
0000000140002030  ff ff ff ff 08 00 00 00 0a 00 00 00 ff ff ff ff
0000000140002040  ff ff ff ff ...(数字 0-9 全为 ff,即非法)
0000000140002060  ff ff ff ff 0f 00 00 00 05 00 00 00 ff ff ff ff
0000000140002070  ff ff ff ff ff ff ff ff 00 00 00 00 03 00 00 00

整理成表(0xffffffff = 非法符号): 2. 再应用 4.6 的 XOR 修正(( 异或 1),得到最终双射映射:

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

16 个合法符号恰好取遍 0–15,无重复 —— 这就是"Sixteen symbols, Sixteen meanings"。 3. derive 的初始状态(0x140002080):

Assembly
0000000140002080  00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f

即恒等排列 state = [0,1,2,...,15]

算法重建与唯一性论证

完整算法(伪代码)

Plain
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 唯一

  1. 校验逻辑强制:长度 19、第 4/9/14 位为 -、其余 16 符号经双射表映射后必须是 0–15 的排列且逐位等于 derive(name)
  2. derive(name) 对给定 name 是确定性的(无随机、无时间、无环境依赖——import 表只有 I/O 四个函数可为证);
  3. 符号表是双射:每个 nibble 值有且仅有一个符号表示。 三者结合 → 对每个合法 Name,被接受的 Serial 存在且唯一。keygen 就是确定性正算,无需搜索/爆破。

Keygen 脚本

Python
#!/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"))

可得:

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

文章标题:看雪 2026 KCTF

文章作者:Crazy0x70

文章链接:https://www.crazy0x70.com/posts/8[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。