首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]2026 KCTF 第二题:巳时·绿光幽语 Writeup
发表于: 2026-8-11 13:22
14
[原创]2026 KCTF 第二题:巳时·绿光幽语 Writeup
awaxiaoyu
2026-8-11 13:22
14
# 2026 KCTF 第二题:巳时·绿光幽语 Writeup > 题面:幽光一隙,如泣如诉;暗影幢幢,孰为真吾? > 最终注册码:`yb6tcr` ## 0. 题目信息 - 官方题号:2026 KCTF 第二题 - 官方题名:巳时·绿光幽语 - 本地目录:`第1关`(不含热身/第 0 关后的编号) - 样本:`题目.exe`(仓库中 `challenge.exe` 为相同文件) - 分析日期:2026-08-11 - 官方规则:[2026 KCTF 攻防赛规则](https://ctf.kanxue.com/rules-27.htm) - 官方赛题页:[第二题:巳时·绿光幽语](https://ctf.kanxue.com/question-27-144.htm) ## 1. 摘要 样本表面上是一个几乎没有业务逻辑的 PyInstaller one-file 程序:解开 CArchive 后,入口 `main` 只有 `import sys; sys.exit(0)`。真正的注册码校验代码被追加到了内嵌修改版 `python313.dll` 的 frozen 标准库模块 `os` 末尾,并在 Python 启动导入 `os` 时先于常规 `main.py` 执行。 隐藏逻辑要求输入 6 个字符,用输入字节循环 XOR 一段密文,并将解密结果动态写入 `my_function.__code__.co_code`;另一段密文解出 `my_function` 的字符串常量。利用未修改前 `my_function` 的合法原始字节码作为 known plaintext,可直接得到 6 字节周期密钥 `yb6tcr`,无需爆破。该 key 同时解出 UTF-8 文本“恭喜成功!”,并在原始进程中动态验证通过。 ## 2. 分析环境与工具 | 项目 | 环境 | |---|---| | 操作系统 | Windows x64 | | Python | 3.13.6 | | `pefile` | 2023.2.7 | | Frida | 17.9.1 | | PyInstaller 辅助分析 | 6.21.0 | | 静态辅助脚本 | `static_probe.py`、`extract_pyi.py`、`extract_frozen.py`、`solve_key.py` | | 动态辅助脚本 | `trace_stage1.py` | 由于 frozen 模块使用 Python 3.13 的 marshal/code object 格式,复现时建议直接使用 Python 3.13。 ## 3. 样本指纹 | 项目 | 值 | |---|---| | 文件大小 | 7,438,179 bytes | | SHA-256 | `f6b192f9ee87c4ad2bc8681291ca428cf8e98fec26fa5d7b82c6e791a7a90ead` | | MD5 | `d2fc58976cac7403cee3a8b8f2560081` | | 文件格式 | PE32+ x64 / Windows CUI | | Machine | `0x8664` | | ImageBase | `0x140000000` | | OEP | RVA `0xD4C0` | | Authenticode | 未签名 | | TLS callback | 无 | PE 节区、导入表和入口代码均符合 PyInstaller bootloader 特征,未发现 UPX、VMProtect、Themida、TLS callback 或 VM dispatcher 等额外保护证据。后续求解实际定位到的注册码逻辑位于 Python bytecode 自修改路径;本文不把题面中的“时间戳算法、硬件密钥”等剧情描述直接等同为样本实现。 ## 4. 第一层:识别并解开 PyInstaller ### 4.1 定位 CArchive 文件尾部可以找到 PyInstaller cookie: ```text MEI\x0c\x0b\x0a\x0b\x0e ``` 解析结果如下: | 字段 | 值 | |---|---:| | Cookie 文件偏移 | `0x717F0B` | | Package 起点 | `0x54600` | | Package 长度 | `0x6C3963` | | TOC 文件偏移 | `0x717BAB` | | TOC 长度 | `0x360` | | Python 版本字段 | `313` | | Python DLL | `python313.dll` | 关键 TOC 项: ```text main file offset 0x59F9E, compressed 0x79, raw 0x9F python313.dll file offset 0x2E8384, raw size 7,127,552 PYZ.pyz file offset 0x5C8895, size 0x14F316 ``` ### 4.2 入口 `main` 无法解释实际交互 解压并 `marshal.loads()` 反序列化 `main` 后,其代码等价于: ```python import sys sys.exit(0) ``` 代码对象总大小仅 159 bytes。`PYZ.pyz` 中的 112 个条目也都是标准库模块,没有正常意义上的题目模块。因此,仅完成常规 PyInstaller 解包会得到“程序直接退出”的错误结论。 ## 5. 第二层:定位修改版 `python313.dll` 内嵌 DLL 指纹: | 项目 | 值 | |---|---| | 文件大小 | 7,127,552 bytes | | SHA-256 | `af3dae93d87cba3ab60d47a1512304d5e0985808e1264107c437e99beaf23729` | | Python 版本资源 | 3.13.1 | | ImageBase | `0x180000000` | CPython 将一部分标准库以 frozen module 形式编译进 `python313.dll`。定位过程为: 1. 查找导出变量 `_PyImport_FrozenStdlib`,RVA 为 `0x625C38`。 2. 读取该变量保存的指针,得到 frozen table VA `0x1804BDC20`。 3. 按 24-byte `struct _frozen` 遍历表项:`name*`、`code*`、`size`、`is_package`。 4. 第 9 项(从 0 计数)为 `os`,其 marshal 数据位置如下: ```text entry RVA = 0x4BDCF8 code VA = 0x1804D1AE0 code RVA = 0x4D1AE0 raw offset = 0x4D00E0 size = 0xB93C (47,420 bytes) SHA-256 = 68b7a8bc5b29f69392f9109d6bdaa61a2b7a92c3e70f65052288b691c0eea02a 来源文件 = extracted_static/frozen_os.marshal ``` 正常 frozen `os` 的顶层逻辑在 `process_cpu_count` 附近结束;题目版本从 `<frozen os>` bytecode offset `0x0AC0`、源码行号 1185 开始追加逻辑,并出现以下异常名字: ```text x, my_function, buffer, input, key, b_key, c, buf, __code__, replace, succ, Exception ``` 其中 `my_function` 的原始定义为: ```python def my_function(): print(666) ``` 对应 Python 3.13 `co_code`: ```text 95005b0100000000000000005301350100000000000020006700 ``` ## 6. 第三层:还原校验算法 ### 6.1 伪代码 对追加 bytecode 反汇编后,可还原为以下等价逻辑: ```python x = 0 while x < 20: def my_function(): print(666) try: x += 1 prompt = bytes(v ^ 0x55 for v in buffer).decode("utf-8") print(prompt) key = input() if len(key) != 6: continue b_key = key.encode() c = bytes.fromhex( "ec626d7563727962367463722a63037563727962367443721e62" ) patched_code = bytes(c[i] ^ b_key[i % len(b_key)] for i in range(len(c))) my = my_function.__code__.replace(co_code=patched_code) succ = bytes.fromhex("9fe39b91f5ee9feaa691e9ed96deb7") patched_text = bytes( succ[i] ^ b_key[i % len(b_key)] for i in range(len(succ)) ) my = my.replace(co_consts=(None, patched_text.decode("utf-8"))) my_function.__code__ = my my_function() return except Exception: pass ``` 错误 key 通常会产生非法 bytecode、非法 UTF-8 或执行异常,异常被吞掉后进入下一轮,最多尝试 20 次。程序没有传统的 `if input == serial` 字符串比较。 ### 6.2 利用合法 bytecode 反推 key 用于构造新 `co_code` 的密文是: ```text c = ec626d7563727962367463722a63037563727962367443721e62 ``` 但 `my_function` 在被替换前的合法 `co_code` 已经存在于同一个 code object 中: ```text original = 95005b0100000000000000005301350100000000000020006700 ``` 由于: ```text patched_code = c XOR repeat(key) ``` 正确 key 必须满足: ```text repeat(key) = c XOR original ``` 逐字节 XOR 得到: ```text 79 62 36 74 63 72 79 62 36 74 63 72 79 62 36 74 63 72 79 62 36 74 63 72 79 62 ``` 转换为 ASCII: ```text yb6tcryb6tcryb6tcryb6tcryb ``` 题目要求输入长度为 6,key stream 也严格以 6 bytes 为周期,因此得到注册码: ```text yb6tcr ``` 这个推导直接来自可执行 bytecode 的完整 known plaintext,不依赖穷举,也不依赖猜测成功文案。 ### 6.3 使用第二组密文交叉验证 成功字符串密文为: ```text succ = 9fe39b91f5ee9feaa691e9ed96deb7 ``` 代入 `yb6tcr`: ```text succ XOR repeat("yb6tcr") = e681ade5969ce68890e58a9fefbc81 = UTF-8 "恭喜成功!" ``` 同一 key 同时满足以下两个独立约束: 1. 完整还原合法的 `my_function.co_code`。 2. 将 `succ` 解码为合法 UTF-8 成功字符串。 因此可以排除偶然得到可打印周期串的可能性。 ## 7. 动态验证 ### 7.1 原程序交互行为 真实 Windows Console 中输入: ```text yb6tcr ``` 程序输出: ```text yb6tcr 恭喜成功! ``` 并以返回码 `0` 退出。输入 `abcdef` 时,程序不会打印错误,而是再次显示完整提示并继续等待下一次输入。 ### 7.2 Frida I/O 证据 `trace_stage1.py` 在父子进程上挂钩 `ReadFile`、`WriteFile`、`ReadConsole*`、`WriteConsole*`,向真实读取路径注入 `yb6tcr\n`。最终 `trace_yb6tcr.jsonl` 中记录到: ```text WriteFile length = 12 hex = b9a7cfb2b3c9b9a6a3a10d0a GBK = 恭喜成功!\r\n ``` 调用栈中存在 `python313.dll` 帧,证明输出来自题目携带的 Python runtime,而不是 solver 自行打印。 ### 7.3 独立验证触发位置 将题目内的 `python313.dll` 放入 Python 3.13.1 embeddable runtime,仅运行: ```python import os print("AFTER_OS") ``` `import os` 时即出现注册码挑战;输入正确 key 后输出: ```text 恭喜成功! AFTER_OS ``` 这进一步证明挑战入口位于修改版 frozen `os`,而不是 CArchive 中仅执行退出逻辑的 `main.py`。 ## 8. Solver 设计 `solve_key.py` 不依赖当前样本的固定文件偏移,流程如下: 1. 从 EXE 尾部查找 PyInstaller cookie。 2. 解析 CArchive TOC,按 cookie 中的 DLL 名提取 `python313.dll`。 3. 用 PE export 定位 `_PyImport_FrozenStdlib`。 4. 遍历 frozen table,提取并 `marshal.loads()` 模块 `os`。 5. 递归寻找名为 `my_function` 的 code object。 6. 在顶层 bytes 常量中寻找与 `my_function.co_code` 等长的候选密文。 7. 计算 `cipher XOR co_code`,要求结果严格为指定长度的可打印周期串。 8. 使用候选 key 解密另一 bytes 常量,并要求结果是包含非 ASCII 字符的合法 UTF-8 文本。 9. 最后再次验证解出的 key 能逐字节重建原始 `co_code`。 关键代码关系: ```python stream = bytes(a ^ b for a, b in zip(encrypted_code, function_code.co_code)) key = stream[:6] assert stream == repeat(key, len(stream)) assert xor_repeating(encrypted_code, key) == function_code.co_code ``` ## 9. 复现步骤 以下命令均在仓库根目录执行。 ### 9.1 安装依赖 ```powershell python -m pip install pefile frida ``` ### 9.2 一步恢复注册码 ```powershell python .\第1关\solve_key.py .\第1关\题目.exe ``` 预期输出: ```text frozen_os_va=0x1804d1ae0 frozen_os_size=0xb93c key=yb6tcr success_text=恭喜成功! verification=PASS ``` ### 9.3 分阶段解包和验证 ```powershell python .\第1关\static_probe.py > .\第1关\static_probe.txt python .\第1关\extract_pyi.py ` .\第1关\题目.exe ` --extract .\第1关\extracted_static python .\第1关\extract_frozen.py ` .\第1关\extracted_static\python313.dll ` --module os ` --output .\第1关\extracted_static\frozen_os.marshal python .\第1关\solve_stage1.py ` .\第1关\extracted_static\frozen_os.marshal ``` ### 9.4 Frida 动态验证 ```powershell python .\第1关\trace_stage1.py ` .\第1关\题目.exe ` --inject yb6tcr ` --seconds 5 ` > .\第1关\trace_yb6tcr.jsonl ``` 检查末尾成功写出: ```powershell Get-Content .\第1关\trace_yb6tcr.jsonl -Encoding utf8 | Select-Object -Last 5 ``` ## 10. 关键证据索引 | 证据 | 文件 | |---|---| | PE / PyInstaller 静态探测 | `static_probe.txt` | | CArchive TOC | `pyi_toc.txt` | | PYZ 模块表 | `pyz_toc.txt` | | frozen table | `frozen_tables.txt` | | frozen `os` 反汇编 | `frozen_os_dis.txt`、`frozen_os_tail_dis.txt` | | `my_function` 代码对象与常量 | `frozen_os_analysis.txt` | | 自动恢复 solver | `solve_key.py` | | 分阶段恢复 solver | `solve_stage1.py` | | 原进程 Frida trace | `trace_yb6tcr.jsonl` | | 静态结论 | `analysis_static.md` | | 动态结论 | `analysis_dynamic.md` | ## 11. 证据边界与提交时的表述建议 1. **不要把题面叙事当作已实现机制。** 静态分析未发现真正的硬件指纹、系统时间戳校验或“16 个存储扇区”结构,应表述为题面背景/干扰信息。 2. **不要依赖 Frida 符号名解释精确内部函数。** Windows 导出符号不足时,`DebugSymbol` 可能给出近似或错误的函数名;可靠证据是模块名、模块偏移、I/O 字节和 Python frame。 3. **`base_library.zip` 的普通 `.pyc` 差异不是核心证据。** 不同 Python/PyInstaller 小版本会导致标准库字节码变化。载荷定位应以 frozen table、`os` 尾部新增 code object 和动态 `import os` 触发为准。 4. **时间无关结论主要来自静态算法。** 动态测试只修改了 `SOURCE_DATE_EPOCH`、`TZ`、`PYTHONHASHSEED`,没有篡改 Windows 系统时钟;因此不应单凭该实验宣称覆盖了所有时间源。 5. **避免扩展唯一性结论。** 由原始 `co_code` 反推出的预期 6-byte 周期注册码是 `yb6tcr`,且原程序验证通过;本文不声称穷举了所有 Unicode 输入或所有偶然可执行 bytecode。 6. **动态注入与人工输入是互补证据。** Frida `ReadFile` 注入证明真实进程接受 key 并输出成功;真实 Console 的 `WriteConsoleInputW` 遥测则证明常规交互路径同样成立。 ## 12. 提交材料与规则符合性 按照官方比赛规则,解题后需要整理完整解题过程,并提交所使用的工具、keygen/注册机、脚本或源代码。本题提交包建议包含: | 类型 | 文件 | 用途 | |---|---|---| | 正式 Writeup | `WRITEUP.md` | 完整静态、动态分析与注册码推导 | | 自动注册机 | `solve_key.py` | 直接从原始 EXE 自动恢复并验证 key | | 分阶段 Solver | `solve_stage1.py` | 对已提取的 frozen `os` 恢复 key | | PyInstaller 提取器 | `extract_pyi.py` | 解析 CArchive 并提取内嵌文件 | | frozen 模块提取器 | `extract_frozen.py` | 从 `python313.dll` 提取指定 frozen module | | 静态探测脚本 | `static_probe.py` | 输出 PE、资源、导入和容器信息 | | 动态验证脚本 | `trace_stage1.py` | Frida 跟踪真实输入输出路径 | 提交及公开时注意: 1. 答案与文章均为本队独立分析结果,文章内容应保持原创。 2. 比赛轮次进行期间不得跨队交流技术要点、关键提示或答案;公开文章应在该轮允许公开后发布。 3. 不在公开文章附件中重新分发官方题目样本;只提交规则要求的自编分析脚本、注册机和源代码。 4. 文章只陈述可由样本、脚本输出或动态记录复核的结论,题面叙事不作为实现机制证据。 官方公开规则未规定统一 Markdown 模板、固定标题字段或字数要求,因此本文采用“题目信息、环境工具、完整过程、算法推导、动态验证、复现命令、附件清单、最终答案”的结构。 ## 13. 最终答案 ```text 注册码(序列号):yb6tcr ``` 其完整验证链为: ```text PyInstaller CArchive -> 仅执行 sys.exit(0) 的常规 main.py -> 提取修改版 python313.dll -> _PyImport_FrozenStdlib -> frozen os 尾部载荷 -> encrypted_code XOR original co_code -> 6-byte 周期 yb6tcr -> 解出“恭喜成功!” -> 原始进程动态验证返回 0 ```
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-8-12 13:54 被kanxue编辑 ,原因:
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
awaxiaoyu
3
发帖
4
回帖
20
RANK
关注
私信
他的文章
KCTF 2026 第5关 cm.exe Write-up
12
[原创]2026 KCTF 第二题:巳时·绿光幽语 Writeup
14
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部