从黑箱中解放——将 Python 可执行文件(.exe)解密为源代码

前言

  • Python 可执行文件(.exe)反编译为 Python 脚本是一项有趣的技术挑战,可以帮助我们理解程序的工作原理,以及可能包含的逻辑和算法。虽然反编译不是一项简单的任务,并且对于使用各种保护措施的程序可能无效,但对于一般情况下的 Python 可执行文件,我们可以尝试使用一些工具来进行反编译。
  • 下面我们就来学习如何将 Python 可执行文件(.exe)反编译为 Python 脚本。

版本

  • Python 3.9

反编译

  • 反编译是将已编译的程序代码还原为其原始源代码的过程。在 Python 中,由于其解释性质,通常没有像编译语言那样生成的二进制文件,但是我们可以将 Python 脚本转换为字节码文件(.pyc),而 .exe 文件通常是由 pyinstaller、cx_Freeze 等工具编译生成的。

Python 可执行文件(.exe)反编译

  • Python 可执行文件(.exe)反编译为 Python 脚本主要分为两个步骤,(1)从 .exe 文件中提取 pyc 文件 (2)将 pyc 文件转换为 Python 脚本。

打包一个简单的 .exe 可执行文件

python 复制代码
# student.py
class Student:
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

    def get_name(self):
        return self.name

    def get_age(self):
        return self.age

    def get_gender(self):
        return self.gender

    def set_name(self, name):
        self.name = name

    def set_age(self, age):
        self.age = age

    def set_gender(self, gender):
        self.gender = gender

    def display_info(self):
        print("Name:", self.name)
        print("Age:", self.age)
        print("Gender:", self.gender)

# main.py
import time

from student import Student

if __name__ == "__main__":
    # Create a student object
    student1 = Student("Alice", 20, "Female")

    # Display student information
    student1.display_info()

    # Update student information
    student1.set_age(21)
    student1.display_info()

    time.sleep(10)

# 使用 pyinstaller 构建可执行 .exe
pyinstaller --onefile   -p venv/Lib/site-packages .\print-student\main.py

提取 pyc 文件

使用脚本提取

python 复制代码
# 使用 pyi-archive_viewer 查看文件并提取
> pyi-archive_viewer .\main.exe

Options in 'main.exe' (PKG/CArchive):
 pyi-contents-directory _internal
Contents of 'main.exe' (PKG/CArchive):
 position, length, uncompressed_length, is_compressed, typecode, name
 0, 199, 269, 1, 'm', 'struct'
 199, 2008, 3700, 1, 'm', 'pyimod01_archive'
 2207, 7671, 17413, 1, 'm', 'pyimod02_importers'
 9878, 1760, 4029, 1, 'm', 'pyimod03_ctypes'
 11638, 644, 1074, 1, 'm', 'pyimod04_pywin32'
 12282, 603, 851, 1, 's', 'pyiboot01_bootstrap'
 12885, 229, 295, 1, 's', 'main'
......
 4721057, 408332, 1123832, 1, 'b', 'unicodedata.pyd'
 5129389, 702999, 702999, 0, 'z', 'PYZ-00.pyz'
?
U: go up one level
O <name>: open embedded archive with given name // 打开包查看文件
X <name>: extract file with given name // 提取文件
S: list the contents of current archive again
Q: quit
? x main        
Output filename? main.pyc
? o PYZ-00.pyz
Contents of 'PYZ-00.pyz' (PYZ):
 is_package, position, length, name
 0, 17, 2647, '_compat_pickle'
......
 0, 543553, 531, 'student'
 0, 544084, 19733, 'subprocess'
 0, 563817, 27425, 'tarfile'
 0, 591242, 5936, 'textwrap'
 0, 597178, 15612, 'threading'
 0, 612790, 1398, 'token'
 0, 614188, 8969, 'tokenize'
 0, 623157, 6659, 'tracemalloc'
 0, 629816, 27711, 'typing'
 1, 657527, 70, 'urllib'
 0, 657597, 13861, 'urllib.parse'
 0, 671458, 2188, 'uu'
 0, 673646, 26812, 'zipfile'
? x student
Output filename? student.pyc
? ls
U: go up one level
O <name>: open embedded archive with given name
X <name>: extract file with given name
S: list the contents of current archive again
Q: quit
? q
  • 在上面的操作中,我们使用 pyi-archive_viewer 提取了 main.pyc、和 student.pyc 文件,当时大家可以很清楚的看到弊端,即需要一个一个手动提取,对于大项目这是十分麻烦的,推荐使用下面的工具提取。

使用工具提取

  • 我们可以使用开源项目 Python-exe-unpacker 中的脚本 pyinstxtractor.py 脚本进行提取,地址:github.com/countercept...
python 复制代码
\print-student> Python pyinstxtractor.py .\main.exe                                            
DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
[*] Processing .\main.exe
[*] Pyinstaller version: 2.1+
[*] Python version: 309
[*] Length of package: 5835756 bytes
[*] Found 59 files in CArchive
[*] Beginning extraction...please standby
[*] Found 81 files in PYZ archive
[*] Successfully extracted pyinstaller archive: .\main.exe

You can now use a python decompiler on the pyc files within the extracted directory

.pyc 文件转换为 Python 脚本

入口运行类

  • 对于从 pyinstaller 提取出来的 pyc 文件并不能直接反编译,入口运行类共16字节的 magic 和 时间戳被去掉了。如果直接进行反编译,例如执行 uncompyle6 main.pyc,则会报出如下错误:
python 复制代码
ImportError: Unknown magic number 227 in main.pyc
  • 我们可以使用支持16进制编辑的文本编辑器进行处理,比如:UltraEdit32
  • 可以看到前16个字节都被去掉了,其中前四个字节是magic,这四个字节会随着系统和Python版本发生变化,需要保持一致。后四个字节包括时间戳和一些其他的信息,都可以随意填写。我们可以通过 UltraEdit32 向提取的文件添加回信息。
  • 这里我写了一个 python 脚本实现这个过程:
python 复制代码
// 读取从pyz目录抽取的pyc文件的前4个字节作基准
pyz_dir = "./main.exe_extracted/PYZ-00.pyz_extracted"
for pyc_file in os.listdir(pyz_dir):
    if pyc_file.endswith(".pyc"):
        file = f"{pyz_dir}/{pyc_file}"
        break
with open(file, "rb") as f:
    head = f.read(4)

// 补全入口类文件
if os.path.exists("pycfile_tmp"):
    shutil.rmtree("pycfile_tmp")
os.mkdir("pycfile_tmp")
main_file_result = "pycfile_tmp/main.pyc"
with open("./main.exe_extracted/main.pyc", "rb") as read, open(main_file_result, "wb") as write:
    write.write(head)
    write.write(b"\0" * 12)
    write.write(read.read())

非入口运行类

  • 对于非入口运行的pyc文件从12字节开始缺4个字节。
python 复制代码
# 补全非入口类文件
pyz_dir = "main.exe_extracted/PYZ-00.pyz_extracted"
for pyc_file in os.listdir(pyz_dir):
    pyc_file_src = f"{pyz_dir}/{pyc_file}"
    pyc_file_dest = f"pycfile_tmp/{pyc_file}"
    print(pyc_file_src, pyc_file_dest)
    with open(pyc_file_src, "rb") as read, open(pyc_file_dest, "wb") as write:
        write.write(read.read(12))
        write.write(b"\0"*4)
        write.write(read.read())

转换补全后的 pyc 文件

uncompyle6 反编译

python 复制代码
pip install uncompyle6
uncompyle6 xxx.pyc>xxx.py

如:uncompyle6 .\pycfile_tmp\main.pyc
# uncompyle6 version 3.9.0
# Python bytecode version base 3.9.0 (3425)
# Decompiled from: Python 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)]
# Embedded file name: main.py

Unsupported Python version, 3.9.0, for decompilation


# Unsupported bytecode in file .\pycfile_tmp\main.pyc
# Unsupported Python version, 3.9.0, for decompilation
  • 由于我使用的是 3.9.0 版本,uncompyle6 不再支持 decompilation,有兴趣的朋友可以去试试。

在线工具

可能遇到的问题

PYZ-00.pyz_extracted 文件为空

  • 构建 .exe 文件 Python 版本和解压包时使用的版本不一致,比如我使用 Python 2.7 进行解包:
python 复制代码
>Python .\pyinstxtractor.py .\main.exe

[*] Processing .\main.exe
[*] Pyinstaller version: 2.1+
[*] Python version: 312
[*] Length of package: 7675728 bytes
[*] Found 60 files in CArchive
[*] Beginning extraction...please standby
[!] Warning: The script is running in a different python version than the one used to build the executable
    Run this script in Python312 to prevent extraction errors(if any) during unmarshalling
[!] Unmarshalling FAILED. Cannot extract PYZ-00.pyz. Extracting remaining files.
[*] Successfully extracted pyinstaller archive: .\main.exe

You can now use a python decompiler on the pyc files within the extracted directory

# 查看解压后的文件
\print-student\main.exe_extracted\PYZ-00.pyz_extracted> ls
\print-student\main.exe_extracted\PYZ-00.pyz_extracted>

如何防止exe被反编译

  • 我们可以在打包命令后面添加 --key 参数来进行加密,例如:
python 复制代码
 pyinstaller --onefile   -p venv/Lib/site-packages .\print-student\main.py --key '1234'
  • 再次解压,抽取的中间结果变为了 .pyc.encrypted,无法正常反编译。

思考

  • Bytecode encryption was removed in PyInstaller v6.0. Please remove your --key=xxx argument. For the rationale and alternatives see https://github.com/pyinstaller/pyinstaller/pull/6999
  • 可以看到在 PyInstaller v6.0 加密参数已经被废弃,大家可以思考一下原因。

总结

  • 反编译 Python 可执行文件可以帮助我们理解程序的工作原理和逻辑,但在实践中可能会受到许多因素的限制。对于复杂的程序,反编译可能只是了解其工作原理的第一步,可能需要进一步的分析和研究。最后,我们需要明白技术没有好坏,需要谨守道德和法律的底线。

个人简介

👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

📖 保持关注我的博客,让我们共同追求技术卓越。

相关推荐
研究点啥好呢1 分钟前
Github热榜项目推荐 | Fireworks Tech Graph:告别手动绘图时代
python·开源·github·claude·skills
ycjunhua2 分钟前
终极入门:uv —— 超快 Python 包 / 环境管理工具(Windows 完整版)
windows·python·uv
2401_883600253 分钟前
SQL视图名称冲突如何避免_建立规范化的命名空间与管理
jvm·数据库·python
de_wizard4 分钟前
Spring Boot 整合 Apollo 配置中心实战
java·spring boot·后端
用户6757049885024 分钟前
AI开发实战1、手摸手教你一行代码不写,全程AI写个小程序——前端布局
后端·aigc·ai编程
JAVA学习通4 分钟前
AI Agent 工具调用机制深度解析与 Spring Boot 工程集成实战(2026版)
java·人工智能·spring boot·python·spring
ooseabiscuit5 分钟前
Spring报错解决一览
java·后端·spring
亿牛云爬虫专家5 分钟前
解决 Python 爬虫代理 407 错误:基于 urllib3 更新与爬虫代理的实战指南-2
爬虫·python·爬虫代理·authentication·urllib3·407·base64 编码
m0_640309306 分钟前
CSS中如何让浮动元素撑开父容器_深度解析清除浮动
jvm·数据库·python
2301_816660216 分钟前
Golang bufio怎么读取用户输入_Golang标准输入读取教程【详解】
jvm·数据库·python