首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[分享][讨论]KCTF 2026 · Rosetta Calibration — Write-up
发表于: 2026-8-25 18:12
460
[分享][讨论]KCTF 2026 · Rosetta Calibration — Write-up
烟雨.
2026-8-25 18:12
460
# KCTF 2026 · Rosetta Calibration — Write-up > 目标:分析 `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=CAAA1C964B15D9B3`,`Serial=$/.;-):,(-#!%*-&?+@`。 - **这个公开对是整个解题的钥匙**:它既是验证我们理解是否正确的手段,也是"训练样本"。 --- ## 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? 主函数里有一行很关键的位操作: ```c 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 ``` > 初学容易踩坑:`0x4210` 是 **bit 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): ```c 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` 里还有一句 ```c 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`) ```c 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 判定 ```c 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 注册机 ```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 = `&/@$-,*?:-!#)%-;+(.`** ---
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
烟雨.
3
发帖
0
回帖
0
RANK
关注
私信
他的文章
KCTF 2026第二题「巳时·绿光幽语」Writeup
434
[分享][讨论]KCTF 2026 · Rosetta Calibration — Write-up
459
[分享]KCTF 2026第二题「巳时·绿光幽语」Writeup
450
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部