简介:面向PyInstaller打包产物还原场景,这款工具专为将Windows可执行文件快速还原为Python源码而设计,适用于开发调试、代码审计、学习分析等场景,适合具备Python环境配置基础的开发者或逆向分析人员。工具自动完成两步核心操作:先通过pyinstxtractor从exe中提取.pyc字节码文件,再由uncompyle6将.pyc反编译为可读的.py脚本,全程无需额外安装第三方库,开箱即用,命令行运行主程序即可启动全流程。压缩包共6个文件,以两个Python脚本为主(分别承担主流程与解包任务),辅以文本说明与工程配置文件,整体仅9KB,轻量易部署。需特别留意的是,本地Python环境版本必须与目标exe所用版本一致,否则会因字节码格式不兼容导致提取或反编译失败;混淆或加壳后的exe不在支持范围内。目前已有112人学习下载,这套工具可帮助用户省去手动收集、调试逆向组件的时间,也能为快速理解PyInstaller打包结构与pyc还原流程提供直观范例。
1. PyInstaller 打包的 exe 快速还原:从 pyc 到 py 的完整链路
接手过一台旧服务器,里面躺着一个只剩 exe 的内部工具,功能正常,但源码早就丢了。想改个输出格式都无从下手。如果你也碰到这种局面,而 exe 又是 PyInstaller 打的包,那 exe 反编译这条路是走得通的:PyInstaller 打包的本质是把 py 源码先编译成 pyc 字节码,再连同 Python 运行时一起塞进 exe 的归档段。所以还原思路就一条:先解包提取 pyc,再做 pyc 还原,最后拼回可读的 Python 源码。这套流程对应一个可以直接复用的资源,也就是标题里说的 PyInstaller 打包 exe 快速还原工具,适合做遗留系统维护、安全审计、取证分析,也适合想确认自家 exe 有没有被轻易还原的开发者。
2. 解包与提取:三步从 exe 里拆出全部 pyc 文件
2.1 PyInstaller 打包产物结构:入口、引导模块与 CArchive
拿到一个可疑的 exe,先判断它是不是 PyInstaller 打包的,这一步很关键,决定后边整条路怎么走。PyInstaller 产物有一个明显特征:exe 内部嵌了一段 CArchive 归档数据,里面保存了所有 pyc 文件和资源文件。运行时引导器会先启动,再把这部分数据加载进内存,交给 Python 解释器解析。CArchive 有固定的头部标记,这也是解包脚本能自动识别的原因。
pyc 文件本身不是机器码,而是 Python 字节码。字节码里保留了操作码序列、常量表、名称表、变量名表,甚至还有行号表。反编译器拿到这些符号表之后,可以把执行逻辑逆向拼回可读的 py 源码。所以只要能把 exe 里嵌着的 pyc 完整抠出来,还原就成功了一半。
这一步常见做法是用 PyInstaller 专用的解包脚本来处理,它会自动定位 CArchive 段、解析头部偏移、按长度把内部文件全部切出来。相比手动拿十六进制工具去抠数据,用脚本既快又不容易出错,处理速度基本是秒级。
2.2 解包命令与输出目录解读
解包脚本是独立的 Python 脚本,和你要处理的 exe 放在同一级目录下,直接执行:
# 解包脚本会扫描 exe 尾部/资源区,定位 PyInstaller 归档段 # 目标 exe 路径作为唯一参数传入 python pyinstxtractor.py 目标程序.exe执行完成后,脚本会在当前目录下生成一个以“目标程序_extracted”命名的文件夹,解包出来的全部内容都在里面。如果脚本输出里出现了类似“CArchive: found”的提示,说明归档段定位成功;如果提示找不到归档,那说明这个 exe 大概率不是 PyInstaller 打包的,后边的路子就换一条。
解包目录里的核心文件大致是这样一个结构:
目标程序_extracted/ ├── main.pyc # 业务入口的字节码(也可能是别的名字) ├── pyiboot01_bootstrap.pyc # PyInstaller 自身引导模块 ├── struct # 打包时的 Python 版本和结构信息 ├── PYZ-00.pyz # 嵌套的 zip 归档,存放依赖模块的 pyc └── ...其他 pyc / pyd / dll 文件这个目录里值得注意的几类文件,我用一张表整理。
| 文件 | 作用说明 |
|---|---|
| 入口同名 pyc(比如 main.pyc) | 用户脚本编译后的字节码,是还原的核心目标 |
| pyiboot01_bootstrap.pyc | PyInstaller 启动引导模块,不是业务代码,不要当入口 |
| struct 文件 | 包含打包时 Python 版本号的原始记录,用于判断字节码格式 |
| PYZ-00.pyz | 实际是 zip 压缩包,里面按文件路径存了所有被依赖的 pyc |
| pyd / dll 文件 | C 扩展或系统库,这部分只能动态加载,没法反编译回 py |
到这里,pyc 已经拿到了,但还不能直接拿去反编译。有一个拦路虎在等你:extracted 目录里的 pyc 文件头通常不是标准的 Python magic number,需要先做处理,否则反编译工具直接拒收。这个处理细节放到第 4 章避坑部分详细讲,这里先知道有这回事。
2.3 版本判断与入口 pyc 定位
pyc 文件头部有四个字节是 magic number,对应编译它的 Python 版本。判断版本有两种方式:一是看 struct 文件里的记录,二是直接查看 pyc 文件头。反编译工具对 Python 版本的匹配要求极高,版本选错,后边基本白干。
查看当前环境的 magic number 可以用这段代码:
# 当前 Python 解释器的 magic number 十六进制 # 用这个值去和 pyc 文件头对比,判断版本是否一致 import importlib.util print(importlib.util.MAGIC_NUMBER.hex())但要注意一点:当前环境的 magic 和 exe 打包时的 magic 不一定相同。正确做法是用目标版本的解释器运行这段代码,得到一个十六进制串,再拿去和 pyc 头部比对。如果一致说明字节码版本对得上,反编译成功率会高不少。
入口 pyc 的定位也有讲究。pyiboot01_bootstrap.pyc 是 PyInstaller 的引导模块,不是业务代码。真正的入口通常是解包目录中和原脚本同名的那个 pyc 文件,比如你的工具原来叫 tool.py,打包后解包目录里大概率能找到 tool.pyc。如果打包时做了改名,那就只能逐个尝试反编译,看哪个文件里包含 main 函数的调用痕迹。
3. pyc 还原为 py 源码:基于 uncompyle6 的还原命令与参数
3.1 uncompyle6 的安装与单文件还原
拿到干净的标准 pyc 文件之后,还原工具首选用 uncompyle6。它专门处理 Python 2.4 到 3.8 版本编译出的字节码,对常规代码结构的还原质量最好。安装命令很简单:
# uncompyle6 是纯 Python 实现的反编译库,pip 直接装 pip install uncompyle6还原单个 pyc 文件,命令行写法如下:
# 参数一:输出文件路径;参数二:目标 pyc 文件 uncompyle6 -o 还原结果.py 目标模块.pyc执行成功时,目标目录下会生成对应的 py 文件,里面是还原出的源码。如果 pyc 文件头部不标准,工具会报错拒绝执行,但别慌,后边有办法绕过去。
uncompyle6 的命令行参数不算多,我实际用到的就那几个,列在下面:
| 参数 | 作用 | 使用场景 |
|---|---|---|
| -o 路径 | 指定输出文件或输出目录 | 单文件还原和批量还原都会用到 |
| -V | 显示工具版本号 | 排查版本兼容问题时先看一眼 |
| -h | 查看帮助和全部参数 | 需要时翻一下就行 |
如果你发现 uncompyle6 对某个 pyc 文件报“unsupported version”,说明打包用的 Python 版本高于 3.8。这时候 uncompyle6 不支持,需要换用另一个工具 pycdc,具体放到第 4 章讲。
3.2 Magic Number 的处理顺序:先修头部再反编译
pyinstxtractor 提取出来的 pyc 文件,有一个非常典型的问题:文件头部不是标准 Python magic number。这是 PyInstaller 在打包时对归档内容做了一层处理,它的目的是让这些 pyc 不能被直接拿去反编译,但处理力度有限,只要把头部修正回标准状态就能绕过。
修正头部的常见做法是写一段小脚本,遍历解包目录下所有 pyc 文件,把前四个字节统一写成对应 Python 版本的 magic number:
# 用当前环境的标准 magic 覆盖 pyc 头部前 4 字节 # 执行前确认当前 Python 版本和打包时版本一致,否则反编译依然失败 import importlib.util import struct from pathlib import Path magic = importlib.util.MAGIC_NUMBER.hex() # 把 hex 字符串转回二进制字节,头部写 4 字节就够 header = bytes.fromhex(magic) for py in Path('目标程序_extracted').rglob('*.pyc'): with open(py, 'r+b') as f: f.write(header) print('已修正:', py.name)这段脚本的逻辑很直接:用 pathlib 递归遍历解包目录下所有 .pyc 文件,以二进制读写方式打开,把文件头四个字节替换成当前 Python 解释器的 magic number。注意一点,这个操作只改头部标记,不影响字节码内容本身,所以不会破坏原始数据。
如果当前 Python 版本和目标 pyc 版本不一致,修正了头部也救不回来。比如 exe 是 Python 3.7 打的包,你却在 3.10 环境里强制改头部,结果就是反编译工具识别过了,但解析字节码时仍然报错。所以说版本对应是第一优先级,宁可多花十分钟搭一个对应版本的环境,也别在错误版本上硬试。
3.3 批量还原脚本:一次处理整个解包目录
单个 pyc 文件用命令行就能搞定,但实际解包目录里往往有一二十个甚至上百个 pyc,手动一个个敲命令效率太低。遇到这种情况,我一般会写一段 Python 脚本调用 uncompyle6 的库接口做批量还原:
# 批量反编译解包目录下所有 pyc 文件 # 遇到单个文件反编译失败时记录日志,不影响整体任务继续执行 from pathlib import Path from uncompyle6.main import decompile_file src_dir = Path('目标程序_extracted') out_dir = Path('还原结果') out_dir.mkdir(exist_ok=True) for pyc in src_dir.rglob('*.pyc'): # 强制跳过引导模块,它不是业务代码,反编译出来也没有意义 if pyc.name.startswith('pyi'): continue try: decompile_file(str(pyc), str(out_dir / (pyc.stem + '.py'))) print('还原成功:', pyc.name) except Exception as e: # 记录失败的 pyc,稍后单独排查 print('还原失败:', pyc.name, e)这段脚本里有几个关键点。rglob 负责递归遍历,不管 pyc 在解包目录还是在嵌套子目录里都会扫到;跳过 pyi 开头的文件是为了避免把 PyInstaller 的运行时钩子当成业务代码;try-except 结构是为了让单个文件失败时不中断整个批量任务,毕竟总有几个 pyc 会因为版本或格式问题还原失败,先跳过、后排查才是实际工作流的常态。
批量还原完,输出目录里会多出一批 py 文件。但是注意,这批 py 文件是平铺的,还原不出原来的包目录结构。要把它们组织成可运行的项目目录,还需要进一步处理这个 pyz 文件里的路径信息。
4. 避坑排查:magic number 错位与反编译失败的四类现场
4.1 现象:“Bad magic number in”报错
反编译时直接弹出类似“Bad magic number in main.pyc”的报错,这是遇到最多的问题,几乎第一次跑解包后的 pyc 都会撞上。
原因:PyInstaller 打包时对 CArchive 里的 pyc 文件头做了改写,提取出来的 pyc 用的不是标准 Python magic number,反编译工具连文件头校验都过不去。
解决:用第 3 章那一段修正脚本,把每个 pyc 文件前四个字节覆盖为对应 Python 版本的标准 magic。注意覆盖前确认版本一致,版本错了照样报错。
4.2 现象:报“unsupported Python version”
exe 是 Python 3.9 或更高版本打包的,uncompyle6 直接拒绝处理。
原因:uncompyle6 目前只完整支持到 Python 3.8,3.9 之后字节码格式有调整,它没有跟进适配。
解决:换用 pycdc。它基于 C++ 实现,对高版本 Python 字节码的支持比 uncompyle6 好。用法也简单,把 pyc 文件直接传给 pycdc 可执行文件,它会输出还原后的 py 源码到控制台,重定向到文件即可:
# pycdc 接收一个 pyc 文件,输出还原结果到标准输出 pycdc 目标模块.pyc > 还原结果.py4.3 现象:还原不报错,但输出文件只有一行“pass”
反编译过程很顺利,打开还原出的 py 文件却发现只有一句 pass,什么都不剩。
原因:你把引导模块当入口了。pyiboot01_bootstrap.pyc 这类文件是 PyInstaller 启动时执行的引导代码,经过特殊处理后结构很简略,反编译出来就是空壳。
解决:回到解包目录里外翻一遍,找到那个真正和原脚本同名的 pyc。判断标准很简单:文件名和 exe 描述的功能一致,或者在反编译结果里能看到业务字符串、界面文案一类的内容。实在找不到就把几个候选 pyc 都反编译出来扫一眼,一眼就能认出哪个是业务入口。
4.4 现象:解包正常,但每个 pyc 都是乱码
如果你处理的是那种带 --key 参数打包的 exe,你会发现文件结构都是对的,但反编译工具对每个文件都报错,十六进制打开 pyc 发现内容明显不是正常字节码。
原因:PyInstaller 的 --key 参数会对字节码做加密,提取出来的 pyc 是密文而不是原始字节码。没有密钥的情况下,密文没有任何还原手段。
解决:先去确认打包时有没有启用加密。没有密钥就直接放弃,这是硬边界,市面上声称能解密的工具基本都是针对未加密的常规包,真正带 AES 加密的 PyInstaller 产物,没有密钥就是无解的。
5. 工程化还原:PYZ 内嵌 zip、批量处理与模块地图重建
5.1 处理 PYZ 内嵌 zip:包结构最完整的还原入口
解包目录里的 PYZ-00.pyz 文件看起来不起眼,但它是整个还原工程里信息最密集的部分。它本质是一个 zip 归档,PyInstaller 打包时会把所有依赖的 Python 模块按原始包路径写进去,条目形式类似 a/b/c.pyc。这个路径结构在还原时非常救命,因为它直接给出了原始项目的包目录组织方式。
处理 PYZ 文件用 Python 的 zipfile 模块就可以:
# 查看 PYZ 内部所有条目路径,获取原始模块结构 import zipfile with zipfile.ZipFile('目标程序_extracted/PYZ-00.pyz') as z: for name in z.namelist(): print(name)打印出来的条目路径就是原始模块的相对位置。比如看到 com/company/module.pyc 这样的条目,说明原项目里有 com 包、company 子包,module 模块在这个包结构下。这个信息后面重建目录结构时直接照着做就行。
5.2 按路径结构还原 py 文件:一步到位重建项目树
知道 PYZ 内部路径之后,直接把它按路径解压出来,再批量反编译,就能把平铺的 py 文件落回对应的包目录里。
# 解压 PYZ 并按条目路径输出 py,保留原始包结构 import zipfile import pathlib def extract_and_rebuild(pyz_path, output_base): with zipfile.ZipFile(pyz_path) as z: for item in z.namelist(): # item 形如 a/b/c.pyc,沿路径重建目录并写入文件 target = output_base / item target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(z.read(item)) extract_and_rebuild('目标程序_extracted/PYZ-00.pyz', pathlib.Path('重建项目'))这段脚本的路由逻辑很直接:把 zip 条目名当成文件路径,先创建父目录再写入文件。pyc 落到正确目录之后,再对重建项目目录批量跑一次 uncompyle6,最终得到的 py 文件就已经是完整的多级目录结构了。
注意一点:PYZ 里有些 pyc 对应的模块名会被 PyInstaller 加过前缀,碰到 pyi_rth 一类名字直接跳过,它不是业务模块,是运行时注入的钩子。
5.3 完整性校验:从 pyc 到 py 的覆盖率和 import 缺口
文件都还原出来之后,别急着说自己搞定了。我一般会做一个覆盖率统计,看看整个解包目录里有多少 pyc 成功转成了 py 文件:
# 对比 pyc 总数和还原成功的 py 总数,评估还原完成度 from pathlib import Path pyc_count = len(list(Path('目标程序_extracted').rglob('*.pyc'))) py_count = len(list(Path('重建项目').rglob('*.py'))) print(f'pyc 数量: {pyc_count}') print(f'成功还原 py 数量: {py_count}')数字不能直接代表还原质量,但能暴露问题。如果 pyc 有 80 个,py 只有 20 个,那就说明有大批文件在批量还原时静默失败了,得回去看日志逐个处理。如果 py 数量接近 pyc 数量,说明批量还原基本跑通。
覆盖数好看只是第一步,还有一个更重要的动作:检查 import 缺口。打开入口 py 文件,把所有 import 语句列出来,逐个确认对应的模块文件是否存在。缺了哪个模块就去解包目录里手动找回它的 pyc 单独还原。这一步做到位,整个项目树才算真正立起来。
6. 结果验证:跑通还原代码并确认行为一致的那个技巧
6.1 行为对比验证
还原出来的代码能不能跑、行为对不对,靠肉眼读代码是判断不出来的。我处理这类任务一直有个固定动作:行为对比验证。
做法是找几组有代表性的输入样例,先用原始 exe 跑一遍,记录标准输出;再用还原出来的入口 py 文件跑同样的输入,记录另一份输出,然后做差异比对:
# 准备固定样例输入,分别用 exe 和还原后的 py 执行 ./原始工具.exe --input sample.txt > exe.out python 重建项目/入口.py --input sample.txt > py.out # 用 diff 比对两份输出,一致则指向还原成功 diff exe.out py.out如果 diff 结果为空,说明还原出来的代码在核心行为上和原始 exe 一致,可以认为还原成功。只要中间有任何一处行为偏差,就要回到还原代码里查——通常问题出在常量表解析错误或者某些控制流结构还原不完整。
6.2 边界说明
这个工具和整条还原链路有明确的边界,处理之前要心里有数。C 扩展模块 pyd 文件是编译好的二进制动态库,任何反编译工具都还原不出它的内部实现,只能看到模块接口。带 --key 加密的 PyInstaller 包,没有密钥就是死路,遇到这种直接告知对方成果预期,不要承诺不现实的还原深度。高版本 Python 打出来的 pyc 需要切换到 pycdc 处理,还原度会比 uncompyle6 略低,代码里有类型注解或复杂生成器时,可能需要人工补一部分结构。
从那以后,我每次做 exe 反编译还原,拿到 py 文件都会强制走一遍行为对比验证,看到 diff 为空才敢把结果交出去。只靠反编译工具不报错就判断成功,迟早要在运行时翻车。这套流程配合解包脚本和还原脚本,能帮你快速拿回 PyInstaller exe 的源码级内容,但前提是版本对上、头部修好、边界认清。希望帮到你。
本文还有配套的精品资源,点击获取