-
-
[原创]2026 KCTF 第二题:巳时·绿光幽语 Writeup
-
发表于: 2026-8-11 13:22 16
-
题面:幽光一隙,如泣如诉;暗影幢幢,孰为真吾?
最终注册码:yb6tcr
样本表面上是一个几乎没有业务逻辑的 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 文本“恭喜成功!”,并在原始进程中动态验证通过。
由于 frozen 模块使用 Python 3.13 的 marshal/code object 格式,复现时建议直接使用 Python 3.13。
PE 节区、导入表和入口代码均符合 PyInstaller bootloader 特征,未发现 UPX、VMProtect、Themida、TLS callback 或 VM dispatcher 等额外保护证据。后续求解实际定位到的注册码逻辑位于 Python bytecode 自修改路径;本文不把题面中的“时间戳算法、硬件密钥”等剧情描述直接等同为样本实现。
文件尾部可以找到 PyInstaller cookie:
解析结果如下:
关键 TOC 项:
解压并 marshal.loads() 反序列化 main 后,其代码等价于:
代码对象总大小仅 159 bytes。PYZ.pyz 中的 112 个条目也都是标准库模块,没有正常意义上的题目模块。因此,仅完成常规 PyInstaller 解包会得到“程序直接退出”的错误结论。
内嵌 DLL 指纹:
CPython 将一部分标准库以 frozen module 形式编译进 python313.dll。定位过程为:
正常 frozen os 的顶层逻辑在 process_cpu_count 附近结束;题目版本从 <frozen os> bytecode offset 0x0AC0、源码行号 1185 开始追加逻辑,并出现以下异常名字:
其中 my_function 的原始定义为:
对应 Python 3.13 co_code:
对追加 bytecode 反汇编后,可还原为以下等价逻辑:
错误 key 通常会产生非法 bytecode、非法 UTF-8 或执行异常,异常被吞掉后进入下一轮,最多尝试 20 次。程序没有传统的 if input == serial 字符串比较。
用于构造新 co_code 的密文是:
但 my_function 在被替换前的合法 co_code 已经存在于同一个 code object 中:
由于:
正确 key 必须满足:
逐字节 XOR 得到:
转换为 ASCII:
题目要求输入长度为 6,key stream 也严格以 6 bytes 为周期,因此得到注册码:
这个推导直接来自可执行 bytecode 的完整 known plaintext,不依赖穷举,也不依赖猜测成功文案。
成功字符串密文为:
代入 yb6tcr:
同一 key 同时满足以下两个独立约束:
因此可以排除偶然得到可打印周期串的可能性。
真实 Windows Console 中输入:
程序输出:
并以返回码 0 退出。输入 abcdef 时,程序不会打印错误,而是再次显示完整提示并继续等待下一次输入。
trace_stage1.py 在父子进程上挂钩 ReadFile、WriteFile、ReadConsole*、WriteConsole*,向真实读取路径注入 yb6tcr\n。最终 trace_yb6tcr.jsonl 中记录到:
调用栈中存在 python313.dll 帧,证明输出来自题目携带的 Python runtime,而不是 solver 自行打印。
将题目内的 python313.dll 放入 Python 3.13.1 embeddable runtime,仅运行:
import os 时即出现注册码挑战;输入正确 key 后输出:
这进一步证明挑战入口位于修改版 frozen os,而不是 CArchive 中仅执行退出逻辑的 main.py。
solve_key.py 不依赖当前样本的固定文件偏移,流程如下:
关键代码关系:
以下命令均在仓库根目录执行。
预期输出:
检查末尾成功写出:
按照官方比赛规则,解题后需要整理完整解题过程,并提交所使用的工具、keygen/注册机、脚本或源代码。本题提交包建议包含:
提交及公开时注意:
官方公开规则未规定统一 Markdown 模板、固定标题字段或字数要求,因此本文采用“题目信息、环境工具、完整过程、算法推导、动态验证、复现命令、附件清单、最终答案”的结构。
其完整验证链为:
| 项目 | 环境 |
|---|---|
| 操作系统 | 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 |
| 项目 | 值 |
|---|---|
| 文件大小 | 7,438,179 bytes |
| SHA-256 | f6b192f9ee87c4ad2bc8681291ca428cf8e98fec26fa5d7b82c6e791a7a90ead |
| MD5 | d2fc58976cac7403cee3a8b8f2560081 |
| 文件格式 | PE32+ x64 / Windows CUI |
| Machine | 0x8664 |
| ImageBase | 0x140000000 |
| OEP | RVA 0xD4C0 |
| Authenticode | 未签名 |
| TLS callback | 无 |
| 字段 | 值 |
|---|---|
| Cookie 文件偏移 | 0x717F0B |
| Package 起点 | 0x54600 |
| Package 长度 | 0x6C3963 |
| TOC 文件偏移 | 0x717BAB |
| TOC 长度 | 0x360 |
| Python 版本字段 | 313 |
| Python DLL | python313.dll |
| 项目 | 值 |
|---|---|
| 文件大小 | 7,127,552 bytes |
| SHA-256 | af3dae93d87cba3ab60d47a1512304d5e0985808e1264107c437e99beaf23729 |
| Python 版本资源 | 3.13.1 |
| ImageBase | 0x180000000 |
| 证据 | 文件 |
|---|---|
| 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 |
| 类型 | 文件 | 用途 |
|---|---|---|
| 正式 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 跟踪真实输入输出路径 |
MEI\x0c\x0b\x0a\x0b\x0e
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
import sys
sys.exit(0)
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
x, my_function, buffer, input, key, b_key, c, buf,
__code__, replace, succ, Exception
def my_function():
print(666)
95005b0100000000000000005301350100000000000020006700
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
c = ec626d7563727962367463722a63037563727962367443721e62
original = 95005b0100000000000000005301350100000000000020006700
patched_code = c XOR repeat(key)
repeat(key) = c XOR original
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。