PyInstaller打包exe还原Python源码:拆包、补头、反编译全流程
简介PyInstaller打包的可执行文件快速还原工具面向需要分析或复用Python程序的开发人员、安全审计人员及编程学习者可自动完成从可执行文件中提取pyc字节码、再将其反编译为原始Python源码的两步流程显著降低逆向还原门槛。工具包共6个文件以Python脚本和Markdown文档为主体包含主程序、解包依赖及使用说明压缩后仅9KB无需额外安装第三方库开箱即用。使用时只要本地Python版本与目标可执行文件一致即可通过类似‘python exe2py.py index.exe’的简单命令行一键启动全流程适合离线或受限场景下的源码还原。该工具已有102人学习适用于开发调试、代码审计和学习分析对未混淆、未加壳的PyInstaller程序还原效果良好。资源内目录结构清晰主程序、解包脚本与说明文档相互分离便于按需查阅与二次修改是初中级开发者理解字节码转换与反编译流程的实用参考。1. PyInstaller打包的exe文件还原Python源码先拆包再反编译拿到一个 PyInstaller 打包的 exe想找回它的 Python 源码脚本这件事没有想象中那么玄。PyInstaller 的打包本质是归档不是加密exe 里躺着的就是完整的 Python 字节码还原路径只有两步把字节码从 exe 里剥出来再反编译回 .py。整个过程不需要 IDA 级别的反汇编手段一个拆包脚本加一个反编译工具就够。下面按这条 exe反编译 路线往下走覆盖工具选型、命令行参数、入口脚本补头以及 3.9 以上新版 Python 的兼容性取舍。适合三类人丢了源码的开发者、做安全审计的测试者以及想搞懂 python转exe文件 背后机制的逆向新手。2. 拆开PyInstaller打包的exe用pyinstxtractor提取pyc的完整流程2.1 打包产物结构CArchive、PYZ与入口脚本说到 python生成exe可执行文件PyInstaller 是绕不开的名字。很多人打包时用的顺手但很少反过来想它的产物到底长什么样。PyInstaller 不编译也不加密代码它只是把 Python 解释器动态库、依赖模块的字节码和入口脚本一起塞进一个自解压归档里。用--onefile打包成单个exe 时exe 尾部追加了一段 CArchive 归档里面装着 python3xx.dll、依赖模块聚合成的 PYZ 归档、入口脚本对应的 pyc以及 PyInstaller 的运行时引导代码。exe 每次运行bootloader 先把归档释放到临时目录再导入模块跑完再清理这也是为什么单文件 exe 启动总是慢半拍。这段结构直接决定了还原思路源码其实一直完整存在于 exe 内部只是从 .py 变成了 .pyc还原就是把 .pyc 抠出来再转回 .py。这里有个关键区别必须分清PYZ 归档里的依赖模块带完整 pyc 文件头解出来后可以直接反编译而入口脚本的 pyc 文件头是被清零存放的需要额外补一道头。很多人第一次还原就卡在入口脚本上本质上就是没区分这两类文件拿处理模块 pyc 的流程去处理入口必然报错。另外--onedir模式只是把归档拆到了外部 .pkg 文件里提取思路完全一样只是目标从单个 exe 换成目录里那个 pkg。2.2 用pyinstxtractor.py提取exe一行命令与输出目录解读拆包的标准工具是 pyinstxtractor.py新版 PyInstaller 和 Python 3.9 场景建议直接上社区维护的 pyinstxtractor-ng。这类工具的工作方式并不神秘在 exe 二进制里定位 CArchive 的魔数标记找到归档起点后按内部索引把每个文件原样切出来。整个过程在本地完成不需要联网对只敢碰离线环境的审计任务很友好。命令只有一个参数python pyinstxtractor.py C:\tools\app.exe不需要指定输出目录跑完会在 exe 同目录生成app.exe_extracted文件夹。终端末尾出现[] Successfully extracted to ...就算成功如果提示找不到归档结构优先怀疑 exe 被 UPX 压缩过解法在第 4 章。Windows 用户注意如果命令行里python指向的是微软商店占位符换成py -3 pyinstxtractor.py或者用你打包时用的那个解释器路径。提取目录里的文件分三类。顶层散落的 pyc 是运行时引导代码和入口脚本base_library.zip与PYZ-00.pyz是标准库和第三方依赖的 zip 归档里面全是带完整头的 pyc配置文件、资源文件也原样躺在目录里。判断哪个 pyc 是入口脚本最可靠的办法是看打包时的入口文件名——app.py打出来的 exe提取目录里一定有一个app.pyc。如果入口名不确定取顶层体积最大的散装 pyc 当入口命中率很高因为业务入口的代码量通常远大于引导模块。2.3 反编译前第一件事核对Python版本提取完成先别急着反编译。反编译工具对字节码版本极敏感3.8 和 3.9 是两个完全不同的世界版本判断错了后面每一条报错都会让你怀疑人生。先随手挑一个模块 pyc 读出魔数用PYZ-00.pyz解出来的ctypes.pyc就行with open(PYZ-00.pyz_extracted/ctypes.pyc, rb) as f: magic f.read(4) print(magic.hex()) # 例如 610d → CPython 3.9把输出对照下面的速查表这是从 3.6 到 3.11 的常见取值Python版本pyc魔数hex小端3.6330d3.7420d3.8550d3.9610d3.106f0d3.11a70d读出的值不在表里时优先怀疑这个 exe 不是 PyInstaller 打的或者打包机用的 Python 版本太新。版本号决定反编译工具的选择3.8 及以下用 uncompyle63.9 及以上要换 pycdc 组合。这一步花两分钟能省两小时属于典型的血泪经验——我在 3.9 时代用 uncompyle6 死磕过一整晚最后发现只是工具选错了。补头时也要用同版本的参考 pyc混用版本会让反编译产物彻底乱掉。3. 从pyc还原到可读源码uncompyle6、decompyle3与pycdc的组合3.1 三条工具线的适用边界版本决定选择反编译 pyc 的工具就三条线。uncompyle6 历史最久支持到 CPython 3.8对 3.6-3.8 的还原质量最好decompyle3 是它的分支主打 3.7/3.8 的稳定性处理 try/except 这类复杂控制流时出错率比旧版低pycdc 用 C 实现是 3.9 及以上字节码的主力选择但对复杂控制流的还原精度不如低版本场景下的 uncompyle6。选择规则就一句话版本越低用 uncompyle6版本越高越往后排。用错工具的症状高度统一报UNKNOWN opcode然后中断或者产出明显缺函数的不完整代码几乎没有第三种表现。工具选型时还要考虑一个现实因素uncompyle6 多年没大更新pycdc 一直在跟进新字节码但功能对等不等于质量对等。如果你要还原的是 3.10/3.11 的产物做好心理准备——pycdc 能给你一份结构大体正确的代码但注释、f-string 细节、部分推导式会走样。3.8 时代的还原体验确实是最好的这也是为什么很多审计报告里都备注建议项目锁定 Python 3.8。3.2 反编译单个pycuncompyle6与decompyle3的标准命令确认 pyc 版本 ≤ 3.8 后安装并反编译pip install uncompyle6 uncompyle6 -o ./decompiled app.pyc-o指定输出目录不带-o时结果直接打到 stdout可以配合重定向存文件。成功时终端会显示Successfully decompiled失败时最常见两条报错bad marshal data说明 pyc 头部有问题对应 3.3 的补头操作Unknown opcode说明工具版本没对上。uncompyle6 在 3.7/3.8 上偶尔也会翻车这时换 decompyle3 重跑一次pip install decompyle3 decompyle3 -o ./decompiled app.pyc它俩的命令参数几乎一样切换成本为零。一个实用习惯是反编译时带上验证开关uncompyle6 支持在输出后自动重新编译比对uncompyle6 --verify -o ./decompiled app.pyc带上--verify后工具会把还原出的代码重新编译再与原始 pyc 的字节码做比对并在终端提示是否一致。这一步能帮你过滤掉长得像源但行为不等价的劣质产物建议默认打开。3.3 入口脚本补头让 main.pyc 起死回生入口脚本 pyc 的头部在打包时被清零直接反编译必然报bad marshal data。修复方法是从同版本普通 pyc 借一个完整头部覆盖上去。Python 3.7 及以后 pyc 头固定 16 字节魔数 4 字节 flags 4 字节 mtime 4 字节 源文件大小 4 字节。补头脚本如下with open(PYZ-00.pyz_extracted/ctypes.pyc, rb) as f: header f.read(16) with open(main.pyc, rb) as f: body f.read()[16:] # 丢掉被清零的16字节占位头 with open(main_fixed.pyc, wb) as f: f.write(header body)补完后用 marshal 验证一次确认它真的是个可解析的 code 对象import marshal with open(main_fixed.pyc, rb) as f: data f.read() code marshal.loads(data[16:]) print(code.co_name, len(code.co_code))能打印出函数名和字节码长度说明头部对了可以交给反编译工具。如果入口 pyc 开头不是完整 16 字节零改成按 12 字节偏移处理那是 Python 3.6 的头长度。提示补头用的参考 pyc 必须和入口 pyc 来自同一版本解释器混用魔数会让反编译产物直接变乱码。这一步是整个还原流程的核心跑通它一个几千行的业务入口脚本就在这十几行代码里复活。4. PyInstaller还原源码的避坑指南5个高频翻车点做 PyInstaller 打包的 exe 反编译坑不是一般的多。下面五条按出现频率排每条都按现象 → 原因 → 解决写对照排查能少走很多弯路。4.1 反编译中断报 UNKNOWN opcode版本错配现象uncompyle6 跑到一半终止抛ValueError: UNKNOWN opcode。原因pyc 的字节码版本比工具支持的新最典型是 3.9 的 pyc 撞上 uncompyle6另一个常见原因是入口 pyc 没补头marshal 数据整体错位工具把偏移后的数据当操作码解。解决先按 2.3 的速查表核实版本3.9 及以上换 pycdc3.8 及以下换 decompyle3 重跑。在这件事上别跟一个工具死磕换工具比查日志快得多。4.2 入口 pyc 报 bad marshal data头部没补现象模块 pyc 反编译全正常唯独 main.pyc 报bad marshal data用十六进制编辑器打开发现开头是一排00。原因PyInstaller 把入口脚本 pyc 的标准头清零存放工具按正常 pyc 解析自然失败。解决执行 3.3 的补头脚本借同版本普通 pyc 的头重建。这条坑占所有排查里的一半以上凡是 main 开头的 pyc 出问题先补头再说别去研究什么玄学。4.3 提取失败或产物乱码exe 被 UPX 压缩过现象pyinstxtractor 提示找不到归档魔数或者提取成功但 pyc 文件打开全是不可读字节。原因打包流程里开了 UPX 压缩exe 尾部归档的定位信息被改写拆包脚本无从下手或者定位到了但内容已经被压扁。解决先脱壳再提取upx -d C:\tools\app.exe python pyinstxtractor.py C:\tools\app.exeupx -d偶尔会提示not packed说明压缩时用了改动过的 UPX 头部这种基本只能手动在二进制里搜归档魔数一段一段抠性价比极低建议直接放弃换目标。4.4 反编译出来只有几十行核心逻辑被 Cython 或 PyArmor 藏起来了现象main.pyc 成功反编译但产物是一堆对pyarmor_runtime或_cython扩展的调用业务代码不在里面。原因打包前用 Cython 把核心模块编译成 pyd/so或用 PyArmor 对字节码做了加密pyc 里只剩一个壳。解决识别封装方式后别在 pyc 上硬耗。Cython 产物去分析 so 的导出符号PyArmor 产物只能走动态调试。安全审计场景下不如直接在干净的虚拟机里跑 exe抓它的文件读写和网络行为。这是还原的边界不是失败。4.5 反编译产物行为对不上工具还原精度有限现象还原出的 py 语法检查通过一跑就抛异常或输出结果和 exe 实际行为不一致。原因反编译器对 try/except/finally、异步代码、部分推导式的还原存在已知短板生成的代码长得像源但不等价这在复杂业务脚本里尤其常见。解决用 pycdc 对同一 pyc 交叉反编译一份关键函数做行为对照——同一输入分别喂给 exe 和还原代码比对输出是否一致。这条坑提醒一句还原结果必须验证不能直接当原始源码用。5. 批量化还原的工程化把exe反编译做成一条流水线5.1 提取补头反编译的一键脚本处理单个 exe 手敲命令还好手上有一批 exe 要还原时必须把流程脚本化。先写提取与入口定位import os, subprocess, sys EXTRACTOR pyinstxtractor-ng.py def restore_entry(exe_path): extract_dir exe_path _extracted if not os.path.exists(extract_dir): subprocess.run([sys.executable, EXTRACTOR, exe_path], checkTrue) pycs [p for p in os.listdir(extract_dir) if p.endswith(.pyc) and not p.startswith(pyi-)] entry max(pycs, keylambda p: os.path.getsize(os.path.join(extract_dir, p))) return os.path.join(extract_dir, entry) if __name__ __main__: for exe in sys.argv[1:]: print(processing, exe) print(restore_entry(exe))入口选择用的启发式是顶层最大 pyc对绝大多数打包配置有效如果打包时入口脚本被改名这个判断会失手兜底方案是把pycs列表全部打印出来人工挑。EXTRACTOR 指向你本地的拆包脚本路径Windows 上建议用绝对路径避免当前工作目录影响。反编译阶段枚举所有 pyc 逐个尝试把失败项落盘import glob for pyc in glob.glob(C:/tools/app.exe_extracted/*.pyc): r subprocess.run([uncompyle6, --verify, -o, restored, pyc], capture_outputTrue, textTrue) if r.returncode ! 0: open(fail.log, a).write(f{pyc}: {r.stderr}\n)带上--verify能顺带过滤不等价产物把 stderr 写进日志是排错的关键反编译失败的原因几乎全在里面。5.2 还原质量的三个验证手段编译、检索、对照反编译产物到手先过三道验证关。第一道是语法与可编译性python -m py_compile restored_main.py能通过说明至少语法完整。第二道是特征串检索从 exe 上用 strings 命令提取 URL、SQL、报错文案再回还原代码里 grepgrep -nE https?://|password|secret|token restored_main.py交集越大还原质量越高。第三道是行为对照在干净环境跑 exe记录文件读写和网络请求和还原代码里出现的路径、域名逐一核对。三道都过这份还原代码才值得信任。5.3 面对3.9新版字节码pycdc与pycdas兜底3.9 以上的 pycuncompyle6 基本退役主力是 pycdc 和配套的 pycdaspycdc 是 C 项目需要本地编译。pycdc 输出可读代码pycdc main_fixed.pyc main_restored.pypycdc 对 3.9-3.11 的主流语法支持得不错但 f-string 细节、注解和部分异常处理会丢失。当 pycdc 也失败时pycdas 是最后防线它输出字节码层级的伪汇编pycdas main_fixed.pyc main_dis.txt从 dis 文件里读常量表、函数名和调用序列虽然慢但能手工还原关键逻辑。3.10 以上 exe 的还原率确实在下降这是字节码演进带来的客观事实不是工具没选对。6. 还原后的源码值不值得信验证方法与真实边界还原完成只是开始。我一般用三个交叉验证确认还原质量第一行为对照——在虚拟机里跑原始 exe记录它读写了哪些文件、访问了哪些域名再回还原代码里找这些痕迹全对得上才算数第二字符串交集——对 exe 跑一遍 strings拿到的 URL、SQL、硬编码密钥去还原代码里搜命中率低于八成就要警惕第三编译往返——还原代码 py_compile 后用 dis 模块对比原始 pyc 的常量表和操作码序列结构一致说明还原基本可信。也要说清真实边界注释、空行、格式化永远回不来函数名和局部变量名大部分保留在 co_varnames 里但 3.11 的帧栈优化可能让部分局部变量变成_0这类占位名3.10 以上的还原产物可能需要手工修补才能运行。所以这份源码的正确用法是辅助理解与审计而不是百分之百还原成当初提交的那份 .py。我自己的教训是头一年做还原总想找一把万能工具一步到位结果在 3.9 上翻车无数次。后来老老实实按版本识别 → 拆包提取 → 入口补头 → 工具交叉验证四步走反而又快又稳。这套流程现在是我拿到任何 PyInstaller 产物的第一动作。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →