这道题表面上是一个普通的 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/derived 或 repro。这样做的好处是每个结论都能回溯到具体文件、哈希和命令。
解包目录中出现了典型 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 数组,再按 code 和 size 导出原始 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_code 和 co_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。
flag、key、input 在 Python 标准库和 OpenSSL 中都很常见。字符串只能作为线索,不能单独证明调用链。
本机默认 Python 3.10 无法可靠解释 Python 3.13 的 code object。使用题目同版本的官方嵌入式 Python 可以避免错误反序列化和错误 opcode 解释。
yb6tcr 可打印并不等于它一定正确。真正的确认来自三层一致性:
本题真正有意思的地方不是 XOR 本身,而是藏逻辑的位置:
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于 2天前
被manyuegong33编辑
,原因: 标题位置写错了