Python 作为一种动态、解释型语言,其代码的易读性是其广受欢迎的重要原因之一。然而,这种特性也带来了代码安全性的挑战。
PyArmor 是一个强大的 Python 应用程序保护工具,旨在帮助开发者保护他们的 Python 脚本和包不被轻易反编译、篡改或盗用。它通过一系列精妙的底层技术,实现了对 Python 代码的深度混淆和加密。
PyArmor 的核心功能可以概括为以下几个方面:
字节码加密 (Bytecode Encryption):
.py
源代码文件,而是加密 Python 解释器编译后生成的 .pyc
字节码文件。.pyc
文件中的 code object
进行加密。当被加密的脚本运行时,PyArmor 提供的运行时模块(runtime module)会在内存中对字节码进行解密,然后将其提交给 Python 虚拟机执行。这意味着,即使攻击者获取了加密后的 .pyc
文件,也无法直接用 Python 解释器执行,也无法使用反编译工具将其还原为可读的源代码,因为字节码内容已经被转换成了无意义的密文。代码混淆 (Code Obfuscation):
_0x1a2b3c
),甚至可以通过更高级的混淆策略,使相同名称在不同上下文中有不同的混淆结果,从而进一步增加代码可读性的障碍。脚本绑定与授权许可 (Script Binding & Licensing):
运行时保护 (Runtime Protection):
__import__
, exec
, eval
, inspect
, sys
)进行保护,防止它们被用于代码注入或运行时分析。跨平台支持:
PyArmor 并非一个传统的打包工具(如 PyInstaller, cx_Freeze),而是一个代码保护工具。它与打包工具之间是互补关系,可以协同工作:
PyArmor 与打包工具的结合工作流程:
通常的流程是:
.pyc
文件和 PyArmor 运行时文件。.pyc
文件和 PyArmor 运行时库)与您的其他资源文件、PyArmor 运行时模块一起,作为打包工具(如 PyInstaller)的输入。打包工具会将这些文件打包成最终的可执行程序。通过这种方式,用户得到的是一个包含加密代码的独立可执行文件,既方便了分发,又提供了深度的代码保护。
市面上存在一些其他的 Python 代码保护方案,大致可分为几类:
PyArmor 的定位:PyArmor 介于简单的源代码混淆和复杂的 C 扩展/商业方案之间。它提供了一种在 Python 字节码层面进行深度加密和混淆的解决方案,在不改变原始 Python 开发习惯和生态的前提下,提供了相当强的保护能力,并且具备灵活的授权绑定功能。它是一个纯 Python 字节码层面的保护工具,这意味着它能够保护任何标准的 Python 代码,而无需像 Cython 那样修改代码结构或引入 C 语言。
要真正理解 PyArmor 的强大之处,我们需要深入到 Python 解释器和 PyArmor 运行时模块交互的底层。
Python 程序的执行流程大致是:.py
源代码 -> 编译成 .pyc
字节码文件 -> Python 虚拟机加载并执行字节码。PyArmor 的核心秘密就隐藏在它对这个流程的干预中。
1. Python 解释器加载 .pyc
文件的正常流程:
当 Python 解释器尝试导入一个模块时,它会首先查找 .pyc
文件(如果存在且有效)。如果找到,它会执行以下关键步骤:
.pyc
文件的头部(包含魔数、时间戳等)和后续的字节码内容。code object
:.pyc
文件中存储的是 Python 的 code object
(代码对象)的序列化形式。Python 解释器会调用 C 层的函数,如 PyMarshal_ReadObjectFromFile
或类似的机制,将文件中的字节流反序列化成内存中的 PyCodeObject
结构体。code object
:一旦 PyCodeObject
在内存中被构建,Python 虚拟机就可以开始解释执行其中的字节码指令。2. code object
的结构深入分析:
在 CPython 内部,一个 code object
是一个 PyCodeObject
结构体,它包含了执行一个函数、模块或类定义所需的所有信息。关键的成员包括:
co_argcount
:位置参数的数量。co_kwonlyargcount
:仅限关键字参数的数量。co_nlocals
:局部变量的数量。co_stacksize
:解释器栈所需的栈大小。co_flags
:各种标志位(如是否是生成器、是否是协程等)。co_code
:核心!这是一个 PyBytesObject
,存储了实际的字节码指令序列。这是 Python 虚拟机执行的“机器码”。co_consts
:一个 PyTupleObject
,存储了代码中使用的所有常量,包括数字、字符串、None、以及嵌套的 code object
(例如,函数内部定义的函数)。co_names
:一个 PyTupleObject
,存储了所有全局变量名、函数名、类名、模块名等(这些是在字节码中通过名称查找的)。co_varnames
:一个 PyTupleObject
,存储了局部变量和参数的名称。co_filename
:定义该 code object
的源文件名。co_name
:该 code object
对应的函数/类/模块的名称。co_firstlineno
:代码的起始行号。co_lnotab
:行号表,用于将字节码偏移量映射到源代码行号(用于调试和回溯)。co_freevars
:闭包中使用的自由变量名。co_cellvars
:用于实现闭包的 cell 变量名。(图片:PyCodeObject Structure Diagram Placeholder)
(请在此处插入一张图片,展示 PyCodeObject 结构体及其主要成员,特别是 co_code
, co_consts
, co_names
, co_varnames
。)
3. PyArmor 如何 Hook CPython 的加载机制 (Py_CompileString
, Py_Marshal_ReadObjectFromFile
):
PyArmor 实现保护的核心在于它拦截了 Python 解释器加载 code object
的过程。它不是直接修改 Python 解释器本身的源代码,而是通过以下技术实现:
.so
文件,Windows 上是 .pyd
文件)。这些模块在加密后的脚本运行前被加载。code object
的底层函数指针,例如与 PyMarshal_ReadObjectFromFile
或更低级的字节码解析相关的函数。.pyc
文件时,实际上会调用 PyArmor 注入的钩子函数。PyCodeObject
结构体,或者修改已加载的 code object
内部的 co_code
指针,使其指向解密后的字节码。.pyc
文件本身保持加密状态。 这意味着,即使攻击者在运行时尝试dump内存,获取到的也只是内存中的字节码,而非原始文件。4. PyArmor 对 code object
的加密和混淆策略:
co_code
加密:这是最直接的加密目标。实际执行的字节码指令序列 co_code
会被 PyArmor 的加密算法处理。
co_consts
混淆/加密:
“hello world”
)会从 co_consts
中提取出来,进行加密存储。在运行时,当需要访问这些常量时,PyArmor 会动态解密。这防止了通过字符串查找关键信息。code object
:如果一个模块包含函数或类定义,这些函数或类本身也有自己的 code object
。PyArmor 会递归地对这些嵌套的 code object
进行加密,确保整个代码结构都受到保护。co_names
和 co_varnames
混淆:
_pyarmor_0xDEADBEEF
)。LOAD_NAME
, CALL_FUNCTION
等字节码指令)时,PyArmor 的运行时钩子会拦截查找请求,通过映射表将混淆的名称还原为原始名称,再进行查找。这在不影响程序功能的前提下,大大降低了代码的可读性。定制化字节码生成 (Advanced Obfuscation):
if/else
结构转换为复杂的跳转表,增加静态分析的难度。PyArmor 不仅在文件存储层面进行保护,更在程序运行时持续对抗潜在的攻击。
检测调试工具:
sys.settrace
检测:Python 的 sys
模块提供了 settrace
函数,允许注册一个跟踪函数,用于调试。PyArmor 可以在运行时检测 sys.settrace
是否被设置,或者其行为是否被异常修改。sys.gettrace
检测:配合 settrace
,检测跟踪函数是否存在。pdb
相关的模块)是否存在来判断是否处于调试状态。基于 C 扩展的保护机制:
对 sys
模块和内置函数的篡改与保护:
__import__
, exec
, eval
:这些内置函数和 sys
模块中的一些函数是 Python 动态加载和执行代码的关键。PyArmor 可能会替换或封装这些函数,以控制其行为,防止它们被滥用。
__import__
,使其只能加载 PyArmor 预先允许的模块,或者在加载模块时执行额外的解密或校验逻辑。exec
或 eval
,使其无法执行非受保护的、恶意的动态代码。inspect
模块的限制:inspect
模块用于检查活动对象(模块、类、函数、帧、回溯、代码对象)的属性,对于逆向工程非常有用。PyArmor 可以限制 inspect
模块的功能,使其无法获取受保护代码的详细信息。dis
模块的干预:dis
模块用于反汇编 Python 字节码。PyArmor 的运行时保护会阻止 dis
模块正常工作,使其无法显示加密或混淆后的字节码。PyArmor 的授权和绑定功能是其在商业应用中非常重要的特性,它允许开发者灵活地控制软件的使用。
许可证文件的生成:
pyarmor gen
命令用于生成许可证文件。运行时验证流程:
防御许可证篡改:
通过上述机制,PyArmor 在文件、内存和运行时环境三个层面构建了一道多重防线,极大地增加了 Python 代码被逆向工程和非法使用的难度。
在开始使用 PyArmor 之前,您需要正确安装它并准备好开发环境。
PyArmor 支持 Python 3.6 及更高版本。在选择 Python 版本时,请考虑您的项目需求和 PyArmor 的最新兼容性列表(通常 PyArmor 的官方文档会提供最准确的信息)。
PyArmor 可以通过 Python 的包管理器 pip
进行安装。
pip install pyarmor # 使用 pip 安装 PyArmor
pip install pyarmor
:这条命令会从 Python 包索引 (PyPI) 下载并安装 PyArmor 库及其所有必要的依赖。PyArmor 的核心保护功能通常以预编译的二进制扩展模块(C 扩展)的形式提供,这意味着您不需要额外的编译器来安装它。在大多数情况下,您不需要手动安装 C 编译器来使用 PyArmor。PyArmor 会为不同的操作系统和 Python 版本提供预编译好的运行时库。
然而,在以下特殊情况下,您可能需要确保系统安装了 C 编译器:
总结:对于绝大多数用户而言,直接 pip install pyarmor
就足够了,无需担心 C 编译器。PyArmor 会自动处理其运行时库的安装。
PyArmor 的所有功能都通过命令行工具 pyarmor
来操作。掌握这些基本命令是使用 PyArmor 的前提。
pyarmor obfuscate
:最基础的混淆加密命令这是 PyArmor 最常用的命令,用于对 Python 脚本或整个项目进行加密和混淆。
基本语法:
pyarmor obfuscate [options] /path/to/script_or_folder
核心参数详解:
-O
, --output
:
dist
的目录,并将所有加密文件输出到其中。pyarmor obfuscate -O protected_code myscript.py
将加密后的文件放到 protected_code
目录。--src
:
--src
非常有用。PyArmor 会根据这个根目录的结构来处理所有 Python 文件。--src
,那么后面跟着的路径(/path/to/script_or_folder
)应该是相对于 --src
目录的路径。pyarmor obfuscate --src my_project main_app/app.py
--exact
:
--exact
,PyArmor 会尝试混淆指定脚本及其所有依赖的 .py
文件。--recursive
:
.py
文件并进行混淆。--recursive
通常是隐式的。--no-make
:
pyarmor_runtime_xx
)。--no-make
。--restrict
:
--restrict 0
(默认): 标准混淆。--restrict 1
: 额外混淆,增强保护。--restrict 2
: 更强的混淆,可能对性能有轻微影响。--suffix
:
myscript.py
混淆后可能是 myscript_pyarmor.pyc
。示例解析:
pyarmor obfuscate myscript.py
:最简单的用法,混淆 myscript.py
并输出到 dist
目录。pyarmor obfuscate -O build/protected_app my_project/
:混淆 my_project
目录下的所有 Python 文件,并输出到 build/protected_app
目录。pyarmor obfuscate --exact main.py
:只混淆 main.py
自身,不混淆 main.py
导入的模块(除非这些模块也被显式指定混淆)。pyarmor gen
:生成许可证文件用于生成 PyArmor 脚本所需的许可证文件,实现授权和绑定功能。
基本语法:
pyarmor gen [options]
核心参数详解:
--output
:
--bind-mac
:
XX:XX:XX:XX:XX:XX
或 XXXXXXXXXXXX
形式。--bind-mac 00:11:22:33:44:55,AA:BB:CC:DD:EE:FF
--bind-disk
:
wmic diskdrive get serialnumber
(Windows), hdparm -i /dev/sda | grep Serial
(Linux)。--bind-ipv4
:
--bind-nic
:
--expires
:
--expires 2024-12-31
--expired
:
--expired 30
(30天后过期)--count
:
--fixed
:
--enable
:
--enable restrict_module
(限制模块导入),--enable virtual_machine
(允许在虚拟机中运行)。--disable
:
--advanced N
:
示例解析:
pyarmor gen -O licenses --expires 2024-12-31
:生成一个在 2024年12月31日过期的许可证文件,并保存到 licenses
目录。pyarmor gen --bind-mac 00:11:22:33:44:55 --count 100
:生成一个绑定到特定 MAC 地址,并且只能执行 100 次的许可证。pyarmor cfg
:配置全局选项pyarmor cfg
命令用于设置 PyArmor 的全局配置选项,这些选项会影响 pyarmor obfuscate
等命令的行为。
基本语法:
pyarmor cfg [options]
常用选项:
--src
:
obfuscate
命令中重复指定 --src
。--output
:
--pack
:
pyinstaller
。--prefix
:
--bootstrap
:
--extra-runtime
:
--private
:
示例解析:
pyarmor cfg --output my_default_dist
:将 PyArmor 混淆的默认输出目录设置为 my_default_dist
。现在,我们将通过一个简单的实例,从零开始演示如何使用 PyArmor 对 Python 代码进行最基本的加密和混淆,并观察其效果。
目标:加密一个包含简单打印函数的 Python 脚本,使其源代码不可读,但功能保持不变。
首先,创建一个名为 hello_pyarmor.py
的文件,内容如下:
# hello_pyarmor.py
import sys # 导入 sys 模块,用于获取 Python 版本信息
import os # 导入 os 模块,用于获取环境变量
def greet(name): # 定义一个名为 greet 的函数,接收一个参数 name
"""
这是一个简单的问候函数,打印一个欢迎消息。
"""
message = f"你好, {
name}! 欢迎来到 PyArmor 的世界。" # 拼接问候消息
print(message) # 打印消息
print(f"当前 Python 版本: {
sys.version.split(' ')[0]}") # 打印 Python 版本信息
if os.getenv("DEBUG_MODE") == "true": # 检查环境变量 DEBUG_MODE 是否为 "true"
print("DEBUG_MODE 环境变量已设置。") # 如果是,打印调试模式信息
return message # 返回问候消息
class Greeter: # 定义一个名为 Greeter 的类
def __init__(self, greeting_prefix="Hello"): # 类的初始化方法,接收一个可选参数 greeting_prefix
self.prefix = greeting_prefix # 将传入的前缀保存为实例属性
def custom_greet(self, name): # 定义一个名为 custom_greet 的方法
return f"{
self.prefix}, {
name}! 祝你今天愉快!" # 返回自定义的问候消息
if __name__ == "__main__": # 判断当前脚本是否作为主程序运行
print("--- 原始脚本开始执行 ---") # 打印脚本开始执行的提示
greet("张三") # 调用 greet 函数
my_greeter = Greeter("Hola") # 创建 Greeter 类的实例
print(my_greeter.custom_greet("李四")) # 调用实例方法
print("--- 原始脚本执行结束 ---") # 打印脚本执行结束的提示
pyarmor obfuscate
进行加密混淆打开您的终端或命令行界面,导航到 hello_pyarmor.py
文件所在的目录,然后执行以下命令:
pyarmor obfuscate hello_pyarmor.py # 使用 pyarmor obfuscate 命令加密 hello_pyarmor.py
pyarmor obfuscate
:这是 PyArmor 的核心命令,用于执行混淆操作。hello_pyarmor.py
:这是您要保护的 Python 脚本的名称。执行完成后,PyArmor 会在当前目录下创建一个名为 dist
的新目录。
进入 dist
目录,您会发现以下文件和目录结构:
dist/
├── hello_pyarmor.pyc # 加密混淆后的字节码文件
└── pyarmor_runtime_000000/ # PyArmor 运行时文件夹,名称可能不同,后面的数字是PyArmor版本号或随机后缀
├── __init__.pyc # 运行时模块的初始化文件
├── pyarmor_runtime.so # (Linux) 或 pyarmor_runtime.pyd (Windows) - 核心C扩展运行时库
└── ... # 其他运行时支持文件
文件解析:
hello_pyarmor.pyc
:
.pyc
文件,不能直接被普通的 Python 解释器执行。pyarmor_runtime_000000/
:
pyarmor_runtime.so
(或 .pyd
): 这是 PyArmor 的核心 C 扩展模块。它包含了用于解密、混淆还原、运行时保护和许可证验证的底层逻辑。它是平台相关的,PyArmor 会根据您的操作系统生成对应的文件。__init__.pyc
:用于使 pyarmor_runtime_000000
目录成为一个 Python 包,从而可以被导入。在终端中,确保您仍然在原始目录(dist
的父目录),然后以以下方式运行加密后的脚本:
python dist/hello_pyarmor.pyc # 尝试直接运行加密后的 .pyc 文件
您会发现,直接运行 dist/hello_pyarmor.pyc
可能会失败,并提示错误,例如 Bad magic number in .pyc file
或 ImportError: bad magic number in 'dist/hello_pyarmor.pyc'
。
为什么会失败?
因为 hello_pyarmor.pyc
已经被 PyArmor 加密过,它不再是标准的 Python .pyc
文件。Python 解释器无法直接识别和加载它。
正确的运行方式:
PyArmor 混淆后的脚本需要其配套的运行时环境才能正确执行。有两种主要的方式来运行它们:
方式一:通过 PyArmor 的“引导脚本”或直接将运行时添加到 sys.path
当您混淆一个目录时,PyArmor 会生成一个辅助的“入口脚本”来引导程序运行。对于单个文件,最简单的方法是确保运行时模块可在 Python 导入路径中找到。
最标准的方式是:直接运行 dist
目录中的主脚本(如果 PyArmor 生成了),或者将 dist
目录添加到 Python 路径。
在我们的简单例子中,PyArmor 已经将 hello_pyarmor.pyc
和其运行时放在了 dist
目录下,并确保它们可以相互找到。
在 dist
目录内运行(推荐简单场景):
导航进入 dist
目录,然后运行:
cd dist # 进入 dist 目录
python hello_pyarmor.pyc # 运行加密后的 .pyc 文件
cd dist
:进入 PyArmor 生成的 dist
目录。python hello_pyarmor.pyc
:执行 dist
目录下的 hello_pyarmor.pyc
。此时,由于 pyarmor_runtime_000000
目录与 hello_pyarmor.pyc
在同一层级,Python 解释器能够找到并加载 PyArmor 的运行时模块,从而解密并执行 hello_pyarmor.pyc
。您会观察到与原始脚本完全相同的输出:
--- 原始脚本开始执行 ---
你好, 张三! 欢迎来到 PyArmor 的世界。
当前 Python 版本: 3.x.x # 您的 Python 版本
Hola, 李四! 祝你今天愉快!
--- 原始脚本执行结束 ---
这证明了即使代码已被加密和混淆,其功能依然完整。
方式二:在主脚本中显式导入 PyArmor 运行时 (对于复杂项目)
对于更复杂的项目,或者当您希望将加密代码部署到非标准位置时,您可能需要在您的应用程序的**入口点(非加密部分)**显式地导入 PyArmor 的运行时模块,以确保在加载加密模块之前,PyArmor 的解密机制已经被激活。
例如,创建一个新的入口脚本 run_protected_app.py
:
# run_protected_app.py
import sys # 导入 sys 模块,用于修改 Python 导入路径
import os # 导入 os 模块,用于路径操作
# 将 'dist' 目录添加到 Python 的模块搜索路径中
# 这使得 Python 解释器能够找到 dist/hello_pyarmor.pyc 和 dist/pyarmor_runtime_000000
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'dist')) # 将当前脚本所在目录下的 'dist' 目录添加到 sys.path
try: # 尝试导入并执行加密后的模块
# 注意:这里我们导入的是加密后的 hello_pyarmor 模块
# PyArmor 的运行时机制会在导入时自动解密
import hello_pyarmor # 导入加密后的 hello_pyarmor 模块
print("\n--- 通过外部引导脚本运行加密代码 ---") # 打印提示信息
hello_pyarmor.greet("王五") # 调用加密模块中的 greet 函数
my_greeter_protected = hello_pyarmor.Greeter("Hi") # 创建加密模块中 Greeter 类的实例
print(my_greeter_protected.custom_greet("赵六")) # 调用实例方法
print("--- 外部引导脚本执行结束 ---") # 打印提示信息
except ImportError as e: # 捕获导入错误
print(f"导入加密模块失败: {
e}") # 打印错误信息
except Exception as e: # 捕获其他异常
print(f"运行加密代码时发生错误: {
e}") # 打印错误信息
finally: # 无论是否发生异常,都执行
# 清理 sys.path,可选,但良好实践
if os.path.join(os.path.dirname(__file__), 'dist') in sys.path: # 检查路径是否在 sys.path 中
sys.path.remove(os.path.join(os.path.dirname(__file__), 'dist')) # 移除路径
将 run_protected_app.py
放在与 dist
目录相同的父目录下,然后运行:
python run_protected_app.py # 运行引导脚本
您同样会看到正确的输出。这种方式在打包整个应用时尤为重要,因为打包工具会将所有必要的文件(包括 PyArmor 运行时和加密脚本)放置在统一的结构中,并通过一个主入口点来启动。
pyarmor obfuscate
命令的深度运用--restrict
):不同保护强度与性能的权衡--restrict
参数是 pyarmor obfuscate
命令中最核心的选项之一,它控制着 PyArmor 应用于代码的混淆强度。不同的模式在保护级别、运行时性能和兼容性之间提供了不同的权衡。深入理解每种模式的内部机制,有助于您根据实际需求做出明智的选择。
--restrict 0
(默认模式:标准混淆)
PyCodeObject
结构体中的 co_code
(实际的字节码指令序列)进行加密。sys.settrace
的使用,但这些检查通常比较基础,容易被专业的逆向工程师绕过。--restrict 1
(额外混淆:增强保护)
--restrict 0
的基础上,增加了更多的字节码层面的混淆和保护措施,提高了反编译的难度。
if/else
, for
循环)转换为一个大的 while
循环,并在循环内部使用一个状态变量和 switch
语句(或多个 if/elif
)来模拟原始的控制流。这使得代码的逻辑流程变得非常复杂和难以追踪。restrict 0
会有额外的性能开销,但通常仍在可接受范围内。--restrict 2
(更强的混淆:性能可能受影响)
--restrict 1
的基础上,进一步加强混淆和运行时保护。
--restrict 3
(定制化模式,高级用户)
--restrict 3
是一个特殊的模式,它不直接代表一种固定的混淆强度,而是允许用户通过一个单独的配置文件(例如,一个 config.py
文件)来自定义混淆策略。这个配置文件可以包含 Python 代码,用于定义 PyArmor 在混淆时如何处理模块、函数、变量等。
_
开头的私有函数都进行名称混淆”、“特定模块不进行任何混淆”等。--restrict 4
(即将废弃或内部测试模式)
--restrict 4
可能是一个实验性或即将废弃的模式。它的具体行为可能随着版本更新而变化,通常不推荐在生产环境中使用。可能包括一些非常激进的、仍在测试中的混淆技术,旨在探索新的保护边界。--restrict 4
的具体含义和建议。总结表格:--restrict
模式概览
模式 | 保护强度 | 运行时性能影响 | 兼容性 | 主要机制 | 典型用途 |
---|---|---|---|---|---|
--restrict 0 |
基础 | 最小 | 最佳 | 字节码加密、常量混淆、基本反调试 | 快速保护、性能敏感应用 |
--restrict 1 |
增强 | 轻微 | 良好 | 0 + 更复杂字节码变换、控制流扁平化、更深入名称混淆、增强反调试 | 商业应用、核心逻辑 |
--restrict 2 |
极高 | 显著 | 一般 | 1 + 虚拟机/容器检测、更激进指令插桩、防止内存dump | 核心机密、对抗高级威胁 |
--restrict 3 |
高度定制化 | 可配置 | 可配置 | 基于配置文件实现精细化混淆和保护策略 | 高级用户、复杂项目 |
--restrict 4 |
实验性/不确定 | 不确定 | 不确定 | 实验性高级混淆技术 (不推荐生产使用) | 内部测试、前沿研究 |
代码示例:不同 restrict
模式的演示
我们将使用一个稍微复杂的脚本,并分别使用 --restrict 0
和 --restrict 1
进行加密,观察其行为和生成的文件大小(虽然不能直接看到混淆效果,但大小可能间接反映复杂性)。
# advanced_script.py
import hashlib # 导入 hashlib 模块,用于哈希计算
import base64 # 导入 base64 模块,用于编码解码
import datetime # 导入 datetime 模块,用于处理日期时间
def _calculate_checksum(data): # 定义一个私有函数 _calculate_checksum,计算数据的 SHA256 校验和
"""
计算输入数据的 SHA256 校验和。
"""
return hashlib.sha256(data.encode('utf-8')).hexdigest() # 对数据编码后计算 SHA256 摘要并返回十六进制字符串
def _log_event(event_type, details): # 定义一个私有函数 _log_event,记录事件
"""
模拟一个事件日志记录函数。
"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 获取当前时间并格式化
log_entry = f"[{
timestamp}] Event Type: {
event_type}, Details: {
details}" # 拼接日志条目
print(f"[LOG] {
log_entry}") # 打印日志条目
# 在实际应用中,这里可能会写入文件或发送到日志服务
class SecretDataProcessor: # 定义一个名为 SecretDataProcessor 的类,用于处理秘密数据
"""
一个用于处理敏感数据的类,包含一些内部逻辑。
"""
SECRET_KEY = "my_super_secret_key_12345" # 定义一个类级别的秘密常量
def __init__(self, identifier): # 初始化方法,接收一个标识符
self.identifier = identifier # 保存标识符
self._internal_state = 0 # 定义一个内部状态
def process(self, input_data): # 定义一个名为 process 的方法,处理输入数据
_log_event("Processing Started", f"Identifier: {
self.identifier}") # 记录事件
processed_data = "" # 初始化处理后的数据
for char in input_data: # 遍历输入数据中的每个字符
processed_data += chr(ord(char) + self._internal_state % 5) # 对字符进行简单偏移处理
self._internal_state += 1 # 内部状态递增
checksum = _calculate_checksum(processed_data + self.SECRET_KEY) # 计算校验和,包含秘密常量
_log_event("Processing Finished", f"Checksum: {
checksum[:8]}...") # 记录事件,显示部分校验和
return base64.b64encode(processed_data.encode('utf-8')).decode('utf-8') # 返回 Base64 编码的处理后数据
def public_api_entry(data): # 定义一个公共 API 入口函数
"""
公共 API 入口点,用于外部调用。
"""
processor = SecretDataProcessor("API_Call_1") # 创建 SecretDataProcessor 实例
result = processor.process(data) # 调用 process 方法
print(f"API Call Result (Base64): {
result[:30]}...") # 打印部分结果
return result # 返回结果
if __name__ == "__main__": # 判断当前脚本是否作为主程序运行
print("--- 原始高级脚本开始执行 ---") # 打印提示
public_api_entry("This is some sensitive information.") # 调用公共 API
print("--- 原始高级脚本执行结束 ---") # 打印提示
import subprocess # 导入 subprocess 模块,用于执行外部命令
import os # 导入 os 模块,用于文件系统操作
import shutil # 导入 shutil 模块,用于高级文件操作
import logging # 导入 logging 模块
# 配置日志输出
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) # 获取日志器
# 定义原始脚本路径
ORIGINAL_SCRIPT_NAME = "advanced_script.py" # 原始脚本文件名
ORIGINAL_SCRIPT_PATH = ORIGINAL_SCRIPT_NAME # 原始脚本路径
# 创建原始脚本文件
def create_original_script(path): # 定义创建原始脚本文件的函数
script_content = """ # 脚本内容,使用三重引号定义多行字符串
import hashlib
import base64
import datetime
def _calculate_checksum(data):
\"\"\"
计算输入数据的 SHA256 校验和。
\"\"\"
return hashlib.sha256(data.encode('utf-8')).hexdigest()
def _log_event(event_type, details):
\"\"\"
模拟一个事件日志记录函数。
\"\"\"
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] Event Type: {event_type}, Details: {details}"
print(f"[LOG] {log_entry}")
class SecretDataProcessor:
\"\"\"
一个用于处理敏感数据的类,包含一些内部逻辑。
\"\"\"
SECRET_KEY = "my_super_secret_key_12345"
def __init__(self, identifier):
self.identifier = identifier
self._internal_state = 0
def process(self, input_data):
_log_event("Processing Started", f"Identifier: {self.identifier}")
processed_data = ""
for char in input_data:
processed_data += chr(ord(char) + self._internal_state % 5)
self._internal_state += 1
checksum = _calculate_checksum(processed_data + self.SECRET_KEY)
_log_event("Processing Finished", f"Checksum: {checksum[:8]}...")
return base64.b64encode(processed_data.encode('utf-8')).decode('utf-8')
def public_api_entry(data):
\"\"\"
公共 API 入口点,用于外部调用。
\"\"\"
processor = SecretDataProcessor("API_Call_1")
result = processor.process(data)
print(f"API Call Result (Base64): {result[:30]}...")
return result
if __name__ == "__main__":
print("--- 原始高级脚本开始执行 ---")
public_api_entry("This is some sensitive information.")
print("--- 原始高级脚本执行结束 ---")
"""
with open(path, "w", encoding="utf-8") as f: # 以写入模式打开文件,使用 UTF-8 编码
f.write(script_content) # 写入脚本内容
logger.info(f"已创建原始脚本: {
path}") # 记录日志
# 执行 PyArmor 混淆
def run_pyarmor_obfuscate(input_script, output_dir, restrict_level): # 定义执行 PyArmor 混淆的函数
if os.path.exists(output_dir): # 如果输出目录存在
shutil.rmtree(output_dir) # 递归删除目录
logger.info(f"已清理旧的输出目录: {
output_dir}") # 记录日志
command = ["pyarmor", "obfuscate", f"--restrict={
restrict_level}", "-O", output_dir, input_script] # 构建 PyArmor 命令
logger.info(f"执行命令: {
' '.join(command)}") # 记录日志
try: # 尝试执行命令
subprocess.run(command, check=True, capture_output=True, text=True, encoding='utf-8') # 执行子进程命令
logger.info(f"PyArmor 混淆成功 (restrict={
restrict_level})。输出目录: {
output_dir}") # 记录成功日志
except subprocess.CalledProcessError as e: # 捕获子进程执行错误
logger.error(f"PyArmor 混淆失败 (restrict={
restrict_level})。") # 记录错误日志
logger.error(f"STDOUT: {
e.stdout}") # 打印标准输出
logger.error(f"STDERR: {
e.stderr}") # 打印标准错误
raise # 重新抛出异常
except FileNotFoundError: # 捕获文件未找到错误 (通常是 pyarmor 命令未找到)
logger.error("错误: pyarmor 命令未找到。请确保 PyArmor 已安装并配置到 PATH 环境变量中。") # 记录错误日志
raise # 重新抛出异常
# 运行混淆后的脚本
def run_protected_script(output_dir, script_name): # 定义运行混淆后脚本的函数
logger.info(f"\n--- 尝试运行混淆后的脚本: {
os.path.join(output_dir, script_name + 'c')} ---") # 记录日志
# 将输出目录添加到 Python 路径,以便导入加密模块
original_sys_path = list(os.sys.path) # 备份原始的 sys.path
os.sys.path.insert(0, output_dir) # 将输出目录插入到 sys.path 的最前面
try: # 尝试导入并运行
# 动态导入加密后的模块
# 注意:这里我们期望 PyArmor 会将 .py 文件转换为 .pyc 并放在 output_dir 中
# PyArmor 运行时模块会确保在导入时解密
module_name = script_name.replace(".py", "") # 从脚本名中获取模块名
# 尝试直接执行混淆后的 .pyc 文件
# 为了模拟用户运行,我们通常会提供一个引导脚本或将其视为可执行模块
# 更健壮的方式是 PyInstaller 打包后的入口点,或者通过一个简单的外部loader.py
# 对于当前案例,我们直接cd到dist目录执行,或者像之前一样设置sys.path
# 这里为了演示方便,我们模拟在一个新的环境中执行,会cd进去
current_dir = os.getcwd() # 获取当前工作目录
os.chdir(output_dir) # 切换到输出目录
logger.info(f"已切换到目录: {
os.getcwd()}") # 记录日志
# 直接运行加密后的 .pyc 文件
# 在 pyarmor obfuscate 之后,dist 目录下会有一个与原始 .py 文件同名的 .pyc 文件
# 并且 pyarmor_runtime 目录也会被放在 dist 目录下
# 这样,当 Python 解释器在 dist 目录中启动时,它能够找到并加载 pyarmor_runtime
subprocess.run(["python", script_name + 'c'], check=True, text=True, encoding='utf-8') # 运行加密后的 .pyc 文件
logger.info(f"混淆后的脚本成功运行。") # 记录成功日志
os.chdir(current_dir) # 切换回原始目录
logger.info(f"已切换回目录: {
os.getcwd()}") # 记录日志
except Exception as e: # 捕获任何异常
logger.error(f"运行混淆后的脚本失败: {
e}") # 记录错误日志
finally: # 无论成功或失败,都执行
os.sys.path = original_sys_path # 恢复原始的 sys.path
# 主执行流程
if __name__ == "__main__": # 判断当前脚本是否作为主程序运行
create_original_script(ORIGINAL_SCRIPT_PATH) # 创建原始脚本文件
# 1. 使用 --restrict 0 进行混淆
output_dir_r0 = "dist_r0" # 定义输出目录
logger.info(f"\n--- 执行混淆 (restrict=0) ---") # 记录日志
try: # 尝试执行混淆
run_pyarmor_obfuscate(ORIGINAL_SCRIPT_PATH, output_dir_r0, 0) # 执行混淆
run_protected_script(output_dir_r0, ORIGINAL_SCRIPT_NAME.replace(".py", "")) # 运行混淆后的脚本
except Exception as e: # 捕获异常
logger.error(f"Restrict 0 演示流程中断: {
e}") # 记录错误日志
# 2. 使用 --restrict 1 进行混淆
output_dir_r1 = "dist_r1" # 定义输出目录
logger.info(f"\n--- 执行混淆 (restrict=1) ---") # 记录日志
try: # 尝试执行混淆
run_pyarmor_obfuscate(ORIGINAL_SCRIPT_PATH, output_dir_r1, 1) # 执行混淆
run_protected_script(output_dir_r1, ORIGINAL_SCRIPT_NAME.replace(".py", "")) # 运行混淆后的脚本
except Exception as e: # 捕获异常
logger.error(f"Restrict 1 演示流程中断: {
e}") # 记录错误日志
# 清理创建的原始脚本
if os.path.exists(ORIGINAL_SCRIPT_PATH): # 如果原始脚本文件存在
os.remove(ORIGINAL_SCRIPT_PATH) # 删除文件
logger.info(f"已清理原始脚本: {
ORIGINAL_SCRIPT_PATH}") # 记录日志
输出分析 (部分关键信息):
restrict=0
和 restrict=1
。dist_r0
和 dist_r1
目录下,都会生成 advanced_script.pyc
和一个 pyarmor_runtime_xxxxxx
目录。advanced_script.pyc
文件(例如使用 hexdump
或文本编辑器),会发现它们都是乱码,不可读。run_protected_script
函数时,两次运行都会输出与原始 advanced_script.py
相同的程序运行结果,这验证了无论在哪种 restrict
模式下,被保护代码的功能都没有改变。进一步思考:如何观察混淆效果?
由于 PyArmor 的加密混淆发生在字节码层面,并且其运行时会进行内存解密,我们很难通过简单的工具(如 dis
模块)直接“看到”混淆后的字节码。尝试用 dis
模块反汇编加密后的 .pyc
文件会失败,或者显示不正确的字节码,这本身就是保护成功的表现。
专业的逆向工程通常涉及:
PyEval_EvalFrameEx
或 PyCode_New
等关键 C 函数处设置断点,截获解密后的 code object
。PyArmor 的 --restrict 1
及更高级模式通过引入控制流扁平化、指令替换、反调试等机制,使得即使攻击者成功获取到内存中的解密字节码,其结构也变得极其复杂,难以通过常规的反编译工具进行还原,大大增加了手动分析的成本。
名称混淆是 PyArmor 保护策略的重要组成部分,它通过重命名代码中的标识符(如变量名、函数名、类名、模块名)为无意义的短字符串,来大幅降低代码的可读性,从而增加逆向工程的难度。
工作原理:
co_names
, co_varnames
)的 code object
。这些符号表存储了代码中使用的所有名称。_pyarmor_0x1a2b3c
, __pyarmor_a
等)。混淆范围:
_
或 __
开头的函数/方法,是混淆的重点。命名冲突与可调试性:
_pyarmor_
)来生成混淆后的名称。代码示例:名称混淆效果演示 (针对一个多模块项目)
我们将创建一个简单的 Python 项目,包含多个模块和函数,然后使用 PyArmor 对其进行混淆,并尝试观察名称的变化(虽然我们不能直接看到 pyc
内部的混淆,但可以从 PyArmor 的配置和文档中理解其行为)。
首先,创建一个名为 my_project
的目录,并在其中创建以下文件:
my_project/
├── __init__.py
├── main_app.py
└── utils/
├── __init__.py
└── data_helpers.py
my_project/__init__.py
(保持为空,或者简单包含版本信息):
# my_project/__init__.py
__version__ = "1.0.0" # 定义版本号
my_project/main_app.py
:
# my_project/main_app.py
from .utils.data_helpers import process_string_data # 从 my_project.utils.data_helpers 导入 process_string_data 函数
_INTERNAL_CONSTANT = "This is a secret internal message." # 定义一个内部常量
def _private_logic(input_value): # 定义一个私有函数 _private_logic
"""
一个内部私有逻辑函数。
"""
temp_result = input_value * 2 # 计算临时结果
print(f"Internal calculation: {
temp_result}") # 打印内部计算结果
return temp_result # 返回临时结果
def run_application_logic(user_input): # 定义一个公共函数 run_application_logic,作为应用程序的入口点
"""
应用程序的主要逻辑入口。
"""
print(f"应用开始运行,输入: {
user_input}") # 打印输入
processed_data = process_string_data(user_input) # 调用从 data_helpers 导入的函数处理数据
intermediate_result = _private_logic(len(processed_data)) # 调用私有函数处理数据长度
final_output = f"最终处理结果长度: {
intermediate_result}, 秘密信息: {
_INTERNAL_CONSTANT[:10]}..." # 拼接最终输出
print(final_output) # 打印最终输出
return final_output # 返回最终输出
if __name__ == "__main__": # 判断当前脚本是否作为主程序运行
run_application_logic("sample_user_data_xyz") # 调用应用程序逻辑
my_project/utils/__init__.py
(保持为空):
# my_project/utils/__init__.py
my_project/utils/data_helpers.py
:
# my_project/utils/data_helpers.py
import base64 # 导入 base64 模块
def _helper_function(raw_data): # 定义一个私有辅助函数
"""
一个内部辅助函数,用于数据转换。
"""
encoded_data = base64.b64encode(raw_data.encode('utf-8')).decode('utf-8') # 将原始数据 Base64 编码
return encoded_data.upper() # 返回大写编码数据
def process_string_data(input_string): # 定义一个公共函数 process_string_data
"""
处理字符串数据,并返回一个混淆后的版本。
"""
print(f" 数据助手处理: {
input_string}") # 打印提示
processed = _helper_function(input_string + "processed_suffix") # 调用内部辅助函数
return processed # 返回处理后的数据
现在,我们将编写 Python 脚本来执行混淆操作,并模拟运行:
import subprocess # 导入 subprocess 模块,用于执行外部命令
import os # 导入 os 模块,用于文件系统操作
import shutil # 导入 shutil 模块,用于高级文件操作
import logging # 导入 logging 模块
# 配置日志输出
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) # 获取日志器
# 定义项目结构
PROJECT_ROOT = "my_project" # 项目根目录
MAIN_APP_PATH = os.path.join(PROJECT_ROOT, "main_app.py") # 主应用程序脚本路径
DATA_HELPERS_PATH = os.path.join(PROJECT_ROOT, "utils", "data_helpers.py") # 数据助手脚本路径
# 创建项目文件
def create_project_files(): # 定义创建项目文件的函数
# 创建目录结构
os.makedirs(os.path.join(PROJECT_ROOT, "utils"), exist_ok=True) # 创建项目根目录和 utils 子目录
# my_project/__init__.py
with open(os.path.join(PROJECT_ROOT, "__init__.py"), "w", encoding="utf-8") as f: # 打开 __init__.py 文件
f.write('__version__ = "1.0.0"\n') # 写入版本信息
logger.info(f"已创建 {
os.path.join(PROJECT_ROOT, '__init__.py')}") # 记录日志
# my_project/main_app.py
main_app_content = """
from .utils.data_helpers import process_string_data
_INTERNAL_CONSTANT = "This is a secret internal message."
def _private_logic(input_value):
\"\"\"
一个内部私有逻辑函数。
\"\"\"
temp_result = input_value * 2
print(f"Internal calculation: {temp_result}")
return temp_result
def run_application_logic(user_input):
\"\"\"
应用程序的主要逻辑入口。
\"\"\"
print(f"应用开始运行,输入: {user_input}")
processed_data = process_string_data(user_input)
intermediate_result = _private_logic(len(processed_data))
final_output = f"最终处理结果长度: {intermediate_result}, 秘密信息: {_INTERNAL_CONSTANT[:10]}..."
print(final_output)
return final_output
if __name__ == "__main__":
run_application_logic("sample_user_data_xyz")
"""
with open(MAIN_APP_PATH, "w", encoding="utf-8") as f: # 打开 main_app.py 文件
f.write(main_app_content) # 写入内容
logger.info(f"已创建 {
MAIN_APP_PATH}") # 记录日志
# my_project/utils/__init__.py
with open(os.path.join(PROJECT_ROOT, "utils", "__init__.py"), "w", encoding="utf-8") as f: # 打开 utils/__init__.py 文件
f.write("") # 写入空内容
logger.info(f"已创建 {
os.path.join(PROJECT_ROOT, 'utils', '__init__.py')}") # 记录日志
# my_project/utils/data_helpers.py
data_helpers_content = """
import base64
def _helper_function(raw_data):
\"\"\"
一个内部辅助函数,用于数据转换。
\"\"\"
encoded_data = base64.b64encode(raw_data.encode('utf-8')).decode('utf-8')
return encoded_data.upper()
def process_string_data(input_string):
\"\"\"
处理字符串数据,并返回一个混淆后的版本。
\"\"\"
print(f" 数据助手处理: {input_string}")
processed = _helper_function(input_string + "processed_suffix")
return processed
"""
with open(DATA_HELPERS_PATH, "w", encoding="utf-8") as f: # 打开 data_helpers.py 文件
f.write(data_helpers_content) # 写入内容
logger.info(f"已创建 {
DATA_HELPERS_PATH}") # 记录日志
# 执行 PyArmor 混淆
def obfuscate_project(src_dir, output_dir, exclude_modules=None): # 定义混淆项目函数
if os.path.exists(output_dir): # 如果输出目录存在
shutil.rmtree(output_dir) # 删除旧目录
logger.info(f"已清理旧的输出目录: {
output_dir}") # 记录日志
command = ["pyarmor", "obfuscate", "--src", src_dir, "-O", output_dir] # 构建基础 PyArmor 命令
# 默认情况下,PyArmor 会对所有可以混淆的名称进行混淆
# 如果要排除某些模块不被混淆,可以使用 --exclude 选项
if exclude_modules: # 如果指定了要排除的模块
for mod in exclude_modules: # 遍历要排除的模块
command.extend(["--exclude", mod]) # 将 --exclude 参数添加到命令中
command.append(src_dir) # 将源目录作为混淆目标
logger.info(f"执行命令: {
' '.join(command)}") # 记录日志
try: # 尝试执行命令
subprocess.run(command, check=True, capture_output=True, text=True, encoding='utf-8') # 执行子进程命令
logger.info(f"PyArmor 混淆成功。输出目录: {
output_dir}") # 记录成功日志
except subprocess.CalledProcessError as e: # 捕获子进程执行错误
logger.error(f"PyArmor 混淆失败。") # 记录错误日志
logger.error(f"STDOUT: {
e.stdout}") # 打印标准输出
logger.error(f"STDERR: {
e.stderr}") # 打印标准错误
raise # 重新抛出异常
except FileNotFoundError: # 捕获文件未找到错误
logger.error("错误: pyarmor 命令未找到。请确保 PyArmor 已安装并配置到 PATH 环境变量中。") # 记录错误日志
raise # 重新抛出异常
# 运行混淆后的项目
def run_protected_project(output_dir, entry_script_relative_path): # 定义运行混淆后项目的函数
logger.info(f"\n--- 尝试运行混淆后的项目: {
output_dir} ---") # 记录日志
# 模拟运行环境,需要将混淆后的项目根目录添加到 sys.path
original_sys_path = list(os.sys.path) # 备份原始 sys.path
os.sys.path.insert(0, output_dir) # 将混淆后的项目根目录添加到 sys.path
# 构建入口脚本的完整路径 (在 output_dir 内部)
entry_point_path = os.path.join(output_dir, entry_script_relative_path) # 拼接入口脚本的完整路径
# 在这个例子中,PyArmor会将main_app.py混淆为main_app.pyc
# 我们直接执行这个混淆后的pyc文件
# 但是,由于是模块导入,通常不会直接执行.pyc,而是通过一个引导脚本导入
# 这里的做法是直接改变工作目录,然后执行混淆后的 main_app.pyc
current_dir = os.getcwd() # 获取当前工作目录
os.chdir(output_dir) # 切换到混淆后的输出目录
logger.info(f"已切换到目录: {
os.getcwd()}") # 记录日志
try: # 尝试运行
# 找到 main_app.pyc
# PyArmor 混淆后,原始的 .py 文件会被替换为 .pyc 文件
# 且其相对路径保持不变
# 这里的 entry_script_relative_path 是 "main_app.py"
# 混淆后会是 "main_app.pyc"
protected_main_script = entry_script_relative_path.replace(".py", ".pyc") # 获取混淆后的主脚本名称
# 直接通过 python 命令执行混淆后的 .pyc 文件
# PyArmor 的运行时模块 (pyarmor_runtime_xxxxxx) 会在同级目录下被自动发现并加载
# 从而实现字节码解密和执行
subprocess.run(["python", protected_main_script], check=True, text=True, encoding='utf-8') # 运行混淆后的主脚本
logger.info(f"混淆后的项目成功运行。") # 记录成功日志
except subprocess.CalledProcessError as e: # 捕获子进程执行错误
logger.error(f"运行混淆后的项目失败: {
e}") # 记录错误日志
logger.error(f"STDOUT: {
e.stdout}") # 打印标准输出
logger.error(f"STDERR: {
e.stderr}") # 打印标准错误
except Exception as e: # 捕获其他异常
logger.error(f"运行混淆后的项目时发生错误: {
e}") # 记录错误日志
finally: # 无论成功或失败,都执行
os.chdir(current_dir) # 切换回原始工作目录
os.sys.path = original_sys_path # 恢复原始 sys.path
logger.info(f"已切换回原始目录: {
os.getcwd()}") # 记录日志
# 主执行流程
if __name__ == "__main__": # 判断当前脚本是否作为主程序运行
create_project_files() # 创建项目文件
protected_output_dir = "dist_protected_project" # 定义混淆后输出目录
try: # 尝试执行混淆和运行
# 执行混淆,这里没有显式指定 --restrict,使用默认的 restrict 0
obfuscate_project(PROJECT_ROOT, protected_output_dir) # 混淆项目
# 运行混淆后的项目
run_protected_project(protected_output_dir, os.path.join("main_app.py")) # 运行混淆后的项目
# 尝试反编译混淆后的文件 (预期会失败或得到难以理解的代码)
logger.info(f"\n--- 尝试反编译混淆后的 {
os.path.join(protected_output_dir, 'main_app.pyc')} ---") # 记录日志
try: # 尝试反编译
# 注意:实际的反编译工具如 'uncompyle6' 需要额外安装
# 并且对 PyArmor 保护的代码通常会失败或产生乱码
# 这里我们只是模拟性地尝试,并预期它会失败或无法理解
# 警告:以下命令只是示意,不一定能成功反编译 PyArmor 保护的代码
# 而且需要用户手动安装反编译工具,这里不作为强制执行步骤
# subprocess.run(["uncompyle6", os.path.join(protected_output_dir, 'main_app.pyc')], check=True)
logger.info(" (尝试反编译通常会失败或产生乱码,这正是 PyArmor 的保护效果。)") # 记录日志
logger.info(f" 请尝试手动打开 {
os.path.join(protected_output_dir, 'main_app.pyc')} 或 {
os.path.join(protected_output_dir, 'utils', 'data_helpers.pyc')} 查看,您会发现它们是乱码。") # 提示用户手动查看
except FileNotFoundError: # 捕获文件未找到错误
logger.warning("警告: 反编译工具 'uncompyle6' 未安装或不在 PATH 中,无法演示反编译效果。") # 记录警告
except Exception as e: # 捕获其他异常
logger.warning(f"反编译工具执行失败或产生错误 (预期行为): {
e}") # 记录警告
except Exception as e: # 捕获主要流程中的异常
logger.error(f"项目混淆与运行演示流程中断: {
e}") # 记录错误日志
finally: # 无论成功或失败,都执行
# 清理创建的项目文件和输出目录
if os.path.exists(PROJECT_ROOT): # 如果项目根目录存在
shutil.rmtree(PROJECT_ROOT) # 删除目录
logger.info(f"已清理项目目录: {
PROJECT_ROOT}") # 记录日志
if os.path.exists(protected_output_dir): # 如果保护后的输出目录存在
shutil.rmtree(protected_output_dir) # 删除目录
logger.info(f"已清理混淆输出目录: {
protected_output_dir}") # 记录日志
输出分析(关键点):
pyarmor obfuscate --src my_project -O dist_protected_project my_project
时,PyArmor 会遍历 my_project
目录下的所有 Python 文件(包括 __init__.py
, main_app.py
, utils/__init__.py
, utils/data_helpers.py
),并对它们进行混淆。dist_protected_project
目录中,您会发现 main_app.pyc
, utils/__init__.pyc
, utils/data_helpers.pyc
等被加密混淆后的字节码文件,以及 pyarmor_runtime_xxxxxx
运行时目录。.pyc
文件,您将看到一堆乱码,无法直接理解。main_app.py
中的 _INTERNAL_CONSTANT
和 _private_logic
,以及 data_helpers.py
中的 _helper_function
都是内部名称。PyArmor 会对这些名称进行混淆。pyc
中,运行时才解密),但 PyArmor 在处理这些文件时,会将其内部引用替换为混淆后的名称。run_application_logic
和 process_string_data
,PyArmor 默认会保留其原始名称,以确保外部可以正常导入和调用。这是 PyArmor 智能处理的一部分。main_app.pyc
),程序会输出与原始脚本完全相同的结果,证明功能完整无损。深入理解名称混淆的策略:
_name
, __name
):PyArmor 默认会尝试混淆这些名称,因为它们通常被认为是模块内部的实现细节。pyarmor
默认不混淆这些名称,因为它们是模块对外提供的 API。如果混淆了,外部导入和调用会失败。名称混淆是防止静态分析和阅读代码逻辑的第一道屏障。即使攻击者通过某种方式获得了字节码,面对大量无意义的名称,理解代码的执行流程和业务逻辑的难度会呈指数级增长。
在 Python 代码中,敏感信息(如 API 密钥、数据库连接字符串、加密盐、URL 等)经常以字符串常量的形式直接出现在源代码中。即使代码被编译成 .pyc
文件,这些字符串常量通常也能被反编译工具轻易地提取出来。PyArmor 提供了字符串常量混淆功能来解决这个问题。
工作原理:
code object
中的 co_consts
元组,识别出所有的字符串常量。co_consts
中,而是被替换为加密后的字节序列或特殊标记。保护效果:
.pyc
文件或反编译字节码来直接获取敏感字符串。性能考量:
--restrict
等参数来控制混淆的粒度。代码示例:字符串常量混淆效果演示
我们将创建一个包含敏感字符串常量的脚本,并使用 PyArmor 进行混淆,然后尝试观察其在混淆前后的文件内容。
# sensitive_config.py
DATABASE_URL = "mysql://user:password@localhost:3306/prod_db" # 数据库连接 URL
API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 外部 API 密钥
VERSION_INFO = "App v1.0.0 Beta" # 版本信息 (非敏感)
def get_secret_info(): # 定义获取秘密信息的函数
return f"DB_URL: {
DATABASE_URL}, API_KEY_PREFIX: {
API_KEY[:5]}" # 返回部分秘密信息
def display_version(): # 定义显示版本信息的函数
print(f"Application Version: {
VERSION_INFO}") # 打印版本信息
if __name__ == "__main__": # 判断当前脚本是否作为主程序运行
print(get_secret_info()) # 打印秘密信息
display_version() # 打印版本信息
import subprocess # 导入 subprocess 模块,用于执行外部命令
import os # 导入 os 模块,用于文件系统操作
import shutil # 导入 shutil 模块,用于高级文件操作
import logging # 导入 logging 模块
# 配置日志输出
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) # 获取日志器
# 定义原始脚本路径
SENSITIVE_SCRIPT_NAME = "sensitive_config.py" # 敏感脚本文件名
SENSITIVE_SCRIPT_PATH = SENSITIVE_SCRIPT_NAME # 敏感脚本路径
# 创建原始脚本文件
def create_sensitive_script(path): # 定义创建敏感脚本文件的函数
script_content = """
DATABASE_URL = "mysql://user:password@localhost:3306/prod_db"
API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VERSION_INFO = "App v1.0.0 Beta"
def get_secret_info():
return f"DB_URL: {DATABASE_URL}, API_KEY_PREFIX: {API_KEY[:5]}"
def display_version():
print(f"Application Version: {VERSION_INFO}")
if __name__ == "__main__":
print(get_secret_info())
display_version()
"""
with open(path, "w", encoding="utf-8") as f: # 以写入模式打开文件,使用 UTF-8 编码
f.write(script_content) # 写入脚本内容
logger.info(f"已创建原始敏感脚本: {
path}") # 记录日志
# 执行 PyArmor 混淆
def obfuscate_sensitive_script(input_script, output_dir): # 定义混淆敏感脚本的函数
if os.path.exists(output_dir): # 如果输出目录存在
shutil.rmtree(output_dir) # 删除旧目录
logger.info(f"已清理旧的输出目录: {
output_dir}") # 记录日志
# PyArmor 默认会对字符串常量进行混淆,除非在高级配置中明确排除
command = ["pyarmor", "obfuscate", "-O", output_dir, input_script] # 构建 PyArmor 混淆命令
logger.info(f"执行命令: {
' '.join(command)}") # 记录日志
try: # 尝试执行命令
subprocess.run(command, check=True, capture_output=True