行业资讯
📅 2026/9/1 5:33:24
PyInstaller打包的exe如何反编译还原Python源码?完整实操指南
简介面向需要逆向分析或调试 PyInstaller 打包程序的开发者这款工具能够将 Windows 平台下由 PyInstaller 生成的 exe 文件快速还原为可读的 Python 源码。工具核心流程分两步先用 pyinstxtractor 从 exe 中提取 pyc 字节码再用 uncompyle6 将 pyc 反编译成 py 文件命令行运行 python exe2py.py index.exe 即可全自动处理。压缩包共 6 个文件、约 9KB包含 exe2py.py 主程序、pyinstxtractor.py 解包脚本、requirements.txt 依赖说明、README.md 使用文档以及 .gitignore 等辅助文件整体轻量且无需额外安装第三方库。已有 98 人学习下载适合开发调试、代码审计、学习分析等场景需注意本地 Python 版本需与目标 exe 一致且不支持混淆或加壳后的文件。对于希望理解 PyInstaller 打包机制或快速恢复旧项目源码的读者这套脚本提供了开箱即用的完整解决方案。1. 这个工具到底解决什么问题做Python开发的谁没遇到过这种场景某个软件只发了一个exe文件没有源码但你就是想看看里面到底用了哪些库、某些逻辑是怎么写的或者你自己当初用PyInstaller打包了项目结果源代码盘丢了只剩下一个打包好的exe现在想改个参数都无从改起。大部分人的第一反应是放弃但说实话PyInstaller打出来的exe还原成本并不高是有固定套路可走的。PyInstaller的打包原理是把Python解释器、依赖库和你的脚本拼成一个自解压的包运行时自己解到内存或临时目录再执行。这就意味着你的原始.py源码其实并没有被“编译”成机器码而是被转换成.pyc字节码文件塞进了一个归档里。只要能把归档解开、把.pyc捞出来、再反编译回.py就完成了绝大部分的还原工作。这篇文章主要写给两类人一是想要从PyInstaller生成的exe中恢复自己丢失源码的开发者二是想学习PE文件结构和Python反编译技术的入门者。整个过程不需要太高深的逆向功底靠几个开源工具和一点耐心就能搞定但有几个细节确实容易卡住人我会在实操部分把踩过的坑和排查思路全部写出来。2. 还原原理先搞清楚PyInstaller的打包机制2.1 PyInstaller把源码藏在了哪里要还原先得知道东西藏在哪。PyInstaller的打包产物看起来是个普通的exe文件但它的结构很有意思最外层是PE文件的壳但往里看其实是一个自定义格式的归档包。它会把Python的.dll、依赖的第三方库、你的脚本编译产物全部塞进去运行时再释放到临时目录由内置的引导程序负责加载。用十六进制编辑器打开一个PyInstaller生成的exe你能在文件末尾附近看到MEI开头的标记后面跟着一堆压缩数据。这一整块就是PyInstaller的归档内容里面存放着你的模块文件、二进制依赖、资源文件等。Python脚本对应生成的是.pyc字节码文件在归档中通常以/为前缀例如/main.pyc来和组织信息区分。也就是说exe本身不是加密的它只是个包裹层。PyInstaller官方提供了pyi-archive_viewer.py工具可以查看归档内容但它只负责“看”不负责结构上的深度解析。咱们要做的第一步其实就是把这个归档干净地拆出来。2.2 为什么有的pyc能还原、有的还原不了这里要先打破一个预期不是所有PyInstaller打包的exe都能100%还原成可运行的.py源码。能否还原取决于你打包时的Python版本和是否使用了加密参数。如果你的程序是用Python 3.7~3.8打包的那么通过uncompyle6或decompyle3基本能还原出可读性较好的源码如果是Python 3.9~3.11打包的主流反编译器uncompyle6、decompyle3会出现语法兼容问题要靠pycdc或者修复字节码指令后部分还原如果你打包时加了--key参数PyInstaller的字节码加密选项那还原难度会大幅上升因为.pyc是加密过的必须先逆向出加密密钥的逻辑才能继续。另外还有个影响因素是打包方式。--onefile和--onedir只是归档组织形式不同不影响还原难度但--onefile模式下生成的exe体积大、临时解压痕迹明显反而更容易判断是PyInstaller打的包。还有不少人会配合Nuitka把Python代码编译成C扩展这种“伪编译”模式下基本没有还原希望只能靠字符串提取和动态调试恢复部分逻辑不能算真正意义上的还原。3. 实操前的准备工具链选型与安装3.1 三件核心工具pyinstxtractor、反编译器、十六进制编辑器整个还原链条需要三件工具各有分工缺一不可。第一件是pyinstxtractor.py它是整个流程的核心。这个工具专门解析PyInstaller的归档结构能把exe里的所有内容解压到当前目录的xxx_extracted文件夹里。注意pyinstxtractor支持的是PyInstaller 2.1到6.x的版本最新的6.x格式也兼容但偶尔有小版本改动需要换用pyinstxtractor-ng一个社区维护的fork版本。建议两个都备着遇到解不开的换着试。第二件是Python反编译器。这里按Python版本选型Python 3.7~3.8用decompyle3或uncompyle6Python 3.9用pycdcC写的命令行工具持续更新。还有一个比较新的选择是pycdaspycdc的姊妹工具它能输出字节码的反汇编在源码还原失败时用来人工分析代码逻辑作为兜底方案。第三件是十六进制编辑器比如HxD或者010 Editor。别小看它后面修复.pyc文件头的时候会频繁用上没有它你很难手工完成magic number的修复。3.2 环境准备与注意事项尽量在Python 3.8以下的环境里跑反编译因为uncompyle6对3.8的兼容性不够好经常报TypeError或ValueError。如果你只有Python 3.11也没关系可以开个Python 3.8的虚拟环境来跑反编译解包阶段对版本要求不高。pycdc是C编译的Windows下建议直接用官方release的exeUbuntu下用apt install pycdc或源码编译都行。运行pyinstxtractor时目标exe路径最好不要带中文和空格否则部分版本会异常退出。注意还原脚本本身有一定的环境敏感度建议全程在干净的目录里操作并且保留原exe的备份避免反复解压污染现场。4. 完整还原实操从exe到py脚本全流程4.1 第一步确认exe确实是PyInstaller打包的拿到一个exe不要上来就解包先花半分钟确认包的类型。最简单的办法是用pyi-archive_viewer.py或者直接在命令行里执行pyi-archive_viewer.exe your_program.exe如果输出里能看到main.exe、PYZ.pyz、PKG这样的条目那基本可以确认是PyInstaller的产物。更多时候你手上没有装PyInstaller这时候可以用PyInstaller自带的一个特征来判断用HxD打开exe在文件尾部搜索MEI字符串能找到就说明是PyInstaller打的包。另外用strings命令Linux自带搜pyi-*相关字符串也能佐证。确认之后处理前建议用PE工具比如DIEDetect It Easy看一眼编译器信息心里有个底。DIE会显示“PyInstaller”的壳特征还能识别出Python主版本号这对后面选反编译器很有用。我见过有人把Electron或Tauri打包的exe拿来硬套PyInstaller的解包流程那当然什么都解不出来。4.2 第二步用pyinstxtractor解包拿到.pyc文件环境准备好后直接执行解包把命令中的文件名换成你自己的python pyinstxtractor.py your_program.exe正常情况下同目录下会生成一个名为your_program_extracted的文件夹里面就是解包得到的全部内容。重点看几个文件/目录main.exe或入口名这是PyInstaller的引导程序不是重点。PYZ.pyz存放所有第三方依赖库的压缩包里面的.pyc不能直接反编译需要先解PYZ。根目录下的各个.pyc文件这些是主脚本和入口模块的字节码优先处理它们。struct.pyc、marshal.pyc等这些是Python标准库的.pcy可以用来恢复Python版本信息。解包过程中最常遇到的问题就是版本不兼容导致的ValueError: Unsupported PyInstaller version。这时候不要慌下载pyinstxtractor-ng再跑一次。或者升级PyInstaller版本后再解解包只需要能识别结构就行不要求版本和打包时完全一致。4.3 第三步修复pyc文件头和Python版本识别这一步是网上教程最容易跳过的但也是新手最容易卡住的地方。PyInstaller解出来的.pyc文件的头部和正常.pyc文件不太一样——它会以MEI\014\013\012\013\016这样的魔数开头实际上是把Python的magic number覆盖掉了反编译器不认识这种头直接丢进去会报错。一般会拿一个同版本的正常.pyc文件作参照把正常文件的头前16个字节含magic number和时间戳、大小信息复制过来替换掉异常的头。具体操作确认目标Python版本。最简单的方法看解出来的struct.pyc文件头前4个字节就是magic number。比如6f 0d 0d 0a对应Python 3.8e3 0c 0d 0a对应Python 3.7。用同版本的Python写一行代码生成一个简单的.pyc文件作为“参照头”import py_compile py_compile.compile(test.py, cfiletest.pyc)用HxD打开test.pyc复制前16个字节再打开待修复的.pyc把这个16字节覆盖到它的开头保存。头修复完之后用反编译器直接读这个.pyc就能识别版本了。有些工具能自动修复头比如pyinstxtractor在解包时其实已经尽量保留了原始头部信息但主脚本.pyc经常是损坏的手动补头几乎是必经步骤。4.4 第四步反编译pyc为Python源码按Python版本选择反编译器。对于Python 3.8及以下推荐用uncompyle6pip install uncompyle6 uncompyle6 main.pyc main.py如果遇到ImportError或语法错误可以换成decompyle3。对于Python 3.9及以上uncompyle6基本无能为力用pycdcpycdc main.pyc main.pypycdc的输出质量比uncompyle6差一些部分复杂语法比如f-string、walrus、match语句会解析失败或漏转换。但好在大部分的.pyc文件是可以直接反编译出完整源码的尤其是不涉及复杂语法的小工具类项目还原率能达到80%~95%。需要说明的是PYZ.pyz里的依赖库.pyc也可以反编译但一般没必要因为第三方库网上都能下到源码只有你自己写的那几个模块才是重点。反编译出来的源码可能有缩进问题或个别语法错误需要人工修补。通常我会把反编译结果和源exe运行时的行为对照着看确认关键逻辑没有丢失。5. 反编译失败怎么办常见问题与排查手段5.1 典型报错一ImportError: load_module() error这个报错在uncompyle6里极其常见原因通常是输入的.pyc文件头信息不完整或者版本不一致。先确认你用的是不是和目标exe相同大版本的Python其次检查.pyc头部修复是否正确尤其要关注时间戳和文件大小字段。还有一个隐性坑PyInstaller解包出来的.pyc可能是“S27”格式即已经用zlib压缩需要先用pyinstxtractor自带的解压逻辑解开如果你手动改过文件扩展名很可能会误伤。5.2 典型报错二反编译后源码逻辑明显不对pycdc反编译出来的代码有时看着能跑但逻辑错误明显比如if分支条件被反转、循环边界差1。这种问题多半出现在Python 3.9的较新字节码格式上pycdc对某些指令组合比如带有CACHE指令的版本支持不完善。我的处理办法是用pycdas看反汇编对比反编译结果手动修正关键分支逻辑。这一步确实累但比重新实现一个模块快得多。如果只是想要改个参数甚至不需要完美还原整个源码——定位到对应模块、对应函数局部修好逻辑就可以直接用了。5.3 如何应对加了--key加密的PyInstaller exe如果你解包后发现所有的.pyc文件都是加密过的头部完全无法匹配任何magic number且内容看起来是随机字节说明打包时用了--key参数。这种场景下直接反编译的路走不通但也不是完全没救方法一动态调试。用x64dbgWindows或gdb ptraceLinux附加到运行中的进程在Python导入模块的入口处下断点等Python解释器解密完字节码后把内存里的.pyc数据dump出来。这个方法成功率很高但需要一定的调试功底。方法二分析PyInstaller的加密实现。较老版本5.x用的是AES加TEA混合加密新版本用的是SALSA20。写一个独立的解密脚本从exe里提取密钥并解密.pyc后再反编译。方法三运行时hook。用sitecustomize.py或模块导入钩子在目标程序运行时把它加载的模块对象dump下来用marshal.dumps导出代码对象再生成.pyc。其中方法三最实用因为不需要对抗保护只需要跑起来就能拿到。我写过一个小工具原理就是启动exe后立刻注入一个sitecustomize钩住builtins.__import__把所有import过的模块缓存下来再通过marshal转成.pyc。这套流程对大部分加--key的exe都有效唯一的弱点是必须在有GUI或正常退出流程的环境里跑部分带反调试的程序会比较棘手。5.4 常见问题速查表问题现象可能原因解决办法解包时提示不支持PyInstaller版本pyinstxtractor版本过旧换pyinstxtractor-ng反编译时ImportErrorpyc头修复错误重新定位magic number确认Python版本反编译后逻辑错乱Python版本高pycdc兼容性差用pycdas看反汇编手工修正所有pyc都识别不了可能用了--key加密动态dump或hook方案exe是控制台程序但没有入口main.pyc入口名被改了看pyi-archive_viewer的完整列表找最大的那个.pyc6. 一些很实在的经验与限制说明整套流程走下来我说几点个人体会。第一尽量在Python虚拟环境里做解包和反编译不要直接装到系统Python里不然uncompyle6/pycdc的依赖冲突迟早要把你的环境搞烂。第二手动修复pyc头千万别图省事只修前4个字节时间戳和大小字段都很重要否则pycdc会拒绝解析或者解析错误。第三解包得到的文件尽量保留原样不要大批量改扩展名pycdc是按内容读的跟扩展名没关系。限制方面也得说实话。还原出来的代码格式化、注释、变量名层面和原始源码会有差距——如果你原代码里变量叫user_input_incoming反编译后可能变成u1或x这是Python字节码根本不存储变量名的结果除非是局部变量调试符号。所以指望“1:1还原出带注释的精美源码”是不现实的但还原出可读、可改、可运行的核心逻辑完全做得到。另外Cython打包的exe不在本文讨论范围内。那种方案下Python代码会被转成C再编译成机器码反编译难度已经接近破解原生程序。但话说回来如果只是个小工具你也不需要硬还原它观察运行行为、抓网络请求、看配置资源可能比反编译更高效。最后送大家一个排查小技巧还原过程中遇到任何报错第一时间先确认Python版本再确认pyc头部信息这两个变量占了我遇到过的80%以上问题的原因。把这套流程跑顺了“exe还原Python源码”也就是个半小时内的活儿。本文还有配套的精品资源点击获取