PyInstaller打包exe逆向还原:pyinstxtractor提取与pyc反编译实战
2026/9/17 17:38:02 网站建设 项目流程

简介:PyInstaller Extractor 是一款面向逆向工程与安全分析人员的 Python 脚本工具,用于解析 PyInstaller 打包生成的 Windows 可执行文件,提取其中的 pyc 字节码,并自动修复文件头,方便后续用 Python 反编译器还原源码。它兼容 Python 2.x/3.x,覆盖 PyInstaller 2.0 至 4.2 多个版本,是分析 Python 程序打包结构、开展恶意代码审计或学习打包原理的实用利器。资源包共 3 个文件,包括主脚本 pyinstxtractor.py、使用说明 README 和开源协议 License,压缩包仅 18KB,轻量且开箱即用。目前已有 820 人学习下载,适合具备一定 Python 和逆向基础的中高级开发者。借助该脚本,读者可以快速了解 PyInstaller 内部格式、pyc 文件提取与修复流程,并可直接在命令行运行脚本开展实际分析,为后续反编译和代码审查提供清晰起点。 拿到一个exe,DIE打开一看,PE区段没几个,文件末尾却挂了一大段数据,第一反应就是PyInstaller。这些年无论是做恶意样本分析、CTF逆向,还是排查自己打包出去的软件在客户机器上报错,都绕不开同一个需求:如何把PyInstaller打包出来的可执行文件还原成Python字节码和依赖文件。市面上关于这个主题的搜索结果,十有八九会指向同一个工具——pyinstxtractor。

这工具名字很直白:PyInstaller extractor,也就是PyInstaller提取器。它负责做的是"提取",不负责"反编译",这俩步骤经常被混在一起,导致很多新手拿到工具后不知道下一步该干什么。这篇文章我会把PyInstaller的打包结构、pyinstxtractor的运行原理、提取后的文件处理、反编译工具链选型,以及不同版本之间的坑全部过一遍,把我这几年实际踩过的经验直接摆出来,省得你再走弯路。

1. 先拆开PyInstaller的壳:CArchive和overlay到底是怎么回事

1.1 PyInstaller可执行文件的真实组成结构

先说个反直觉的事实:PyInstaller打出来的exe,本质上不是"一个程序",而是"引导程序+压缩归档数据包"拼接起来的一个文件。

打包过程大致分三步。第一步,PyInstaller把你写的Python脚本编译成字节码,也就是pyc;第二步,分析代码里import了哪些模块、用了哪些动态库,把这些依赖全部收集起来;第三步,用你自己选的bootloader(PyInstaller内置的一个C语言编写的Python启动器)做外壳,把pyc和依赖文件压成一个归档数据段,拼接到bootloader的尾部。

所以最终产物的大小往往远大于你脚本本身,原因就在这里——它内置了一个完整的Python解释器运行环境相关文件,包括Python动态库、用到的第三方包、各种资源文件。

关键在于这个"归档数据段"的格式。PyInstaller把它称为CArchive,是PyInstaller自己定义的二进制归档格式,不是zip,也不是tar。它内部有一个条目表,每个条目记录着文件名、在归档内的偏移、原始长度、压缩后长度、压缩标志等信息。数据段内部用zlib做压缩,但外层没有zip那种标准头部和中央目录结构,所以用普通的解压软件打开根本没用。

1.2 pyinstxtractor定位归档数据的核心机制

Bootloader运行时需要知道属于它的归档数据从哪里开始、到哪里结束,这些信息就存放在文件末尾的一小块cookie结构里。

这块cookie记录了三个关键信息:一个用于标识Python/PyInstaller版本的magic值、CArchive在文件中的偏移位置、CArchive的长度。pyinstxtractor的思路很直接:先读取文件末尾的cookie,拿到偏移和长度,然后按CArchive的条目表格式逐条解析,把每个文件切出来写入磁盘。

文件末尾这个cookie很脆弱,如果exe被二次处理过——比如用UPX加过壳、用签名工具重新签名时改变文件尾部——cookie会被破坏,pyinstxtractor就会报"Input file is not a valid PyInstaller archive"之类的错误。这种情况下要先去壳、修正文件尾部,再谈提取。

1.3 为什么常规解压和Binwalk都搞不定

很多人拿到PyInstaller打包的程序,第一反应是扔给Binwalk或7-Zip,结果发现要么识别不出有效文件系统,要么散落出一堆无意义的碎块。

原因在于CArchive的条目表是顺序排列的,条目里的偏移值是相对于归档起始位置的绝对偏移,而且数据是压缩和不压缩混合存储的,单靠特征码扫描难以还原完整文件边界。pyinstxtractor之所以能通用,是因为它严格按照CArchive头部的解析逻辑来读取,先拿到条目总数,再逐条读取条目信息,根据条目里的偏移和长度精确切分数据,最后按条目名写入文件。从文件里恢复出一个个完整的独立文件,这一步才是整个分析链路的地基。

2. 跑通提取:环境选型与典型输出结构

2.1 工具获取和运行环境

pyinstxtractor本身是一个Python脚本,新版存储在GitHub上,下载后直接运行即可,没有复杂的安装依赖。需要强调的是,这个脚本最好用Python 3环境跑,因为代码本身就兼容Python 2和Python 3的写法会因版本不同有所差异,实际测试下来在Python 3.8或更高的版本上运行更稳定。

如果你只是临时用一下,直接执行:

python pyinstxtractor.py demo.exe

脚本会自动生成一个demo.exe_extracted目录,所有提取结果都在里面。

新版pyinstxtractor还支持把输出目录作为第二个参数传进去。比如你想把结果放到指定路径:

python pyinstxtractor.py demo.exe ./output_dir

这么做的好处是可以把多个样本的提取结果分类存放,避免默认目录名撞车。

2.2 提取成功的判断标准

运行结束后,注意看两处。一处是屏幕输出有没有出现类似这样的信息:

[*] Successfully extracted PyInstaller archive: demo.exe [+] Python version: 3.10

另一处是生成的目录里是否有与主程序同名的pyc文件(比如demo.pycdemo.exe.pyc),以及是否存在PYZ.pyz文件。这两个文件如果都在,基本说明提取成功了。

如果只看到一堆dll和pkg文件,但没有主程序pyc,通常有两种情况:一是入口脚本名特殊,二是在某些老版本PyInstaller中,主程序条目的命名与可执行文件名不一致,需要去条目列表里找。

2.3 提取结果目录逐项说明

我第一次跑通pyinstxtractor时,面对一堆文件其实有点懵。这里把常见输出项的作用整理一下:

文件/目录内容说明
demo.pyc主程序入口的字节码,后续反编译的核心对象
PYZ.pyzPyInstaller动态模块库,内含所有通过import引入的第三方模块的code对象
struct/一个专门的目录,里面放的是struct模块的pyc,用于比对当前Python版本的pyc头部格式
*.pkgPyInstaller以资源形式打包的数据文件,可能是图片、配置、模型文件等
*.dll/*.so程序运行时需要的二进制动态库
pyinstxtractor.py工具自证,用于验证当前环境的提取效果

这里特别说下struct目录。很多教程不提它,但它很关键——后续修复pyc文件头时需要从struct/struct.pyc里截取合法的pyc头部。换句话说,这个目录相当于给你提供了一份当前Python版本的标准pyc头参考样本。

2.4 一个容易忽略的版本信号

提取时输出的Python版本号,决定了你后面选择哪条反编译路线。Python 3.8和Python 3.10的pyc格式和反编译工具支持情况差别很大,如果忽略了这个版本信号,直接拿uncompyle6去解3.10的pyc,大概率会报错退出。这个我在后面的反编译章节会展开。

3. 提取之后才是重头戏:pyc修复、PYZ解包与反编译工具链

3.1 第一关:pyc文件头修复

pyinstxtractor提取出来的主程序pyc,并不一定能直接扔给反编译器。不同版本的PyInstaller在打包时对pyc头部的处理逻辑不同。有的版本提取后是完整的带头部pyc,有的版本则是把pyc头部信息单独抽出来了,你拿到的是一个"裸"的pyc——头部缺失或只有部分数据。

反编译器对pyc头部很敏感,头部缺失会直接报"Bad magic number"或"Not a valid pyc file"。

修复方法很简单:从struct/struct.pyc里读取合法的头部,拼到缺失头部的pyc前面。由于Python 3.7之后的pyc头部固定为16字节(4字节magic + 4字节flags + 4字节mtime + 4字节source size),Python 3.6及之前是12字节,所以修复时先确认版本再说。

我常用的修复脚本大概是这样的:

import sys # 从struct目录取合法的pyc头部 with open('struct/struct.pyc', 'rb') as f: header = f.read(16) # Python 3.7+ 用16字节,老版本用12字节 # 读取待修复的pyc with open(sys.argv[1], 'rb') as f: data = f.read() # 如果数据里已经包含了部分头部,需要根据实际情况截断 if not data[:4] == header[:4]: # 需要修复的情况 fixed = header + data else: print('文件头已存在,无需修复') fixed = data with open(sys.argv[1] + '_fixed.pyc', 'wb') as f: f.write(fixed)

实际上操作时,还可以用010 Editor或HxD这样的十六进制编辑器手动修,但脚本批量处理更方便,尤其是要同时修复几十个pyc时。

3.2 拆开PYZ.pyz这个"模块仓库"

主程序pyc处理完后,你会看到很多import语句,但对应的第三方模块代码并不在主程序pyc里,而是躺在PYZ.pyz里。

PYZ.pyz内部的结构和CArchive类似,也有一张条目表,每个条目对应一个模块,条目里存的是marshal序列化后的code对象。pyinstxtractor只把整个PYZ当作一个文件提取出来了,并不会帮你拆成一个个独立的pyc。如果你想查看某个第三方库的逻辑,就需要自己解析。

解析方法可以自己写:读取PYZ的索引,依次marshal.loads每个code对象,再写成pyc文件。不过社区里已经有现成脚本,而且在反编译时,pycdc这类工具本身能够直接处理PYZ条目,所以很多时候你不必提前拆开PYZ。

但有一个场景必须拆PYZ:当你发现主程序逻辑依赖了某个关键处理函数,而这个函数在第三方库里,你需要单独看这个库的实现时,就需要把对应模块的code对象单独导出来。

3.3 反编译工具链的选型表

pyinstxtractor只提取不反编译,所以接下来的工具选型是关键。根据目标Python版本的差异,我的建议表格如下:

目标Python版本推荐工具说明
Python 2.xuncompyle6 / decompyle2uncompyle6对Python 2支持比较完整
Python 3.7 - 3.8decompyle3 / uncompyle6uncompyle6在3.8上有一定成功率和部分低级错误
Python 3.9+pycdc / pycdasuncompyle6基本没法用了,pycdc是主力
Python 3.12+pycdc的维护分支需要找较新的fork版本

pycdc和pycdas是C++写的工具。pycdas会输出字节码级的反汇编结果,pycdc则尽力还原成Python源码。两者结合看效果更好:pycdc输出内容缺失或明显不对时,用pycdas看字节码人工还原逻辑。

提示:不同反编译工具对同一份pyc的反编译结果会有细微差异,不要死守一个工具。多试几个版本,哪个输出更完整就用哪个,这是非常正常的流程。

3.4 反编译结果不完美的心理预期

需要提前做好心理准备的是,pyc反编译的结果永远不可能和原始源代码完全一致。变量名通常会丢失(除非保留在co_names里),注释和文档字符串部分保留,控制流在某些优化场景下会变形。

但真正有用的信息——函数调用关系、字符串使用、关键运算逻辑——通常都能还原出来。做安全分析时,拿到这些信息已经足够判断样本行为;做自己程序的故障恢复时,结合反编译结果和堆栈日志,也能很快定位问题点。

4. 版本暗坑与增强版工具:为什么老脚本在PyInstaller 6.x上会翻车

4.1 PyInstaller 6.x修改了对pyc头部的处理

PyInstaller从6.x开始,对pyc文件头的处理逻辑有了明显变化。老版本的pyinstxtractor脚本提取出来的pyc经常出现文件头错乱、识别不了Python版本的情况。原因之一是新版PyInstaller调整了打包时写入的pyc元数据,另一个原因是部分打包项目使用了一键打包工具,对PyInstaller生成的中间文件做了额外加工。

如果你用官方原版pyinstxtractor提取6.x打包的文件失败,不要第一时间怀疑文件不是PyInstaller打包的,而是检查一下pyinstxtractor的更新情况,或者直接换用增强版本。

4.2 pyinstxtractor-ng和_kyz分支的选择

社区目前维护比较活跃的增强版有两个方向。

pyinstxtractor-ng是对原版脚本的重写,底层解析逻辑更清晰,支持Python 3.9+的解析和更多边界情况的处理,对Windows路径和长文件名的兼容也更好。另一个是pyinstxtractor_kyz,在原版基础上扩展了对入口脚本名的识别,并且对部分缺失cookie的情况有更好的容错。

实际使用时,我通常的做法是:先用官方原版,报错或结果异常时再换ng版。ng版的命令行参数更丰富,有些场景下甚至无需指定输出路径就能自动推断。

pyinstxtractor-ng demo.exe

4.3 Cython扩展和PyArmor加密时的边界

pyinstxtractor不是万能的。它提取的是PyInstaller归档内的数据,但归档内如果是Cython编译出的pyd/so扩展,那你拿到的是编译后的机器码,不是Python字节码,常规反编译工具无能为力,只能通过分析导出函数和字符串来推断行为。

如果项目用了PyArmor这类的代码加密方案,pyinstxtractor提取出的pyc的code对象是经过混淆或加密的,直接反编译大概率得到一堆乱代码或直接失败。判断是否走了加密,可以看提取出来的pyc文件是否明显大于正常大小,或者用pycdas反汇编时看到大量无意义的LOAD_CONST序列。

遇到这两种情况,已经超出了pyinstxtractor的能力范围,需要转向动态调试或更底层的分析手段,而不是在反编译工具上死磕。

4.4 跨平台提取的细节差异

pyinstxtractor既可以在Windows上跑,也可以在Linux或macOS上跑,用来提取Windows的exe一般也没问题。但跨平台时有几个点要注意:

  • 文件名分隔符:CArchive条目里记录的文件路径用的是打包目标系统的分隔符,跨平台处理时脚本会做转换,偶尔会出现路径拼接异常,ng版处理得更好。
  • 大小写和扩展名:Windows下不区分大小写,但Linux下区分,如果提取后的文件名有重复冲突,会直接覆盖掉同名文件。
  • 提取后的dll和so:在Linux上提取Windows的exe得到的dll是PE格式,不能用本机工具直接分析,需要单独处理。

5. 一次完整提取复盘:从exe到可读代码的全过程

5.1 识别、提取、反编译三步走的实际情况

讲这么多理论,不如直接还原一次真实分析过程。

前阵子我拿到一个约20MB的exe样本,先用DIE查看,区段很少,文件末尾有巨大overlay,初步判断是PyInstaller。然后执行:

python pyinstxtractor.py sample.exe

输出显示Python版本为3.10,成功生成sample.exe_extracted目录。目录里找到了sample.pycPYZ.pyz

接下来按版本选工具,3.10的pyc用uncompyle6是不可能了,直接用pycdc:

pycdc sample.pyc > sample_decompiled.py

pycdc输出了一份可读的Python脚本。虽然代码中部分函数的分支结构还原不到位,但关键字符串、网络请求地址和加密逻辑都完整暴露了出来,配合PYZ里找到的自定义加解密模块,整个样本的行为链就清晰了。

5.2 我踩过的三个坑

第一个坑:一上来拿notepad打开pyc,全是乱码,以为文件坏了。其实pyc本来就是二进制字节码文件,必须先反编译再查看。

第二个坑:补文件头时补错了magic。有一次样本明确是Python 3.9打包,我从另一个Python 3.8环境拷了头补进去,结果反编译器一直报"Bad magic number",最后用struct/struct.pyc里的头才解决。所以补头前先确认版本,不要拿本机Python环境的头硬套。

第三个坑:找错主程序pyc。PyInstaller打包后的主程序文件可能叫main.pyc,也可能叫入口名.pyc,有的版本还会带后缀。判断方法是看文件名是否和原始exe名一致,或者看PYZ里有没有对应条目被引用。

5.3 工具的边界和合法使用范围

最后必须说一句——pyinstxtractor只是一个提取工具,不涉及对程序运行过程的修改,也没有绕过加密或授权机制。它能处理的范围限于公开的PyInstaller归档格式,适用场景也应该是你自己的程序、你已获得授权的测试样本,以及用于研究和学习的公开样本。

在使用时,注意遵守样本的授权协议和相关法律法规,不要拿去提取、反编译他人的商业软件或者恶意用途。这是逆向分析这个领域的底线,也是每个从业者该有的自觉。

工具本身不复杂,一条命令就能跑通,难的是对PyInstaller归档格式的理解和对后续反编译工具链的掌握。把前四章的内容消化掉,绝大部分PyInstaller提取需求都能拿下了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询