python-exe-unpacker 完全指南:三步把 PyInstaller/py2exe 打包的 EXE 还原成 Python 源码
【免费下载链接】python-exe-unpackerA helper script for unpacking and decompiling EXEs compiled from python code.项目地址: https://gitcode.com/gh_mirrors/py/python-exe-unpacker
凌晨一点,同事丢给你一个 exe:"帮我看看这程序到底干了什么。"你打开它,界面一片黑,日志空空如也。但直觉告诉你,这类工具多半是 Python 写的,只是被 PyInstaller 或 py2exe 打包成了独立可执行文件——而 python-exe-unpacker 正是专门把这类 EXE 拆开、还原成 Python 源码的利器。这篇文章会从"为什么能还原"讲起,再带你亲手跑通识别、解包、反编译的完整流程。
一个陌生 EXE 引发的"考古"需求
先别急着敲命令,想想我们到底在解决什么问题。
遇到一个来源不明的 Python 可执行文件,常见的诉求有三类:一是安全研究,需要确认程序里有没有恶意逻辑;二是代码审计,接手了遗留系统但原始工程早已丢失;三是最朴素的——学习别人怎么实现的。这三类场景殊途同归:你需要的不是看懂汇编,而是拿到接近原始形态的 Python 源码。
这正是 Python 逆向与 C/C++ 逆向最大的不同。C 程序编译后,源代码信息基本被抹掉,逆向等于从机器码里"考古";而 Python 程序在运行时依赖字节码,打包工具再怎么封装,字节码结构依然完整保留在文件里。字节码是可逆的,所以"还原源码"不是天方夜谭,而是一条有明确路径的技术活。
先看清包裹里装了什么
想拆包裹,先得知道包裹的结构。PyInstaller 生成的可执行文件本质上是一个自解压归档,由三部分组成:
- 引导加载器(bootloader):负责初始化 Python 运行环境;
- CArchive:存放依赖的动态库和 Python 模块;
- PYZ 归档:压缩存放的 Python 字节码集合,解包后通常对应
out00-PYZ.pyz_extracted目录。
py2exe 的路子略有不同:它把 Python 字节码当作一个名为PYTHONSCRIPT的 PE 资源塞进可执行文件,靠资源段索引来定位。
关键结论是:无论哪种打包方式,只要作者没有特意加密或混淆,字节码(以及它对应的版本魔数)就老老实实躺在文件里。这就是整个逆向流程能成立的前提。
自己动手拆,才发现零件是散的
知道了结构,你是不是想自己写脚本去解析?社区里其实早有现成零件,但拼起来并不省心。
拿 PyInstaller 来说,社区流传最广的提取脚本是 pyinstxtractor。单独跑它,你能得到一整个解包目录,但问题马上来了:主逻辑文件(通常是那个没有扩展名的文件)缺了.pyc文件头,uncompyle6 直接拒绝反编译。你得自己补上魔术数和时间戳,还得先搞清楚该补哪个版本的魔数。
再换 py2exe 试试。unpy2exe 能取出字节码,但它的实现依赖marshal模块,而 Python 2 和 Python 3 的 marshal 数据格式并不相同——一旦版本对不上,报错信息只会告诉你"unpacking 失败",具体哪里失败,全靠猜。
更麻烦的是 PyInstaller 的加密选项:如果作者用--key参数加密了字节码,解出来的是一堆.pyc.encrypted文件,还需要先逆向出密钥、再用 AES-CFB 解密。这一整套"拆包→补头→解密→反编译"的胶水逻辑,正是大多数人倒在半路的地方。
它把散装的工具拧成一根线
python-exe-unpacker 的价值,就是把这些散装零件拧成一根线。它的主脚本python_exe_unpack.py干了两件事:自动识别格式,再分派给对应组件处理。
识别逻辑很巧妙,全部基于文件特征,不需要你手动指定:
- 先解析 PE 资源段,如果找到
PYTHONSCRIPT资源且内部魔数匹配(0x78563412),判定为 py2exe; - 再向后扫描 CArchive 的
MEI\014\013\012\013\016魔数,判定为 PyInstaller。
各组件分工如下:
| 组件 | 负责的事 |
|---|---|
| pyinstxtractor.py | 解析 PyInstaller 的 CArchive 与 PYZ 归档,逐个提取文件 |
| unpy2exe | 处理 py2exe 格式,取出嵌入 PE 资源的字节码 |
| uncompyle6 | 把.pyc字节码反编译成可读的 Python 源码 |
| xdis | 识别并处理 Python 版本魔术数,修复残缺的 pyc 文件 |
安装也简单,克隆仓库后装依赖即可:
git clone https://gitcode.com/gh_mirrors/py/python-exe-unpacker cd python-exe-unpacker pip install -r requirements.txt💡 小提示:
requirements.txt里的 pycrypto 是 Python 2 时代的老库,在 Python 3 下编译可能失败。如果装不上,先安装pycryptodome(提供相同的Crypto模块接口),其余包大多能顺利装上。
这条命令跑完,你的环境里就有一套完整的"识别 + 拆包 + 反编译"流水线了。
三分钟跑通,源码已经在眼前
环境就绪后,正式开工只需一条命令:
python python_exe_unpack.py -i target.exe运行后会发生三件事:工具先打印当前 Python 版本,然后自动判断目标是 py2exe 还是 PyInstaller,最后解包并反编译。输出默认放在当前目录下的unpacked/<原文件名>/文件夹里,也可以用-o参数把结果定向输出到任意位置。
以 PyInstaller 为例,解包目录里会出现out00-PYZ.pyz_extracted(第三方依赖的字节码)、一堆.pyd/.dll(运行时库),以及一个没有扩展名的文件——它通常就是主入口,包含核心业务逻辑。uncompyle6 会把它反编译成同名的.py文件,这才是你真正要找的东西。
第一次跑通时你会明显感觉到:之前手动拼装要折腾半个下午的流程,现在从识别到出源码,全程不需要干预。
当源码披着"加密"和"残缺"两层外衣
现实中的样本不会那么乖巧,最常见的是两种"外衣"。
加密的字节码。如果 PyInstaller 使用了--key加密,解包后会出现pyimod00_crypto_key这个密钥模块和一批.pyc.encrypted文件。此时工具会停下来问你:
[*] Encrypted pyc file is found. Decrypt it? [y/n]
输入y,它会先把密钥模块反编译出来、从中提取 AES 密钥,再用 AES-CFB 模式逐文件解密,最后统一反编译。整个过程依旧是"一条命令",只是中间多了一次确认。
缺失魔术数的 PYC。这是最容易被新手绊倒的场景:主文件明明解出来了,uncompyle6 却不认,报错多半和 "missing magic number" 有关。此时用-p参数让它自动补头:
python python_exe_unpack.py -p hello工具会先用 xdis 检查文件头是否已有可识别的魔术数:有就直接反编译,没有就补上默认的 Python 2.7 魔术数\x03\xf3\x0d\x0a和时间戳,再交给 uncompyle6。一句话总结:-i负责整包,-p负责救单个残缺文件。
最隐蔽的坑:Python 2 与 Python 3 的版本战争
如果说上面都是流程问题,那版本问题就是最大的"环境坑"。最典型的是反编译 py2exe 样本时,屏幕上突然出现这样一行:
⚠️ [-] Error in unpacking the exe. Probably due to version incompatibility (exe created using python 2 and run this script with python 3)
原因前面提过:unpy2exe 依赖marshal,而 Python 2 和 Python 3 的 marshal 序列化格式不兼容。这是工具无法替你绕过的硬约束,唯一可靠的办法是让运行工具的解释器版本和目标一致。项目 README 给出的土办法很直接:
alias python=python2 python python_exe_unpack.py -i target.exe同理,uncompyle6 在"解释器版本 == 目标 exe 的 Python 版本"时反编译成功率最高。建议动手前先确认目标特征:解包后看魔术数对应哪个版本,或者直接看目录里的python27.dll、python36.dll之类的运行时库文件名,一目了然。
批量分析、边界与你的下一步
当你需要同时分析多个样本时,配合 shell 脚本就能批量跑:
#!/bin/bash for file in *.exe; do echo "正在处理 $file ..." python python_exe_unpack.py -i "$file" done一次批量下来,每个样本的源码都会落在各自的输出目录里,非常适合安全研究场景的初筛。
当然,任何工具都有边界,用之前先想清楚两件事。第一是合法性:python-exe-unpacker 适合分析你拥有版权的软件、或经过授权的安全测试,别把它用在未经许可的逆向上。第二是技术上限:经过混淆、加壳或动态生成代码的程序,反编译结果可能丢失部分可读性;PyInstaller 新版本的打包结构变化,也可能让旧版提取脚本失效——遇到异常时,按"Python 版本 → 文件完整性 → 依赖安装 → 报错日志"的顺序排查,大部分问题都能定位。
回到开头那个失眠的夜晚。现在你知道,遇到一个疑似 Python 打包的 EXE,正确的姿势是:先判断打包器,再拆包,补上残缺的头部,最后反编译——而这四步,python-exe-unpacker 已经把前两步和后两步都串成了同一条命令。读完这篇,你已经掌握了它从安装、基础使用到处理加密与版本问题的完整路径。下一步,克隆仓库、准备一个自己写的测试程序用 PyInstaller 打包,然后跑一遍python python_exe_unpack.py -i your_test.exe——亲手把"我写的代码"再还原回来一次,比任何教程都更能建立信心。
【免费下载链接】python-exe-unpackerA helper script for unpacking and decompiling EXEs compiled from python code.项目地址: https://gitcode.com/gh_mirrors/py/python-exe-unpacker
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考