-
-
[原创]KCTF 2026 第二题:巳时·绿光幽语 writeup
-
发表于: 14小时前 217
-
题目给了一个 Windows 程序,运行后要求输入 6 个字符来恢复下面的函数:

用 Detect It Easy 看了一眼,样本是 64 位 PE,文件末尾带有很大的 overlay。基本可以确定是 PyInstaller one-file打包。

先用 pyinstxtractor 解包:
解包后比较值得注意的只有几项:

题目使用 Python 3.13,使用 pycdc 查看main.pyc

main 内容等价于:
这个结果和实际运行现象明显矛盾:程序明明会打印提示并读取 key,入口脚本却直接退出。说明真正的逻辑不在 main,而是在启动过程中加载的其他组件里。
这里检查对比一下官方dll和题目dll的签名信息,可以明显看出dll是未进行官方签名的
官方3.13.15版本python313.dll

题目python313.dll

把 python313.dll 单独载入 IDA,在 Strings 窗口搜索:

字符串位于 .rdata,附近还能看到一些信息:

process_cpu_count 是 Python 3.13 os 模块中的函数。结合周围的 marshal 类型字节,可以判断题目把自定义逻辑塞进了冻结的 os 模块。
这里没有正常的代码交叉引用并不奇怪。my_function 不是 C 函数名,而是序列化 CodeObject 中的字符串,解释器通过冻结模块表和 marshal 解析器间接读取。
Python 3.13 marshal 中常见的类型标记如下:
参考如下资料 /3.13/Python/marshal.c
高位 0x80 是引用标记,所以 F3 & 0x7F == 73。
沿着 process_cpu_count 向后看,可以确定它的 CodeObject 尾部结束于 0x1804DC68E 左右。随后出现一个新的 63,也就是 my_function CodeObject:
我用 IDAPython 把这一段直接导出:
这 125 字节虽然是完整的嵌套 CodeObject,但不能直接 marshal.load()。里面存在 72 xx xx xx xx 回引用,引用表是在更早的父 marshal 流中建立的。
按照 Python 3.13 CodeObject 的字段顺序手工读取,可以得到:
把这些字段装进一个最小 CodeType,再用 Python 3.13 自带的 dis 反汇编:
所以 DLL 中原本放着的是一个诱饵函数:
继续向后整理 marshal 数据,可以看到一组很不自然的常量:
这些数据连在一起,已经能提出几个比较靠谱的判断:
这时还只是猜测,需要用数据本身验证。
把 8 个 16 字节块拼起来,逐字节与 0x55 异或后得到:
这也解释了旁边为什么同时出现 0x55、utf-8 和 6。
注意,0x55 对应的是字符 U,但只是解开提示所用的掩码,不是最终 key。


当前诱饵函数的 co_code 长度正好是 26 字节:
后面紧挨着 co_code 字符串的可疑数据同样是 26 字节:
既然长度一致,而且旁边明确写着 co_code,最直接的验证就是把两段逐字节异或:
两者逐字节异或,前六字节为:
后续 20 字节继续以相同周期重复,所以 key 为:
再用这个周期 key 解密 15 字节数据:
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。