首页
社区
课程
招聘
[原创]看雪 CTF《巳时·绿光幽语》逆向:用 r0crawl-skills 穿透 PyInstaller 诱饵
发表于: 2天前 79

[原创]看雪 CTF《巳时·绿光幽语》逆向:用 r0crawl-skills 穿透 PyInstaller 诱饵

2天前
79

这道题表面上是一个普通的 Windows 可执行文件,题面却反复提示“古典密码、时间戳算法、硬件密钥”和“十六个碎片”。直接解包后,入口脚本又只剩一句 sys.exit(0),很容易让分析陷入两个方向:要么怀疑解包失败,要么转头硬啃 PyInstaller 启动器。

本文使用 manyuegong33/r0crawl_skills 组织完整分析过程。

项目地址:<242K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6E0j5h3&6&6N6h3g2Y4L8$3&6Y4x3K6y4Q4x3V1k6J5x3r3y4J5j5i4N6D9i4K6g2X3M7$3E0A6L8r3I4K6i4K6t1$3k6%4c8Q4x3@1t1`.

r0crawl-skills 在本题中的价值不是替代逆向工具,而是持续回答三个问题:

最终恢复出的原始 key 为:

将它输入原程序后,程序输出:

样本信息如下:

这里的“破解”不能只停留在找到一段可读字符串。按照 r0crawl-skills 的质量门槛,最终结果必须同时满足:

本题实际使用到的模块如下:

对应的分析主线可以概括为:

这个分层很重要。若不先确认行为位于哪一层,很容易在 PyInstaller bootloader 或 CPython 解释器内部花费大量时间,却始终碰不到真正的题目逻辑。

r0crawl-skills 推荐先建立独立 case,而不是边试命令边覆盖样本:

本题保留原始 题目.exe 不变,所有 DLL、marshal、反汇编和求解脚本都放进 evidence/derivedrepro。这样做的好处是每个结论都能回溯到具体文件、哈希和命令。

解包目录中出现了典型 PyInstaller 文件:

python313.dll 的版本资源显示为 CPython 3.13.1。入口 main.pyc 的文件头也是 Python 3.13 对应的 magic:

因此第一层结论是:

外层 PE 主要负责释放并启动 Python 运行时,题目逻辑更可能位于 Python 字节码、冻结模块或修改后的运行库中。

用 Python 3.13 对 main.pyc 反汇编后,逻辑等价于:

这与题目描述、交互界面和加密提示完全不匹配。

python-frozen-app-reversing 对这种情况有一条非常关键的规则:

抽取出的入口脚本没有行为,不代表抽取失败;应依次检查 runtime hook、冻结模块、修改版 Python 运行库、bootloader 前置逻辑和运行时生成的 code object。

此时建立两个竞争假设:

预测:题目内置的 python313.dll 与官方 3.13.1 DLL 明显不同,并包含被修改的冻结模块。

最小实验:下载官方同版本、同架构运行库,比较文件、PE 节和冻结模块。

预测:外层 EXE 在执行 main.pyc 之前直接运行校验逻辑。

转向条件:只有 H1 被否定后,才把主要精力放到 bootloader。

这就是 ctf-reverse-pivot-control 的作用:保留多个假设,但先执行成本最低、区分度最高的实验。

技能自带的 scan_frozen_artifacts.py 可以扫描整个解包树,而不是只搜索 main.pyc

宽泛关键字会在标准库、OpenSSL 和扩展模块中产生许多噪声。因此这里不能把某个 flag 字符串直接当作调用链证据。

更有价值的事实是:

下载官方嵌入式发行包:

两份 DLL 的信息如下:

PE 节对比进一步显示:

题目版 .text 增加了约 1 MB。这不是简单在文件尾部追加数据,而是重新编译过的 CPython 运行时。

至此 H1 的置信度显著上升,分析焦点从外层 PE 转向 CPython 冻结模块。

CPython 在 python313.dll 中导出了以下符号:

CPython 3.13 的 _frozen 结构为:

因此可以解析 PE 导出表,读取三个 _frozen 数组,再按 codesize 导出原始 marshal 数据。

本文的提取器调用方式为:

同样提取官方 DLL 后,对 29 个冻结模块逐一比较 SHA-256,结果非常集中:

os 模块的关键数据如下:

这一轮差分把约 7 MB 的运行库压缩成了一个 47 KB 的 marshal 代码对象。

PyInstaller 启动阶段会很早导入 os。题目的真实执行链为:

错误 key 触发的真实运行时栈也验证了这条链:

这构成了两个独立证据:

用官方 Python 3.13.1 对题目版 os.marshal 执行 marshal.loads() 和递归 dis.dis(),可以看到标准库代码末尾被加入了以下逻辑。

整理后的伪代码如下:

第一组“十六个碎片”只是界面提示。全部拼接后逐字节 XOR 0x55,得到:

题面中的“硬件密钥”和“时间戳算法”在这里起到了叙事和干扰作用,真正决定结果的是 6 字符输入对两组数据进行的周期 XOR。

ctf-key-recovery 建议优先使用最便宜的可逆关系。对于周期为 6 的重复 XOR:

本题天然提供了已知明文。

程序先定义:

随后只替换该函数的 co_codeco_consts。原始 dummy 函数的 Python 3.13 字节码为:

加密字节码为:

使用技能自带脚本:

输出:

26 个明文字节覆盖了 key 的每个槽位至少 4 次,所有重复约束完全一致,因此这不是靠可打印字符碰出的候选值。

再用该 key 解密成功消息:

结果为:

下面的脚本直接读取提取出的 os.marshal,自动定位 my_function、加密字节码和成功消息,并完成恢复:

注意:marshal 格式和 Python 字节码都与版本强相关,必须使用题目对应的 Python 3.13.1 执行该脚本。

运行结果:

输出:

进程退出码为 0

连续输入 20 次 bad

说明 len(key) != 6 会进入下一轮,不会执行成功函数。

将首字节改成 xb6tcr 后,解密出的字节码失效,原程序报错:

调用栈落在 <frozen os>my_function,没有出现成功文本。该结果既验证了 key 的敏感性,也再次确认了真实执行位置。

入口脚本确实只有 sys.exit(0),但真实行为在它之前已经由冻结 os 模块完成。把入口脚本当作全部逻辑会直接漏题。

题目版 .text 超过 3.8 MB。若不先比较官方基线,静态分析会淹没在 CPython 内部实现中。冻结模块哈希差分能把范围迅速压缩到 os.marshal

flagkeyinput 在 Python 标准库和 OpenSSL 中都很常见。字符串只能作为线索,不能单独证明调用链。

本机默认 Python 3.10 无法可靠解释 Python 3.13 的 code object。使用题目同版本的官方嵌入式 Python 可以避免错误反序列化和错误 opcode 解释。

yb6tcr 可打印并不等于它一定正确。真正的确认来自三层一致性:

本题真正有意思的地方不是 XOR 本身,而是藏逻辑的位置:


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

最后于 2天前 被manyuegong33编辑 ,原因: 标题位置写错了
收藏
免费 7
打赏
分享
最新回复 (1)
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2天前
0
游客
登录 | 注册 方可回帖
返回