高级用法¶
加密和使用多个包¶
假定有三个包 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'
注解
输出目录 dist 下面的运行辅助包 pytransform
可以被拷贝到任何的 Python
可以导入的目录下面。
从 v5.7.2 之后,运行辅助包也可以使用命令 runtime 单独生成:
pyarmor runtime
如何加密能和其他加密包共存的包¶
注解
New in v5.8.7
假设有两个包分别被两个不同的开发人员进行加密,那么这两个包能不能在同一个 Python 解释器中运行呢?
如果这两个包都是被试用版加密,那么没有问题。但是如果任何一个是被注册版本的 PyArmor 加密,那么答案是否定的。
从 v5.8.7 开始,使用选项 --enable-suffix
来加密时, 运行辅助包 的名称
不在固定为 pytransform
,而是会有一个唯一性的后缀,这样不同的加密包就可以实
现共存。例如:
pyarmor obfuscate --enable-suffix foo.py
加密后的输出目录结构如下:
dist/
foo.py
pytransform_vax_000001/
__init__.py
...
其中后缀 _vax_000001
是基于 PyArmor 的注册码生成的具有唯一性的字符串。
对于使用工程加密的方式,则需要使用命令 config 来设置:
pyarmor config --enable-suffix 1
pyarmor build -B
使用下面的方式可以禁用后缀模式:
pyarmor config --enable-suffix 0
pyarmor build -B
跨平台发布加密脚本¶
因为加密脚本的运行文件中有平台相关的动态库,所以跨平台发布需要指定目标平台。
首先使用命令 download 列出所有支持的标准平台名称:
pyarmor download
pyarmor download --help-platform
使用选项 --list
会显示详细的动态库特征信息:
pyarmor download --list
pyarmor download --list windows
pyarmor download --list windows.x86_64
然后在加密脚本的时候指定目标平台名称:
pyarmor obfuscate --platform linux.armv7 foo.py
# For project
pyarmor build --platform linux.armv7
使用不同特征的动态库¶
同一个平台下面,包含多个可用的动态库,不同的动态库具备不同的特征。例如,在相同的
平台 windwos.x86_64
下面,有两个动态库 windows.x86_64.0
和
windows.x86_64.7
,其中最后的数字表示动态库的特征:
- 0: 没有反调试、动态代码、高级模式等特征,速度最快
- 7: 包含反调试、动态代码、高级模式等特征,安全性最高
如果不指定特征码,默认是选择安全性最高的动态库,有些不平台只支持部分特征的动态库。 在跨平台加密的时候也可以指定平台的特征,例如:
pyarmor obfuscate --platform linux.x86_64.7 foo.py
需要注意的是不同特征的动态库相互并不兼容,例如,默认情况下 Windows 平台下使用的
是高特征的动态库,直接在 Windows 平台下面加密低特征的动态库,是无法在目标平台运
行的。例如,下面加密后的代码是无法在 linux
平台下面运行的:
pyarmor obfuscate --platform linux.x86_64.0 foo.py
必须使用环境变量 PYARMOR_PLATFORM
设置当前平台使用低特征动态库才可以。例如,
下面的命令加密后的脚本,就可以在 linux
平台下面运行的:
PYARMOR_PLATFORM=windows.x86_64.0 pyarmor obfuscate --platform linux.x86_64.0 foo.py
让加密脚本可以在多个平台运行¶
从 v5.7.5 版本开始,平台名称已经标准化,所有可用名称在这里 标准平台名称 ,并且支持运行加密脚本在多个平台。
为了支持加密脚本在多个平台运行,需要把把相关平台的动态库都添加到 运行辅助 包 中,这样就可以在这些平台正常运行加密脚本。例如,使用下面的命令可以加密一个可 运行于 Windows/Linux/MacOS 下面的脚本:
pyarmor obfuscate --platform windows.x86_64 \
--platform linux.x86_64 \
--platform darwin.x86_64 \
foo.py
也可以使用命令 runtime 单独生成可以运行多个平台的 运行辅助包 ,这样就不 需要每次加密的时候都生成这些辅助文件。例如:
pyarmor runtime --platform windows.x86_64,linux.x86_64,darwin.x86_64
pyarmor obfuscate --no-runtime --recursive \
--platform windows.x86_64,linux.x86_64,darwin.x86_64 \
foo.py
即便使用了 --no-runtime
,在加密脚本的时候也需要指定运行的平台,因为加密脚本
会在启动的时候检查动态库,只有指定的动态库才能通过检查。如果指定了选项
--no-cross-protection
,加密脚本就不会在检查动态库,那么加密的时候就不需要指
定运行平台,例如:
pyarmor obfuscate --no-runtime --recursive --no-cross-protection foo.py
注解
如果指定了平台的特征,例如 windows.x86_64.7
,那么需要注意的是所有的平台
必须具备相同的特征,不同特征的动态库是无法共存在同一个包里面的。
注解
如果加密后的脚本无法运行,可以尝试使用下面的命令升级已经下载的动态库:
pyarmor download --update
也可以直接删除缓存的动态库目录,默认是 $HOME/.pyarmor/platforms
使用不同版本 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 %*
在没有加密的脚本中运行引导代码¶
有时候需要在普通脚本中运行引导代码,这样就可以正常的导入其他加密脚本, 而不需要在每一个加密脚本中到插入引导代码。在 v5.7.0 之前,可以直接把两 行 引导代码 插入到普通脚本中,但是之后的版本,为了提高安全性, 已经不允许在普通脚本中直接运行引导代码,必须使用一种折衷的方式。
首先使用命令 runtime 生成一个引导辅助包 pytransform_bootstrap
:
pyarmor runtime -i
然后把生成的辅助包拷贝到脚本所在的目录:
mv dist/pytransform_bootstrap /path/to/script
也可以把这个包拷贝到 Python 的库目录,例如:
mv dist/pytransform_bootstrap /usr/lib/python3.5/ (For Linux)
mv dist/pytransform_bootstrap C:/Python35/Lib/ (For Windows)
最后修改普通脚本,在其中插入一条语句:
import pytransform_bootstrap
这样就可以在其后导入并使用其他加密模块。
注解
在 v5.8.1 之前,需要使用人工的方式生成这个引导辅助包:
echo "" > __init__.py
pyarmor obfuscate -O dist/pytransform_bootstrap --exact __init__.py
下面是一个实际的例子,运行加密脚本的单元测试用例。
运行加密脚本的单元测试¶
因为大部分的加密脚本都没有 引导代码 ,所以在运行单元测试之前,必须首先运 行引导代码。
假设单元测试脚本为 /path/to/tests/test_foo.py
,那么首先修改这个单元测试
脚本,参考 在没有加密的脚本中运行引导代码
这样,这个测试脚本就可以导入被加密的模块进行正常的测试:
cd /path/to/tests
python test_foo.py
还有一种方式就是直接修改系统包 unittest
,首先要把引导辅助包拷贝到 Python
的系统库路径下面,参考 在没有加密的脚本中运行引导代码
然后在修改 /path/to/unittest/__init__.py
,插入语句:
import pytransform_bootstrap
这样,所有的单元测试脚本就都可以直接来测试加密后的模块了。如果有很多单 元测试脚本,这种方式会更方便一些。
让 Python 自动识别加密脚本¶
下面有几种情况可能会需要让 Python 自动识别加密脚本:
- 几乎所有的脚本都会被作为主脚本来运行
- 在加密脚本中使用模块 multiprocessing 创建新进程
- 使用到 Popen 或者 os.exec 等调用加密后的脚本
- 其他任何需要在很多脚本里面插入引导代码的情况
一种解决方案就是为每一个相关的加密脚本添加引导代码,但是这会有些麻烦。 另外一种比较简单的解决方案就是让 Python 能够自动识别加密脚本,这样任何 一个加密脚本不需要引导代码就可以正常运行。
下面是基本操作步骤:
首先生成引导辅助包
pytransform_bootstrap
:pyarmor runtime -i
在 v5.8.1 之前,需要通过加密一个空脚本的方式生成引导辅助包:
echo "" > __init__.py pyarmor obfuscate -O dist/pytransform_bootstrap --exact __init__.py
其次需要建立一个运行加密脚本的虚拟环境,把引导辅助包拷贝到虚拟环境 的库路径,例如:
# For windows mv dist/pytransform_bootstrap venv/Lib/ # For linux mv dist/pytransform_bootstrap venv/lib/python3.5/
最后修改
venv/lib/pythonX.Y/site.py
或者venv/lib/site.py
, 插入一条导入语句:import pytransform_bootstrap if __name__ == '__main__': ...
也可以把这行代码添加到 main
函数里面,总之,只要能得到执行就可以。
这样就可以使用虚拟环境中 python
直接运行加密脚本了。 这主要使用到
了 Python 在启动过程中默认会自动导入模块 site
的特性来实现,参考
https://docs.python.org/3/library/site.html
注解
这里配置的是运行加密脚本的环境,在这里 pyarmor 是无法运行的。
注解
在 v5.7.0 之前,需要根据 运行辅助文件 人工创建引导辅助包
使用不同的模式来加密脚本¶
高级模式 是从 PyArmor 5.5.0 引入的新特性,默认情况下是没有启用的。如果需
要使用高级模式来加密脚本,额外指定选项 --advanced
:
pyarmor obfuscate --advanced 1 foo.py
从 PyArmor 5.2 开始, 约束模式 是默认设置。
使用选项 --restrict
指定其他约束模式,例如:
pyarmor obfuscate --restrict=2 foo.py
pyarmor obfuscate --restrict=3 foo.py
# For project
cd /path/to/project
pyarmor config --restrict 4
pyarmor build -B
如果需要禁用各种约束,那么使用下面的命令加密脚本:
pyarmor obfuscate --restrict=0 foo.py
# For project
pyarmor config --restrict=0
pyarmor build -B
指定 代码加密模式, 代码包裹模式, 模块加密模式 需 要 使用工程 来加密脚本,直接使用命令 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()
这样插件输出的函数就可以被脚本调用。
插件对应的文件一般存放在当前目录,或者 $HOME/.pyarmor/plugins
。如果存放在其
他目录的话,可以指定绝对路径,例如:
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
其中 --onefile
是 PyInstaller 的选项,使用 -e
可以传递任何
Pyinstaller 支持的选项,例如,指定可执行文件的图标:
pyarmor pack -e " --onefile --icon logo.ico" foo.py
如果不想把加密脚本的许可文件 license.lic
打包到可执行文件,而是和可执行文件
放在一起,这样方便为不同的用户生成不同的许可文件。那么需要使用 PyInstaller 提
供的 --runtime-hook
功能在加密脚本运行之前把许可文件拷贝到指定目录,下面是具
体的操作步骤:
新建一个文件 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())
运行下面的命令打包加密脚本:
pyarmor pack --clean --without-license -x " --exclude copy_license.py" \ -e " --onefile --icon logo.ico --runtime-hook copy_license.py" foo.py
选项
--without-license
告诉 pack 不要把加密脚本的许可文件打包进去, 使用 PyInstaller 的选项--runtime-hook
可以让打包好的可执行文件,在启动 的时候首先去调用copy_licesen.py
,把许可文件拷贝到相应的目录。命令执行成功之后,会生成一个打包好的文件
dist/foo.exe
尝试运行这个可执行文件,应该会报错。
使用命令 licenses 生成新的许可文件,并拷贝到
dist/
下面:pyarmor licenses -e 2020-01-01 tom cp license/tom/license.lic dist/
这时候在双击运行
dist/foo.exe
,在 2020-01-01 之前应该就可以正常运行
使用定制的 .spec 文件打包加密脚本¶
如果已经有写好的 .spec 文件能够成功打包,例如:
pyinstaller myscript.spec
那么对这个文件进行少量修改之后,就可以用来直接打包加密脚本:
- 增加模块
pytransform
到 hiddenimports - 增加额外的路径
DISTPATH/obf/temp
到 pathex 和 hookspath
修改后的文件大概会是这样子的:
a = Analysis(['myscript.py'],
pathex=[os.path.join(DISTPATH, 'obf', 'temp'), ...],
binaries=[],
datas=[],
hiddenimports=['pytransform', ...],
hookspath=[os.path.join(DISTPATH, 'obf', 'temp'), ...],
现在使用下面的方式运行命令 pack:
pyarmor pack -s myscript.spec myscript.py
这样就可以加密并打包 myscript.py 。
注解
这个功能是在 v5.8.0 新增加的
在 v5.8.2 之前,额外的路径是 DISTPATH/obf
而不是 DISTPATH/obf/temp
使用约束模式增加加密脚本安全性¶
默认约束模式仅限制不能修改加密脚本,为了提高安全性,可以使用约束模式 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')
# Also check a piece of byte code for special function
if s.__name__ == 'connect':
if s.__code__.co_code[10:12] != b'\x90\xA2':
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 之前,都会检查被调用的函数是否被加密,如果没有被加 密,直接抛出异常。
使用插件的实现方式¶
首先在当前目录下定义插件文件 asser_armored.py:
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')
# Also check a piece of byte code for special function
if s.__name__ == 'connect':
if s.__code__.co_code[10:12] != b'\x90\xA2':
raise RuntimeError('Access violate')
return func(*args, **kwargs)
return _execute
return wrapper
然后修改 main.py, 增加相应的插件注释桩,例如:
import foo
# {PyArmor Plugins}
# PyArmor Plugin: @assert_armored(foo.connect, foo.connect2)
def start_server():
foo.connect('root', 'root password')
...
这样基本不影响原来的脚本调试,在加密脚本的时候只需要指定插件就可以:
pyarmor obfuscate --plugin assert_armored main.py
注解
在 v5.7.2 之后,还支持这种格式的插件桩:
# @pyarmor_assert_armored(foo.connect, foo.connect2)
第三方解释器的支持¶
对于第三方的解释器(例如 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 的地址。
在 Python 脚本内部调用 pyarmor¶
在 Python 脚本内部,也可以直接调用 pyarmor ,不需要使用 os.exec 或者 subprocess.Popen 等命令行的方式。例如
from pyarmor.pyarmor import main as call_pyarmor
call_pyarmor(['obfuscate', '--recursive', '--output', 'dist', 'foo.py'])
使用选项 --silent
可以不显示所有输出
from pyarmor.pyarmor import main as call_pyarmor
call_pyarmor(['--silent', 'obfuscate', '--recursive', '--output', 'dist', 'foo.py'])
从 v5.7.3 开始,如果以这种方式调用 pyarmor 出现了错误,会抛出异常,而不是调用 sys.exit 直接退出。