PyArmor's 文档

版本:PyArmor 5.6
主页:http://pyarmor.dashingsoft.com/index-zh.html
联系方式:jondy.zhao@gmail.com
作者:赵俊德

PyArmor 是一个用于加密和保护 Python 脚本的工具。它能够在运行时刻保护 Python脚本的二进制代码不被泄露,设置加密后 Python 源代码的有效期限,绑 定加密后的Python源代码到硬盘、网卡等硬件设备。它的保障机制主要包括

  • 加密编译后的代码块,保护模块中的字符串和常量
  • 在脚本运行时候动态加密和解密每一个函数(代码块)的二进制代码
  • 代码块执行完成之后清空堆栈局部变量
  • 通过授权文件限制加密后脚本的有效期和设备环境

PyArmor 支持 Python 2.6, 2.7 和 Python 3

PyArmor 在下列平台进行了充分测试: Windows, Mac OS X, and Linux

PyArmor 已经成功应用于 FreeBSD 和嵌入式系统,例如 Raspberry Pi, Banana Pi, Orange Pi, TS-4600 / TS-7600 等,但是这些 平台下面没有进行充分测试。

内容:

安装 PyArmor

PyArmor 可以直接从这里 PyPi 下载, 但是更方便的方式是通过 pip ,直接 运行下面的命令进行安装:

pip install pyarmor

需要升级的话,执行下面的命令:

pip install --upgrade pyarmor

一旦成功安装,就可以直接运行命令 pyarmor ,例如:

pyarmor --version

这个命令会显示版本信息 PyArmor Version X.Y.Z 或者 PyArmor Trial Version X.Y.Z.

如果命令无法执行,请检查环境变量中是否包含可执行文件所在的路径。

可用的命令

使用 pip 安装之后,有两个可用的命令:

  • pyarmor 这是主要的工具,参考 使用 PyArmor.
  • pyarmor-webui 用来打开一个简单的网页版的可视化界面

如果没有使用 pip 进行安装,上述命令无法使用,需要运行 Python 来执行 相应的脚本。 pyarmor 等价于执行 pyarmor-folder/pyarmor.py, pyarmor-webui 等价于执行 pyarmor-folder/pyarmor-webui.py

使用 PyArmor

命令 pyarmor 的基本语法为:

pyarmor [command] [options]

加密脚本

命令 obfuscate 用来加密脚本。最常用的一种情况是切换到脚本 myscript.py 所在的路径,然后执行:

pyarmor obfuscate myscript.py

PyArmor 会加密 myscript.py 和相同目录下面的所有 *.py 文件:

  • 在用户根目录下面创建 .pyarmor_capsule.zip (仅当不存在的时候创建)
  • 创建输出子目录 dist
  • 生成加密的主脚本 myscript.py 保存在输出目录 dist
  • 加密相同目录下其他所有 *.py 文件,保存到输出目录 dist
  • 生成运行加密脚本所需要的全部辅助文件,保存到输出目录 dist

输出目录 dist 包含运行加密脚本所需要的全部文件:

myscript.py

pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic

除了加密脚本之外,其他文件都叫做 辅助文件 ,它们都是运行加密脚本不 可缺少的文件。

通常情况下第一个脚本叫做主脚本,它加密后的内容如下:

from pytransform import pyarmor_runtime
pyarmor_runtime()

__pyarmor__(__name__, __file__, b'\x06\x0f...')

其中前两行是 引导代码, 它们只在主脚本出现,并且只能被运行一次。对于 其他所有加密脚本,只有这样一行:

__pyarmor__(__name__, __file__, b'\x0a\x02...')

运行加密脚本:

cd dist
python myscript.py

默认情况下,只有和主脚本相同目录的其他 *.py 会被同时加密。如果 想递归加密子目录下的所有 *.py 文件,使用下面的命令:

pyarmor obfuscate --recursive myscript.py

发布加密的脚本

发布加密脚本给客户只需要把输出路径 dist 的所有文件拷贝过去即可,客户 并不需要安装 PyArmor

关于加密脚本的安全性的说明,参考 PyArmor 的安全性

生成新的许可文件

命令 licenses 用来为加密脚本生成新的许可文件 license.lic

默认情况下,加密脚本的同时会在输出目录下面生成一个许可文件 dist/license.lic ,它允许加密脚本运行在任何设备上并且永不过期。

如果需要设置加密脚本的使用期限,首先使用下面的命令生成一个带有效期的认 证文件:

pyarmor licenses --expired 2019-01-01 code-001

执行这条命令,PyArmor 会生成新的许可文件:

  • .pyarmor_capsule.zip 读取相关数据
  • 创建 license.lic ,保存在 licenses/code-001
  • 创建 license.lic.txt ,保存在 licenses/code-001

然后,使用新生成的许可文件覆盖默认的许可文件:

cp licenses/code-001/license.lic dist/

这样,加密脚本在2009年1月1日之后就无法在运行了。

如果想绑定加密脚本到固定机器上,首先在该机器上面运行下面的命令获取硬件 信息:

pyarmor hdinfo

然后在生成绑定到固定机器的许可文件:

pyarmor licenses --bind-disk "100304PBN2081SF3NJ5T" --bind-mac "20:c1:d2:2f:a0:96" code-002

同样,覆盖默认许可文件,这样加密脚本就只能在指定机器上运行:

cp licenses/code-002/license.lic dist/

cd dist/
python myscript.py

扩展其他认证方式

除了上述认证方式之外,还可以在 Python 脚本中增加其他任何认证代码,因为 加密的脚本对于客户来说就是黑盒子。例如,使用网络时间来设置加密脚本的使 用期限等,具体示例可参考 运行时刻模块 pytransform

加密单个模块

如果只需要单独加密一个模块,使用选项 --exact:

pyarmor obfuscate --exact foo.py

这样,就只有 foo.py 被加密,导入这个加密模块:

cd dist
python -c "import foo"

加密整个 Python 包

加密整个 Python 包使用下面的命令:

pyarmor obfuscate --recursive --output dist/mypkg mykpg/__init__.py

使用这个加密的包:

cd dist
python -c "import mypkg"

打包加密脚本

命令 pack 用来打包并加密脚本

首先需要安装 PyInstaller:

pip install pyinstaller

然后运行下面的命令:

pyarmor pack myscript.py

PyArmor 通过以下的步骤将所有需要的文件打包成为一个独立可运行的安装包:

  • 执行 pyarmor obfuscate 加密脚本 myscript.py 和同目录下的所有其他脚本
  • 执行 pyinstaller myscipt.py 创建 myscript.spec
  • 修改 myscript.spec, 把原来的脚本替换成为加密后的脚本
  • 再次执行 pyinstaller myscript.spec ,生成最终的安装包

输出的文件在目录 dist/myscript ,这里面包含了脱离 Python 环境可以运行的所有文件。

运行打包好的可执行文件:

dist/myscript/myscript

检查脚本是否加密。如果加密,下面的第二条命令应该执行失败:

rm dist/myscript/license.lic
dist/myscript/myscript

为加密脚本设置有效期:

pyarmor licenses --expired 2019-01-01 code-003
cp licenses/code-003/license.lic dist/myscript

dist/myscript/myscript

对于复杂的应用,例如传入额外的参数到 Pyinstaller 等,请参考命令 pack如何打包加密脚本

运行时刻模块 pytransform

如果你意识到加密后的脚本对用户来说就是黑盒子,那么有很多事情都可以在 Python 脚本里来做。在这个时候,模块 pytransform 会提供很多有用的 函数和功能。

模块 pytransform 是和加密脚本一起发布,在运行加密脚本之前必须被 导入进来,所以,你可以在你的脚本中直接导入这个模块,使用里面的函数。

内容

exception PytransformError

任何 PyArmor 的 api 失败都会抛出这个异常,传入的参数是错误发生的原因。

get_expired_days()

返回加密脚本的剩余的有效天数。

>0: 剩余的有效天数

-1: 永不过期

注解

如果加密脚本已经过期,会直接抛出异常并退出。加密脚本里面的任何代码 都不会被执行,所以一般情况下这个函数是不会返回 0 的。

get_license_info()

返回一个字典,包含加密脚本的认证文件信息。

常用的键值有: expired, CODE, IFMAC.

其中 expired is == -1 表示认证文件永不过期。

如果已经过期,会抛出异常 PytransformError

get_license_code()

返回字符串,该字符串是生成许可文件时指定的参数。

如果认证文件非法或者无效,抛出异常 PytransformError

get_hd_info(hdtype, size=256)

得到当前机器的硬件信息,通过 hdtype 传入需要获取的硬件类型,可用的 值如下:

HT_HARDDISK 返回硬盘序列号

HT_IFMAC 返回网卡Mac地址

无法获取硬件信息会抛出异常 PytransformError

HT_HARDDISK, HT_IFMAC

调用 get_hd_info() 时候 hdtype 的可以使用的常量

示例

下面是一些示例,拷贝这些代码到需要加密的脚本里面,然后加密脚本,运行加密脚本查看效果。

显示加密脚本的剩余的有效天数

from pytransform import PytransformError, get_license_info, get_expired_days
try:
    code = get_license_info()['CODE']
    left_days = get_expired_days()
    if left_days == -1:
        print('This license for %s is never expired' % code)
    else:
        print('This license for %s will be expired in %d days' % (code, left_days))
except PytransformError as e:
    print(e)

更多内容,请参考 使用插件扩展认证方式

PyArmor 的安全性

PyArmor 使用分片式技术来保护 Python 脚本。所谓分片保护,就是单独加密 每一个函数, 在运行脚本的时候,只有当前调用的函数被解密,其他函数都没有 解密。而一旦函数执行完成,就又会重新加密,这是 PyArmor 的特点之一。

例如,下面这个脚本 foo.py:

def hello():
    print('Hello world!')

def sum(a, b):
    return a + b

if __name == '__main__':
    hello()
    print('1 + 1 = %d' % sum(1, 1))

PyArmor 会首先加密函数 hellosum ,然后在加密整个模块,进行两次 加密。当运行加密的 hello 的时候, sum 依旧是加密的。hello 执行完 成之后,会被重新加密,然后才开始解密并执行 sum

交叉保护机制

PyArmor 的核心代码使用 c 来编写,所有的加密和解密算法都在动态链接库中 实现。 首先 _pytransform 自身会使用 JIT 技术,即动态生成代码的方式 来保护自己,加密后的 Python 脚本由动态库 _pytransform 来保护,反过来, 在加密的 Python 的脚本里面,也会来校验动态库,确保其没有进行任何修改。 这就是交叉保护的原理,Python 代码和 c 代码相互进行校验和保护,大大提 高了安全性。

动态库保护的核心有两点:

  1. 用户不能通过修改代码段指令来获得没有授权的使用。例如,将指令 JZ 修 改为 JNZ ,从而使得认证失败可以继续执行
  2. 加密 Python 脚本使用的键值不能通过反向跟踪的方式获取到

那么, JIT 是如何来做到的呢?

PyArmor 定义了一套自己的指令系统(基于 GNU lightning),然后把核心函数, 主要是获取键值的算法,加解密的过程等,使用自己的指令系统生成数据代码。 数据代码存放在一个单独的 c 文件中,内容如下:

t_instruction protect_set_key_iv = {
    // function 1
    0x80001,
    0x50020,
    ...

    // function 2
    0x80001,
    0xA0F80,
    ...
}

t_instruction protect_decrypt_buffer = {
    // function 1
    0x80021,
    0x52029,
    ...

    // function 2
    0x80001,
    0xC0901,
    ...
}

这是两个受保护的函数,每一个受保护的函数里面会有很多小函数段。随后编译 动态库,计算代码段的校验和,使用这个真实的代码段的校验和替换相关的指令, 并对数据代码进行混淆,修改后的文件如下:

t_instruction protect_set_key_iv = {
    // function 1, 不混淆
    0x80001,
    0x50020,
    ...

    // function 2,混淆下面的数据指令
    0xXXXXX,
    0xXXXXX,
    ...
}

t_instruction protect_decrypt_buffer = {
    // function 1, 不混淆
    0x80021,
    0x52029,
    ...

    // function 2,混淆下面的数据指令
    0xXXXXX,
    0xXXXXX,
    ...
}

使用修改后的文件重新编译生成动态库,这个动态库会发布给客户。

当加密脚本运行的时候,每一次调用被保护的函数的时候,就会进入 JIT 动态 代码保护例程:

  1. 读取 functiion 1 的数据代码,动态生成 function 1

  2. 执行 function 1:

    检查代码段的校验和,如果不一致,退出
    检查当前是否有调试器,如果发现,退出
    检查执行时间是否太长,如果执行时间太长,退出
    如果可能的话,清除硬件断点寄存器
    恢复下一个函数 `function 2` 的数据代码
    
  3. 读取 functiion 2 的数据代码,动态生成 function 2

  4. 重复步骤 2 的操作

这样循环有限次之后,真正受保护的代码才被执行。总之,主要达到的目的是开 始执行受保护的代码的时候,不能被调试器中断。

为了在 Python 端保护动态库没有被进行任何修改,在加密主脚本的时候,会插 入额外的一段代码来检查和保护动态链接库,详细工作原理参考 对主脚本的特殊处理

加密脚本的性能

运行命令 banchmark 可以检查加密脚本的性能:

pyarmor benchmark

下面是输出结果的示例:

INFO     Start benchmark test ...
INFO     Obfuscate module mode: 1
INFO     Obfuscate code mode: 1
INFO     Obfuscate wrap mode: 1
INFO     Benchmark bootstrap ...
INFO     Benchmark bootstrap OK.
INFO     Run benchmark test ...
Test script: bfoo.py
Obfuscated script: obfoo.py
--------------------------------------

load_pytransform: 28.429590911694085 ms
init_pytransform: 10.701080723946758 ms
verify_license: 0.515428636879825 ms
total_extra_init_time: 40.34842417122847 ms

import_no_obfuscated_module: 9.601499631936461 ms
import_obfuscated_module: 6.858413569322354 ms

re_import_no_obfuscated_module: 0.007263492985840059 ms
re_import_obfuscated_module: 0.0058666674116400475 ms

run_empty_no_obfuscated_code_object: 0.015085716201360122 ms
run_empty_obfuscated_code_object: 0.0058666674116400475 ms

run_one_thousand_no_obfuscated_bytecode: 0.003911111607760032 ms
run_one_thousand_obfuscated_bytecode: 0.005307937181960043 ms

run_ten_thousand_no_obfuscated_bytecode: 0.003911111607760032 ms
run_ten_thousand_obfuscated_bytecode: 0.005587302296800045 ms

--------------------------------------
INFO     Remove test path: .\.benchtest
INFO     Finish benchmark test.

其中额外的初始化时间大约是 40ms ,这包括装载动态库、初始化动态库和校 验授权文件的总时间。

上面结果中,导入加密模块的时间还少于导入正常模块的时间,这主要是因为加 密脚本已经被编译成为字节码文件,而原始文件需要额外的时间来进行编译。

这里执行加密函数需要的额外时间一般在 0.002ms 左右,也就是执行 1000 个函数,加密脚本额外消耗的时间大约为 2ms

不同的机器可能结果不同,需要根据实际环境下运行结果来进行评估。

测试不同加密模式的性能,使用下面的方式:

pyarmor benchmark --wrap-mode 0

查看测试命令使用的脚本,使用选项 --debug 保留生成的中间文件:

pyarmor benchmark --debug

了解加密脚本

全局密钥箱

全局密钥箱是存放在用户主目录的一个文件 .pyarmor_capsule.zip 。 当执行命令 pyarmor obfuscate 如果该文件还没有存在,那么会自动创建一 个全局密钥箱,加密脚本和为加密脚本生成认证文件都需要从这里读取相关数据。

加密后的脚本

和原来的脚本相比,被 PyArmor 加密后的脚本需要额外的运行辅助文件,下面是 加密后在输出目录 dist 下的所有文件清单:

myscript.py
mymodule.py

pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic

被加密的脚本也是一个普通的 Python 脚本,模块 dist/mymodule.py 加密后 会是这样:

__pyarmor__(__name__, __file__, b'\x06\x0f...')

而主脚本 dist/myscript.py 被加密后则会是这样:

from pytransform import pyarmor_runtime
pyarmor_runtime()

__pyarmor__(__name__, __file__, b'\x0a\x02...')

引导代码

主脚本的前两行就是 引导代码 ,它一般出现在主脚本中:

from pytransform import pyarmor_runtime
pyarmor_runtime()

运行辅助文件

除了加密脚本之外的其他文件都是 运行辅助文件:

  • pytransform.py, 这是一个普通的 Python 模块
  • _pytransform.so 或者 _pytransform.dll 或者 _pytransform.dylib 是核心的动态链接库
  • pytransform.key 是数据文件
  • license.lic 是加密脚本的许可文件

在客户机上运行加密脚本不需要安装 PyArmor, 但是必须要把所有的 运行辅助 文件 拷贝过去。

加密脚本的许可文件

运行辅助文件中的 license.lic 作用比较特殊,它包含着对加密脚本的运行许 可信息。在加密脚本的同时会在输出目录下面生成一个默认许可文件,该文件允 许加密脚本运行在任何机器并且永不过期。

如果需要为加密脚本设置新的许可,例如设置有效期,那么需要运行命令 pyarmor licenses 生成新的相应的许可文件,然后用新生成的 license.lic 覆盖原来的许可文件。

使用加密脚本的基本原则

  • 加密后的脚本也是一个正常的 Python 脚本,它可以无缝替换原来的脚本

  • 唯一的改变是,在使用加密脚本之前,下面这两行 引导代码 必须被首 先执行:

    from pytransform import pyarmor_runtime
    pyarmor_runtime()
    
  • 运行辅助文件 pytransform.py 必须位于能够被 Python 导入的路径,模块 pytransform 会使用 ctypes 装载动态库。动态库是平台相关的,所有预编 译的动态库列表在这里 支持的平台列表

  • pytransform.py 会在同目录下面搜索动态库 _pytransform ,具体装载过 程可以查看函数 pytransform._load_library 的源代码

  • 所有其他的 运行辅助文件 和动态库要在同一个目录下面

  • 如果 运行辅助文件 位于其他目录,必须在 引导代码 中指出:

    from pytransform import pyarmor_runtime
    pyarmor_runtime('/path/to/runtime-files')
    

加密脚本的运行

加密脚本也是一个正常的 Python 脚本,它可以像运行普通脚本一样被运行:

cd dist
python myscript.py

前两行的 引导代码 会首先被执行:

  • 从文件 pytransform.py 中导入 pyarmor_runtime
  • 执行 pyarmor_runtime ,进行如下操作
    • 使用 ctypes 装载动态链接库 _pytransform
    • 检查许可文件 license.lic 的有效性
    • 添加三个内置函数 __pyarmor__, __enter_armor__, __exit_armor__

然后执行第三条语句:

  • 调用 __pyarmor__ 导入加密的模块
  • 当每一个函数被执行的时候,调用内置函数 __enter_armor__ 恢复被加密的函数
  • 单每一个函数执行完成之后,调用 __exit_armor__ 重新加密函数

详细的执行过程,请参考 如何加密脚本 and 如何运行加密脚本

运行时刻文件 license.licpytransform.key 的查找路径

加密脚本会首先在当前目录查看是否存在 license.lic 和运行时刻文件 pytransform.key, 如果存在,那么使用当前目录下面的这些文件。

其次加密脚本会查看运行时刻模块 pytransform.py 所在的目录,看看有没有 license.licpytransform.key

如果当前目录下面存在任何一个文件,但是和加密脚本对应的运行时刻文件不匹 配,就会报下列错误:

Invalid input packet.
Check license failed.

两个不同类型的 license.lic

PyArmor 中有两个不同类型的 license.lic 文件

  • 一个是 PyArmor 的许可文件,位于 PyArmor 的安装路径下面,它是由 PyArmor 开发者生成并发布
  • 一个是加密脚本的许可文件, 一般和加密脚本在相同的目录,它是由 PyArmor 的用户使用 PyArmor 提供的命令来生成。

这两者之间的关系如下:

license.lic of PyArmor --> .pyarmor_capsule.zip --> license.lic of Obfuscated Scripts

生成加密脚本的许可文件需要读取 全局密钥箱 的数据,而生成 全局密钥箱 ,则需要读取 PyArmor 的许可文件里面的相关数据。

加密模式

PyArmor 提供多种加密模式,以满足安全和性能方面的平衡。通常情况下,默认 的加密模式能够满足绝大多数的需要,一般情况下也无不需要对加密模式有详细 的了解。仅当对性能有特别的要求或者默认加密模式无法满足需求的时候,才需 要改变加密模式,这就需要理解 PyArmor 的不同加密模式。

高级模式

高级模式是从 PyArmor 5.5.0 开始增加的新功能,在这种模式下,加密脚本中 代码块的 PyCode_Type 的结构会被修改,同时在加密脚本运行的时候,会在 Python 动态库中注入一个钩子函数,来处理这种非正常的代码块。在注入钩子 函数的同时,也会检查 Python 解释器是否被修改,如果解释器行为不正常,加 密脚本就不会在继续执行。这个特性需要分析汇编指令,目前只在 X86/X64 系 列的 CPU 上实现,并且还依赖于编译器。一些使用低版本 GCC 编译的 Python 解释器可能无法被 PyArmor 正确识别,所以目前高级模式默认是没有启用的。

使用下面的命令可以启用高级模式加密脚本:

pyarmor obfuscate --advanced foo.py

在下一个主版本中高级模式有可能会默认启用。

对于使用老版本的用户来说,升级之前请确保生产环境的 Python 解释器支持高 级模式,参考下面文档来评估 Python 解释器

https://github.com/dashingsoft/pyarmor-core/tree/v5.3.0/tests/advanced_mode/README.md

建议已经在生产环境中使用加密脚本的用户在下一个主版本高级模式稳定之后在升级。

注解

高级模式在试用版本中的限制是每一个模块中的函数(方法)等代码块总数 不能超过大约 30 个左右,超过这个限制将无法被加密(但是依旧可以使用 普通模式进行加密)。

注解

如果在使用过程中出现正常的 Python 解释器无法在高级模式下运行,欢迎 报告问题到 https://github.com/dashingsoft/pyarmor/issues 或者发送邮 件到 jondy.zhao@gmail.com

代码加密模式

一个 Python 文件中,通常有很多个函数(代码块)

  • obf_code == 0

不会加密函数对应的代码块

  • obf_code == 1 (默认值)

在这种情况下,会加密每一个函数对应的代码块,根据 代码包裹模式 的设置,使用不同的加密方式

  • obf_code == 2

和 obf_mode 1 类似,但是使用更为复杂的算法来加密代码块(bytecode),所 以比前者要慢一些。

代码包裹模式

  • wrap_mode == 0

当包裹模式关闭,代码块使用下面的方式进行加密:

0   JUMP_ABSOLUTE            n = 3 + len(bytecode)

3    ...
     ... 这里是加密后的代码块
     ...

n   LOAD_GLOBAL              ? (__armor__)
n+3 CALL_FUNCTION            0
n+6 POP_TOP
n+7 JUMP_ABSOLUTE            0

在开始插入了一条绝对跳转指令,当执行加密后的代码块时

  1. 首先执行 JUMP_ABSOLUTE, 直接跳转到偏移 n
  2. 偏移 n 处是调用一个 PyCFunction __armor__ 。这个函数的功能会恢复 上面加密后的代码块,并且把代码块移动到最开始(向前移动3个字节)
  3. 执行完函数之后,跳转到偏移 0,开始执行原来的函数。

这种模式下,除了函数的第一次调用需要额外的恢复之外,随后的函数调用就和 原来的代码完全一样。

  • wrap_mode == 1 (默认值)

当打开包裹模式之后,代码块会使用 try...finally 语句包裹起来:

LOAD_GLOBALS    N (__armor_enter__)     N = co_consts 的长度
CALL_FUNCTION   0
POP_TOP
SETUP_FINALLY   X (jump to wrap footer) X = 原来代码块的长度

这里是加密的后的代码块

LOAD_GLOBALS    N + 1 (__armor_exit__)
CALL_FUNCTION   0
POP_TOP
END_FINALLY

这样,当被加密的函数开始执行的时候

  1. 首先会调用 __armor_enter__ 恢复加密后的代码块
  2. 然后执行真正的函数代码
  3. 在函数执行完成之后,进入 final 块,调用 __armor_exit__ 重新加密 函数对应的代码块

在这种模式下,函数的每一次执行完成都会重新加密,每一次执行都需要恢复。

模块加密模式

  • obf_mod == 1 (默认值)

在这种模式下,最终生成的加密脚本如下:

__pyarmor__(__name__, __file__, b'\x02\x0a...', 1)

其中第三个参数是代码块转换而成的字符串,它通过下面的伪代码生成:

PyObject *co = Py_CompileString( source, filename, Py_file_input );
obfuscate_each_function_in_module( co, obf_mode );
char *original_code = marshal.dumps( co );
char *obfuscated_code = obfuscate_algorithm( original_code  );
sprintf( buffer, "__pyarmor__(__name__, __file__, b'%s', 1)", obfuscated_code );
  • obf_mod == 0

在这种模式下,最终生成的代码如下(最后一个参数为 0):

__pyarmor__(__name__, __file__, b'\x02\x0a...', 0)

第三个参数的生成方式和上面的基本相同,除了最后一条语句替换为:

sprintf( buffer, "__pyarmor__(__name__, __file__, b'%s', 0)", original_code );

默认情况下以上三种加密模式都是启用的,如果要改变加密模式,必须使用工程 来加密脚本。具体使用方法请参考工程的 使用不同加密模式

约束模式

从 PyArmor 5.5.6 开始,约束模式有四种形式 。

  • 模式 1

在此约束模式下,加密脚本必须是下面的形式之一:

__pyarmor__(__name__, __file__, b'...')

Or

from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'...')

Or

from pytransform import pyarmor_runtime
pyarmor_runtime('...')
__pyarmor__(__name__, __file__, b'...')

例如,下面的这个加密脚本就无法运行,因为有一条额外的语句 print:

$ cat b.py
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'...')
print(__name__)

$ python b.py
  • 模式 2

在此约束模式下,除了加密脚本不能被修改约束外,主脚本必须是加密脚本,并 且加密脚本不能被非加密脚本导入和使用,一般用于提高 Python 开发的独立的 应用程序的安全性。

例如,使用下面的方式导入使用约束模式 2 加密后的主脚本 foo 会出错:

$ python -c'import foo'
  • 模式 3

在此约束模式下,除了满足约束模式 2 之外,加密脚本里面的函数只能被加密 后的模块(函数)调用。

  • 模式 4

此约束模式和模式 3 基本相似,只是主脚本不需要是加密脚本。一般用于加密 Python 包的部分脚本,以提高加密脚本安全性。

典型的应用是使用约束模式 1 加密 Python 包中 __init__.py 和其他需要被 外部使用的脚本,而使用约束模式 4 来加密那些只是在包内部使用的脚本。

注解

约束模式 2 和 3 不能用于加密 Python 包,否则加密后的包是无法被非加 密的脚本导入的。

注解

约束模式是针对单个脚本的,不同的脚本可以有不同的约束模式。

从 PyArmor 5.2 开始, 约束模式 1 是默认设置。如果需要禁用约束模式, 那么使 用下面的命令加密脚本:

pyarmor obfuscate --restrict=0 foo.py

如果需要使用其他约束模式,使用下面的命令:

pyarmor obfuscate --restrict=2 foo.py
pyarmor obfuscate --restrict=4 foo.py

详细示例请参考 使用约束模式增加加密脚本安全性

PyArmor 的工作原理

让我们看看一个普通的 Python 脚本 foo.py 加密之后是什么样子。下面是加 密脚本所在的目录 dist 下的所有文件列表:

foo.py

pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic

dist/foo.py 是加密后的脚本,它的内容如下:

from pytransform import pyarmor_runtime
pyarmor_runtime()

__pyarmor__(__name__, __file__, b'\x06\x0f...')

所有其他文件叫做 运行辅助文件 ,它们是运行加密脚本所必须的。并 且只要这里面的模块 pytransform.py 能被正常导入进来,加密脚本 dist/foo.py 就可以像正常脚本一样被运行。

这是 PyArmor 的一个重要特征: 加密脚本无缝替换 Python 源代码

如何加密脚本

PyArmor 是怎么加密 Python 源代码呢?

首先把源代码编译成代码块:

char *filename = "foo.py";
char *source = read_file( filename );
PyCodeObject *co = Py_CompileString( source, "<frozen foo>", Py_file_input );

接着对这个代码块进行如下处理

  • 使用 try...finally 语句把代码块的代码段 co_code 包裹起来:

    新添加一个头部,对应于 try 语句:
    
            LOAD_GLOBALS    N (__armor_enter__)     N = length of co_consts
            CALL_FUNCTION   0
            POP_TOP
            SETUP_FINALLY   X (jump to wrap footer) X = size of original byte code
    
    接着是处理过的原始代码段:
    
            对于所有的绝对跳转指令,操作数增加头部字节数
    
            加密修改过的所有指令代码
    
            ...
    
    追加一个尾部,对应于 finally 块:
    
            LOAD_GLOBALS    N + 1 (__armor_exit__)
            CALL_FUNCTION   0
            POP_TOP
            END_FINALLY
    
  • 添加字符串名称 __armor_enter, __armor_exit__co_consts

  • 如果 co_stacksize 小于 4,那么设置为 4

  • co_flags 设置自定义的标志位 CO_OBFUSCAED (0x80000000)

  • 按照上面的方式递归修改 co_consts 中的所有类型为代码块的常量

然后把改装后的代码块转换成为字符串,把字符串进行加密,保护其中的常量和字符串:

char *string_code = marshal.dumps( co );
char *obfuscated_code = obfuscate_algorithm( string_code  );

最后生成加密后的脚本,写入到磁盘文件:

sprintf( buf, "__pyarmor__(__name__, __file__, b'%s')", obfuscated_code );
save_file( "dist/foo.py", buf );

单纯加密后的脚本就是一个正常的函数调用语句,长得就像这个样子:

__pyarmor__(__name__, __file__, b'\x01\x0a...')

如何运行加密脚本

那么,一个普通的 Python 解释器运行加密脚本 dist/foo.py 的过程是什么样呢?

上面我们看到 dist/foo.py 的前两行是这个样子:

from pytransform import pyarmor_runtime
pyarmor_runtime()

这两行叫做 引导代码 ,在运行任何加密脚本之前,它们必须先要被执行。 它们有着重要的使命

  • 使用 ctypes 来装载动态库 _pytransform
  • 检查授权文件 dist/license.lic 是否合法
  • 添加三个内置函数到模块 builtins * __pyarmor__ * __armor_enter__ * __armor_exit__

最主要的是添加了三个内置函数,这样 dist/foo.py 的下一行代码才不会出错, 因为它马上要调用函数 __pyarmor__:

__pyarmor__(__name__, __file__, b'\x01\x0a...')

__pyarmor__ 被调用,它的主要功能是导入加密的模块,实现的伪代码如下:

static PyObject *
__pyarmor__(char *name, char *pathname, unsigned char *obfuscated_code)
{
    char *string_code = restore_obfuscated_code( obfuscated_code );
    PyCodeObject *co = marshal.loads( string_code );
    return PyImport_ExecCodeModuleEx( name, co, pathname );
}

从现在开始,在整个 Python 解释器的生命周期中

  • 每一个函数(代码块)一旦被调用,首先就会执行函数 __armor_enter__ , 它负责恢复代码块。其实现原理如下所示:

    static PyObject *
    __armor_enter__(PyObject *self, PyObject *args)
    {
        // Got code object
        PyFrameObject *frame = PyEval_GetFrame();
        PyCodeObject *f_code = frame->f_code;
    
        // Increase refcalls of this code object
        // Borrow co_names->ob_refcnt as call counter
        // Generally it will not increased  by Python Interpreter
        PyObject *refcalls = f_code->co_names;
        refcalls->ob_refcnt ++;
    
        // Restore byte code if it's obfuscated
        if (IS_OBFUSCATED(f_code->co_flags)) {
            restore_byte_code(f_code->co_code);
            clear_obfuscated_flag(f_code);
        }
    
        Py_RETURN_NONE;
    }
    
  • 因为每一个代码块都被人为的使用 try...finally 块包裹了一下,所以代码 块执行完之后,在返回上一级之前,就会调用 __armor_exit__ 。它会重新加 密代码块,同时清空堆栈内的局部变量:

    static PyObject *
    __armor_exit__(PyObject *self, PyObject *args)
    {
        // Got code object
        PyFrameObject *frame = PyEval_GetFrame();
        PyCodeObject *f_code = frame->f_code;
    
        // Decrease refcalls of this code object
        PyObject *refcalls = f_code->co_names;
        refcalls->ob_refcnt --;
    
        // Obfuscate byte code only if this code object isn't used by any function
        // In multi-threads or recursive call, one code object may be referenced
        // by many functions at the same time
        if (refcalls->ob_refcnt == 1) {
            obfuscate_byte_code(f_code->co_code);
            set_obfuscated_flag(f_code);
        }
    
        // Clear f_locals in this frame
        clear_frame_locals(frame);
    
        Py_RETURN_NONE;
    }
    

对主脚本的特殊处理

和其他模块不一样,PyArmor 对主脚本有额外的处理:

  • 在加密之前,修改主脚本,插入保护代码
  • 在加密之后,修改加密脚本,插入引导代码

在加密主脚本之前,PyArmor 会逐行扫描源代码。如果发现下面的一行:

# {PyArmor Protection Code}

PyArmor 就会把这一行替换成为保护代码。

如果发现了下面这一行:

# {No PyArmor Protection Code}

PyArmor 就不会在主脚本中插入保护代码。

如果上面两个特征行都没有,那么在看看有没有这样的行:

if __name__ == '__main__'

如果有,插入保护代码到这条语句的前面。如果没有,那么不添加保护代码。

默认的保护代码的模板如下:

def protect_pytransform():

    import pytransform

    def check_obfuscated_script():
        CO_SIZES = 49, 46, 38, 36
        CO_NAMES = set(['pytransform', 'pyarmor_runtime', '__pyarmor__',
                        '__name__', '__file__'])
        co = pytransform.sys._getframe(3).f_code
        if not ((set(co.co_names) <= CO_NAMES)
                and (len(co.co_code) in CO_SIZES)):
            raise RuntimeError('Unexpected obfuscated script')

    def check_mod_pytransform():
        CO_NAMES = set(['Exception', 'LoadLibrary', 'None', 'PYFUNCTYPE',
                        'PytransformError', '__file__', '_debug_mode',
                        '_get_error_msg', '_handle', '_load_library',
                        '_pytransform', 'abspath', 'basename', 'byteorder',
                        'c_char_p', 'c_int', 'c_void_p', 'calcsize', 'cdll',
                        'dirname', 'encode', 'exists', 'exit',
                        'format_platname', 'get_error_msg', 'init_pytransform',
                        'init_runtime', 'int', 'isinstance', 'join', 'lower',
                        'normpath', 'os', 'path', 'platform', 'print',
                        'pyarmor_init', 'pythonapi', 'restype', 'set_option',
                        'str', 'struct', 'sys', 'system', 'version_info'])

        colist = []

        for name in ('dllmethod', 'init_pytransform', 'init_runtime',
                     '_load_library', 'pyarmor_init', 'pyarmor_runtime'):
            colist.append(getattr(pytransform, name).{code})

        for name in ('init_pytransform', 'init_runtime'):
            colist.append(getattr(pytransform, name).{closure}[0].cell_contents.{code})
        colist.append(pytransform.dllmethod.{code}.co_consts[1])

        for co in colist:
            if not (set(co.co_names) < CO_NAMES):
                raise RuntimeError('Unexpected pytransform.py')

    def check_lib_pytransform():
        filename = pytransform.os.path.join({rpath}, {filename})
        size = {size}
        n = size >> 2
        with open(filename, 'rb') as f:
            buf = f.read(size)
        fmt = 'I' * n
        checksum = sum(pytransform.struct.unpack(fmt, buf)) & 0xFFFFFFFF
        if not checksum == {checksum}:
            raise RuntimeError("Unexpected %s" % filename)
    try:
        check_obfuscated_script()
        check_mod_pytransform()
        check_lib_pytransform()
    except Exception as e:
        print("Protection Fault: %s" % e)
        pytransform.sys.exit(1)

protect_pytransform()

在加密脚本的时候, PyArmor 会使用真实的值来替换其中的字符串模板 {xxx}

如果不想让 PyArmor 添加保护代码,除了在脚本中添加上面所示的标志行之外, 也可以使用命令行选项 --no-cross-protection ,例如:

pyarmor obfuscate --no-cross-protection foo.py

主脚本被加密之后, PyArmor 会在最前面插入 引导代码

如何打包加密脚本

虽然加密脚本可以无缝替换原来的脚本,但是打包的时候还是存在一个问题:

加密之后所有的依赖包无法自动获取

解决这个问题的基本思路是

  1. 使用没有加密的脚本找到所有的依赖文件
  2. 使用加密脚本替换原来的脚本
  3. 添加加密脚本需要的运行辅助文件到安装包
  4. 替换主脚本,因为主脚本会被编译成为可执行文件

PyArmor 需要 PyInstaller 来完成加密脚本的打包工作,如果没有安装的话,首先执行 下面的命令进行安装:

pip install pyinstaller

PyArmor 提供了一个命令 pack 可以用来直接打包脚本,它会首先加密脚本,然后 调用 PyInstaller 打包,但是在某些情况下,可以打包会失败。这里详细描述了命令 pack 的内部工作原理,可以帮助定位问题所在,同时也可以作为自己直接使用 PyInstaller 打包加密脚本的使用方法。

pyarmor pack 命令的第一步是加密所有的脚本,保存到 dist/obf:

pyarmor obfuscate --output dist/obf hello.py

第二步是生成 .spec 文件,这是 PyInstaller 需要的,把加密脚本需要的运行辅助文 件也添加到里面:

pyinstaller --add-data dist/obf/license.lic
            --add-data dist/obf/pytransform.key
            --add-data dist/obf/_pytransform.*
            hello.py dist/obf/hello.py

第三步是修改 hello.spec, 在 Analysis 之后插入下面的语句,主要作用是打包的时 候使用加密后的脚本,而不是原来的脚本:

a.scripts[-1] = 'hello', r'dist/obf/hello.py', 'PYSOURCE'
for i in range(len(a.pure)):
    if a.pure[i][1].startswith(a.pathex[0]):
        x = a.pure[i][1].replace(a.pathex[0], os.path.abspath('dist/obf'))
        if os.path.exists(x):
            if hasattr(a.pure, '_code_cache'):
                with open(x) as f:
                    a.pure._code_cache[a.pure[i][0]] = compile(f.read(), a.pure[i][1], 'exec')
            a.pure[i] = a.pure[i][0], x, a.pure[i][2]

最后运行这个修改过的文件,生成最终的安装包:

pyinstaller --clean -y hello.spec

注解

必须要指定选项 --clean ,否则不会把原来的脚本替换成为加密脚本。

检查一下安装包中的脚本是否已经加密:

# It works
dist/hello/hello.exe

rm dist/hello/license.lic

# It should not work
dist/hello/hello.exe

使用工程

工程是一个包含配置文件的目录,可以用来方便的管理加密脚本。

使用工程管理脚本的有下列优点:

  • 可以递增式加密脚本,仅仅加密修改过的脚本,适用于需要加密脚本很多的项目
  • 定制选择工程包含的脚本文件,而不是一个目录下全部脚本
  • 设置加密模式和定制保护代码
  • 更加方便的管理加密脚本

使用工程管理加密脚本

首先使用命令 init 创建一个工程:

cd examples/pybench
pyarmor init --entry=pybench.py

这会在当前目录下面创建一个工程配置文件 .pyarmor_config 。 也可 以在其他目录创建一个工程:

pyarmor init --src=examples/pybench --entry=pybench.py projects/pybench

新创建的工程配置文件存放在 projects/pybench

使用工程的方式一般是切换当前路径到工程目录,然后运行工程相关命令:

cd projects/pybench
pyarmor info

也可以直接在代码所在路径创建一个工程:

pyarmor info

使用下面的命令加密工程中包含的所有脚本:

pyarmor build

当某些脚本修改之后,再次运行 build ,加密这些修改过的脚本:

pyarmor build

选择设定工程脚本使用 --manifest 选项。下面示例是把 dist, test 目录下面的所有 .py 排除在工程之外:

pyarmor config --manifest "include *.py, prune dist, prune test"

默认情况下 build 仅仅加密修改过的文件,强制加密所有脚本:

pyarmor build --force

运行加密后的脚本:

cd dist
python pybench.py

使用不同加密模式

配置不同的加密模式:

pyarmor config --obf-mod=1 --obf-code=0

使用新的加密模式重新加密脚本:

pyarmor build -B

工程配置文件

每一个工程都有一个 JSON 格式的工程配置文件,它包含的属性如下:

  • name

    工程名称

  • title

    工程标题

  • src

    工程所包含脚本的路径,通常情况下是一个绝对路径

  • manifest

    选择和设置工程包含的脚本,其支持的格式和 Python Distutils 中的 MANIFEST.in 是一样的。默认值为 src 下面的所有 .py 文件:

    global-include *.py
    

    多个模式使用逗号分开,例如:

    global-include *.py, exclude __mainfest__.py, prune test
    

    关于所有支持的模式,参考 https://docs.python.org/2/distutils/sourcedist.html#commands

  • is_package

    可用值: 0, 1, None

    主要会影响到加密脚本的保存路径,如果设置为 1,那么输出路径会额外包 含包的名称。

  • restrict_mode

    加密脚本的约束模式,可用值: 0, 1, 2, 3, 4

    默认值为 1,即加密脚本不能被修改。

    参考 约束模式

注解

属性 disable_restrict_mode 从 v5.5.6 之后不再使用,而是转换为等价 的 restrict_mode

  • entry

    工程的主脚本,可以是多个,以逗号分开:

    main.py, another/main.py, /usr/local/myapp/main.py
    

    主脚本可以是绝对路径,也可以是相对路径,相对于工程路径。

  • output

    输出路径,保存加密后的脚本和运行辅助文件,相对于工程路径。

  • capsule

    工程使用的密钥箱,默认是 全局密钥箱

  • obf_code

    是否加密每一个函数(代码块):

    • 0

    不加密

    • 1

    加密每一个函数

    • 2

    使用比模式 1 更复杂的算法来加密每一个函数

    默认值为 1,参考 代码加密模式

  • wrap_mode

    是否使用 try..final 结构包裹原理的代码块

    • 0

    不包裹

    • 1

    包裹每一个代码块

    默认值为 1,参考 包裹模式

  • obf_mod

    是否加密整个模块:

    • 0

    不加密

    • 1

    加密模块

    默认值为 1,参考 模块加密模式

  • cross_protection

    是否在主脚本插入交叉保护代码:

    • 0

    不插入

    • 1

    插入默认的保护代码,参考 对主脚本的特殊处理

    • 文件名称

    使用文件指定的自定义模板

  • runtime_path

    None 或者任何路径名称

    用来告诉加密脚本去哪里装载动态库 _pytransform

    默认值为 None , 是指和模块 pytransform.py 在相同的路径。

    主要用于使用打包工具(例如 py2exe)把加密脚本压缩到一个 .zip 文 件的时候,无法正确定位动态库,这时候把 runtime_path 设置为空字符 串可以解决这个问题。

  • plugins

    None 或者列表

    用来扩展加密脚本的认证方式,支持多个插件,例如:

    plugins: ["check_ntp_time", "show_license_info"]
    

    关于插件的使用实例,请参考 使用插件扩展认证方式

加密脚本和原脚本的区别

加密脚本和原来的脚本相比,存在下列一些的不同:

  • 运行加密脚本的 Python 主版本和加密脚本使用的 Python 主版本应该要一致, 因为加密的脚本实际上已经是 .pyc 文件,如果主版本不一致,有些指令无 法识别或者会出错。尤其是 Python3.6,在这个版本引入了新的指令系统,所 以和 Python3.5 以及之前的版本完全不同。

  • 执行加密角本的 Python 不能是调试版,准确的说,不能是设置了 Py_TRACE_REFS 或者 Py_DEBUG 生成的 Python

  • 使用 sys.settrace, sys.setprofile, threading.settracethreading.setprofile 设置的回调函数在加密脚本中将被忽略

  • 代码块的属性 __file__ 在加密脚本是 <frozen name> ,而不是文件 名称,在异常信息中会看到文件名的显示是 <frozen name>

    需要注意的是模块的属性 __file__ 还和原来的一样,还是文件名称。加 密下面的脚本并运行,就可以看到输出结果的不同:

    def hello(msg):
        print(msg)
    
    # The output will be 'foo.py'
    print(__file__)
    
    # The output will be '<frozen foo>'
    print(hello.__file__)
    

第三方解释器的支持

对于第三方的解释器(例如 Jython 等)以及通过嵌入 Python C/C++ 代码调用 加密脚本,需要满足下列条件:

  • 第三方解释器或者嵌入的 Python 代码必须装载 Python 官方的动态库,动态 库的源代码在 https://github.com/python/cpython ,并且核心代码不能被 修改,修改后的代码可能会导致加密脚本无法执行。
  • 在 Linux 下面 装载 Python 动态库 libpythonXY.so 的时候 dlopen 必 须设置 RTLD_GLOBAL ,否则加密脚本无法运行。

注解

Boost::python,默认装载 Python 动态库是没有设置 RTLD_GLOAL 的,运 行加密脚本的时候会报错 "No PyCode_Type found" 。解决方法就是在初始 化的调用方法 sys.setdlopenflags(os.RTLD_GLOBAL) ,这样就可以共享 动态库输出的函数和变量。

  • 模块 ctypes 必须存在并且 ctypes.pythonapi._handle 必须被设置为 Python 动态库的句柄,PyArmor 会通过该句柄获取 Python C API 的地址。

高级用法

加密和使用多个包

假定有三个包 pkg1, pkg2, pkg2 需要加密,使用公共的运行辅助文件, 然后可以从其他脚本导入这些加密的包。

首先切换到工作路径,创建三个工程:

mkdir build
cd build

pyarmor init --src /path/to/pkg1 --entry __init__.py pkg1
pyarmor init --src /path/to/pkg2 --entry __init__.py pkg2
pyarmor init --src /path/to/pkg3 --entry __init__.py pkg3

生成公共的运行辅助文件,保存在 dist 目录下面:

pyarmor build --output dist --only-runtime pkg1

分别加密三个包,也保存到 dist 下面:

pyarmor build --output dist --no-runtime pkg1
pyarmor build --output dist --no-runtime pkg2
pyarmor build --output dist --no-runtime pkg3

查看并使用加密的包:

ls dist/

cd dist
python -c 'import pkg1
import pkg2
import pkg3'

跨平台发布加密脚本

因为加密脚本的运行文件中有平台相关的动态库,所以跨平台发布需要指定目标 平台。

首先使用命令 download 查看和下载目标平台的动态库文件:

pyarmor download --list
pyarmor download linux_x86_64

然后在加密脚本的时候指定目标平台名称:

pyarmor obfuscate --platform linux_x86_64 foo.py

如果使用工程,那么:

pyarmor build --platform linux_x86_64

使用不同版本 Python 加密脚本

如果装了多个版本的 Python ,那么使用 pip 安装的 pyarmor 使用的是默 认的 Python 版本。如果需要使用其他版本的 Python 来加密脚本,需要显示指 定 Python 解释器。

例如,首先找到 pyarmor.py 的位置:

find /usr/local/lib -name pyarmor.py

通常在大多数 linux 系统,它会在 /usr/local/lib/python2.7/dist-packages/pyarmor

然后使用下面的方式运行:

/usr/bin/python3.6 /usr/local/lib/python2.7/dist-packages/pyarmor/pyarmor.py

也可以创建一个便捷脚本 /usr/local/bin/pyarmor3 ,内容如下:

/usr/bin/python3.6 /usr/local/lib/python2.7/dist-packages/pyarmor/pyarmor.py "$*"

赋予其执行权限:

chmod +x /usr/local/bin/pyarmor3

然后就可以直接使用 pyarmor3

在 Windows 下面就需要创建一个批处理 pyarmor3.bat ,内容如下:

C:\Python36\python C:\Python27\Lib\site-packages\pyarmor\pyarmor.py %*

让 Python 自动识别加密脚本

下面有几种情况可能会需要让 Python 自动识别加密脚本:

  • 几乎所有的脚本都会被作为主脚本来运行
  • 在加密脚本中使用模块 multiprocessing 创建新进程
  • 使用到 Popen 或者 os.exec 等调用加密后的脚本
  • 其他任何需要在很多脚本里面插入引导代码的情况

一种解决方案就是为每一个相关的加密脚本添加引导代码,但是这会有些麻烦。 另外一种比较简单的解决方案就是让 Python 能够自动识别加密脚本,这样任何 一个加密脚本不需要引导代码就可以正常运行。

下面是基本操作步骤:

  1. 首先生成运行时刻文件,可以随便加密一个简单脚本,在 dist 目录下面 会找到四个运行时刻需要的文件:
  • pytransform.py
  • pytransform.key
  • _pytransform.so (.dll or .dylib)
  • license.lic
  1. lib/site-packages (Windows) 或者 lib/pythonX.Y/site-packages (Linux) 下面创建一个子目录 pytransform

  2. 拷贝四个文件到新创建的子目录,并且把 pytransform.py 重命名为 __init__.py

  3. 编辑文件 lib/site.py (Windows) 或者 lib/pythonX.Y/site.py , 在 行 if __name__ == '__main__': 的前面增加两行代码:

    from pytransform import pyarmor_runtime
    pyarmor_runtime()
    

也可以把这两行代码添加到 site.main 里面,总之,只要能得到执行就可以。

这样就可以使用 python 直接运行加密脚本了。 这主要使用到了 Python 在 启动过程中默认会自动导入模块 site 的特性来实现,参考

https://docs.python.org/3/library/site.html

使用不同的模式来加密脚本

高级模式 是从 PyArmor 5.5.0 引入的新特性,默认情况下是没有启用 的。如果需要使用高级模式来加密脚本,额外指定选项 --advanced:

pyarmor obfuscate --advanced foo.py

从 PyArmor 5.2 开始, 约束模式 是默认设置。如果需要禁用约束模式, 那么使用下面的命令加密脚本:

pyarmor obfuscate --restrict=0 foo.py

指定 代码加密模式, 代码包裹模式, 模块加密模式 需 要 使用工程 来加密脚本,直接使用命令 obfuscate 无法改变 这些加密模式。例如:

pyarmor init --src=src --entry=main.py .
pyarmor config --obf-mod=1 --obf-code=1 --wrap-mode=0
pyarmor build

使用插件扩展认证方式

PyArmor 可以通过插件来扩展加密脚本的认证方式,例如检查网络时间而不是本 地时间来校验有效期。

首先定义插件文件 check_ntp_time.py:

# 当调试这个脚本的时候(还没有加密),需要把下面的两行代码前面的注释
# 去掉,否则在无法使用 pytransform 模块的功能
# from pytransform import pyarmor_init
# pyarmor_init()

from ntplib import NTPClient
from time import mktime, strptime
import sys

def get_license_data():
    from ctypes import py_object, PYFUNCTYPE
    from pytransform import _pytransform
    prototype = PYFUNCTYPE(py_object)
    dlfunc = prototype(('get_registration_code', _pytransform))
    rcode = dlfunc().decode()
    index = rcode.find(';', rcode.find('*CODE:'))
    return rcode[index+1:]

def check_expired():
    NTP_SERVER = 'europe.pool.ntp.org'
    EXPIRED_DATE = get_license_data()
    c = NTPClient()
    response = c.request(NTP_SERVER, version=3)
    if response.tx_time > mktime(strptime(EXPIRED_DATE, '%Y%m%d')):
        sys.exit(1)

然后在主脚本 foo.py 插入下列两行注释:

...

# {PyArmor Plugins}

...

def main():
    # PyArmor Plugin: check_expired()

if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO)
    main()

执行下面的命令进行加密:

pyarmor obfuscate --plugin check_ntp_time foo.py

这样,在加密之前,文件 check_ntp_time.py 会插入到第一个注释标 志行之后:

# {PyArmor Plugins}

... check_ntp_time.py 的文件内容

同时,第二个注释标志行的注释标志会被删除,替换后的内容为:

def main():
    # PyArmor Plugin: check_expired()
    check_expired()

这样插件输出的函数就可以被脚本调用。

插件对应的文件一般存放在当前目录,如果存放在其他目录的话,可以指定绝对 路径,例如:

pyarmor obfuscate --plugin /usr/share/pyarmor/check_ntp_time foo.py

也可以设置环境变量 PYARMOR_PLUGIN ,例如:

export PYARMOR_PLUGIN=/usr/share/pyarmor/plugins
pyarmor obfuscate --plugin check_ntp_time foo.py

最后为加密脚本生成许可文件,使用 -x 把自定义的有效期存储到认证文件:

pyarmor licenses -x 20190501 MYPRODUCT-0001
cp licenses/MYPRODUCT-0001/license.lic dist/

注解

为了提高安全性,可以要把 ntplib.py 内容全部拷贝过来,这样就不需要 从外部导入 NTPClient

打包加密脚本成为一个单独的可执行文件

使用下面的命令可以把脚本 foo.py 加密之后并打包成为一个单独的可执行文 件:

pyarmor pack -e " --onefile" foo.py

其中 --onefilePyInstaller 的选项,使用 -e 可以传递任何 Pyinstaller 支持的选项,例如,指定可执行文件的图标:

pyarmor pack -e " --onefile --icon logo.ico" foo.py

如果不想把加密脚本的许可文件 license.lic 打包到可执行文件,而是和可 执行文件放在一起,这样方便为不同的用户生成不同的许可文件。那么需要使用 PyInstaller 提供的 --runtime-hook 功能在加密脚本运行之前把许可文件 拷贝到指定目录,下面是操作步骤:

  1. 新建一个文件 copy_license.py:

    import sys
    from os.path import join, dirname
    with open(join(dirname(sys.executable), 'license.lic'), 'rb') as src:
        with open(join(sys._MEIPASS, 'license.lic'), 'wb') as dst:
            dst.write(src.read())
    
  2. 运行下面的命令打包加密脚本:

    pyarmor pack --clean --without-license \
            -e " --onefile --icon logo.ico --runtime-hook copy_license.py" foo.py
    

选项 --without-license 告诉 pyamor 不要把加密脚本的许可文件打包进 去,使用 PyInstaller 的选项 --runtime-hook 可以让打包好的可执行文 件,在启动的时候首先去调用 copy_licesen.py ,把许可文件拷贝到相应的 目录。

命令执行成功之后,会生成一个打包好的文件 dist/foo.exe

尝试运行这个可执行文件,应该会报错。

  1. 使用命令 pyarmor licenses 生成新的许可文件,并拷贝到 dist/ 下面
  2. 这时候在双击运行 dist/foo.exe

使用约束模式增加加密脚本安全性

默认约束模式仅限制不能修改加密脚本,为了提高安全性,可以使用约束模式 2 来加密 Python 应用程序,例如:

pyarmor obfuscate --restrict 2 foo.py

约束模式 2 不允许从没有加密的脚本中导入加密的脚本,从而更高程度的保护 了加密脚本的安全性。

如果对安全性要求更高,可以使用约束模式 3 ,例如:

pyarmor obfuscate --restrict 3 foo.py

约束模式 3 会检查每一个加密函数的调用,不允许加密的函数被非加密的脚本 调用。

上述两种模式并不适用于 Python 包的加密,因为对于 Python 包来说,必须允 许加密的脚本被其他非加密的脚本导入和调用。为了提高 Python 包的安全性, 可以采取下面的方案:

  • 把需要供外部使用的函数集中到包的某一个或者几个文件
  • 使用约束模式 1 加密这些需要被外部调用的文件
  • 使用约束模式 4 加密其他的脚本文件

例如:

cd /path/to/mypkg
pyarmor obfuscate --exact __init__.py exported_func.py
pyarmor obfuscate --restrict 4 --recursive \
        --exclude __init__.py --exclude exported_func.py .

关于约束模式的详细说明,请参考 约束模式

检查被调用的函数是否经过加密

假设主脚本为 main.py, 需要调用模块 foo.py 里面的方法 connect, 并 且需要传递敏感数据作为参数。两个脚本都已经被加密,但是用户可以自己写一 个 foo.py 来代替加密的 foo.py ,例如:

def connect(username, password):
    print('password is %s', password)

然后调用加密的主脚本 main.py ,虽然功能不能正常完成,但是敏感数据却 被泄露。

为了避免这种情况发生,需要在主脚本里面检查 foo.py 必须也是被加密的脚 本。目前的解决方案是在脚本 main.py 里面增加修饰函数 assert_armored ,例如:

import foo

# 新增的修饰函数
def assert_armored(*names):
    def wrapper(func):
        def _execute(*args, **kwargs):
            for s in names:
                # For Python2
                # if not (s.func_code.co_flags & 0x20000000):
                # For Python3
                if not (s.__code__.co_flags & 0x20000000):
                    raise RuntimeError('Access violate')
            return func(*args, **kwargs)
        return _execute
    return wrapper

# 使用修饰函数,把需要检查的函数名称都作为参数传递进去
@assert_armored(foo.connect, foo.connect2)
def start_server():
    foo.connect('root', 'root password')
    foo.connect2('user', 'user password')

这样在每次运行 start_server 之前,都会检查被调用的函数是否被加密,如 果没有被加密,直接抛出异常。

命令手册

PyArmor 是一个命令行工具,用来加密脚本,绑定加密脚本到固定机器或者设置加密脚本的有效期。

pyarmor 的语法格式:

pyarmor <command> [options]

常用的命令包括:

obfuscate    加密脚本
licenses     为加密脚本生成新的许可文件
pack         打包加密脚本
hdinfo       获取硬件信息

和工程相关的命令:

init         创建一个工程,用于管理需要加密的脚本
config       修改工程配置信息
build        加密工程里面的脚本

info         显示工程信息
check        检查工程配置信息是否正确

其他不常使用的命令:

benchmark    测试加密脚本的性能
register     生效注册文件
download     查看和下载预编译的动态库

可以运行 pyarmor <command> -h 查看各个命令的详细使用方法。

obfuscate

加密 Python 脚本。

语法:

pyarmor obfuscate <options> SCRIPT...

选项

-O, --output PATH
 输出路径,默认是 dist
-r, --recursive
 递归模式加密所有的脚本
--exclude PATH 在递归模式下排除某些目录,多个目录使用逗号分开,或者使用该选项多次
--exact 只加密命令行中列出的脚本
--no-bootstrap 在主脚本中不要插入引导代码
--no-cross-protection
 在主脚本中不要插入交叉保护代码
--plugin NAME 在加密之前,向主脚本中插入代码
--platform NAME
 指定运行加密脚本的平台
--advanced 使用高级模式加密脚本
--restrict <0,1,2,3,4>
 设置约束模式
--package-runtime <0,1>
 是否保存运行文件到一个单独的目录

描述

PyArmor 首先检查用户根目录下面是否存在 .pyarmor_capsule.zip , 如果不存在,那么创建一个新的。

接着搜索需要加密的脚本,共有三种搜索模式:

  • 默认模式: 搜索和主脚本相同目录下面的所有 .py 文件
  • 递归模式: 递归搜索和主脚本相同目录下面的所有 .py 文件
  • 精准模式: 仅仅加密命令行中列出的脚本

PyArmor 会修改主脚本,插入交叉保护代码,然后把搜索到脚本全部加密,保存 到输出目录 dist

在为加密脚本生成默认的许可文件 license.lic 以及所有其他的 运行辅助文件 ,也到保存到输出目录 dist

最后插入 引导代码 到主脚本。

如果命令行有多个脚本的话,除了第一个脚本,不会在其他脚本中插入引导代码和交叉保护 代码。

选项 --plugin 主要用于扩展加密脚本的授权方式,例如检查网络时间来校验有效期等, 这个选项指定的插件名称会在加密之前插入到主脚本中。插件对应的文件为当前目录下面 名称.py ,如果插件存放在其他路径,可以使用绝对路径指定插件名称,也可以设置环境 变量 PYARMOR_PLUGIN 为相应路径名称。

关于插件的使用实例,请参考 使用插件扩展认证方式

选项 --platform 用于指定加密脚本的运行平台,仅用于跨平台发布。因为加密脚本的运 行文件中包括平台相关的动态库,所以跨平台发布需要指定该选项。

选项 --restrict 用于指定加密脚本的约束模式,关于约束模式的详细说明,参考 约束模式

如果选项 --package-runtime 设置为 1 ,那么所有运行时刻文件会作为包 保存在一个单独的目录 pytransform 下面:

pytransform/
    __init__.py
    _pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
    pytransform.key
    license.lic

其他任何情况都是和加密脚本在相同的目录下面:

pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic

示例

  • 加密当前目录下的所有 .py 脚本,保存到 dist 目录:

    pyarmor obfuscate foo.py
    
  • 递归加密当前目录下面的所有 .py 脚本,保存到 dist 目录:

    pyarmor obfuscate --recursive foo.py
    
  • 除了 builddist 之外,递归加密当前目录下面的所有 .py 脚本, 保存到 dist 目录:

    pyarmor obfuscate --recursive --exclude build,dist foo.py
    pyarmor obfuscate --recursive --exclude build --exclude tests foo.py
    
  • 仅仅加密两个脚本 foo.py, moda.py:

    pyarmor obfuscate --exact foo.py moda.py
    
  • 加密包 mypkg 所在目录下面的所有 .py 文件:

    pyarmor obfuscate --output dist/mypkg mypkg/__init__.py
    
  • 加密当前目录下面所有的 .py 文件,但是不要插入交叉保护代码到主脚本 dist/foo.py:

    pyarmor obfuscate --no-cross-protection foo.py
    
  • 加密当前目录下面所有的 .py 文件,但是不要插入引导代码到主脚本 dist/foo.py:

    pyarmor obfuscate --no-bootstrap foo.py
    
  • 在加密 foo.py 之前,把当前目录下面的 check_ntp_time.py 的内容 插入到 foo.py 中:

    pyarmor obfuscate --plugin check_ntp_time foo.py
    
  • 在 MacOS 平台下加密脚本,这些加密脚本将在 Ubuntu 下面运行,使用下面 的命令进行加密:

    pyarmor download --list
    pyarmor download linux_x86_64
    
    pyarmor obfuscate --platform linux_x86_64 foo.py
    
  • 使用高级模式加密脚本:

    pyarmor obfuscate --advanced foo.py
    
  • 使用约束模式 2 加密脚本:

    pyarmor obfuscate --restrict 2 foo.py
    
  • 使用约束模式 4 加密当前目录下面除了 __init__.py 之外的所有 .py 文件:

    pyarmor obfuscate --restrict 4 --exclude __init__.py --recursive .
    

licenses

为加密脚本生成新的许可文件

语法:

pyarmor licenses <options> CODE

选项

-O OUTPUT, --output OUTPUT
 输出路径
-e YYYY-MM-DD, --expired YYYY-MM-DD
 加密脚本的有效期
-d SN, --bind-disk SN
 绑定加密脚本到硬盘序列号
-4 IPV4, --bind-ipv4 IPV4
 绑定加密脚本到指定IP地址
-m MACADDR, --bind-mac MACADDR
 绑定加密脚本到网卡的Mac地址
-x, --bind-data DATA
 用于扩展认证类型的时候传递认证数据信息

描述

运行加密脚本必须有一个认证文件 license.lic 。一般在加密脚本的 同时,会自动生成一个缺省的认证文件。但是这个缺省的认证文件允许加密脚本 运行在任何机器并且永不过期。如果你需要对加密脚本进行限制,那么需要使用 该命令生成新的许可文件,并覆盖原来的许可文件。

例如,下面的命令生成一个有使用期限的认证文件:

pyarmor licenses --expired 2019-10-10 mycode

生成的新的认证文件保存在默认输出路径和注册码组合路径 licenses/mycode 下面,使用这个新的许可文件覆盖默认的许可文件:

cp licenses/mycode/license.lic dist/

另外一个例子,限制加密脚本在固定 Mac 地址,同时设置使用期限:

pyarmor licenses --expired 2019-10-10 --bind-mac 2a:33:50:46:8f tom
cp licenses/tom/license.lic dist/

在这之前,一般需要运行命令 hdinfo 得到硬件的相关信息:

pyarmor hdinfo

选项 -x 可以把任意字符串数据存放到许可文件里面,主要用于自定义认证类 型的时候,传递参数给自定义认证函数。例如:

pyarmor licenses -x "2019-02-15" tom

然后在加密脚本中,可以从认证文件的信息中查询到传入的数据。例如:

from pytransfrom import get_license_info
info = get_license_info()
print(info['DATA'])

注解

这里有一个实际使用的例子 使用插件扩展认证方式

pack

加密并打包脚本

语法:

pyarmor pack <options> SCRIPT

选项

-t TYPE, --type TYPE
 cx_Freeze, py2exe, py2app, PyInstaller(default).
-O OUTPUT, --output OUTPUT
 输出路径
-e OPTIONS, --options OPTIONS
 运行外部打包工具的额外参数
-x OPTIONS, --xoptions OPTIONS
 加密脚本的额外参数
--clean 打包之前删除上一次生成的中间文件
--without-license
 不要将加密脚本的许可文件打包进去
--debug 不要删除打包过程生成的中间文件

描述

PyArmor 首先调用第三方工具(例如,PyInstaller)对脚本打包,得到相关的 依赖文件。

接着加密主脚本所在路径下面所有 .py 文件,注意依赖的库中的 .py 文件不会被加密的。

然后使用加密脚本替换原来的脚本。

最后在把所有的文件打包到一起。

选项 --options 用来传递额外的参数给外部打包工具。例如,PyInstaller 是通过下面的方式调用的:

pyinstaller --distpath DIST -y EXTRA_OPTIONS SCRIPT

其中 EXTRA_OPTIONS 会被该选项所替换。

选项 --xoptions 用来传递额外的参数来加密脚本。 pack 使用下面的命令来加密脚本:

pyarmor obfuscate -r --output DIST EXTRA_OPTIONS SCRIPT

其中 EXTRA_OPTIONS 会被该选项所替换。

更多详细说明,请参考 如何打包加密脚本.

重要

不要使用 pack 打包加密过的脚本,直接打包原来的脚本就可以

示例

  • 加密脚本 foo.py 并打包到 dist/foo 下面:

    pyarmor pack foo.py
    
  • 传递额外的参数运行 PyInstaller:

    pyarmor pack --options '-w --icon app.ico' foo.py
    
  • 打包的时候不要加密目录 venvtest 下面的所有文件:

    pyarmor pack -x " --exclude venv --exclude test" foo.py
    
  • 使用高级模式加密脚本,然后打包成为一个可执行文件:

    pyarmor pack -e " --onefile" -x " --advanced" foo.py
    
  • 如果使用了 PyInstaller 的选项 -n 改变了打包文件的名称,必须同时 使用选项 -s, 例如:

    pyarmor pack -e " -n my_app" -s "my_app.spec" foo.py
    

hdinfo

显示当前机器的硬件信息,例如硬盘序列号,网卡Mac地址等。

这些信息主要用来为加密脚本生成许可文件的时候使用。

语法:

pyarmor hdinfo

如果没有装 pyarmor, 也可以在这里下载获取硬件信息的小工具 hdinfo

然后直接运行:

hdinfo

获取得到的硬件信息和这里显示的是一样的。

init

创建管理加密脚本的工程文件。

语法:

pyarmor init <options> PATH

选项:

-t, --type <auto,app,pkg>
 工程类型,默认是 auto
-s, --src SRC 脚本所在路径,默认是当前路径
-e, --entry ENTRY
 主脚本名称

描述

这个命令会在 PATH 指定的路径创建一个工程配置文件 .pyarmor_config ,这个一个 JSON 格式的文件。

如果选项 --typeauto (也是默认情况),那么工程类型根据主脚本命 令来判断。如果主脚本是 __init__.py , 那么工程类型就是 pkg , 否则就 是 app

如果新的工程类型为 pkg ,不管是自动判断还是选项指定, init 命令都 会设置工程属性 is_package1 ,这个属性的默认值是 0

工程创建之后,可以使用命令 config 进行修改和配置。

示例

  • 在当前路径创建一个工程:

    pyarmor init --entry foo.py
    
  • 创建一个工程在构建路径 obf:

    pyarmor init --entry foo.py obf
    
  • 创建一个 pkg 类型的工程:

    pyarmor init --entry __init__.py
    
  • 在构建路径 obf 创建一个工程,管理在 /path/to/src 处的脚本:

    pyarmor init --src /path/to/src --entry foo.py obf
    

config

修改工程配置。

语法:

pyarmor config <options> [PATH]

选项

--name NAME 内部名称
--title TITLE 显示标题
--src SRC 脚本所在路径
--output OUTPUT
 保存加密脚本的输出路径
--manifest TEMPLATE
 过滤脚本的模板语句
--entry SCRIPT 工程主脚本,可以多个,使用逗号分开
--is-package <0,1>
 管理的脚本是一个 Python 包类型
--restrict-mode <0,1,2,3,4>
 设置约束模式
--obf-mod <0,1>
 是否加密整个模块对象
--obf-code <0,1>
 是否加密每一个函数
--wrap-mode <0,1>
 是否启用包裹模式加密函数
--advanced-mode <0,1>
 是否使用高级模式加密脚本
--cross-protection <0,1>
 是否插入交叉保护代码到主脚本
--runtime-path RPATH
 设置运行文件所在路径
--plugin NAME 设置需要插入到主脚本的代码文件

描述

在工程所在路径运行该命令,修改一个或者多个工程属性:

pyarmor config --option new-value

或者在命令的最后面指定工程所在的路径:

pyarmor config --option new-value /path/to/project

选项 --entry 用来指定工程主脚本,可以是多个,以逗号分开:

main.py, another/main.py, /usr/local/myapp/main.py

主脚本可以是绝对路径,也可以是相对路径,相对于 src 指定的路径。

选项 --manifest 用来选择和设置工程包含的脚本。默认值为 src 下面的 所有 .py 文件:

global-include *.py

多个模式使用逗号分开,例如:

global-include *.py, exclude __mainfest__.py, prune test

关于所有支持的模式,参考 https://docs.python.org/2/distutils/sourcedist.html#commands

示例

  • 修改工程名称和标题:

    pyarmor config --name "project-1"  --title "My PyArmor Project"
    
  • 修改工程主脚本:

    pyarmor config --entry foo.py,hello.py
    
  • 排除路径 builddist ,下面的的所有 .py 文件会被忽略:

    pyarmor config --manifest "global-include *.py, prune build, prune dist"
    
  • 使用非包裹模式加密脚本,这样可以提高加密脚本运行速度,但是会降低安全性:

    pyarmor config --wrap-mode 0
    
  • 配置主脚本的插件,下面的例子中会把 check_ntp_time.py 的内容插入到 主脚本,这个脚本会检查网络时间,超过有效期会自动退出:

    pyarmor config --plugin check_ntp_time.py
    
  • 清除所有插件:

    pyarmor config --plugin clear
    

build

加密工程中的所有脚本。

选项

-B, --force 强制加密所有脚本,默认情况只加密上次构建之后修改过的脚本
-r, --only-runtime
 只生成运行依赖文件
-n, --no-runtime
 只加密脚本,不要生成运行依赖文件
-O, --output OUTPUT
 输出路径,如果设置,那么工程属性里面的输出路径就无效
--platform NAME
 指定加密脚本的运行平台,仅用于跨平台发布
--package-runtime <0,1>
 是否保存运行文件到一个单独的目录

描述

可以直接在工程所在路径运行该命令:

pyarmor build

或者在命令行指定工程所在路径:

pyarmor build /path/to/project

示例

  • 递增式加密工程中的脚本,上次运行该命令之后,没有修改过的脚本不会再被 加密:

    pyarmor build
    
  • 强制加密工程中的所有脚本,即便是没有修改:

    pyarmor build -B
    
  • 仅仅生成运行加密脚本需要的依赖文件,不要加密脚本:

    pyarmor build -r
    
  • 只加密脚本,不要生成其他的依赖文件:

    pyarmor build -n
    
  • 忽略工程中设置的输出路径,保存加密脚本到新路径:

    pyarmor build -B -O /path/to/other
    
  • 在 MacOS 平台下加密脚本,这些加密脚本将在 Ubuntu 下面运行,使用下面 的命令进行加密:

    pyarmor download --list
    pyarmor download linux_x86_64
    
    pyarmor build -B --platform linux_x86_64
    
  • 把运行时刻文件打包生成到输出目录下面的子目录 pytransform 中:

    pyarmor build --only-runtime --package-runtime
    

info

显示工程配置信息。

语法:

pyarmor info [PATH]

描述

可以直接在工程所在路径运行该命令:

pyarmor info

或者在命令行指定工程所在路径:

pyarmor info /path/to/project

check

检查工程文件的配置是否正确。

语法:

pyarmor check [PATH]

描述

可以直接在工程所在路径运行该命令:

pyarmor check

或者在命令行指定工程所在路径:

pyarmor check /path/to/project

banchmark

测试加密脚本的性能。

语法:

pyarmor benchmark <options>

选项:

-m, --obf-mode <0,1>
 是否加密模块
-c, --obf-code <0,1>
 是否单独加密每一个函数
-w, --wrap-mode <0,1>
 是否使用包裹模式加密函数
--debug 不要清理生成的测试脚本

描述

主要用来检查加密脚本的性能,命令输出包括初始化加密脚本运行环境需要的额 外时间,以及不同加密模式下面导入模块、运行不同大小的代码块需要消耗的额 外时间。

示例

  • 测试默认加密模式的性能:

    pyarmor benchmark
    
  • 测试不使用包裹模式的性能:

    pyarmor benchmark --wrap-mode 0
    
  • 查看测试过程中使用的脚本,保存在 .benchtest 目录下面:

    pyarmor benchmark --debug
    

register

生效注册文件,显示注册信息。

语法:

pyarmor register [KEYFILE]

描述

购买 PyArmor 之后会通过邮件发送注册文件给你,然后使用这个命令使之生效:

pyarmor register /path/to/pyarmor-regfile-1.zip

查看注册信息:

pyarmor register

download

查看和下载不同平台下面的预编译的动态库。

语法:

pyarmor download <options> PLAT-ID

选项:

--list PATTERN 查看所有可用的预编译动态库
-O, --output NAME
 下载之后保存的名称

描述

常用平台的预编译动态库已经和 PyArmor 的安装包一起发布,大部分的嵌入式 设备可以自动下载相应的预编译动态库。但是对于部分无法识别平台的嵌入式设 备,就需要人工下载。例如,启动过程提示:

ERROR: Unsupport platform linux32/armv7l

那么首先查看所有的预编译动态库:

pyarmor download --list

在列表中发现平台 armv7 可以使用,那么就可以使用下面的命令进行下载:

pyarmor download --output linux32/armv7l armv7

也可以对平台进行过滤,例如查看 linux32 下所有可用的预编译动态库:

pyarmor download --list linux32

使用实例

下面是一些使用 PyArmor 的实例。

加密并打包 PyQt 应用

文字统计工具 easy-han 使用 PyQt 开发,主要文件如下:

config.json

main.py
ui_main.py
readers/
    __init__.py
    msexcel.py

tests/
vnev/py36

加密打包脚本如下:

cd /path/to/src
pyarmor pack -e " --name easy-han --hidden-import comtypes --add-data 'config.json;.'" \
             -x " --exclude vnev --exclude tests" -s "easy-han.spec" main.py

cd dist/easy-han
./easy-han

使用 -e 传入额外的参数去运行 PyInstaller ,要确认 PyInstaller 使 用这些选项可以正常打包:

cd /path/to/src
pyinstaller --name easy-han --hidden-import comtypes --add-data 'config.json;.' main.py

cd dist/easy-han
./easy-han

使用 -x 传入额外的参数去加密脚本,因为 testsvnev 下面也有很 多脚本,但是这些不需要加密,所以使用 --exclude 选项把它们排除。要确 认可以使用这些选项可以正常加密脚本:

cd /path/to/src
pyarmor obfuscate --exclude vnev --exclude tests main.py

使用 -s 参数主要是因为 PyInstaller 使用 --name 修改了安装包的名称, 默认生成的 .spec 文件不再是主脚本名称,所以要告诉 pack 命令修改后 的 .spec 文件名称。

重要

命令 pack 会自动加密脚本,所以不要使用该命令去打包加密后的脚本, 打包加密脚本会导致错误,因为脚本加密之后是无法自动找到的其他被引用 的模块的。

注解

从 PyArmor 5.5.0 开始,开始传入选项 --advanced 启用高级模式来更进 一步的提高加密脚本的安全性。例如:

pyarmor pack -x " --advanced 1 --exclude tests" foo.py

使用 Apache 的 mod_wsgi 发布加密的 Django 应用

下面是一个 Django 应用的目录结构:

/path/to/mysite/
    db.sqlite3
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    polls/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        urls.py
        views.py

首先加密所有脚本:

# 创建加密后脚本的存放路径
mkdir -p /var/www/obf_site

# 先把原来的文件都拷贝过去,因为 pyarmor 不会处理数据文件
cp -a /path/to/mysite/* /var/www/obf_site/

cd /path/to/mysite

# 递归加密当前目录下面的所有 .py 文件,并指定主文件为 wsgi.py
# 加密后的脚本保存到 /var/www/obf_site 下面,覆盖原来的 .py 文件
pyarmor obfuscate --src="." -r --output=/var/www/obf_site mysite/wsgi.py

然后修改 Apache 的配置文件:

WSGIScriptAlias / /var/www/obf_site/mysite/wsgi.py
WSGIPythonHome /path/to/venv

# pyarmor 的运行文件在这个目录下面,所以需要增加到 Python 路径里面
WSGIPythonPath /var/www/obf_site

<Directory /path/to/mysite.com/mysite>
    <Files wsgi.py>
        Require all granted
    </Files>
</Directory>

最后重新启动 Apache:

apachectl restart

常见问题

当出现问题的时候,首先使用下面的方式运行以得到更多的错误信息:

python -d pyarmor.py ...
PYTHONDEBUG=y pyarmor ...

Segment fault

下面的情况都会导致导致程序崩溃

  • 使用调试版本的 Python 来运行加密脚本
  • 使用 Python 2.6 加密脚本,但是却使用 Python 2.7 来运行加密脚本

如果使用的是 PyArmor v5.5.0 之后的版本,有的机器可能因为不支持高级模式 而崩溃。一个快速的解决方案是禁用高级模式,直接修改 pyarmor 安装包路径 下面的 pytransform.py , 找到函数 _load_library ,把禁用高级模式的 注释去掉,修改成为下面的样子:

# Disable advanced mode if required
m.set_option(5, c_char_p(1))

Could not find _pytransform

通常情况下动态库 _pytransform 和加密脚本在相同的目录下:

  • _pytransform.so in Linux
  • _pytransform.dll in Windows
  • _pytransform.dylib in MacOS

首先检查文件是否存在。如果文件存在:

  • 检查文件权限是否正确。如果没有执行权限,在 Windows 系统会报错:

    [Error 5] Access is denied
    
  • 检查 ctypes 是否可以直接装载 _pytransform:

    from pytransform import _load_library
    m = _load_library(path='/path/to/dist')
    
  • 如果上面的语句执行失败,尝试在 引导代码 中设置运行时刻路径:

    from pytransform import pyarmor_runtime
    pyarmor_runtime('/path/to/dist')
    

如果还是有问题,那么请报告 issue

The license.lic generated doesn't work

通常情况下是因为加密脚本使用的密钥箱和生成许可文件时候使用的密钥箱不一 样,例如在试用版本加密脚本,但是在正式版本下面生成许可文件。

通用的解决方法就是重新把加密脚本生成一下,然后在重新生成许可文件。

NameError: name '__pyarmor__' is not defined

原因是 引导代码 没有被执行。

当使用模块 subprocess 或者 multiprocessing , 调用 Popen 或者 Process 创建新的进程的时候,确保 引导代码 在新进程中也得到执 行。否则新进程是无法使用加密脚本的。

Marshal loads failed when running xxx.py

当出现这个问题,依次进行下面的检查

  1. 检查运行加密脚本的 Python 的版本和加密脚本的 Python 版本是否一致
  2. 尝试移动全局密钥箱 ~/.pyarmor_capsule.zip 到其他任何目录,然后重 新加密脚本
  3. 确保生成许可使用的密钥箱和加密脚本使用的密钥箱是相同的(当运行 PyArmor 的命令时,该命令使用的密钥箱的文件名称会显示在控制台)

_pytransform can not be loaded twice

如果 引导代码 被执行两次,就会报这个错误。通常的情况下,是因为 在加密模块中插入了 引导代码 。 因为引导代码在主脚本已经执行过, 所以导入这样的加密模块就出现了问题。

注解

这个限制是从 PyArmor 5.1 引入的,并且在 PyArmor 5.3.5 中已经移除, 之后的版本都没有这个限制。

Check restrict mode failed

违反了加密脚本的使用约束,默认情况下,加密脚本是不能被进行任何修改的。 更多信息参考 约束模式

如果使用 pack 命令去打包加密后的脚本,也会出现这个错误提示。命令 pack 正确的使用方式是直接打包原始的脚本。

Protection Fault: unexpected xxx

违反了加密脚本的使用约束,默认情况下,下列文件不能进行任何修改:

  • pytransform.py
  • _pytransform.so/.dll/.dylib

更多信息参考 对主脚本的特殊处理

Warning: code object xxxx isn't wrapped

这是因为函数中包含特殊情况的跳转指令而引起的,例如,某一个函数编译成为 byte code 之后如果有类似这样的一条指令 JMP 255

在加密之前,这条指令占用两个字节。而在加密之后,因为在函数头部插入了额 外的指令,这个跳转指令的操作数变成了 267 。在 Python3.6 之后,这条指 令需要 4 个字节:

EXTEND 1
JMP 11

遇到这种情况,目前 PyArmor 就不会加密这个函数, 并显示这个警告信息。当 然,只是这个函数没有被加密,当前模块中的其他函数还是被正常加密的。

如果想要避免这个问题,可以尝试在函数里面增加一些冗余的语句,让跳转长度 不要在临界值即可。

后面的版本会考虑解决这个问题,因为一旦修改原来的指令长度,代码块所有的 相对跳转、绝对跳转指令都需要调整,情况比较复杂,所以遇到这种情况,暂时 忽略了。

Error: Try to run unauthorized function

试图使用没有授权的功能。出现这个问题一般是当前目录下面存在 license.lic`或者 `pytransform.key 而导致的认证问题,解决方案是一是删 除这些不必要文件,或者升级到 PyArmor 5.4.5 以后的版本。

在 Linux 下面无法获取硬盘序列号

获取硬盘序列号需要超级用户权限,首先确认有相关权限。

其次检查一下目录 /dev/disk/by-id ,这里会列出已经挂载的硬盘的接口和 序列号。如果这里没有文件,那么是无法获取硬盘序列号信息的。在 Docker 环 境里面的话,确保运行 docker 的时候挂载硬盘设备。

目前支持的硬盘接口包括 IDE,SCSI 以及 NVME 固态硬盘,对于其他接口的尚 不支持。

运行脚本时候提示 Check license failed: Invalid input package.

检查当前目录下面是否有存在文件 license.lic 或者 pytransform.key , 如果存在的话,确保它们是加密脚本对应的运行时刻文件。

加密脚本会首先在当前目录查看是否存在 license.lic 和运行时刻文件 pytransform.key, 如果存在,那么使用当前目录下面的这些文件。

其次加密脚本会查看运行时刻模块 pytransform.py 所在的目录,看看有没有 license.licpytransform.key

如果当前目录下面存在任何一个文件,但是和加密脚本对应的运行时刻文件不匹 配,就会报这个错误。

'GBK' codec can't decode byte 0xXX

在源代码的第一行指定字符编码,例如:

# -*- coding: utf-8 -*-

关于源文件字符编码,请参考 https://docs.python.org/2.7/tutorial/interpreter.html#source-code-encoding

/lib64/libc.so.6: version 'GLIBC_2.14' not found

在一些没用 GLIBC_2.14 的机器上,会报这个错误。

解决方案是对动态库 _pytransform.so 打补丁。

首先查看依赖的版本信息:

readelf -V /path/to/_pytransform.so
...

Version needs section '.gnu.version_r' contains 2 entries:
 Addr: 0x00000000000056e8  Offset: 0x0056e8  Link: 4 (.dynstr)
  000000: Version: 1  File: libdl.so.2  Cnt: 1
  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 7
  0x0020: Version: 1  File: libc.so.6  Cnt: 6
  0x0030:   Name: GLIBC_2.7  Flags: none  Version: 8
  0x0040:   Name: GLIBC_2.14  Flags: none Version: 6
  0x0050:   Name: GLIBC_2.4  Flags: none  Version: 5
  0x0060:   Name: GLIBC_2.3.4  Flags: none  Version: 4
  0x0070:   Name: GLIBC_2.2.5  Flags: none  Version: 3
  0x0080:   Name: GLIBC_2.3  Flags: none  Version: 2

然后把版本依赖项 GLIBC_2.14 替换为 GLIBC_2.2.5:

  • 从 0x56e8+0x10=0x56f8 拷贝四个字节到 0x56e8+0x40=0x5728
  • 从 0x56e8+0x18=0x5700 拷贝四个字节到 0x56e8+0x48=0x5730

下面是使用 xxd 进行打补丁的脚本命令:

xxd -s 0x56f8 -l 4 _pytransform.so | sed "s/56f8/5728/" | xxd -r - _pytransform.so
xxd -s 0x5700 -l 4 _pytransform.so | sed "s/5700/5730/" | xxd -r - _pytransform.so

软件许可

PyArmor 是一个共享软件。试用版永不过期,试用版的限制是

  • 最大可加密的脚本大小(编译成为 .pyc 之后)是 32768 个字节
  • 在试用版中中生成的加密脚本不是私有的,也就是说,其他任何人也可以为这 些加密脚本生成新的许可文件。
  • 任何人都可以使用本软件加密非商业用途的Python脚本,未经许可不得用于商 业用途。

关于加密脚本的许可文件,可以参考 加密脚本的许可文件

生成私有的加密脚本和加密任意大小的脚本需要购买下列任意一种许可证。

PyArmor 有两种类型的许可证:

  1. 个人用户许可。个人用户购买一个许可证可以在自己所有的计算机和相关硬 件设备上使用。

    个人用户许可证允许使用本软件加密任何属于自己的 Python 脚本,为加密 脚本生成私有许可文件,发布加密后的脚本和必要的辅助文件到任何其他设 备。

  2. 企业用户许可。企业用户购买一个软件许可证可以在同一个产品系列的各个 项目中使用。

    企业用户许可证允许使用本软件在任何设备上,加密属于该产品系列的 Python 脚本,为加密脚本生成私有许可文件,发布加密后的脚本和必要的辅 助文件到任何其他设备。

    除非有许可人的许可,否则企业用户许可证不可以用于完全独立的其他产品 系列。如果需要在其他产品系列中使用,必须为其他产品单独购买软件许可。

购买

使用微信或者支付宝通过下面的链接购买许可

https://pyarmor.dashingsoft.com/cart/order.html

如果需要发票,请通过下面的连接购买许可

https://order.shareit.com/cart/add?vendorid=200089125&PRODUCT[300871197]=1

支付成功之后注册文件会自动通过电子邮件发送过去。注册文件是一个压缩文件, 里面包含 3 个文件:

  • README.txt
  • license.lic (注册码)
  • .pyarmor_capsule.zip (私有密钥箱)

当收到包含注册文件的邮件之后,保存附件为 profile-regfile-1.zip ,然 后使用下面的命令生效注册文件:

pyarmor register pyarmor-regfile-1.zip

运行下面的命令查看注册信息

pyarmor register

注册码生效之后,使用试用版本加密的脚本需要全部重新加密。

如果你使用的是 PyArmor 5.6 之前的版本,使用下面的方式注册:

  1. 解压注册文件
  2. 拷贝解压后的 "license.lic" 到 PyArmor 的安装目录下面
  3. 拷贝解压后的 ".pyarmor_capsule.zip" 到用户的 HOME 目录

软件注册码永久有效,可以一直使用,但是不能转接或者租用.

支持的平台列表

PyArmor 的核心函数使用 C 来实现,对于常用的平台和部分嵌入式系统都已经 有编译好的动态库。

最常用平台的动态库已经打包在 PyArmor 的安装包里面,只要安装好之后即可 使用,参考 预安装的动态库清单

其他平台的动态库并没有随着安装包发布,参考 其他平台的动态库清单 。 在这些平台下面,使用 PyArmor 之前,需要下载相应的动态库,并存放到 PyArmor 安装的路径之下(通常情况下和 pyarmor.py 在相同目录)。

如果需要在上面没有列出的平台使用 PyArmor,请发送邮件到 jondy.zhao@gmail.com

表-1. 预安装的动态库清单
操作系统 CPU架构 特征 下载 说明
Windows i686 反调试、JIT、高级模式 _pytransform.dll 使用 i686-pc-mingw32-gcc 交叉编译
Windows AMD64 反调试、JIT、高级模式 _pytransform.dll 使用 x86_64-w64-mingw32-gcc 交叉编译
Linux i686 反调试、JIT、高级模式 _pytransform.so 使用 GCC 编译
Linux x86_64 反调试、JIT、高级模式 _pytransform.so 使用 GCC 编译
MacOSX x86_64, intel 反调试、JIT、高级模式 _pytransform.dylib 使用 CLang 编译(MacOSX10.11)
表-2. 其他平台的动态库清单
操作系统 CPU架构 特征 下载 说明
Windows x86   _pytransform.dll 使用 VS2015 编译
Windows x64   _pytransform.dll 使用 VS2015 编译
Linux armv5   _pytransform.so 32-bit Armv5 (arm926ej-s)
Linux armv7 反调试、JIT _pytransform.so 32-bit Armv7 Cortex-A, hard-float, little-endian
Linux aarch32 反调试、JIT _pytransform.so 32-bit Armv8 Cortex-A, hard-float, little-endian
Linux aarch64 反调试、JIT _pytransform.so 64-bit Armv8 Cortex-A, little-endian
Linux ppc64le   _pytransform.so 适用于 POWER8
iOS arm64   _pytransform.dylib 使用 CLang 编译(iPhoneOS9.3sdk)
FreeBSD x86_64   _pytransform.so 不支持获取硬盘序列号
Alpine Linux x86_64   _pytransform.so 可用于 Docker(musl-1.1.21)
Alpine Linux arm   _pytransform.so 可用于 Docker(musl-1.1.21), 32 bit Armv5T, hard-float, little-endian
Inel Quark i586   _pytransform.so 使用 i586-poky-linux 交叉编译
Android aarch64   _pytransform.so Build by android-ndk-r20/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang