欢迎光临
我们一直在努力

Python逆向工程指南:从pyc到源码的完整链路解析(以2048游戏为例)

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文件末尾都有一个特殊的归档结构,包含了所有打包文件的索引和内容。脚本通过以下步骤完成解包:

  • 定位归档尾部:从文件末尾开始搜索特定的魔数字节序列(\’MEI\’开头的标识)
  • 解析目录表:读取归档的目录结构,获取每个打包文件的偏移量、大小和压缩状态
  • 提取文件:根据目录信息逐个提取文件,如果是压缩文件则进行解压
  • 处理PYZ归档:PyInstaller会将多个Python模块打包成PYZ格式的归档,需要进一步解包
  • 实际操作时,我们首先需要获取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文件包含以下部分:

    偏移量
    长度
    内容
    Python版本差异
    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文件头部。操作步骤如下:

  • 打开struct文件:在解包目录中找到struct文件,用WinHex打开
  • 复制头部信息:选中前16个字节(Python 3.7+)或12个字节(Python 3.2-3.6)
  • 打开目标.pyc文件:将game2048文件重命名为game2048.pyc后用WinHex打开
  • 插入头部:在文件开头插入复制的字节,保存文件
  • 更简单的方法是通过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文件不可用,还有其他几种获取正确魔数的方法:

  • 从PYZ归档中提取:解包目录中的PYZ-00.pyz_extracted文件夹里有很多.pyc文件,它们的头部通常是正确的
  • 手动生成:用目标Python版本创建一个简单的.pyc文件,提取其头部
  • 查询魔数表:Python官方文档和社区维护了各版本的魔数对应表
  • 这里提供一个Python 3.7-3.11常见版本的魔数参考:

    MAGIC_NUMBERS = {
    # Python版本: (魔数十六进制,

    赞(0)
    未经允许不得转载:171主机测评 » Python逆向工程指南:从pyc到源码的完整链路解析(以2048游戏为例)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址