Python逆向工程实战:从打包EXE到源码还原的完整技术栈
最近在分析一个用Python编写的2048游戏时,我遇到了一个典型场景:手头只有一个打包好的EXE文件,但需要了解其内部逻辑。这种需求在安全分析、代码审计甚至日常开发中都很常见。今天我就来系统梳理一下Python逆向工程的完整技术栈,从EXE解包到源码还原,每个环节都有实际可操作的技术细节。
1. 理解Python打包与反编译的基本原理
Python程序打包成EXE的过程,本质上是一个将解释器、依赖库和源代码字节码打包成单一文件的过程。常见的打包工具如PyInstaller、Py2exe、cx_Freeze等,它们的工作原理大同小异:将Python脚本编译成.pyc字节码文件,然后与Python解释器一起打包。
字节码(Bytecode)是Python源代码编译后的中间表示形式,存储在.pyc文件中。每个.pyc文件都包含一个魔数(Magic Number),用于标识生成该字节码的Python版本。这个魔数对于后续的反编译至关重要,因为不同Python版本的字节码格式可能有细微差异。
注意:Python 3.2之后,.pyc文件的头部结构发生了变化,增加了时间戳和文件大小信息。了解这些细节有助于我们在修复损坏的.pyc文件时做出正确判断。
2. PyInstaller打包文件的解包实战
PyInstaller是目前最流行的Python打包工具之一,它生成的EXE文件结构相对规整,便于逆向分析。解包过程主要依赖pyinstxtractor.py脚本,这个工具能够解析PyInstaller的打包格式,提取出其中的资源文件、依赖库和最重要的.pyc字节码文件。
2.1 pyinstxtractor.py的工作原理剖析
pyinstxtractor.py的核心逻辑是解析PyInstaller的CArchive结构。每个PyInstaller打包的EXE文件末尾都有一个特殊的归档结构,包含了所有打包文件的索引和内容。脚本通过以下步骤完成解包:
实际操作时,我们首先需要获取pyinstxtractor.py脚本。可以从GitHub的python-exe-unpacker仓库下载最新版本:
# 下载pyinstxtractor.py
wget https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
# 或者使用pyinstxtractor-ng(新版)
wget https://github.com/pyinstxtractor/pyinstxtractor-ng/releases/latest/download/pyinstxtractor-ng.py
2.2 实际解包操作步骤
假设我们有一个名为game2048.exe的文件,解包过程如下:
# 使用Python运行解包脚本
python pyinstxtractor.py game2048.exe
# 或者使用新版
python pyinstxtractor-ng.py game2048.exe
执行成功后,会在当前目录生成game2048.exe_extracted文件夹。进入这个文件夹,你会看到类似这样的结构:
game2048.exe_extracted/
├── PYZ-00.pyz_extracted/ # 依赖库的字节码文件
├── struct # 结构信息文件
├── game2048 # 主程序的字节码文件(无后缀)
├── pyiboot01_bootstrap.pyc
└── pyimod01_os_path.pyc
这里的关键文件是game2048(主程序)和struct(包含头部信息)。注意,解包出来的主程序文件通常没有.pyc后缀,需要手动添加。
3. .pyc文件的修复与头部重建
从PyInstaller提取出的.pyc文件通常缺少完整的头部信息,这是因为PyInstaller为了优化体积会移除部分冗余数据。我们需要从其他来源获取正确的头部信息,然后修复目标文件。
3.1 理解.pyc文件结构
一个完整的.pyc文件包含以下部分:
| 0-3 | 4字节 | 魔数(Magic Number) | 标识Python版本 |
| 4-7 | 4字节 | 时间戳(Timestamp) | Python 3.2+需要 |
| 8-11 | 4字节 | 文件大小(File Size) | Python 3.3+需要 |
| 12+ | 可变 | 字节码数据 | 实际代码内容 |
魔数是修复过程中最关键的部分。每个Python小版本(如3.8.0、3.8.1)都有特定的魔数。如果魔数不匹配,反编译工具会报错\”Unsupported Python version\”。
3.2 使用WinHex进行二进制修复
WinHex是Windows下功能强大的十六进制编辑器,我们可以用它来手动修复.pyc文件头部。操作步骤如下:
更简单的方法是通过Python脚本自动完成这个过程:
def fix_pyc_header(pyc_path, struct_path):
\”\”\”修复.pyc文件头部\”\”\”
with open(struct_path, \’rb\’) as f:
struct_data = f.read()
# 获取struct文件的前16字节作为头部
header = struct_data[:16]
with open(pyc_path, \’rb\’) as f:
pyc_data = f.read()
# 检查是否已经有头部(通过魔数判断)
if pyc_data[:4] in [b\’\\x42\\x0d\\x0d\\x0a\’, b\’\\x16\\x0d\\x0d\\x0a\’, b\’\\xee\\x0c\\x0d\\x0a\’]:
print(\”文件已有完整头部,无需修复\”)
return
# 写入修复后的文件
with open(pyc_path + \’.fixed\’, \’wb\’) as f:
f.write(header + pyc_data)
print(f\”已修复文件保存为: {pyc_path}.fixed\”)
# 使用示例
fix_pyc_header(\’game2048.pyc\’, \’struct\’)
3.3 获取正确魔数的其他方法
如果struct文件不可用,还有其他几种获取正确魔数的方法:
这里提供一个Python 3.7-3.11常见版本的魔数参考:
MAGIC_NUMBERS = {
# Python版本: (魔数十六进制,

