拓冰建站拓冰建站
首页 / 资讯中心 / 正文

PyInstaller打包exe如何还原成Python源码?完整逆向实操指南

简介这款工具面向 PyInstaller 打包程序逆向需求专注于将 exe 还原为可读 Python 源码帮助开发者与安全分析人员在调试、审计或学习时快速还原原始逻辑。工具内部整合解包与反编译功能自动完成提取 pyc 字节码并转写为 .py 文件使用者只需命令行运行主脚本并指定目标 exe 即可。需注意本地 Python 版本须与打包版本一致否则可能失败且不支持混淆或加壳的文件。压缩包共 6 个文件包含 2 个 Python 核心脚本、1 份说明文档、1 份依赖清单及 2 个配置管理文件整包仅 9KB便携轻量。已有 98 人学习解压后无需安装第三方库即可自动完成还原流程显著提升代码理解与排错效率适合各类 Python 逆向与二次开发场景。 很多用 PyInstaller 打包过 Python 项目的人应该都经历过这种尴尬几个月前写的脚本当时打包成 exe 发给了同事或者客户原文件早不知道扔到哪个备份盘里了结果那边反馈说程序报错让你赶紧看看逻辑哪里出了问题。你翻遍整个项目目录只找到当初那个 exe源码却像从世界上蒸发了一样。这时候如果手里有一把能直接把 exe 还原成 Python 源码的“钥匙”至少能先看看当初自己到底写了什么。这篇文章要聊的就是这套还原流程。我整理了基于 PyInstaller 打包产物逆向回源码的完整实操方法包括解包工具 pyinstxtractor 的正确用法、pyc 文件头修复、反编译器选型以及我实际踩过的各种坑。如果你手里正好有这类 exe 需要分析或者只是出于好奇想研究 PyInstaller 生成的二进制里到底藏着多少信息这篇内容应该能帮你省下大量试错时间。1. PyInstaller 打包后的 exe 里到底存了什么为什么能还原先搞清楚一个底层事实PyInstaller 本质上不是把 Python 代码编译成机器码而是把 Python 解释器、你的脚本、依赖库和资源文件一起打包成一个可执行文件。运行时它会在临时目录里解压这些内容然后由内置的 Python 解释器加载执行。所以你的代码逻辑实际还是以字节码pyc形式存在的只是被藏在了二进制文件里。这一点决定了还原的可行性。由于 exe 里保留的是 Python 字节码而 Python 字节码有公开的格式规范通过逆向提取再交给反编译器还原理论上就能得到与源码逻辑等价的 Python 脚本。展开看PyInstaller 生成的单文件 exe 内部结构大致分几块CArchive 归档区域这是核心里面按特定格式存放了 PYZ 归档、依赖的二进制动态库.dll/.pyd、配置文件等。PYZ 归档所有纯 Python 模块的 pyc 文件都压缩存储在 PYZ 里包括你引用的第三方库requests、flask_socketio、pandas 等。入口脚本 pyc你指定的主入口脚本通常就是你写的主程序单独存放在 CArchive 中并标记为要优先执行。二进制扩展模块用 C/C 编译的 .pyd 文件比如一些涉及加密、高性能计算的模块这部分无法直接还原成 Python 源码只能做导出函数分析。正是因为主逻辑和依赖库都以 pyc 形式存在所以“PyInstaller 打包的 exe 还原为 Python 源码”这条路才走得通。但要注意PyInstaller 在打包时会对 pyc 做处理最典型的是去除标准 pyc 文件头的信息。这就会在反编译时产生一些坑后面会仔细讲。另外很多人下意识认为 exe 里既然有源码那加个压缩壳UPX就安全了。实际上 PyInstaller 官方支持 UPX 压缩但对逆向来说只是多一步解壳并不构成实质性障碍。理解完这些下面进入实战操作。2. 解包实操用 pyinstxtractor 从 exe 里提取 pyc 文件在正式开始之前先说明一下环境和工具的选择。我自己建议在 Python 3.8 左右的 64 位环境中操作因为反编译工具链对 3.9 以上版本支持不太好遇到兼容性问题时你能少走弯路。当然工具本身只是分析 exe和运行环境是隔离开的所以你甚至可以把它放在一个干净的虚拟环境里。2.1 选择解包工具pyinstxtractor 还是 pyinstxtractor-ng经典工具是pyinstxtractor.py在 GitHub 上很容易找到。它通过递归扫描 PyInstaller 的 CArchive 数据结构把归档里的所有内容解压到原文件名_extracted目录里。原理上它不依赖被分析 exe 的 Python 版本兼容性比较广。后来社区又搞出了pyinstxtractor-ng算是原版工具的改进版。主要优化点是提高了对 PyInstaller 新版本6.x和不同打包配置的兼容性并解决了原版在某些归档结构上解析失败的问题。如果你打包时用的 PyInstaller 版本比较新比如 5.13 以上我建议直接优先用 pyinstxtractor-ng。工具是单文件 Python 脚本意味着你不需要安装直接命令行运行即可。2.2 完整解包命令与输出解析假设你的目标文件叫demo.exe用 pyinstxtractor-ng 解包的命令是python pyinstxtractor-ng.py demo.exe执行之后命令行会输出类似下面的信息[] Processing demo.exe [] Pyinstaller version: 2.1 [] Python version: 3.8 [] Length of package: 1234567 bytes [] Found 25 files in CArchive [] Successfully extracted py files in: demo.exe_extracted需要重点看两个信息Python version和Found files 数量。如果 PyInstaller 版本和 Python 版本识别不准确后续操作基本没法进行。输出里的demo.exe_extracted文件夹就是解包后的产物里面通常包含主脚本名.pyc入口脚本编译后的字节码名字和你打包时的主文件同名比如main.pyc。PYZ.pyz解包出来的第三方库归档直接用时会看到大量 .pyc 文件。一个子目录结构包含 .dll 和 .pyd 等二进制依赖。若干配置文件和信息文件比如pyimod00_crypto_key之类的加密密钥文件仅当打包时用到了加密选项才会出现。进入解包目录后第一件事就是找主脚本对应的 pyc。这里有一种常见情况入口脚本的 pyc 文件会被 PyInstaller 特意标记文件名可能是main.pyc.manifest或类似后缀需要手动去掉后缀才能正常参与反编译。也有些版本不会但文件位置一般比较明确。2.3 为什么有时解包会失败pyinstxtractor 解包失败不是罕见事。我遇到过的典型场景包括exe 本身经过了 UPX 加壳PyInstaller 打包时勾选了 UPX归档内的数据块被压缩过。解决办法是先对 exe 脱壳比如用 upx -d然后再解包。文件被篡改或损坏网上流传的“绿色版”或破解版的 exe 可能内部结构被改过PyInstaller 的魔数验证过不了。PyInstaller 版本过新导致归档结构变化此时优先考虑 pyinstxtractor-ng它对新版本适配更好。解包成功只是第一步因为接下来要面对最繁琐的 pyc 反编译环节而这里就藏着一个非常容易踩的坑。3. 反编译前必须搞定的 pyc 文件头magic number 的识别与修复把 pyc 文件直接丢进反编译器结果大概率是一堆报错。原因在于 PyInstaller 打包时把 pyc 文件的头部信息删除了。这里说的头部是 Python 字节码文件开头的固定结构包括 magic number、flags、时间戳和源文件大小。反编译器需要靠这些信息来识别字节码版本和校验合法性。3.1 PyInstaller 对 pyc 头部做了什么PyInstaller 在把源码编译成字节码后写入归档时并不会保留完整标准 pyc 头。它只保留了一个 4 字节的 magic用来标识 Python 版本新版 PyInstaller 会保留 8 字节或更多但依然不完整。这样带来的问题是直接从demo.exe_extracted目录里拿到的 pyc 文件反编译器根本认不出来。如果你解包出来的 pyc 第一眼看到的是这样一堆字符bx00x00x00x00x00x00x00x00...那基本可以确定头部信息缺失或错位。3.2 标准 pyc 文件头的结构在 Python 3.7 及以上版本中标准 pyc 文件头是 16 字节偏移字节数内容说明04magic number标识 Python 编译器版本44flags位标志如是否启用了 hash 校验84时间戳源文件修改时间可选124源文件大小源文件字节长度可选Python 3.7 以下的版本头更短通常是 8 字节。3.3 最稳的修复方案从同版本编译环境中复制头部修复文件的思路很简单找一个和目标 pyc 相同 Python 版本环境下生成的正常 pyc 文件复制它的前 16 字节覆盖到损坏文件的头部上。我在实际操作时的方法是这样的检查解包目录里是否有其他完整的 pyc 文件PyInstaller 解包出来的第三方库 pyc 有时会保留部分头部信息。如果找不到就在本机用和识别出的 Python 版本完全一致的解释器执行一条命令生成基准文件python -c import test; print(test.__file__)或者直接python -m py_compile sample.py然后取生成的__pycache__/sample.cpython-38.pyc文件作为头部来源。用十六进制编辑器比如 010 Editor、Hex Fiend或者 Python 脚本从基准文件中截取前 16 字节拼接到需要修复的 pyc 前面。如果嫌手工麻烦可以直接写一个小脚本批量处理import struct import pathlib # 以 Python 3.8 为例 reference_pyc pathlib.Path(reference.cpython-38.pyc) header reference_pyc.read_bytes()[:16] target pathlib.Path(main.pyc) data bytearray(target.read_bytes()) # magic 已经在原文件中存在但为稳妥起见直接用完整头部替换 target.write_bytes(header data[16:])实际处理时如果原文件的开头已经有少量 magic 数据你只需要补充缺失的部分。但既然都是程序化处理直接整体替换前 16 字节最省事。3.4 如何识别 Python 版本如果你在解包输出里没看到清晰的 Python 版本号可以用工具直接读 magic。常见对应关系如下magic (十六进制)Python 版本0x0A0D0D553.80x610D0D553.90x6C0D0D553.100xD70D0D553.110xC00D0D553.12当然这里列出的只是部分。网上有完整对照表遇到不确定时先查表也可以用xdis这个库自动识别。4. 反编译器选型uncompyle6、decompyle3 与 pycdc 的取舍文件头修复之后pyc 就可以交给反编译器了。但反编译器的选择非常考验版本匹配度选错了工具得到的结果可能是一堆反编译失败的错误信息。我自己做过多种组合的对比测试下面的经验可以帮你快速定位最适合目标 Python 版本的工具。4.1 uncompyle6老牌经典但维护停滞uncompyle6 是使用范围最广的 Python 字节码反编译器支持到 Python 3.8。对 3.8 及以下版本的 pyc 还原效果最好尤其是结构简单的脚本反编译出来的代码可读性很高基本上接近原始源码。但它的维护状态不太乐观对 Python 3.9 及以上版本几乎无能为力。如果你解包出的 pyc 是 3.9 或更高版本编译的uncompyle6 基本可以直接放弃。安装pip install uncompyle6使用命令uncompyle6 -o output_dir main.pyc也可以直接输出到终端uncompyle6 main.pyc4.2 decompyle3对 3.7-3.8 的补充decompyle3 其实是 uncompyle6 的分支重点优化了 Python 3.7 到 3.8 的反编译质量尤其是一些较为复杂的控制流结构比如try/except、with语句的还原。如果 uncompyle6 在某个 pyc 上报错或输出乱码可以换 decompyle3 试试。它的安装和使用方式与 uncompyle6 几乎一样pip install decompyle3 decompyle3 main.pyc4.3 pycdc跨版本支持广但可读性略差pycdc 是 C 实现的反编译器来自 Decompyle 项目。它的最大优势是支持较新的 Python 版本包括 3.9、3.10 甚至 3.12基本不存在“版本过新无法处理”的硬伤。缺点是生成的代码可读性一般部分语法结构还原得不完美比如闭包和控制流会有差异但至少逻辑是能看懂的。使用前需要自己编译。步骤大致是git clone https://github.com/zrax/pycdc.git cd pycdc cmake . make编译完成后会在当前目录生成pycdc和pycdas两个可执行文件。反编译命令./pycdc main.pyc main.py4.4 我的选择策略在实际分析中我的选择逻辑是这样优先级目标 Python 版本 3.9先试 uncompyle6如果失败换 decompyle3。目标 Python 版本 3.9直接用 pycdc同时配合pycdas字节码反汇编器人工分析异常部分。如果反编译结果有语法错误先用pycdas查看反汇编字节码手工恢复关键逻辑。下面用一个真实例子对比三种工具的输出效果。假设原始源码是一个简单的函数def calculate(data, factor2): result {} for key, value in data.items(): if value 10: result[key] value * factor return result反编译后 uncompyle6 的输出基本和原文一致pycdc 则可能把if value 10处理成if value.__gt__(10)可读性立刻下降但逻辑不变。5. 进阶还原依赖库 pyc 和加密打包的分析思路主脚本还原出来之后下一步就是处理第三方依赖库的 pyc。这些文件散落在 PYZ.pyz 解出来的目录里数量往往非常多。不过绝大多数的第三方库requests、flask 等本身就开源你不需要反编译它们直接用官方源码看逻辑就行。真正需要反编译的是那些和业务强相关的自定义模块。5.1 定位自定义业务模块怎么从一堆 pyc 里找出自己的业务代码我的习惯是看文件名。PyInstaller 打包时保留的是模块路径所以my_project、config、utils这类一眼就能认出来的自定义模块非常明显。如果你不确定哪些是自研模块可以用关键字搜索 pyc 文件内容比如你记得某个函数名或字符串。找到目标 pyc 后按前面说的方法修复文件头然后反编译即可。5.2 处理 PYZ 归档里的 pyc 文件PYZ.pyz 是一个压缩归档可以用archive_viewer.pyPyInstaller 自带工具或者 pyinstxtractor 的辅助脚本提取其中内容。在解包目录下通常会直接生成PYZ.pyz_extracted文件夹里面就是已经解压出的所有 pyc。这里有个细节PYZ 里的 pyc 文件头有时比主脚本的更完整你可以直接对这些文件尝试反编译如果报错再补头。5.3 遇到带加密选项打包的 exe 怎么办PyInstaller 支持通过--key参数对归档内容进行加密。如果解包时看到了pyimod00_crypto_key文件说明打包时用了 AES 加密。这种情况下主脚本和 PYZ 里的 pyc 都是加密状态直接修复头部毫无意义。但有几种情况可以绕过去如果你能确认打包时使用的--key字符串可以使用 pyinstxtractor 配套的解密逻辑还原明文 pyc。如果密钥是硬编码在代码里生成的很多人的做法那它也会以字符串形式出现在二进制文件中可以在 exe 中搜索可读字符串找到密钥后手动解密。如果实在拿不到密钥基本只能放弃还原改走动态分析路线比如监控临时目录释放的文件。5.4 哪些内容无法还原说明白一点任何以 .pyd 形式存在的扩展模块都无法还原为 Python 源码。这些是 C/C 编译出的二进制逆向的话只能分析导出函数和内部逻辑工作量等同于逆向一个 C 程序。如果你项目中大量核心逻辑用 Cython 编译成了 pyd那 pyinstxtractor 只能帮你看到调用关系还原源码的梦想基本破灭。还有--onefile模式有时会生成额外加载器逻辑但加载器本身对业务功能分析没有太大影响。6. 踩坑实录与验证方法还原出来的代码到底对不对只有反编译结果能跑通还原工作才算结束。但反编译代码“能看”和“能跑”是两码事。我在几次还原任务中总结了一套验证流程和几个高频坑位这里一并分享。6.1 高频坑反编译后的代码报 SyntaxError反编译器并不是万能的尤其遇到一些不常见语法结构时很容易生成语法有问题的代码。比如 pycdc 生成的结果经常会有多余的或), 需要手工调整。我的经验是先删除空行和多余括号跑一遍python -m py_compile根据报错行号定位修复。6.2 高频坑函数执行结果与预期不符即使代码能运行结果也可能和原始逻辑不一致。一个典型例子是 Python 3.8 之后海象运算符:在某些上下文中的反编译失真。还有一部分async/await语法在反编译后可能丢失某些协程细节。遇到这种情况建议对比 pycdas 的反汇编输出看关键跳转分支是否一致。反汇编是确定性的反编译只是它的近似还原。6.3 有效验证的三步法我自己在执行还原任务时会严格按照下面的步骤验证语法检查反编译后的文件先跑python -m py_compile保证无语法错误。单元测试覆盖如果原项目有测试用例最好没有的话基于反编译代码重新构造几组输入输出验证关键函数逻辑。资源/路径核对检查代码里引用的数据文件、模型文件、ini 配置等是否和解包目录中的资源一致PyInstaller 打包时经常会把资源路径改成临时目录下的相对路径还原时要手动调整。6.4 还原过程中的“度”的把握这里要说一句比较实际的提醒还原工具和技术本身是中性的可以用来找回自己丢失的源码、分析遗留系统、做安全研究。但用的时候一定要想清楚你是否有权分析这个文件。如果是工作里接手的旧项目先跟负责人确认一下如果是从网上下载的别人的程序只能用于个人学习和安全研究不要拿去破解、篡改或二次分发。不要因为“技术上行得通”就忽略了合规边界。我自己现在养成的习惯是重要项目强制保留源码备份同时把 PyInstaller 打包命令写进自动化脚本每次发版都留一份带 build-id 的构建记录。这样即便源码后来丢了也能精确找回当时打包用的版本和参数。与其事后费劲反编译不如事前多留一手。如果你手头还有别的 PyInstaller 打包的 exe 需要反编译不妨按照上面的流程走一遍。遇到新版本 Python 或者特殊打包配置的兼容问题时优先检查工具版本和解包输出中的 Python 版本提示这两点能解决一大半问题。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门