Python逆向工程实战:从PyInstaller打包的exe还原源码全流程指南
当你接手一个遗留项目或需要分析某个Python应用时,经常会遇到只有打包后的exe文件的情况。本文将带你深入探索如何系统性地解构PyInstaller打包的可执行文件,逐步还原出原始Python代码。不同于基础教程,我们会重点关注版本差异处理、加密文件解密等高级实战技巧。
1. 逆向工程前的准备工作
逆向PyInstaller打包的程序需要一套专门的工具链。以下是核心工具清单及其作用:
- Detect It Easy (DIE):用于快速识别文件类型和打包方式
- pyinstxtractor:解包PyInstaller生成的exe文件
- uncompyle6:将pyc字节码反编译为Python源代码
- 010 Editor:十六进制编辑器,用于分析文件头信息
建议环境配置:
pip install uncompyle6==3.8.0 git clone https://github.com/extremecoders-re/pyinstxtractor.git版本匹配原则:
- 解包使用的Python版本必须与打包时一致
- uncompyle6需要匹配Python字节码版本
- 加密文件解密方式随PyInstaller版本变化
注意:实际操作前请确认你有权分析目标文件,避免法律风险
2. 解包PyInstaller生成的exe文件
使用pyinstxtractor进行解包的基本命令很简单:
python pyinstxtractor.py target.exe但实际操作中会遇到几个关键问题:
2.1 解包后的文件结构分析
成功解包后会生成target.exe_extracted目录,包含以下重要文件:
| 文件类型 | 说明 | 处理方式 |
|---|---|---|
| main.pyc | 主程序字节码 | 直接反编译 |
| PYZ-00.pyz | 依赖库集合 | 需进一步解压 |
| pyimod*.pyc | PyInstaller模块 | 可能含加密密钥 |
2.2 版本兼容性问题处理
不同PyInstaller版本打包的文件结构差异:
4.0之前版本:
- 使用PyCrypto进行CFB模式加密
- 依赖库存储在PYZ-00.pyz中
4.0及之后版本:
- 采用tinyaes进行CTR模式加密
- 可能出现pyc.encrypted扩展名
识别版本技巧:
# 检查pyimod01_archive.pyc是否引用tinyaes uncompyle6 pyimod01_archive.pyc | grep tinyaes3. 处理加密的字节码文件
当发现PYZ-00.pyz_extracted目录下存在.pyc.encrypted文件时,需要先解密才能反编译。
3.1 获取加密密钥
密钥通常存储在pyimod00_crypto_key.pyc中:
uncompyle6 -o key.py pyimod00_crypto_key.pyc3.2 解密脚本选择
根据PyInstaller版本选择对应的解密方案:
PyInstaller < 4.0 解密脚本:
from Crypto.Cipher import AES import zlib def decrypt_legacy(input_path, output_path, key): with open(input_path, 'rb') as inf, open(output_path, 'wb') as outf: iv = inf.read(16) cipher = AES.new(key, AES.MODE_CFB, iv) plaintext = zlib.decompress(cipher.decrypt(inf.read())) outf.write(plaintext)PyInstaller ≥ 4.0 解密脚本:
import tinyaes import zlib def decrypt_new(input_path, output_path, key): with open(input_path, 'rb') as inf, open(output_path, 'wb') as outf: iv = inf.read(16) cipher = tinyaes.AES(key, iv) plaintext = zlib.decompress(cipher.CTR_xcrypt_buffer(inf.read())) outf.write(plaintext)关键点:解密后需要手动添加正确的pyc文件头才能被uncompyle6识别
4. 反编译字节码为源代码
获得标准pyc文件后,使用uncompyle6进行反编译:
uncompyle6 -o output.py input.pyc4.1 常见反编译问题解决
Magic Number不匹配:
- 症状:
ValueError: Bad magic number in... - 解决:使用正确版本的Python头信息
- 症状:
混淆代码处理:
- 症状:反编译后出现乱码或异常控制流
- 应对:尝试使用
decompyle3等替代工具
依赖缺失导致失败:
- 症状:
ImportErrorduring decompilation - 解决:确保原环境依赖已安装
- 症状:
4.2 批量反编译技巧
对于大量pyc文件,可以使用脚本批量处理:
import os from pathlib import Path def batch_decompile(root_dir): for pyc in Path(root_dir).rglob('*.pyc'): try: os.system(f'uncompyle6 -o {pyc.with_suffix(".py")} {pyc}') except Exception as e: print(f"Failed on {pyc}: {str(e)}")5. 高级技巧与疑难问题处理
5.1 Python版本头修复
当pyc文件头损坏时,需要手动修复。各版本Python的头信息:
| Python版本 | 魔数前缀 |
|---|---|
| 3.7 | \x42\x0d\x0d\x0a |
| 3.8 | \x55\x0d\x0d\x0a |
| 3.9 | \x61\x0d\x0d\x0a |
| 3.10 | \x6f\x0d\x0d\x0a |
修复示例:
def fix_pyc_header(pyc_path, version): headers = { '3.8': b'\x55\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0' } with open(pyc_path, 'rb+') as f: original = f.read() f.seek(0) f.write(headers[version] + original[16:])5.2 依赖库路径修复
解包后的依赖可能路径混乱,需要调整:
- 检查
PYZ-00.pyz_extracted内容 - 将需要的库复制到项目目录
- 修改导入语句匹配新路径
5.3 反编译失败后的备选方案
当标准工具失效时,可以尝试:
- pycdc:支持更多Python版本的字节码
- 手动分析dis模块输出:
import dis, marshal with open('file.pyc', 'rb') as f: f.seek(16) # Skip header dis.dis(marshal.load(f)) - 商业工具:如Decompyle++等
在实际项目中,我遇到过PyInstaller 5.7打包的程序,其加密方式又有变化。这时需要分析pyimod00_crypto_key.pyc的具体实现来调整解密逻辑。记住逆向工程是不断试错的过程,保持耐心是关键。