简介:面向Python开发者和逆向分析人员,这份一键反编译工具能直接处理PyInstaller打包生成的exe文件,将其还原为可阅读的py源码,适合需要找回丢失源码、分析他人程序逻辑或学习exe反编译流程的人群。工具以两个核心Python脚本组成完整反编译链:利用pyinstxtractor从exe中提取pyc中间文件,再借助uncompyle6将pyc反编译为Python代码,并由主控脚本一键串联整个过程;另附README.md说明文档,压缩包共3个文件、仅6KB,轻量小巧,便于下载后快速投入使用。实际效果与Python版本相关,使用时需保证当前环境版本与打包时一致,否则可能出现反编译失败或内容偏差。目前已有578人学习下载,这份工具能省去手动拆包、转换和还原的繁琐步骤,显著提升反编译效率,也可作为学习逆向工程的入门范例,值得开发者收藏备查。
1. 为什么能一键反编译:先看 PyInstaller 的壳
接手一个没有源码的 Python 工具,只有打包好的 exe,第一反应往往不是放弃,而是想拆开看看里面到底写了什么。PyInstaller 打包出来的程序并不是真正编译成机器码,它只是把 Python 解释器、依赖库和你的.pyc文件塞进同一个可执行文件里。这意味着只要能把 exe 里嵌着的 pyc 提取出来,再经过反编译,源码就有很大概率还原。exe2py 这个工具就是把这套流程封装成一条命令,底层依赖 pyinstxtractor 完成 exe 到 pyc 的提取,再由 uncompyle6 完成 pyc 到 py 的反编译。适合丢失源码、接手历史项目、或者想研究别人打包脚本做法的开发者,也适合学习 PyInstaller 内部结构的入门者。但前提是环境对齐,尤其是 Python 版本这一项,差一个小版本都可能导致反编译失败。
2. 拆解 exe2py:pyinstxtractor 与 uncompyle6 的分工
2.1 PyInstaller 把源码藏在哪里
PyInstaller 打包时会把项目里的.py编译成字节码,也就是 pyc 文件,然后连同 Python 解释器核心库、第三方依赖的 pyc 一起打包。在生成的 exe 末尾通常有一个自定义的归档段,PyInstaller 通过CArchive结构记录每个文件的名字、偏移量和压缩状态。pyinstxtractor 做的事就是这个归档段的解析器,它扫描 exe 的二进制数据,找到MEI标记和对应的目录表,再按偏移量把每个 pyc 还原成独立文件。
其中一个关键点是,PyInstaller 会改写 pyc 的头部。普通 pyc 文件以 16 字节的 magic number 开头,而 PyInstaller 打包后的 pyc 头部会被替换成自定义的MAGIC字节,通常是93 4d 41 47 49 43(对应\x93MAGIC),目的是防止简单提取后直接反编译。pyinstxtractor 在输出时会尝试修复这个头部,把标准 16 字节的 magic 补回去,这样 uncompyle6 才能识别。
2.2 两个脚本各自干了什么
exe2py.py 是一个薄薄的调度层,它的逻辑非常直白:先调用同目录下的pyinstxtractor.py把 exe 解开,再遍历提取出来的 pyc 文件,逐个交给 uncompyle6 去反编译。从目录结构上看,exe2py-master 里只有exe2py.py、pyinstxtractor.py和 README,不打包第三方依赖,uncompyle6 需要自己按装,pyinstxtractor 则直接以单文件方式调用。
这里有个容易误会的点:很多人以为 exe2py 是集成工具,其实它更像一个自动化脚本,两个阶段各自独立。pyinstxtractor 负责处理 PyInstaller 的容器格式,uncompyle6 负责解析 Python 字节码并还原为接近原始的源码。理解这个分层,后续遇到某一阶段失败时,才能准确判断是提取问题还是反编译问题。
2.3 工具包的工程结构说明
拿到exe2py-master.zip解压后,里面只有三个文件,结构如下:
| 文件 | 作用 | 依赖 |
|---|---|---|
| exe2py.py | 主入口,串联提取和反编译流程 | 需要 uncompyle6 |
| pyinstxtractor.py | 从 exe 中提取 pyc 文件 | 仅标准库 |
| README.md | 使用说明 | 无 |
这种极简结构有个好处:pyinstxtractor 是纯 Python 标准库实现,不需要额外安装,换个机器也能跑。exe2py.py 本身的实现也不复杂,核心代码大约几十行,处理流程是持久化参数、调用 pyinstxtractor 提取到index.exe_extracted目录、找到所有pyc文件、过滤掉pyinstaller自带运行库的 pyc(比如pyimod01_os_path.pyc这类引导模块),最后逐一代入 uncompyle6 执行。
所以如果你想扩展它,比如提取后先做一遍字节码版本检查,只需要在 exe2py.py 的主流程里插入一个marshal.loads或读取 pyc 前四个字节的逻辑,非常容易改。这也是我推荐先用这个工具而不是一步到位的商业反编译器的原因——每一层都是透明的。
3. 实操:从 EXE 还原成可读的 Python 源码
3.1 搭建能跑起 exe2py 的环境
首先需要一个 Python 环境,PyPI 上的 uncompyle6 对 Python 2.4 到 3.8 的字节码支持较好,所以最省事的做法是使用 Python 3.8 或者更高版本解释器来运行反编译脚本本身。注意这里有两层版本概念:一层是执行 exe2py.py 所用解释器的版本,另一层是目标 exe 当初打包时用的 Python 版本。工具脚本本身只需要能跑起来,通常 Python 3.7+ 即可,而目标 pyc 的解析则取决于被反编译文件的字节码来源。
建议新建一个干净环境,避免全局安装污染:
mkdir exe2py-work && cd exe2py-work python -m venv venv source venv/bin/activate # Windows 上执行 venv\Scripts\activate pip install uncompyle6参数说明:这里创建虚拟环境是防止 uncompyle6 与系统里其他依赖冲突。pip install uncompyle6会自动拉取它依赖的spark-parser和xdis,这两个库分别负责语法树解析和字节码识别。安装完成后,把exe2py-master里的全部文件复制到工作目录,或者直接把 exe2py.py 和 pyinstxtractor.py 放进 PATH 所在目录。
3.2 一条命令反编译:python exe2py.py index.exe
假设待反编译的程序是index.exe,在终端执行:
python exe2py.py index.exe如果一切顺利,你会在当前目录看到index.exe_extracted文件夹,里面是 PyInstaller 释放出来的所有 pyc 和依赖文件;紧接着 exe2py.py 会循环处理每个非依赖 pyc,在index.exe_extracted下生成同名.py文件。
执行过程里最重要的输出是反编译成功的文件名列表。比如:
[+] Loading module: index [+] Decompiled to index.py如果某个模块反编译失败,它不会中断整个流程,而是会打印一条Failed to decompile ...的警告,继续处理下一个文件。
这里值得说明的是,exe2py 对输入的参数处理很简单:sys.argv[1]就是要反编译的 exe 路径。如果 exe 在别的目录,最好用绝对路径。命令里index.exe可以是相对路径,但 pyinstxtractor 会把输出目录创建在当前工作目录,而不是 exe 所在目录。建议先cd到专门的工作目录再执行,不然输出文件会散落在源目录里。
3.3 反编译产物目录里找什么
打开index.exe_extracted,你会看到不少文件,但真正值得关注的是:
index.exe_extracted/ ├── index.py # 主程序反编译结果 ├── index.pyc # 提取出来的原始字节码 ├── struct.pyc # 第三方依赖的字节码 ├── base_library.zip # 标准库压缩包 └── pyi_rth_... # PyInstaller 运行时钩子其中base_library.zip里是标准库的 pyc,一般不需要反编译,除非你想确认某个标准库函数的调用细节。第三方依赖如struct.pyc、requests.pyc反编译出来是能读的,但通常不是你的代码。真正的主逻辑在与你项目同名或入口文件同名的 pyc 里,比如index.pyc反编译后的index.py。
3.4 常见的中途报错与处理
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
Error: Unsupported or invalid magic number | exe 打包时的 Python 版本与 uncompyle6 支持的版本不匹配 | 确认打包版本,或改用能解析该版本的 decompyle3/pycdc |
pyinstxtractor.py: error: Invalid pyinstaller archive | exe 并不是 PyInstaller 打包的,或者被加壳/压缩过 | 用 DIE(Detect It Easy)或file命令确认文件格式 |
IndexError: list index out of range | PyInstaller 版本较新,归档头结构有变化 | 更新 pyinstxtractor 脚本,或手动替换 magic 头 |
| 反编译出的 py 文件只有空函数 | 代码被 Nuitka 或 Cython 编译,不存在标准 pyc | 这种情况下只能做行为分析,工具无能为力 |
uncompyle6的报错信息还算直白,看到Unsupported开头的错误一般就是版本不对。这时候不要去改源码,先去确认目标 exe 用的 Python 是 3.6 还是 3.8,版本对齐是第一优先级。
4. 版本对应关系是最大的坑
4.1 为什么必须同版本:pyc 的 magic number 与 code object 字节码
Python 每次发布新版本时,字节码的指令集合都可能变化。比如 Python 3.7 的COMPARE_OP参数映射与 3.8 就不完全一致,3.9 又加了一些新的操作码。pyc 文件的前四个字节存储的是 magic number,用来标识产生该 pyc 的 Python 版本,uncompyle6 通过这个 magic 选择对应的反编译器规则表。
exe2py 的 README 里特别强调「python 版本和 exe 对应的 python 版本要对应」,这句话的完整含义是:uncompyle6使用的解析规则必须能覆盖目标 pyc 的字节码版本。如果 exe 是用 Python 3.8 的 PyInstaller 打包的,那么你的环境里最好用支持 3.8 的反编译库,而不只是解释器版本要大于等于 3.8。
4.2 跨版本反编译会看到什么现象
当你拿 Python 3.12 环境去跑 uncompyle6,反编译一个 3.7 时代打包的 exe 时,最常见的两条错误是:
KeyError: 'MAKE_FUNCTION' ValueError: v3.7.0 is not supported第一条是因为新版本 xdis 解析旧字节码时操作码表对不上;第二条是 uncompyle6 直接判定版本超出支持范围。反过来,用旧版本去解新 exe 也会失败。解决思路有三种:一是安装与目标 exe 相同版本的 Python,再安装 uncompyle6;二是换用支持到 3.9 的 decompyle3(它是 uncompyle6 的分支,支持到 Python 3.9);三是使用跨版本的 pycdc,一款用 C++ 写的反编译工具。
4.3 onefile 与 onedir 的差异对反编译路径的影响
PyInstaller 有--onefile和--onedir两种打包模式。onedir 模式下,exe 外面会有一个_internal目录,里面放着大量 pyc/dll;onefile 模式下,PyInstaller 会把整个归档压缩进 exe,运行时解压到临时目录。这两种模式对 pyinstxtractor 来说都能处理,但 onefile 的 exe 因为归档后面还跟着压缩过的依赖,提取时需要更准确地定位MEI段。
实际使用中,onefile 提取失败的几率略高于 onedir,原因是某些 PyInstaller 版本会在归档后追加签名段,导致 pyinstxtractor 扫描到错误的偏移。如果你在公司或网上下载到的是 onefile 版,一次失败不要急着放弃,试试先运行一次 exe,让它在临时目录里解压出_MEIxxxxxx目录(Windows 下在%TEMP%),然后直接从那个目录里找 pyc,再用 uncompyle6 单独反编译。我经常用这个迂回方案。
4.4 压缩、加密、加壳后的处理边界
PyInstaller 自身支持--upx参数,UPX 压缩只影响二进制资源部分,对 pyc 的内容没有影响,pyinstxtractor 提取时自动忽略 UPX 段,所以 UPX 压缩的 exe 可以正常处理。但要注意:如果 exe 用了 PyInstaller 的--key参数,即对 pyc 进行 AES 加密,那么提取出来的 pyc 是没有MAGIC头的,直接反编译会提示Bad magic。这种情况下 exe2py 自己的工作区在解包阶段就完成了一半,你拿到的更像是被加密的 code object,需要先知道密钥才能继续。
更极端的情况是,打包前就用了pyarmor或者 Rust 调 Python 嵌入包,这种已经不是标准 PyInstaller 产物,pyinstxtractor 会报 invalid archive,或者反编译出的代码只有一层调用壳,真正的逻辑被 Obfuscated 掉。到这个程度,建议放弃静态反编译,改用运行时 hook 或逻辑侧推。
5. 工具还原不了时的备选路线:直接从 pyc 手工提代码
5.1 先用 pyinstxtractor 单独提 pyc
当 exe2py 整体反向时报错,但错误集中在 uncompyle6 阶段,可以退回第一步,只用 pyinstxtractor 提取 pyc:
python pyinstxtractor.py attack.exe这一步会生成attack.exe_extracted,里面的 pyc 是保留原始文件名的。如果提取后某个 pyc 的头部仍然是MAGIC,可以用十六进制编辑器把它修改为标准 pyc 头。Python 3.8 的标准头是前 16 字节:
55 0d 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00也就是\x55\r\r\n加上全零的时间戳和大小字段。从 PyInstaller 提取的 pyc 通常需要把头部替换成这个 16 字节。
5.2 uncompyle6 失效时换 decompyle3 与 pycdc
对于 Python 3.7 到 3.9 的项目,如果 uncompyle6 报版本不支持,优先尝试:
pip install decompyle3 python -m decompyle3 attack.pyc > attack.pydecompyle3 的语法还原能力与 uncompyle6 相当,但对新版操作码处理更完整。再不行就编译 pycdc:
git clone https://github.com/zrax/pycdc.git cd pycdc && cmake . && make ./pycdc attack.pyc > attack.pypycdc 是 C++ 写的,对高版本 Python(3.10 / 3.11 / 3.12)的支持持续更新,但还原出来的代码在变量名和装饰器上有时会失真。一个实用技巧是,如果 pycdc 输出的代码有语法错误但整体结构可读,配合dis模块查看字节码来手动修复,能省大量时间:
python -m dis attack.pyc会列出每一行对应的操作码,即使 pyc 是 3.11 的也能识别。
5.3 依赖混淆与动态代码的妥协方案
反编译出来的代码并不是每次都整齐,尤其是那种在运行时动态生成函数、用exec()拼接代码的模块。pyinstxtractor 会原样提取所有 pyc,如果 exe 里嵌入了加密后的字符串,反编译结果里会有大量长字符串字面量,但无法直接识别逻辑。这时我会先用pyc文件跑一遍程序,在关键函数入口加钩子或打印调用栈,把实际执行时的参数和返回值记录下来,再结合反编译出的骨架人工还原主流程。
最常遇到的情景是目标程序使用了__slots__或大量内联缓存,这类代码反编译后会有很多LOAD_CONST加CALL_FUNCTION序列,看着像是噪声,其实它们对应的是 Python 优化后的属性访问。不需要全都还原到原始变量名,能确认函数入口、参数列表和关键业务条件就够了。最终能不能恢复出可二次开发的源码,取决于你对 Python 字节码的熟悉程度,而 exe2py 只负责把上游的提取和基础还原做到一键化。
本文还有配套的精品资源,点击获取