首页
社区
课程
招聘
[分享]KCTF 2026第二题「巳时·绿光幽语」Writeup
发表于: 2026-8-25 18:10 176

[分享]KCTF 2026第二题「巳时·绿光幽语」Writeup

2026-8-25 18:10
176

KCTF 2026第二题「巳时·绿光幽语」Writeup

目标:分析 kctf_rosetta.exe,求出 Name = KCTF 的唯一 Serial。
这是一道典型的「注册码 / 序列号验证」类题目,属于 CTF 逆向的入门题。


0. 题目信息

Final EXE SHA256: caaa1c964b15d9b3250be0dc64412e5f64c29e297a971c226e8dd705d6b87e67

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

Target:
Find the unique Serial for Name KCTF.
  • 文件类型:64 位 PE(x64),入口点 start @ 0x140001000,只有 5 个函数。
  • 题目给了一个「合法对」:Name=CAAA1C964B15D9B3Serial=$/.;-):,(-#!%*-&?+@
  • 这个公开对是整个解题的钥匙:它既是验证我们理解是否正确的手段,也是"训练样本"。

1. 环境与工具

用途 工具
反汇编 / 反编译 IDA Pro(Hex-Rays F5 反编译)
代码验证 Python 3(自己写注册机)
运行验证 直接在命令行跑 kctf_rosetta.exe

找关键代码的通用姿势:程序会打印提示语,先到字符串窗口(IDA 的 Strings / Shift+F12)看有哪些字符串,尤其是成功/失败提示,然后从它们 Ctrl+X(交叉引用)跳到引用它的代码。

本题的关键字符串:

[+] The symbols have found their meanings.   ← 成功提示
[+] Calibration accepted. Key correct!
[-] Calibration rejected. Key incorrect.     ← 比对失败
[-] The inscription cannot be translated.    ← Serial 格式错误
[-] Invalid name format.                     ← Name 格式错误

顺着成功提示 0x14000211C 的引用,就找到了主函数 start


2. 程序整体流程(反编译后)

1. 打印标题
2. 读取 Name   (读到回车为止,存进 v16[],长度 v5)
3. 读取 Serial (读到回车为止,存进内存,长度 n19)
4. 检查 Name 格式:
   - 长度 >= 1
   - 每个字符在 ASCII 33 ~ 126(可打印字符)之间
5. 检查 Serial 格式:
   - 必须正好 19 个字符
   - 格式是 16"符号" + 3'-' 分隔符(见第 3.2 节)
6.16 个符号逐个映射成 0~15 的数字(见第 3.3 节)
   - 要求 16 个数字互不重复,即恰好是 0~15 的排列
7. 用同样的 Name 算出 16 个数字(也是 0~15 的排列,见第 3.4 节)
8. 用 SSE 指令逐字节比较两组 16 字节:
   pcmpeqb(比较) + pmovmskb(取出比较结果掩码),全 1 则通过

翻译成人话就是:

Serial 本质上是 Name 的"加密结果"。 把 Name 算出一个 16 字节序列,Serial 的 16 个符号解码后必须和它逐字节相等,且每个数字 0~15 恰好出现一次。


3. 详细逻辑链(核心推导)

3.1 Serial 为什么是 19 字符?分组为什么是 4-4-4-4?

主函数里有一行很关键的位操作:

n16912 = 16912;           // 0x4210
...
while ( n0xE > 0xE || !_bittest(&n16912, n0xE) )
{
    // 把第 n0xE 个字符当作"符号"映射成数字
    ...
}
if ( serial[n0xE] != 45 ) // 45 就是 '-',ASCII 破折号
    goto reject;

16912 = 0x4210,拆开看二进制位:

0x4210 = 0x4000 + 0x200 + 0x10
       = bit14 + bit9  + bit4

初学容易踩坑:0x4210bit 14、9、4 置位,不是 bit 12!0x1000 才是 bit 12,0x4000 是 bit 14。

_bittest 判断第 n0xE 位是否为 1。当位置 n0xE 的 bit 是 1(且 n0xE <= 14)时,程序认为这里应该是一个 - 分隔符。所以:

索引:  0  1  2  3 | 4 | 5  6  7  8 | 9 | 10 11 12 13 | 14 | 15 16 17 18
内容:  符 符 符 符 | - | 符 符 符 符 | - | 符  符 符  符 | -  | 符  符 符  符
分组:    4 个符号    |   4 个符号    |   4 个符号    |    4 个符号

结论:Serial = 16 个符号,分成 4-4-4-4,中间用 - 连接,共 19 字符。

验证公开对:$/.; - ):,( - #!%* - &?+@ ✅ 完全吻合。

3.2 符号字典("Rosetta 字母表")

sub_140001540 把一个字符按 char - 33 作为下标查表 dword_140002000(32 个 dword,覆盖 ASCII 33~64):

n0x1F = a1 - 33;
if ( n0x1F <= 0x1F )
    return dword_140002000[n0x1F];
return 0xFFFFFFFF;   // 查不到 → 非法字符

读出整张表(用 IDA 的 get_int 逐个读取更保险,别手数 hex)后,有效的符号只有 16 个,其余都是 -1

字符 ASCII-33 下标
! 0 4
# 2 7
$ 3 12
% 4 1
& 5 9
( 7 3
) 8 11
* 9 14
+ 10 6
, 11 13
. 13 8
/ 14 10
: 25 15
; 26 5
? 30 0
@ 31 3

⚠️ 一个反编译器容易看漏的细节sub_140001520 里还有一句

return (n40 == 40) ^ sub_140001540();   // '(' 是 ASCII 40

也就是字符 ( 特殊处理:查表结果再 XOR 1。表里 ( = 3,3 ^ 1 = 2,所以 ( 实际映射到 2

初学提醒:反编译结果 ≠ 逐字节事实。凡是有 ==setz^ 这类可疑写法,回到汇编层 sub_140001520 看一眼(cmp bl, 28h; setz cl; xor eax, ecx),确认确实是 ( 特例。

3.3 核心:Name 如何变成 16 个数字(sub_1400013E0

v10[0] = xmmword_140002080;   // 初始表 = {0,1,2,...,15}
v5 = 0; v3 = 0;
for ( i = 0; i < name_len; i++ )
{
    v6 = name[i] ^ (name[i] >> 4);   // 8 位混淆值
    v5 += v6;                        // 累加和(8 位回绕)
    v7 = ((v3 & 0xFF) ^ v6) & 0xF;   // 交换位置
    swap(table[i & 0xF], table[v7]);
    v3 += 3;
}
// 输出 16 字节:
out[0..7]  = table[(v5 + 0) & 0xF] ... table[(v5 + 7) & 0xF]
out[8]     = table[(v5 & 0xF) ^ 8]      // 等价于 table[(v5+8) & 0xF]
out[9..14] = table[(v5 + 9) & 0xF] ... table[(v5 + 14) & 0xF]
out[15]    = table[(v5 - 1) & 0xF]

要点:

  1. 初始表 xmmword_140002080 读出就是 00 01 02 ... 0F,是一个 0~15 的排列。
  2. 洗牌只做交换,所以交换后仍然是 0~15 的排列
  3. 输出的 16 个下标是 v5+0..7, v5+8, v5+9..14, v5+15,mod 16 后恰好覆盖 0~15 全部下标(因为 (v5 & 0xF) ^ 8 == (v5 + 8) & 0xF,二进制下加 8 就是翻转 bit3)。
  4. 因此 Name 算出来的 16 个字节,天然就是 0~15 各一次 → 和 Serial 的检查(n0xFFFF == 0xFFFF)自洽。

为什么是"排列"很关键:题目说 "Sixteen symbols. Sixteen meanings."(16 个符号、16 种含义)就是暗示这套一一映射。

3.4 判定

sub_1400013E0(name, name_len, &computed);          // 算 Name 的 16 字节
if ( _mm_movemask_epi8(_mm_cmpeq_epi8(serial_nibbles, computed)) == 0xFFFF )
    // 16 字节全相等 → 成功

4. 求解过程(如何得到 KCTF 的序列号)

第 1 步:用公开对建立信任

先不要急着解 KCTF。先确认你读懂的算法是对的 —— 拿公开对 CAAA1C964B15D9B3 反算一遍,如果结果等于题目给的 $/.;-):,(-#!%*-&?+@,说明算法理解正确。

第 2 步:写 Python 注册机

# ============ 1) 符号字典(来自 dword_140002000 + '(' 特例) ============
symbol_map = {
    '!':4, '#':7, '$':12, '%':1, '&':9, '(':2, ')':11, '*':14,
    '+':6, ',':13, '.':8, '/':10, ':':15, ';':5, '?':0, '@':3,
}
inv = {v: k for k, v in symbol_map.items()}   # 数字 -> 符号(求序列号用反向表)

# ============ 2) Name -> 16 个数字(sub_1400013E0) ============
def name_to_nibbles(name):
    table = list(range(16))       # 初始表 0..15
    v3, v5 = 0, 0
    for i, ch in enumerate(name.encode('latin1')):
        v6 = ch ^ (ch >> 4)
        v5 = (v5 + v6) & 0xFF
        v7 = ((v3 & 0xFF) ^ v6) & 0xF
        table[i & 0xF], table[v7] = table[v7], table[i & 0xF]
        v3 = (v3 + 3) & 0xFF
    out = [table[(v5 + i) & 0xF] for i in range(8)]
    out.append(table[(v5 & 0xF) ^ 8])
    out += [table[(v5 + i) & 0xF] for i in range(9, 15)]
    out.append(table[(v5 - 1) & 0xF])
    return out

# ============ 3) 用公开对验证 ============
pub_name, pub_serial = "CAAA1C964B15D9B3", "$/.;-):,(-#!%*-&?+@"
pub_syms = [c for c in pub_serial if c != '-']
assert name_to_nibbles(pub_name) == [symbol_map[c] for c in pub_syms], "算法理解错了!"
print("[*] 公开对验证通过")

# ============ 4) 求 KCTF 的序列号 ============
target = name_to_nibbles("KCTF")
assert sorted(target) == list(range(16))        # 必须恰好 0..15 各一次
serial = "".join(inv[v] for v in target)        # 数字 -> 符号
serial = "-".join([serial[0:4], serial[4:8], serial[8:12], serial[12:16]])
print("[+] Serial for KCTF:", serial)

第 3 步:结果 & 实测

KCTF nibbles : [9, 10, 3, 12, 13, 14, 0, 15, 4, 7, 11, 1, 5, 6, 2, 8]
KCTF serial  : &/@$-,*?:-!#)%-;+(.

运行程序验证:

$ printf 'KCTF\n&/@$-,*?:-!#)%-;+(.\n' | ./kctf_rosetta.exe
...
[+] The symbols have found their meanings.
[+] Calibration accepted. Key correct!        ← 成功!

最终答案:Name = KCTF,Serial = &/@$-,*?:-!#)%-;+(.



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

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回