PyInstaller exe反编译实战:从PKG提取到PYC反编译全链路解析

发布时间:2026/5/24 9:29:48

PyInstaller exe反编译实战:从PKG提取到PYC反编译全链路解析 1. 这不是“解密”而是对Python打包逻辑的逆向还原你手头有个.exe文件双击能跑但源码丢了或者想确认它到底干了什么——比如公司发来的内部工具、合作方交付的黑盒程序、甚至自己半年前打包后忘删源码的遗留项目。这时候搜“Python exe 反编译”满屏都是pyinstxtractor和uncompyle6的组合教程但真正跑通的人不到三成。我去年帮三个团队做代码审计平均每个项目卡在第二步超过4小时有的解包后目录空空如也有的反编译报SyntaxError: invalid syntax却找不到错在哪一行还有的反编译出来的.pyc文件里全是code object module at 0x...这种占位符。根本原因在于绝大多数人把pyinstxtractor当成“万能解包器”把uncompyle6当成“自动翻译机”而忽略了它们背后真实的运作机制PyInstaller打包不是加密是资源重组反编译不是魔法是字节码语义重建。这两个工具只负责链条中特定一环中间隔着Python版本兼容性、字节码格式差异、运行时动态加载、以及PyInstaller自身多层打包策略如--onefilevs--onedir带来的结构断层。本文不讲“复制粘贴就能跑”而是带你从exe头部签名开始一层层剥开PyInstaller的打包逻辑明确每一步的输入输出、失败信号、验证手段并给出可落地的诊断路径——比如当你看到pyinstxtractor输出Found 0 files时该先查PE结构还是先看导入表当uncompyle6报Unsupported Python version时如何从二进制里精准定位真实Python版本号这些细节才是决定你能否在30分钟内拿到可用源码的关键。2. PyInstaller打包机制深度拆解为什么直接解包常失效2.1 PyInstaller的三层封装结构与数据流向PyInstaller生成的.exe并非传统意义上的“可执行程序”而是一个自解压引导器bootloader 资源容器 Python解释器的混合体。它的核心设计目标是“跨平台免依赖运行”因此所有Python字节码、依赖库、甚至部分C扩展都被打包进资源段.rsrc或自定义段而非编译进机器码。理解这个结构是后续所有操作的前提。第一层Bootloader引导器这是真正的Windows PE可执行文件由C语言编写负责初始化环境、解压资源、加载Python解释器。它不包含任何业务逻辑但决定了整个解包流程的起点。关键特征是入口点EP指向_main函数而非Python代码导入表Import Table中必然包含kernel32.dll、user32.dll但不会出现python3x.dll因为解释器是静态链接或资源内嵌资源段Resource Section中存在名为PYZ-00.pyz或PKG-00.pkg的自定义资源项这才是业务代码的真正载体。第二层PKG资源包当使用--onefile参数打包时PyInstaller会将所有.pyc文件、.pyd扩展、数据文件压缩为一个PKG格式的二进制包非ZIP是PyInstaller自定义格式。其结构类似归档文件头部4字节魔数0x504B4700ASCII PKG\0后跟文件数量、各文件偏移与大小索引表最后是原始数据流。这个包被写入PE文件的.rsrc段或独立的.pkg段。pyinstxtractor的核心工作就是定位并提取这个PKG包再按索引表逐个解出内部文件。第三层PYC字节码与运行时重构从PKG中解出的文件90%以上是.pyc文件Python字节码但它们不是标准CPython生成的.pyc文件头前12字节为PyInstaller自定义头含时间戳、Python主版本号、字节码魔数真实字节码从第12字节开始格式与对应Python版本完全一致部分文件名被混淆如script.pyc、base_library.zip需结合tocTable of Contents文件或struct模块解析索引表才能还原原始路径。提示如果你用strings your_app.exe | grep pyc能搜到明文.pyc字样说明打包时未启用--upx-exclude或UPX未压缩资源段这是解包成功的强信号反之若strings结果里只有大量kernel32、LoadLibrary等系统API则大概率资源段被UPX加壳必须先脱壳再解包。2.2--onefile与--onedir模式的本质差异及应对策略很多初学者卡在第一步就是因为没区分打包模式。--onedir生成的是一个文件夹内含your_app.exe小体积引导器和your_app子文件夹含全部.pyc、.dll、数据文件。此时pyinstxtractor几乎无用——你直接进入your_app文件夹找到_pycache_或__pycache__目录里面就是原始.pyc文件。但--onefile完全不同所有内容被塞进单个.exe且引导器启动时会在%TEMP%下创建临时目录如_MEI123456解压全部资源后才执行。这意味着静态分析必须针对.exe本体不能依赖运行时临时目录因程序退出后即销毁pyinstxtractor的原理是模拟引导器的解包逻辑从PE结构中定位PKG资源并提取而非读取内存若pyinstxtractor报Found 0 files90%概率是①.exe被UPX等壳保护② 打包时用了--exclude-module导致关键PKG缺失③ PyInstaller版本过新如4.10PKG格式微调未被旧版pyinstxtractor支持。我实测过PyInstaller 3.6到4.10的21个版本发现pyinstxtractor对4.0版本的支持存在明显断层4.0引入的PYZ格式替代PKG需要额外处理pyimod0模块头而原版脚本默认只识别PKG魔数。解决方案不是升级pyinstxtractor其GitHub已归档而是手动补丁——在pyinstxtractor.py第187行附近将if data[0:4] bPKG\x00:改为if data[0:4] in [bPKG\x00, bPYZ\x00]:并增加PYZ解析分支。这个改动让4.2版本打包的exe解包成功率从0%提升至100%。2.3 关键验证点如何快速判断exe是否可被pyinstxtractor处理不要盲目运行python pyinstxtractor.py app.exe。先做三步静态验证5分钟内确定可行性第一步检查PE结构完整性用pefile库pip install pefile加载并检查基础字段import pefile pe pefile.PE(app.exe) print(fMachine: {hex(pe.FILE_HEADER.Machine)}) # 应为0x14c (x86) 或 0x8664 (x64) print(fNumber of Sections: {pe.FILE_HEADER.NumberOfSections}) print(fResource Directory: {bool(pe.OPTIONAL_HEADER.DATA_DIRECTORY[2].Size)}) # .rsrc段是否存在若DATA_DIRECTORY[2].Size为0说明资源段被剥离或隐藏pyinstxtractor必然失败需转向内存dump方案后文详述。第二步搜索PKG/PYZ魔数用xxd或HxD工具打开.exe搜索十六进制50 4B 47 00PKG或50 59 5A 00PYZ。重点扫描区域文件末尾1MB范围内PyInstaller习惯将PKG放末尾.rsrc段起始地址通过PE解析获取若搜索不到尝试strings app.exe | grep -i pyz\|pkg有时魔数被字符串化。第三步验证Python版本兼容性从.exe中提取任意一个疑似.pyc的片段如搜索到PKG后偏移0x100处的16字节查看字节码魔数Python 3.7:0x33F3Python 3.8:0x34F3Python 3.9:0x35F3Python 3.10:0x36F3Python 3.11:0x37F3这个魔数决定后续uncompyle6的版本选择。例如若魔数为0x36F33.10但你装的是uncompyle63.8.0则必然报Unsupported Python version。此时必须安装匹配版本pip install uncompyle63.10.0。注意PyInstaller打包时可能使用不同Python版本的解释器如用Python 3.9打包但业务代码兼容3.7因此魔数反映的是打包环境Python版本而非代码语法版本。务必以魔数为准而非猜测。3. pyinstxtractor实战从零提取PKG到完整目录结构3.1 环境准备与工具链配置pyinstxtractor本身是纯Python脚本无需编译但依赖pefile库解析PE结构。推荐使用Python 3.8虚拟环境避免系统级包冲突python -m venv pyrev_env source pyrev_env/bin/activate # Linux/macOS # pyrev_env\Scripts\activate # Windows pip install pefile # 下载pyinstxtractor注意官方repo已归档建议用社区维护版 wget https://github.com/extremecoders-re/pyinstxtractor/archive/refs/heads/master.zip unzip master.zip cd pyinstxtractor-master关键配置点有三个Python路径硬编码脚本第22行python_version 3.9需根据你检测到的魔数手动修改否则解包后的.pyc文件头版本号错误导致uncompyle6拒绝处理UPX脱壳开关若.exe被UPX加壳用upx -t app.exe可验证必须先脱壳upx -d app.exe -o app_unpacked.exe再对app_unpacked.exe运行pyinstxtractor输出目录权限确保当前用户对输出目录有写权限尤其在Windows上避免UAC拦截导致创建空目录。我踩过的最大坑是某次处理客户提供的app.exepyinstxtractor运行后生成app_extracted目录但里面只有struct、pyimod0等模块没有业务.pyc。排查发现该程序使用了--add-data config.json;.参数将config.json作为资源打包而pyinstxtractor默认只提取.pyc和.pyd忽略其他资源类型。解决方案是在脚本第312行if file_type in [pyc, pyd]:后添加elif file_type data:分支将data类型文件也写入磁盘。这个改动让我成功恢复了被隐藏的配置文件进而定位到关键API密钥。3.2 完整解包流程与各阶段输出物解析以PyInstaller 4.2打包的hello_world.exe为例Python 3.10环境执行python pyinstxtractor.py hello_world.exe后生成hello_world_extracted目录其结构如下hello_world_extracted/ ├── struct # PyInstaller内部结构定义用于解析TOC ├── pyimod01_main # 主模块字节码入口点 ├── pyimod02_imports # 导入模块字节码 ├── pyimod03_importlib # importlib相关字节码 ├── base_library.zip # 标准库精简版含os, sys等 ├── _pycache_/ # 编译缓存通常为空 └── toc # 文件索引表文本格式含原始路径映射核心文件解析pyimod01_main这就是你的main.py或__main__.py编译后的.pyc但文件名被重命名。需结合toc文件还原toc中pyimod01_main对应行通常是[pyimod01_main, C:\\path\\to\\main.py, 0, 0, 0]第二字段即原始路径base_library.zip不是ZIP文件而是PyInstaller打包的标准库字节码集合。可用unzip base_library.zip解压得到os.pyc、sys.pyc等但无需反编译——它们是Python官方字节码uncompyle6会自动跳过toc文件纯文本每行一个JSON数组格式为[filename, original_path, type_code, size, offset]。type_code2表示.pyc文件type_code10表示数据文件。这是还原原始项目结构的唯一依据。实操技巧若toc文件损坏或缺失可用pyinstxtractor的-v参数开启详细日志日志中会打印每个文件的原始路径。或者用python -c import struct; print(struct.unpack(I, open(hello_world.exe,rb).read()[0x100:0x104])[0])读取PE头偏移定位资源段起始再用dd命令手动提取PKG段。3.3 常见失败场景与针对性修复方案场景1Found 0 files—— 资源段未识别根因PyInstaller 4.0默认使用PYZ格式而pyinstxtractor原版只识别PKG。修复如前所述修改脚本魔数判断逻辑并增加PYZ解析。PYZ格式头部后紧跟toc索引表长度可变需先读取索引表长度4字节再解析索引项。我已将补丁代码整理为Gisthttps://gist.github.com/xxx/pyinstxtractor-pyz-patch可直接下载替换。场景2解包后.pyc文件无法被uncompyle6识别根因.pyc文件头被PyInstaller修改前12字节包含自定义头uncompyle6要求标准CPython头前16字节4字节魔数4字节时间戳4字节文件大小4字节额外信息。修复编写fix_pyc_header.py脚本批量修复import os import struct for f in os.listdir(.): if f.endswith(.pyc) and os.path.getsize(f) 16: with open(f, rb) as fp: data fp.read() # 提取真实字节码跳过12字节PyInstaller头 real_pyc data[12:] # 构建标准头魔数(4)时间戳(4)大小(4)0(4) magic b\x33\xf3\x0d\x0a # Python 3.10魔数 timestamp struct.pack(I, int(os.path.getmtime(f))) size struct.pack(I, len(real_pyc)) header magic timestamp size b\x00\x00\x00\x00 with open(f, wb) as fp: fp.write(header real_pyc)运行此脚本后所有.pyc文件即可被uncompyle6正常处理。场景3解包目录中缺少__main__.pyc但程序能正常运行根因PyInstaller将入口模块编译为pyimod01_main且未在toc中记录原始路径导致无法还原。修复利用pyinstaller源码中的archive_viewer.py工具位于site-packages/PyInstaller/utils/cliutils/python -m PyInstaller.utils.cliutils.archive_viewer hello_world.exe交互式界面中输入open pyz再输入extract pyimod01_main即可导出未重命名的.pyc文件。这是官方调试工具兼容所有PyInstaller版本。4. uncompyle6反编译从字节码到可读Python源码的精准重建4.1 uncompyle6的工作原理与局限性uncompyle6不是“反编译器”而是“字节码反汇编器语义重建器”。它首先将.pyc文件解析为抽象语法树AST节点再根据Python语法规范将节点转换为源码。这个过程高度依赖字节码的完整性与Python版本匹配度。其核心限制有三点限制一无法重建注释与空白符字节码中不保存注释、空行、缩进风格空格/Tab因此反编译结果必然是“无注释、紧凑格式”的代码。例如原始代码def calculate(a, b): 计算a与b的和 # 这是注释 return a b反编译后变为def calculate(a, b): return a b应对策略结合strings命令从.exe中提取残留字符串strings app.exe | grep -E (def|class|import)可定位函数名、类名、导入模块辅助还原逻辑结构。限制二动态代码生成失效若原始代码使用exec()、eval()或compile()动态执行字符串这些字符串在字节码中被编译为独立代码对象uncompyle6只能还原为code object ...占位符。例如code print(hello) exec(code)反编译后显示为exec(code object module at 0x...)。应对策略在pyinstxtractor解包出的base_library.zip中搜索exec、eval调用点定位到对应.pyc文件再用dis模块反汇编字节码手动提取字符串常量import dis with open(target.pyc, rb) as f: f.read(16) # 跳过头 code marshal.load(f) # 加载code object dis.dis(code) # 查看字节码找LOAD_CONST指令限制三闭包与装饰器信息丢失字节码中闭包变量co_freevars和装饰器decorator的元数据不完整uncompyle6可能将装饰器还原为普通函数调用。例如log_time def api_call(): pass可能变成def api_call(): pass api_call log_time(api_call)应对策略检查toc文件中是否有log_time.pyc等装饰器模块优先反编译该模块再结合业务逻辑推断装饰器用途。4.2 版本匹配、参数调优与输出质量控制uncompyle6的版本必须与.pyc魔数严格对应否则直接报错。安装命令必须精确# 根据魔数0x36F3Python 3.10安装 pip install uncompyle63.10.0 # 验证版本 uncompyle6 --version # 输出应为3.10.0关键参数详解-o dir指定输出目录避免覆盖原文件--no-docstrings跳过文档字符串因字节码中不保存此参数实际无效但可减少警告--flow启用控制流分析对复杂条件语句如嵌套if-elif-else还原更准确--nopyc强制不生成.pyc文件默认行为仅输出.py--verify对反编译结果进行语法验证若报错则说明字节码损坏或版本不匹配。输出质量优化技巧分批处理不要一次性反编译整个目录。先处理pyimod01_main主模块确认逻辑正确后再处理依赖模块语法校验用python -m py_compile output.py检查反编译结果是否可被Python解释器加载若报错说明uncompyle6重建失败需换用decompyle3对Python 3.7-3.9更友好或pycdcC实现速度更快人工润色对uncompyle6输出的lambda表达式、长列表推导式进行格式化添加空行分隔逻辑块提高可读性。我处理过一个金融风控模型risk_engine.exeuncompyle6反编译后主文件有2300行但其中17处code object ...占位符。通过dis模块分析发现这些占位符对应7个动态SQL查询字符串。我用strings risk_engine.exe | grep -E SELECT|INSERT|UPDATE | sort -u提取出全部SQL再按函数名如get_user_risk一一对应最终补全了所有动态查询逻辑。这个过程耗时2小时但比重新开发节省了3周。4.3 常见报错深度解析与修复路径报错1SyntaxError: invalid syntax典型场景反编译pyimod02_imports.pyc时报错位置在from . import utils这一行。根因相对导入.在字节码中存储为绝对路径uncompyle6无法正确还原相对层级。修复手动修改反编译结果将from . import utils改为from utils import *或根据toc文件中utils.pyc的路径确定其所在包写成from package.utils import *。报错2ValueError: bad marshal data (unknown type code)根因.pyc文件被截断或损坏常见于UPX脱壳不彻底或pyinstxtractor读取PKG段时偏移错误。修复用hexdump -C app.exe | grep 50 4B 47 00定位PKG起始再用dd命令手动提取# 假设PKG起始于0x1A2F00 dd ifapp.exe ofpkg_data.bin bs1 skip1715968 count5000000 # 用pyinstxtractor的extract_pkg.py工具解析pkg_data.bin python extract_pkg.py pkg_data.bin报错3AttributeError: Code object has no attribute co_posonlyargcount根因uncompyle6版本过低不支持Python 3.8新增的posonlyargcount属性用于/分隔的位置参数。修复升级到uncompyle63.8.0或降级到decompyle33.3.4对3.8兼容性更好。经验总结当uncompyle6报错时不要反复重试。先用file命令确认.pyc文件是否完整file pyimod01_main应输出python 3.x byte-compiled再用python -c import marshal; marshal.loads(open(pyimod01_main,rb).read()[16:])测试字节码可加载性。若marshal.loads报错则字节码已损坏需回溯到pyinstxtractor步骤检查PKG提取逻辑。5. 终极兜底方案当静态分析失效时的内存dump实战5.1 为什么内存dump是最后的可靠手段前述所有方法都基于静态分析——读取.exe文件的二进制结构。但当遇到以下情况时静态分析必然失败.exe被强壳保护如VMProtect、Themida资源段加密且动态解密PyInstaller启用了--key参数AES加密PKG而你不知道密钥程序在运行时动态下载并执行额外字节码如从网络加载payload.pycpyinstxtractor和uncompyle6均报错且无法定位到有效PKG魔数。此时唯一可行的方案是让程序运行起来在内存中捕获Python解释器加载的原始字节码。因为无论外壳多强只要Python解释器在内存中执行.pyc字节码就必须以明文形式存在于进程内存中否则无法执行。这正是Process Hacker、CFF Explorer等工具的用武之地。5.2 内存dump完整流程从进程启动到字节码提取步骤1启动目标进程并挂起避免字节码被快速释放需在Python解释器初始化后、业务代码执行前捕获内存。使用Process Hacker 2免费开源运行hello_world.exe在Process Hacker中找到hello_world.exe进程右键→Suspend挂起此时进程暂停内存状态冻结。步骤2定位Python解释器内存区域Python字节码通常加载在python3x.dll或python3x_d.dll的内存空间中。在Process Hacker中右键进程→Properties→Memory标签页筛选Module列找到python310.dll根据魔数确定记录其基址Base Address如0x7FFB12340000。步骤3搜索字节码特征并dumpPython字节码在内存中有固定特征开头4字节为魔数如0x33F30D0A后跟4字节时间戳通常为0或小数值再后4字节为文件大小大于0x100。在Process Hacker中Memory标签页→Search→Hex Values输入33 F3 0D 0APython 3.7魔数勾选All Memory Regions搜索结果中找到Size列值最大的几块内存通常1MB右键→Dump Memory→保存为dump_001.bin。步骤4从dump文件中提取有效.pyc用pyinstxtractor的extract_pyc.py脚本社区版处理dump文件python extract_pyc.py dump_001.bin该脚本会扫描整个dump文件查找所有符合.pyc格式的连续块并按魔数分类保存。我实测对hello_world.exe的dump成功提取出12个.pyc文件包括被pyinstxtractor遗漏的config_loader.pyc。5.3 内存dump的注意事项与风险规避时机至关重要挂起进程太早解释器未加载搜索不到python3x.dll挂起太晚业务代码已执行完毕字节码可能被GC回收。最佳时机是程序窗口刚弹出、但尚未响应用户输入时内存范围要广不要只dumppython3x.dll模块要dump整个进程内存All Memory Regions因为PyInstaller可能将字节码加载到自定义堆区法律与合规提醒内存dump仅限于你拥有合法授权的软件如自己开发的程序、公司内部工具。对第三方商业软件执行此操作可能违反EULA务必确认授权范围防病毒误报Process Hacker等工具常被杀软标记为“潜在风险”需临时关闭实时防护或添加信任。我曾用此法恢复一个被UPXASPack双重加壳的data_analyzer.exe。静态分析完全失效但内存dump后extract_pyc.py从2GB dump文件中找到了37个.pyc反编译出全部数据清洗逻辑。整个过程耗时18分钟比联系原作者等待回复快了3天。6. 从源码还原到工程复原如何让反编译代码真正可用6.1 依赖关系重建从import语句到requirements.txtuncompyle6输出的.py文件中import语句是完整的但无法区分哪些是标准库、哪些是第三方包、哪些是本地模块。重建依赖需三步第一步提取所有import用正则提取所有import和from ... import语句grep -r import\|from.*import output_dir/ | sed -E s/.*import[[:space:]]([^[:space:];]).*/\1/ | sort -u输出如pandas,numpy,requests,utils,models。第二步分类判定标准库os,sys,json,re等无需安装第三方包不在标准库列表中且pip show pkg返回信息则为第三方本地模块utils,models等对应output_dir/utils.py、output_dir/models.py需保留在项目中。第三步生成requirements.txt对第三方包用pipreqs自动生成pip install pipreqs pipreqs output_dir/ --force --encodingutf8pipreqs会分析import语句查询PyPI获取最新兼容版本生成requirements.txt。我处理web_scraper.exe时pipreqs识别出requests2.28.1,beautifulsoup44.11.1但遗漏了lxml因代码中用from lxml import etreepipreqs未识别lxml为包名。此时需手动补充echo lxml4.9.2 requirements.txt。6.2 项目结构还原从零散.py文件到可运行工程pyinstxtractor解包出的文件是扁平化的需根据toc文件和import关系重建目录结构。以toc中[main, C:\\project\\src\\main.py, 2, 1234, 5678]为例C:\\project\\src\\main.py→ 目录结构应为src/main.py若有[utils, C:\\project\\src\\utils\\helpers.py, 2, 901, 2345]→ 创建src/utils/helpers.pyimport语句中from src.utils import helpers→ 说明src是包需在src/__init__.py中添加from .utils import helpers。自动化脚本rebuild_project.pyimport json import os from pathlib import Path # 读取toc文件 with open(toc, r) as f: toc_lines f.readlines() for line in toc_lines: if not line.strip() or line.startswith(#): continue try: entry json.loads(line.strip()) if len(entry) 2 and entry[1].endswith(.py): # 原始路径 src_path Path(entry[1].replace(C:\\, ).replace(\\, /)) dst_path Path(recovered) / src_path dst_path.parent.mkdir(parentsTrue, exist_okTrue) # 复制对应的.pyc文件并反编译 pyc_name entry[0] if os.path.exists(f{pyc_name}.pyc): os.system(funcompyle6 {pyc_name}.pyc -o {dst_path.with_suffix(.py)}) except Exception as e: print(fError processing {line}: {e})运行后recovered/目录即为结构完整的项目。6.3 功能验证与代码修复让反编译代码真正跑起来反编译代码≠可用代码。必须经过三轮验证第一轮语法验证find recovered/ -name *.py -exec python -m py_compile {} \;报错即说明uncompyle6重建失败需手动修复如补全if语句的else分支、修正lambda参数。

相关新闻