PyArmor's 文档¶
版本: | PyArmor 5.8 |
---|---|
主页: | 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_capsule.zip
~/.pyarmor/license.lic (从 v5.8.0 开始)
~/.pyarmor/platforms/
{pyarmor-folder}/license.lic (在 v5.8.0 之前)
执行下面的命令进行完全卸载:
pip uninstall pyarmor
rm -rf {pyarmor-folder}
rm -rf ~/.pyarmor_capsule.zip
rm -rf ~/.pyarmor
使用 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
包含运行加密脚本所需要的全部文件:
dist/
myscript.py
pytransform
__init__.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic
除了加密脚本之外,额外的那个目录 pytransform 叫做 运行辅助包 ,它是运行 加密脚本不可缺少的。
通常情况下第一个脚本叫做主脚本,它加密后的内容如下:
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 的所有文件拷贝过去即可。需要 注意的是 运行辅助包 必须也拷贝到运行环境,否则无法运行加密脚本。
运行辅助包 并不是必须和加密脚本在一起,也可以拷贝到运行环境的任 意的 Python 路径下面,只有能正确 import pytransform 就可以。
关于加密脚本的安全性的说明,参考 PyArmor 的安全性
注解
运行加密脚本不需要安装 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/pytransform/
这样,加密脚本在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/pytransform/
cd dist/
python myscript.py
注解
在 v5.7.0 之前,默认许可文件 license.lic
没有在 dist/pytransform
,而是在 dist
扩展其他认证方式¶
除了上述认证方式之外,还可以在 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
高级用法¶
加密和使用多个包¶
假定有三个包 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
跨平台发布加密脚本¶
因为加密脚本的运行文件中有平台相关的动态库,所以跨平台发布需要指定目标 平台。
首先使用命令 download 列出所有支持的标准平台名称:
pyarmor download
pyarmor download --help-platform
使用选项 --list
会显示详细的动态库特征信息:
pyarmor download --list
pyarmor download --list windows
pyarmor download --list windows.x86_64
如果目标平台是 表-1. 预安装的动态库清单 中的任意一个,那么可以直接使 用,否则需要使用 download 指定平台名称下载对应的动态库:
pyarmor download linux.armv7
然后在加密脚本的时候指定目标平台名称:
pyarmor obfuscate --platform linux.armv7 foo.py
# For project
pyarmor build --platform linux.armv7
让加密脚本可以在多个平台运行¶
从 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 --platform linux.x86_64 --platform darwin.x86_64
pyarmor obfuscate --no-runtime --recursive \
--platform windows.x86_64 --platform linux.x86_64 --platform darwin.x86_64 \
foo.py
即便使用了 --no-runtime
,在加密脚本的时候也需要指定运行的平台,因为加密脚本
会在启动的时候检查动态库,只有指定的动态库才能通过检查。如果指定了选项
--no-cross-protection
,加密脚本就不会在检查动态库,那么加密的时候就不需要指
定运行平台,例如:
pyarmor obfuscate --no-runtime --recursive --no-cross-protection foo.py
注解
升级 pyarmor 之后,下载的动态库不会自动升级。如果加密后的脚本无法 运行,使用命令 download 重新下载相应的动态库。
从 v5.7.6 开始,可以直接使用下面的命令升级已经下载的动态库:
pyarmor download --update
使用不同版本 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 能够自动识别加密脚本,这样任何 一个加密脚本不需要引导代码就可以正常运行。
下面是基本操作步骤:
首先加密一个空的脚本,同时生成 运行辅助包:
echo "" > pytransform_bootstrap.py pyarmor obfuscate pytransform_bootstrap.py
其次把 运行辅助包 拷贝到任意的 Python 路径。例如:
# For windows mv dist/pytransform C:/Python37/Lib/site-packages/ # For linux mv dist/pytransform /usr/local/lib/python3.5/dist-packages/
然后把加密后的空脚本 dist/pytransform_bootstrap.py (包含 引 导代码) 拷贝到任意的 Python 路径。例如:
mv dist/pytransform_bootstrap.py C:/Python37/Lib/ mv dist/pytransform_bootstrap.py /usr/lib/python3.5/
修改 {prefix}/lib/site.py (Windows) 或者 {prefix}/lib/pythonX.Y/site.py (Linux), 插入一条导入语句,导入 pytransform_bootstrap:
import pytransform_bootstrap if __name__ == '__main__': ...
也可以把这行代码添加到 site.main 里面,总之,只要能得到执行就可以。
这样就可以使用 python 直接运行加密脚本了。 这主要使用到了 Python 在启动过程中 默认会自动导入模块 site 的特性来实现,参考
使用不同的模式来加密脚本¶
高级模式 是从 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()
这样插件输出的函数就可以被脚本调用。
插件对应的文件一般存放在当前目录,如果存放在其他目录的话,可以指定绝对 路径,例如:
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 \ -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
到 pathex 和 hookspath
修改后的文件大概会是这样子的:
a = Analysis(['myscript.py'],
pathex=[os.path.join(DISTPATH, 'obf'), ...],
binaries=[],
datas=[],
hiddenimports=['pytransform', ...],
hookspath=[os.path.join(DISTPATH, 'obf'), ...],
现在使用下面的方式运行命令 pack:
pyarmor pack -s myscript.spec myscript.py
这样就可以加密并打包 myscript.py 。
注解
这个功能是在 v5.8.0 新增加的
使用约束模式增加加密脚本安全性¶
默认约束模式仅限制不能修改加密脚本,为了提高安全性,可以使用约束模式 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 直接退出。
使用实例¶
下面是一些使用 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
传入额外的参数去加密脚本,因为 tests 和 vnev 下面也有很多脚本,
但是这些不需要加密,所以使用 --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 /var/www/obf_site/mysite>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
最后重新启动 Apache:
apachectl restart
使用工程¶
工程是一个包含配置文件的目录,可以用来方便的管理加密脚本。
使用工程管理脚本的有下列优点:
- 可以递增式加密脚本,仅仅加密修改过的脚本,适用于需要加密脚本很多的项目
- 定制选择工程包含的脚本文件,而不是一个目录下全部脚本
- 设置加密模式和定制保护代码
- 更加方便的管理加密脚本
使用工程管理加密脚本¶
首先使用命令 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
使用命令 build 加密工程中包含的所有脚本:
pyarmor build
当某些脚本修改之后,再次运行 build ,加密这些修改过的脚本:
pyarmor build
使用命令 config 来修改工程的配置。
例如,设置工程的 --manifest
选项,把 dist
, test
目
录下面的所有 .py 排除在工程之外:
pyarmor config --manifest "include *.py, prune dist, prune test"
默认情况下 build 仅仅加密修改过的文件,强制加密所有脚本:
pyarmor build --force
运行加密后的脚本:
cd dist
python pybench.py
工程配置文件¶
每一个工程都有一个 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
注解
属性 disable_restrict_mode 从 v5.5.6 之后不再使用,而是转换为等价 的 restrict_mode。
entry
工程的主脚本,可以是多个,以逗号分开:
main.py, another/main.py, /usr/local/myapp/main.py
主脚本可以是绝对路径,也可以是相对路径,相对于工程的 src 路径。
output
输出路径,保存加密后的脚本和运行辅助文件,相对于工程配置文件所在的 目录。
capsule
工程使用的密钥箱,默认是 全局密钥箱 。
obf_code
是否加密每一个函数(代码块):
- 0
不加密
- 1
加密每一个函数
- 2
使用比模式 1 更复杂的算法来加密每一个函数
默认值为 1,参考 代码加密模式
wrap_mode
是否使用 try..final 结构包裹原理的代码块
- 0
不包裹
- 1
包裹每一个代码块
默认值为 1,参考 包裹模式
obf_mod
是否加密整个模块:
- 0
不加密
- 1
加密模块
默认值为 1,参考 模块加密模式
cross_protection
是否在主脚本插入交叉保护代码:
runtime_path
None 或者任何路径名称
用来告诉加密脚本去哪里装载动态库 _pytransform 。
默认值为 None , 是指在 运行辅助包 里面。
主要用于使用打包工具(例如 py2exe)把加密脚本压缩到一个 .zip 文 件的时候,无法正确定位动态库,这时候把 runtime_path 设置为空字符 串可以解决这个问题。
package_runtime
保存运行辅助文件的方式:
- 0
和加密脚本存放在相同目录,这也是 v5.7.0 之前的存放方式
- 1 (默认值)
把运行辅助文件作为包存放在子目录 pytransform ,这样目录结构更清晰
- 2
和 1 类似,只不过在运行时刻,包 pytransform 可能是在其他目录。 设置这个选项之后,在主脚本插入引导代码的时候就不会在使用相对导入的方式。
plugins
None 或者列表
用来扩展加密脚本的认证方式,支持多个插件,例如:
plugins: ["check_ntp_time", "show_license_info"]
关于插件的使用实例,请参考 使用插件扩展认证方式
命令手册¶
PyArmor 是一个命令行工具,用来加密脚本,绑定加密脚本到固定机器或者设置加密脚本的有效期。
pyarmor 的语法格式:
pyarmor <command> [options]
常用的命令包括:
obfuscate 加密脚本
licenses 为加密脚本生成新的许可文件
pack 打包加密脚本
hdinfo 获取硬件信息
runtime 创建运行辅助包
和工程相关的命令:
init 创建一个工程,用于管理需要加密的脚本
config 修改工程配置信息
build 加密工程里面的脚本
info 显示工程信息
check 检查工程配置信息是否正确
其他不常使用的命令:
benchmark 测试加密脚本的性能
register 生效注册文件
download 查看和下载预编译的动态库
可以运行 pyarmor <command> -h 查看各个命令的详细使用方法。
注解
从 v5.7.1 开始,下面这些命令的首字母可以作为别名直接使用:
obfuscate, licenses, pack, init, config, build
例如:
pyarmor o 等价于 pyarmor obfuscate
obfuscate¶
加密 Python 脚本。
语法:
pyarmor obfuscate <options> SCRIPT...
选项
-O, --output PATH | |
输出路径,默认是 dist | |
-r, --recursive | |
递归模式加密所有的脚本 | |
-s, --src PATH | 当主脚本不在顶层目录的时候指定搜索脚本的路径 |
--exclude PATH | 在递归模式下排除某些目录,多个目录使用逗号分开,或者使用该选项多次 |
--exact | 只加密命令行中列出的脚本 |
--no-bootstrap | 在主脚本中不要插入引导代码 |
--no-cross-protection | |
在主脚本中不要插入交叉保护代码 | |
--plugin NAME | 在加密之前,向主脚本中插入代码。这个选项可以使用多次。 |
--platform NAME | |
指定运行加密脚本的平台 | |
--advanced <0,1> | |
使用高级模式加密脚本 | |
--restrict <0,1,2,3,4> | |
设置约束模式 | |
--package-runtime <0,1,2> | |
是否保存运行文件到一个单独的目录 | |
--no-runtime | 不生成任何运行辅助文件,只加密脚本 |
描述
PyArmor 首先检查用户根目录下面是否存在 .pyarmor_capsule.zip
,如果不存在,
那么创建一个新的。
接着搜索需要加密的脚本,共有三种搜索模式:
- 默认模式: 搜索和主脚本相同目录下面的所有 .py 文件
- 递归模式: 递归搜索和主脚本相同目录下面的所有 .py 文件
- 精准模式: 仅仅加密命令行中列出的脚本
PyArmor 会修改主脚本,插入交叉保护代码,然后把搜索到脚本全部加密,保存到输出目录 dist
然后创建 运行辅助包 ,保存到输出目录 dist
最后插入 引导代码 到主脚本。
如果命令行有多个脚本的话,只有第一个脚本是主脚本。除了第一个脚本,不会在其他脚本 中插入引导代码和交叉保护代码。
当主脚本没有在顶层目录的时候,需要使用选项 --src
用来指定搜索 .py 文件的路径。
例如:
# 没有 --src 的话,"./mysite" 是搜索脚本的路径
pyarmor obfuscate --src "." --recursive mysite/wsgi.py
选项 --plugin
主要用于扩展加密脚本的授权方式,例如检查网络时间来校验有效期等,
这个选项指定的插件名称会在加密之前插入到主脚本中。插件对应的文件为当前目录下面
名称.py ,如果插件存放在其他路径,可以使用绝对路径指定插件名称,也可以设置环境
变量 PYARMOR_PLUGIN 为相应路径名称,更多详细的内容参考 如何处理插件 。
关于插件的使用实例,请参考 使用插件扩展认证方式
选项 --platform
用于指定加密脚本的运行平台,仅用于跨平台发布。因为加密脚本的
运行文件中包括平台相关的动态库,所以跨平台发布需要指定该选项。这个选项可以使用多
次,以支持加密脚本运行于不同平台。从 v5.7.5 开始,所有平台名称已经标准化,可用的
平台名称可以使用命令 download 查看。
选项 --restrict
用于指定加密脚本的约束模式,关于约束模式的详细说明,参考
约束模式
默认情况下,所有运行时刻文件会作为包保存在一个单独的目录 pytransform 下面:
pytransform/
__init__.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic
如果选项 --package-runtime
设置为 0 ,那么生成的运行辅助文件和加密脚本存放
在相同的目录下面:
pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pytransform.key
license.lic
如果 --package-runtime
设置为 2 ,就是指 运行辅助包 在运行时刻并不
会和加密脚本存放在一起,而是在其他路径,所以这时候主脚本中的 引导代码 在
任何情况下面,都是使用绝对导入方式:
from pytransform import pyarmor_runtime
pyarmor_runtime()
否则当主脚本是 __init__.py
的时候, 会使用包含一个 .
的相对导入的方式:
from .pytransform import pyarmor_runtime
pyarmor_runtime()
示例
加密当前目录下的所有 .py 脚本,保存到 dist 目录:
pyarmor obfuscate foo.py
递归加密当前目录下面的所有 .py 脚本,保存到 dist 目录:
pyarmor obfuscate --recursive foo.py
递归加密当前目录下面的所有脚本,主脚本在子目录 mysite/ 下面:
pyarmor obfuscate --src "." --recursive mysite/wsgi.py
仅仅递归加密指定目录下面的所有 .py 脚本,没有主脚本,也不生成运行辅助文件:
pyarmor obfuscate --recursive --no-runtime . pyarmor obfuscate --recursive --no-runtime src/
除了 build 和 dist 之外,递归加密当前目录下面的所有 .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
只加密主脚本 foo.py ,不要生成其他任何运行文件:
pyarmor obfuscate --no-runtime --exact foo.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
仅当插件中的函数 assert_armored 被调用的时候导入插件 assert_armored:
pyarmor obfuscate --plugin @assert_armored foo.py
在 MacOS 平台下加密脚本,这些加密脚本将在 Ubuntu 下面运行,使用下面 的命令进行加密:
pyarmor download pyarmor download linux.x86_64 pyarmor obfuscate --platform linux.x86_64 foo.py
使用高级模式加密脚本:
pyarmor obfuscate --advanced 1 foo.py
使用约束模式 2 加密脚本:
pyarmor obfuscate --restrict 2 foo.py
使用约束模式 4 加密当前目录下面除了 __init__.py 之外的所有 .py 文件:
pyarmor obfuscate --restrict 4 --exclude __init__.py --recursive .
加密一个模块,并且把运行时刻文件保存为一个单独的包:
cd /path/to/mypkg pyarmor obfuscate -r --package-runtime 2 --output dist/mypkg __init__.py
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/pytransorm/
另外一个例子,限制加密脚本在固定 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
选项
-O, --output PATH | |
输出路径 | |
-e, --options OPTIONS | |
传递额外的参数到 PyInstaller | |
-x, --xoptions OPTIONS | |
传递额外的参数到 obfuscate 去加密脚本 | |
-s FILE | 指定 pyinstaller 使用的 .spec 文件 |
--clean | 打包之前删除缓存的 .spec 文件 |
--without-license | |
不要将加密脚本的许可文件打包进去 | |
--debug | 不要删除打包过程生成的中间文件 |
描述
命令 pack 首先调用 PyInstaller 生成一个和主脚本同名的 .spec 文件,选项
--options
的值会原封不动的被传递给 PyInstaller ,但是不能传递选项
--distpath
。
注解
如果当前目录下有一个 .spec 已经存在,PyArmor 会直接使用这个文件,而不是重新
创建一个新的。但是如果命令行使用了选项 --clean
,那么 PyArmor 总是会调用
PyInstaller 去创建一个新的,同时覆盖老的。
当 pack 命令失败的时候,首先要确认这个 .spec 文件可以直接用 PyInstaller 打包成功。例如:
pyinstaller myscript.spec
接下来 pack 会递归加密主脚本所在目录下面的所有 .py 文件。它使用选项 -r
,
--output
以及 --xoptions
中指定的额外选项来调用 pyarmor obfuscate
然后 pack 会基于原来的 .spec 文件,创建一个新的 .spec 文件,增加一些语句 用于把原来的脚本替换为加密后的脚本。
最后 pack 再次调用 PyInstaller 使用这个打过补丁的 .spec 文件来创建最终的输出。
更多详细说明,请参考 如何打包加密脚本.
重要
命令 pack 会自动加密脚本,所以不要使用该命令去打包加密后的脚本,打包加密脚 本会导致错误,因为脚本加密之后是无法自动找到的其他被引用的模块的。
示例
加密脚本 foo.py 并打包到 dist/foo 下面:
pyarmor pack foo.py
删除缓存的 foo.spec 和其他中间文件,开始一个全新的打包:
pyarmor pack --clean foo.py
使用已经写好的 myfoo.spec 来打包加密脚本:
pyarmor pack -s myfoo.spec foo.py
传递额外的参数运行 PyInstaller:
pyarmor pack --options '-w --icon app.ico' foo.py
打包的时候不要加密目录 venv 和 test 下面的所有文件:
pyarmor pack -x " --exclude venv --exclude test" foo.py
使用高级模式加密脚本,然后打包成为一个可执行文件:
pyarmor pack -e " --onefile" -x " --advanced 1" 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 格式的文件。
如果选项 --type
是 auto (也是默认情况),那么工程类型根据主脚本命令来判断。
如果主脚本是 __init__.py , 那么工程类型就是 pkg , 否则就是 app 。
如果新的工程类型为 pkg ,不管是自动判断还是选项指定, init 命令都会设置工程 属性 is_package 为 1 ,这个属性的默认值是 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,2> | |
是否加密每一个函数 | |
--wrap-mode <0,1> | |
是否启用包裹模式加密函数 | |
--advanced-mode <0,1> | |
是否使用高级模式加密脚本 | |
--cross-protection <0,1> | |
是否插入交叉保护代码到主脚本 | |
--runtime-path RPATH | |
设置运行文件所在路径 | |
--plugin NAME | 设置需要插入到主脚本的代码文件,这个选项可以使用多次 |
--package-runtime <0,1,2> | |
是否保存运行文件到一个单独的目录 |
描述
在工程所在路径运行该命令,修改一个或者多个工程属性:
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
选项 --plugin
有一个特殊的值 clear ,用来清除所有的插件。
示例
修改工程名称和标题:
pyarmor config --name "project-1" --title "My PyArmor Project"
修改工程主脚本:
pyarmor config --entry foo.py,hello.py
排除路径 build 和 dist ,下面的的所有 .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¶
加密工程中的所有脚本。
语法:
pyarmor build <options> [PATH]
选项
-B, --force | 强制加密所有脚本,默认情况只加密上次构建之后修改过的脚本 |
-r, --only-runtime | |
只生成运行依赖文件 | |
-n, --no-runtime | |
只加密脚本,不要生成运行依赖文件 | |
-O, --output OUTPUT | |
输出路径,如果设置,那么工程属性里面的输出路径就无效 | |
--platform NAME | |
指定加密脚本的运行平台,仅用于跨平台发布 | |
--package-runtime <0,1,2> | |
是否把运行文件作为包保存到一个单独的目录 |
描述
可以直接在工程所在路径运行该命令:
pyarmor build
或者在命令行指定工程所在路径:
pyarmor build /path/to/project
选项 --platform
和 --package-runtime
的使用,请参考命令 obfuscate
示例
递增式加密工程中的脚本,上次运行该命令之后,没有修改过的脚本不会再被 加密:
pyarmor build
强制加密工程中的所有脚本,即便是没有修改:
pyarmor build -B
仅仅生成运行加密脚本需要的依赖文件,不要加密脚本:
pyarmor build -r
只加密脚本,不要生成其他的依赖文件:
pyarmor build -n
忽略工程中设置的输出路径,保存加密脚本到新路径:
pyarmor build -B -O /path/to/other
在 MacOS 平台下加密脚本,这些加密脚本将在 Ubuntu 下面运行,使用下面 的命令进行加密:
pyarmor download pyarmor download linux.x86_64 pyarmor build -B --platform linux.x86_64
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> NAME
选项:
--help-platform | |
显示所有支持的规范化平台名称 | |
--list PATTERN | 查看所有可用的预编译动态库 |
-O, --output PATH | |
下载之后保存的路径 | |
--update | 更新已经下载的动态库 |
描述
这个命令主要是用来下载其他平台的动态库,一般用于交叉平台的发布。
列出所有可用平台的规范化名称,例如:
pyarmor download
pyarmor download --help-platform
pyarmor download --help-platform windows
pyarmor download --help-platform linux.x86_64
下载其中的一个。例如:
pyarmor download linux.armv7
pyarmor download linux.x86_64
默认情况下,下载的文件是保存在目录 ~/.pyarmor/platforms
下面,使用相应的路径
来存放不同平台的动态库。
选项 --list
会显示详细的动态库信息,同时也可以过滤平台,搜索名称、CPU 架构、
动态库特征等。例如:
pyarmor download --list
pyarmor download --list windows
pyarmor download --list windows.x86_64
pyarmor download --list JIT
pyarmor download --list armv7
当 pyarmor 升级之后,已经下载的动态库不会自动更新,需要使用 --update
进行
更新。例如:
pyarmor download --update
runtime¶
创建 运行辅助包
SYNOPSIS:
pyarmor runtime <options>
OPTIONS:
-O, --output PATH | |
输出路径,默认是 dist | |
-n, --no-package | |
不要使用包的形式来存放生成运行文件 | |
-L, --with-license FILE | |
使用这个文件替换默认的加密脚本许可文件 | |
--platform NAME | |
生成其他平台下的运行辅助包 |
DESCRIPTION
这个命令主要用来创建 运行辅助包
因为使用相同的 全局密钥箱 加密的脚本可以共享 运行辅助包 ,所以单独 创建运行辅助包之后,在加密脚本的时候就不需要每一次都重新生成运行辅助文件。
选项 --platform
的使用,请参考命令 obfuscate
EXAMPLES
了解加密脚本¶
全局密钥箱¶
全局密钥箱是存放在用户主目录的一个文件 .pyarmor_capsule.zip
。当 PyArmor
加密脚本或者生成加密脚本的许可文件的时候,都需要从这个文件中读取数据。
所有的试用版本使用同一个密钥箱 公共密钥箱 ,这个密钥箱使用 1024 位 RSA 密钥对。
而对于正式版本,每一个用户都会收到一个专用的 私有密钥箱 ,这种密钥箱使用 2048 位 RSA 密钥对。
通过密钥箱是无法恢复加密后的脚本,所以即便都使用 公共密钥箱 ,加密后的脚本也是 安全的,但是使用相同的密钥箱为加密脚本生成的许可是通用的。使用 公共密钥箱 是无 法真正的限制加密脚本的使用期限或者绑定到某一台指定设备上,因为别人同样可以使用 公共密钥箱 为你的加密脚本生成合法的许可。
运行加密脚本并不需要密钥箱,只有在加密脚本和为加密脚本生成许可的时候才会用到。
加密后的脚本¶
和原来的脚本相比,被 PyArmor 加密后的脚本需要额外的运行辅助文件,下面是加密后在 输出目录 dist 下的所有文件清单:
myscript.py
mymodule.py
pytransform/
__init__.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...')
主脚本¶
在 PyArmor 中,主脚本并不一定是启动脚本,而是在运行 Python 解释器之后,第一个被 执行(导入)的加密脚本。例如,如果只有一个 Python 包的被加密,那么这个包的 __init__.py 就是主脚本。
引导代码¶
主脚本的前两行就是 引导代码 ,它一般出现在主脚本中:
from pytransform import pyarmor_runtime
pyarmor_runtime()
对于被加密的 Python 包,其主脚本是 __init__.py ,那么引导代码会使用相对导入的 方式:
from .pytransform import pyarmor_runtime
pyarmor_runtime()
如果加密脚本的时候指定了运行时刻路径,引导代码会是这样的形式:
from pytransform import pyarmor_runtime
pyarmor_runtime('/path/to/runtime')
运行辅助包¶
和加密脚本一起生成的目录 pytransform 是一个 Python 的包,叫做 运行辅助包 。 它是运行加密脚本的唯一依赖,只要它在任何一个 Python 路径下面,加密脚本就可以像普 通脚本一样被正常执行。
通常情况下面运行辅助包和加密脚本在相同目录下,但是也可以在其他任何路径,只要可以 使用 import pytransform 能够正常导入就可以。并且使用相同 全局密钥箱 加 密的脚本都可以共用这个包。 你完全可以把这个包放到 Python 的系统库路径下面,这样 加密后的脚本目录结构就和原来完全一样。
运行辅助包里面有四个文件:
pytransform/
__init__.py Python 模块文件
_pytransform.so/.dll/.lib 动态链接库,核心功能的实现
pytransform.key 数据文件
license.lic 加密脚本的许可文件
在 v5.7.0 之前, 运行辅助包是另外一种存放形式 运行辅助文件
运行辅助文件¶
它们不是以 Python 包的形式存在,而是四个单独文件:
pytransform.py Python 模块文件
_pytransform.so/.dll/.lib 动态链接库,核心功能的实现
pytransform.key 数据文件
license.lic 加密脚本的许可文件
很明显,使用运行辅助包的形式使得加密后的脚本目录结构更清晰。
加密脚本的许可文件¶
运行辅助文件中的 license.lic 作用比较特殊,它包含着对加密脚本的运行许可信息。 在加密脚本的同时会在输出目录下面生成一个默认许可文件,该文件允许加密脚本运行在任 何机器并且永不过期。
如果需要为加密脚本设置新的许可,例如设置有效期,限制加密脚本在特定机器上运行,需 要运行命令 licenses 生成新的相应的许可文件,然后用新生成的 license.lic 覆盖原来的许可文件。
注解
PyArmor 的安装目录下面也有一个 license.lic ,这个文件主要是设置 PyArmor 自 身的许可,这个许可是由我来发布的,:)
使用加密脚本的基本原则¶
加密后的脚本也是一个正常的 Python 脚本,它可以无缝替换原来的脚本
唯一的改变是, 引导代码 必须被首先执行,加密脚本才能正常运行,否则会报错。
引导代码 会使用 ctypes 装载动态库 _pytransform.so/.dll/.dylib 。动态库 是平台相关的,所有预编译的动态库列表在这里 支持的平台列表
默认情况下, 引导代码 会在 运行辅助包 里面搜索动态库 _pytransform ,具 体装载过程可以查看函数 pytransform._load_library 的源代码。
如果动态库 _pytransform 没有在默认位置,需要修改 引导代码 ,设置运行时刻路 径:
from pytransform import pyarmor_runtime pyarmor_runtime('/path/to/runtime')
运行辅助文件 license.lic 和 pytransform.key 必须也在这个目录下面
如果在代码中动态创建新的执行环境,例如 multiprocssing.Process, os.exec, subprocess.Popen 等, 要确保 引导代码 在新的执行环境被首先执行,否则加密脚 本会报错。
加密脚本和原脚本的区别¶
加密脚本和原来的脚本相比,存在下列一些的不同:
运行加密脚本的 Python 主版本和加密脚本使用的 Python 主版本应该要一致, 因为加密的脚本实际上已经是 .pyc 文件,如果主版本不一致,有些指令无 法识别或者会出错。尤其是 Python3.6,在这个版本引入了新的指令系统,所 以和 Python3.5 以及之前的版本完全不同。
执行加密角本的 Python 不能是调试版,准确的说,不能是设置了 Py_TRACE_REFS 或者 Py_DEBUG 生成的 Python
使用
sys.settrace
,sys.setprofile
,threading.settrace
和threading.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__)
PyArmor 的工作原理¶
让我们看看一个普通的 Python 脚本 foo.py 加密之后是什么样子。下面是加 密脚本所在的目录 dist 下的所有文件列表:
foo.py
pytransform/
__init__.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 叫做 运行辅助包 ,它是运行 加密脚本所必须的。只要这个包能被正常导入进来,加密脚本 dist/foo.py 就可以像正常脚本一样被运行。
这是 PyArmor 的一个重要特征: 加密脚本无缝替换原来的脚本
如何加密脚本¶
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...')
如何处理插件¶
在 PyArmor 中插件主要用于在加密的过程中向脚本中注入代码,例如:
pyarmor obfuscate --plugin check_multi_mac --plugin @assert_armored foo.py
也可以指定插件所在路径,例如:
pyarmor obfuscate --plugin /path/to/check_ntp_time foo.py
每一个插件对应一个 Python 的脚本文件, PyArmor 搜索插件的顺序:
- 如果插件指定了绝对路径,那么直接在这个路径下面查找对应的 .py 文件
- 如果是相对路径,在当前目录下面找对应的 .py 文件,当前目录下面找不到,就在环
境变量
PYARMOR_PLGUIN
指定的路径下查找 - 没有找到就抛出异常
如果在加密脚本的时候指定了插件,PyArmor 在加密脚本之前,会逐行扫描源代码的注释去 查找插件桩。插件桩分为两种类型:
- 插件定义桩
- 插件调用桩
插件定义桩的格式如下:
# {PyArmor Plugins}
插件定义桩必须单独占一行,不能有缩进,必须在其他插件调用桩的前面。而且一个脚本中 只能有一个插件定义桩,所有插件对应的脚本文件会被原封不动的插入到下面。需要注意的 是,如果脚本中没有插件定义桩,任何插件定义代码都不会被注入到加密脚本中。
插件调用桩有三种格式,只要注释行的前缀和下面的任意模式匹配,就是一个插件调用桩:
# PyArmor Plugin:
# pyarmor_
# @pyarmor_
插件调用桩可以有缩进,可以在模块的任何地方,但是必须在插件调用桩之后,插件调用桩 可以有任意多个。
对于第一种格式 # PyArmor Plugin:
,PyArmor 只是简单的把匹配的部分和紧随其后
的一个空格删除,只剩下后半部分的代码。例如:
# PyArmor Plugin: check_ntp_time() ==> check_ntp_time()
只要在命令行指定了插件,这种替换就会发生,它的后半部分可以是任何有效的 Python 语 句,例如:
# PyArmor Plugin: print('This is plugin code') ==> print('This is plugin code')
# PyArmor Plugin: if sys.flags.debug: ==> if sys.flags.debug:
# PyArmor Plugin: check_something(): ==> check_something()
第二种格式 # pyarmor_
则只用于调用插件函数,并且仅仅当这个函数作为插件名称出
现在命令行中时候才进行替换。例如,使用插件 check_multi_mac 加密脚本的时候,第
一个调用桩会被替换,第二个不会被替换:
# pyarmor_check_multi_mac() ==> check_multi_mac()
# pyarmor_check_code() ==> # pyarmor_check_code()
第三种格式和第二种类似,只是 # @pyarmor_
会被替换成为 @
,主要用于注入修
饰函数。例如:
# @pyarmor_assert_obfuscated(foo.connect) ==> @assert_obfuscated(foo.connect)
在命令行指定插件名称的时候,如果插件前面没有前置字母 @
,插件总是会被注入到
插件定义桩下面。例如, 即便没有任何插件调用语句,脚本 check_multi_mac.py 和
assert_armored.py 总是会被注入到插件定义桩下面:
pyarmor obfuscate --plugin check_multi_mac foo.py
如果插件有一个前置字母 @
,那么只有在插件调用桩中出现的插件才会被导入进来。
例如,如果 foo.py 中没有任何插件调用桩,下面的两个插件都会被忽略掉:
pyarmor obfuscate --plugin @assert_armored foo.py
pyarmor obfuscate --plugin @/path/to/check_ntp_time foo.py
对主脚本的特殊处理¶
和其他模块不一样,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():
def _check_co_key(co, v):
return (len(co.co_names), len(co.co_consts), len(co.co_code)) == v
for k, (v1, v2, v3) in {keylist}:
co = getattr(pytransform, k).{code}
if not _check_co_key(co, v1):
raise RuntimeError('unexpected pytransform.py')
if v2:
if not _check_co_key(co.co_consts[1], v2):
raise RuntimeError('unexpected pytransform.py')
if v3:
if not _check_co_key(co.{closure}[0].cell_contents.{code}, v3):
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 会在最前面插入 引导代码 。
如何运行加密脚本¶
那么,一个普通的 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 提供了一个命令 pack 可以用来直接打包脚本,它会首先加密脚本,然后 调用 PyInstaller 打包,但是在某些情况下,打包可能会失败。这里详细描述了命令 pack 的内部工作原理,可以帮助定位问题所在,同时也可以作为自己直接使用 PyInstaller 打包加密脚本的使用手册。
PyArmor 需要 PyInstaller 来完成加密脚本的打包工作,如果没有安装的话,首先执行 下面的命令进行安装:
pip install 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.*
-p dist/obf --hidden-import pytransform
hello.py
第三步是修改 hello.spec, 在 Analysis 之后插入下面的语句,主要作用是打包的时 候使用加密后的脚本,而不是原来的脚本:
src = os.path.abspath('.')
obf_src = os.path.abspath('dist/obf')
for i in range(len(a.scripts)):
if a.scripts[i][1].startswith(src):
x = a.scripts[i][1].replace(src, obf_src)
if os.path.exists(x):
a.scripts[i] = a.scripts[i][0], x, a.scripts[i][2]
for i in range(len(a.pure)):
if a.pure[i][1].startswith(src):
x = a.pure[i][1].replace(src, obf_src)
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
运行时刻模块 pytransform¶
如果你意识到加密后的脚本对用户来说就是黑盒子,那么有很多事情都可以在
Python 脚本里来做。在这个时候,模块 pytransform
会提供很多有用
的函数和功能。
模块 pytransform
是和加密脚本一起发布,在运行加密脚本之前必须被
导入进来,所以,你可以在你的脚本中直接导入这个模块,使用里面的函数。
内容¶
-
exception
PytransformError
¶ 任何 PyArmor 的 api 失败都会抛出这个异常,传入的参数是错误发生的原因。
-
get_expired_days
()¶ 返回加密脚本的剩余的有效天数。
>0: 剩余的有效天数
-1: 永不过期
注解
如果加密脚本已经过期,会直接抛出异常并退出。加密脚本里面的任何代码 都不会被执行,所以一般情况下这个函数是不会返回 0 的。
-
get_license_info
()¶ 获取加密脚本许可证的相关信息。
返回一个字典,可能的键名有:
- expired: 许可过期的日期
- IFMAC:绑定的网卡 MAC 地址
- HARDDISK: 绑定的硬盘序列号
- IPV4:绑定的 IPv4 地址
- DATA:自定义的数据,用于扩展认证方式
- CODE:许可注册码
如果许可证没有包含对应的键名,那么其值为 None
如果许可文件非法,例如已经过期,会抛出异常
Exception
-
get_license_code
()¶ 返回字符串,该字符串是生成许可文件时指定的注册码参数。
如果认证文件非法或者无效,抛出异常
Exception
-
get_hd_info
(hdtype, size=256)¶ 得到当前机器的硬件信息,通过 hdtype 传入需要获取的硬件类型,可用的 值如下:
- HT_HARDDISK 返回硬盘序列号
- HT_IFMAC 返回网卡Mac地址
无法获取硬件信息会抛出异常
Exception
-
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 Exception as e:
print(e)
更多内容,请参考 使用插件扩展认证方式
支持的平台列表¶
PyArmor 的核心函数使用 C 来实现,对于常用的平台和部分嵌入式系统都已经 有编译好的动态库。
最常用平台的动态库已经打包在 PyArmor 的安装包里面,只要安装好之后即可 使用,参考 预安装的动态库清单 。
其他平台的动态库并没有随着安装包发布,参考 其他平台的动态库清单 。
在这些平台下面, pyarmor 会搜索 ~/.pyarmor/platforms/SYSTEM/ARCH
,其中 SYSTEM.ARCH
是一个 标准平台名称 。如果还没有下载,那么会
从远程服务器下载相应平台的动态库。
有些平台 pyarmor 无法自动识别,但是在 其他平台的动态库清单 中有可
用的动态库。可以直接下载下来,保存到这个平台的搜索路径
~/.pyarmor/platforms/SYSTEM/ARCH
下面。如果不能确定存放的路径,可
以使用命令 pyarmor -d download
查看,在开始的时候会显示 pyarmor
去那里查找动态库。如果需要让 pyarmor 自动识别这个平台,请把下面这个
脚本的输出发送到 jondy.zhao@gmail.com
from platform import *
print('system name: %s' % system())
print('machine: %s' % machine())
print('processor: %s' % processor())
print('aliased terse platform: %s' % platform(aliased=1, terse=1))
if system().lower().startswith('linux'):
print('libc: %s' % libc_ver())
print('distribution: %s' % linux_distribution())
如果需要在上面没有列出的平台使用 PyArmor,请发送邮件到 jondy.zhao@gmail.com
标准平台名称¶
这些名称可用于命令 obfuscate, build, runtime, download 中来指定平台名称。
- windows.x86
- windows.x86_64
- linux.x86
- linux.x86_64
- darwin.x86_64
- vs2015.x86
- vs2015.x86_64
- linux.arm
- linux.armv7
- linux.aarch32
- linux.aarch64
- android.aarch64
- linux.ppc64
- darwin.arm64
- freebsd.x86_64
- alpine.x86_64
- alpine.arm
- poky.x86
名称 | 操作系统 | CPU架构 | 特征 | 下载 | 说明 |
---|---|---|---|---|---|
windows.x86 | Windows | i686 | 反调试、JIT、高级模式 | _pytransform.dll | 使用 i686-pc-mingw32-gcc 交叉编译 |
windows.x86_64 | Windows | AMD64 | 反调试、JIT、高级模式 | _pytransform.dll | 使用 x86_64-w64-mingw32-gcc 交叉编译 |
linux.x86 | Linux | i686 | 反调试、JIT、高级模式 | _pytransform.so | 使用 GCC 编译 |
linux.x86_64 | Linux | x86_64 | 反调试、JIT、高级模式 | _pytransform.so | 使用 GCC 编译 |
darwin.x86_64 | MacOSX | x86_64, intel | 反调试、JIT、高级模式 | _pytransform.dylib | 使用 CLang 编译(MacOSX10.11) |
名称 | 操作系统 | CPU架构 | 特征 | 下载 | 说明 |
---|---|---|---|---|---|
vs2015.x86 | Windows | x86 | _pytransform.dll | 使用 VS2015 编译 | |
vs2015.x86_64 | Windows | x64 | _pytransform.dll | 使用 VS2015 编译 | |
linux.arm | Linux | armv5 | _pytransform.so | 32-bit Armv5 (arm926ej-s) | |
linux.armv7 | Linux | armv7 | 反调试、JIT | _pytransform.so | 32-bit Armv7 Cortex-A, hard-float, little-endian |
linux.aarch32 | Linux | aarch32 | 反调试、JIT | _pytransform.so | 32-bit Armv8 Cortex-A, hard-float, little-endian |
linux.aarch64 | Linux | aarch64 | 反调试、JIT | _pytransform.so | 64-bit Armv8 Cortex-A, little-endian |
linux.ppc64 | Linux | ppc64le | _pytransform.so | 适用于 POWER8 | |
darwin.arm64 | iOS | arm64 | _pytransform.dylib | 使用 CLang 编译(iPhoneOS9.3sdk) | |
freebsd.x86_64 | FreeBSD | x86_64 | _pytransform.so | 不支持获取硬盘序列号 | |
alpine.x86_64 | Alpine Linux | x86_64 | _pytransform.so | 可用于 Docker(musl-1.1.21) | |
alpine.arm | Alpine Linux | arm | _pytransform.so | 可用于 Docker(musl-1.1.21), 32 bit Armv5T, hard-float, little-endian | |
poky.x86 | Inel Quark | i586 | _pytransform.so | 使用 i586-poky-linux 交叉编译 | |
android.aarch64 | Android | aarch64 | _pytransform.so | Build by android-ndk-r20/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang |
加密模式¶
PyArmor 提供多种加密模式,以满足安全和性能方面的平衡。通常情况下,默认 的加密模式能够满足绝大多数的需要,一般情况下也无不需要对加密模式有详细 的了解。仅当对性能有特别的要求或者默认加密模式无法满足需求的时候,才需 要改变加密模式,这就需要理解 PyArmor 的不同加密模式。
高级模式¶
高级模式是从 PyArmor 5.5.0 开始增加的新功能,在这种模式下,加密脚本中 代码块的 PyCode_Type 的结构会被修改,同时在加密脚本运行的时候,会在 Python 动态库中注入一个钩子函数,来处理这种非正常的代码块。在注入钩子 函数的同时,也会检查 Python 解释器是否被修改,如果解释器行为不正常,加 密脚本就不会在继续执行。这个特性需要分析汇编指令,目前只在 X86/X64 系 列的 CPU 上实现,并且还依赖于编译器。一些使用低版本 GCC 编译的 Python 解释器可能无法被 PyArmor 正确识别,所以目前高级模式默认是没有启用的。
使用下面的命令可以启用高级模式加密脚本:
pyarmor obfuscate --advanced 1 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
在开始插入了一条绝对跳转指令,当执行加密后的代码块时
- 首先执行 JUMP_ABSOLUTE, 直接跳转到偏移 n
- 偏移 n 处是调用一个 PyCFunction __armor__ 。这个函数的功能会恢复 上面加密后的代码块,并且把代码块移动到最开始(向前移动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
这样,当被加密的函数开始执行的时候
- 首先会调用 __armor_enter__ 恢复加密后的代码块
- 然后执行真正的函数代码
- 在函数执行完成之后,进入 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.7.0 开始, 引导代码 必须在加密脚本中,并且加密脚本 还必须是主脚本。例如,同一个目录下有两个文件 foo.py 和 test.py , 使用下面的命令加密:
pyarmor obfuscate foo.py
如果直接往加密后的脚本 dist/test.py 中插入 引导代码 ,这个加密脚本 就无法使用,因为这个脚本加密的时候没有被指定为主脚本。只能使用下面的命 令来插入 引导代码:
pyarmor obfuscate --no-runtime --exact test.py
如果需要在没有加密的脚本中运行 引导代码 ,可以使用一种变通方式。 首先加密一个空脚本:
echo "" > pytransform_bootstrap.py
pyarmor obfuscate --no-runtime --exact pytransform_bootstrap.py
然后在导入这个加密后的空脚本 import pytransform_bootstrap 。
从 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 来加密那些只是在包内部使用的脚本。
例如,加密后 mypkg/__init__.py 的可访问性如下
# mypkg/
# __init__.py 使用约束模式 1 加密
# foo.py 使用约束模式 4 加密
# "foo.hello" 不能被没有加密的脚本直接调用
from .foo import hello
# "open_hello" 可以被其他脚本直接调用
def open_hello(msg):
print('This is public hello: %s' % msg)
# "proxy_hello" 可以被其他脚本直接调用
def proxy_hello(msg):
print('This is proxy hello: %s' % msg)
# "foo.hello" 可以被加密脚本 "__init__.py" 调用
hello(msg)
注解
约束模式 2 和 3 不能用于加密 Python 包,否则加密后的包是无法被非加 密的脚本导入的。
注解
约束模式是针对单个脚本的,不同的脚本可以有不同的约束模式。
从 PyArmor 5.2 开始, 约束模式 1 是默认设置。
如果需要使用其他约束模式加密脚本,通过选项 --restrict
指定。例如:
pyarmor obfuscate --restrict=2 foo.py
pyarmor obfuscate --restrict=4 foo.py
# For project
pyarmor config --restrict=2
pyarmor build -B
如果需要禁用上面全部的约束, 那么使用下面的命令加密脚本:
pyarmor obfuscate --restrict=0 foo.py
# For project
pyarmor config --restrict=0
pyarmor build -B
详细示例请参考 使用约束模式增加加密脚本安全性
加密脚本的性能¶
运行命令 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 -h
组合上面列出的可用加密模式,测试不同加密模式的性能。例如:
pyarmor benchmark --wrap-mode 0
查看测试命令使用的脚本,使用选项 --debug
保留生成的中间文件,所有的
中间文件保存在目录 .benchtest 下面:
pyarmor benchmark --debug
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 会首先加密函数 hello 和 sum ,然后在加密整个模块,进行两次 加密。当运行加密的 hello 的时候, sum 依旧是加密的。hello 执行完 成之后,会被重新加密,然后才开始解密并执行 sum 。
交叉保护机制¶
PyArmor 的核心代码使用 c 来编写,所有的加密和解密算法都在动态链接库中 实现。 首先 _pytransform 自身会使用 JIT 技术,即动态生成代码的方式 来保护自己,加密后的 Python 脚本由动态库 _pytransform 来保护,反过来, 在加密的 Python 的脚本里面,也会来校验动态库,确保其没有进行任何修改。 这就是交叉保护的原理,Python 代码和 c 代码相互进行校验和保护,大大提 高了安全性。
动态库保护的核心有两点:
- 用户不能通过修改代码段指令来获得没有授权的使用。例如,将指令 JZ 修 改为 JNZ ,从而使得认证失败可以继续执行
- 加密 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 动态 代码保护例程:
读取 functiion 1 的数据代码,动态生成 function 1
执行 function 1:
检查代码段的校验和,如果不一致,退出 检查当前是否有调试器,如果发现,退出 检查执行时间是否太长,如果执行时间太长,退出 如果可能的话,清除硬件断点寄存器 恢复下一个函数 `function 2` 的数据代码
读取 functiion 2 的数据代码,动态生成 function 2
重复步骤 2 的操作
这样循环有限次之后,真正受保护的代码才被执行。总之,主要达到的目的是开 始执行受保护的代码的时候,不能被调试器中断。
为了在 Python 端保护动态库没有被进行任何修改,在加密主脚本的时候,会插 入额外的一段代码来检查和保护动态链接库,详细工作原理参考 对主脚本的特殊处理
常见问题¶
当出现问题的时候,先尝试一些方法看能否解决它。
对于运行 pyarmor 出现的问题:
查看控制台的输出,有没有什么路径错误,和一些有效的错误信息
使用调试选项
-d
运行,显示执行堆栈和更多的调试信息。例如:pyarmor -d obfuscate --recurisve foo.py PYTHONDEBUG=y pyarmor -d obfuscate --recurisve foo.py
对于运行加密脚本出现的问题:
尝试打开 Python 的调试选项查看更多的错误信息。例如:
python -d obf_foo.py PYTHONDEBUG=y python obf_foo.py
在任何情况下,打开 Python 的调试开关之后,会在当前目录创建一个日志文件 pytransform.log ,里面包含有帮助定位问题的更多信息。
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 在 运行辅助包 里面(在 v5.7.0 之前,是和加密脚本在相同的目录下):
- _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
ERROR: Unsupport platform linux.xxx¶
有些平台 pyarmor 无法自动识别,如果在 `其他平台的动态库清单`_ 中有可
用的动态库。可以直接下载下来,保存到这个平台的搜索路径
~/.pyarmor/platforms/SYSTEM/ARCH
下面。如果不能确定存放的路径,可
以使用命令 pyarmor -d download
查看,在开始的时候会显示 pyarmor
去那里查找动态库。
如果需要在上面没有列出的平台使用 PyArmor,请发送邮件到 jondy.zhao@gmail.com
/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
注解
从 v5.7.9 开始,在 linux/x86_64 平台(例如,CentOS6)已经不需要这个补丁就可以工作了。
也可以在使用下面的命令在其他平台加密脚本:
pyarmor obfuscate --platform centos6.x86_64 foo.py
加密脚本的问题¶
'GBK' codec can't decode byte 0xXX¶
在源代码的第一行指定字符编码,例如:
# -*- coding: utf-8 -*-
关于源文件字符编码,请参考 https://docs.python.org/2.7/tutorial/interpreter.html#source-code-encoding
Warning: code object xxxx isn't wrapped¶
这是因为函数中包含特殊情况的跳转指令而引起的,例如,某一个函数编译成为 byte code 之后如果有类似这样的一条指令 JMP 255
在加密之前,这条指令占用两个字节。而在加密之后,因为在函数头部插入了额 外的指令,这个跳转指令的操作数变成了 267 。在 Python3.6 之后,这条指 令需要 4 个字节:
EXTEND 1
JMP 11
遇到这种情况,目前 PyArmor 就不会加密这个函数, 并显示这个警告信息。当 然,只是这个函数没有被加密,当前模块中的其他函数还是被正常加密的。
如果想要避免这个问题,可以尝试在函数里面增加一些冗余的语句,让跳转长度 不要在临界值即可。
后面的版本会考虑解决这个问题,因为一旦修改原来的指令长度,代码块所有的 相对跳转、绝对跳转指令都需要调整,情况比较复杂,所以遇到这种情况,暂时 忽略了。
注解
这个警告在 v5.5.0 已经修正,对于这种情况,会使用非包裹方式加密。
Error: Try to run unauthorized function¶
试图使用没有授权的功能。出现这个问题一般是当前目录下面存在 license.lic`或者 `pytransform.key 而导致的认证问题,解决方案是一是删 除这些不必要文件,或者升级到 PyArmor 5.4.5 以后的版本。
运行加密脚本的问题¶
NameError: name '__pyarmor__' is not defined¶
原因是 引导代码 没有被执行。
当使用模块 subprocess 或者 multiprocessing , 调用 Popen 或者 Process 创建新的进程的时候,确保 引导代码 在新进程中也得到执 行。否则新进程是无法使用加密脚本的。
Marshal loads failed when running xxx.py¶
当出现这个问题,依次进行下面的检查
- 检查运行加密脚本的 Python 的版本和加密脚本的 Python 版本是否一致
- 使用 python -d 运行加密脚本,查看更多的错误信息
- 确保生成许可使用的密钥箱和加密脚本使用的密钥箱是相同的(当运行 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
更多信息参考 对主脚本的特殊处理
运行脚本时候提示: Invalid input pack¶
如果加密脚本是在不同的平台下进行加密,参考 跨平台发布加密脚本 里面的备注。
在 v5.7.0 之前,检查当前目录下面是否有存在文件 license.lic 或者 pytransform.key ,如果存在的话,确保它们是加密脚本对应的运行时刻文件。
因为加密脚本会首先在当前目录查看是否存在 license.lic 和运行时刻文件 pytransform.key, 如果存在,那么使用当前目录下面的这些文件。
其次加密脚本会查看运行时刻模块 pytransform.py 所在的目录,看看有没有 license.lic 和 pytransform.key
如果当前目录下面存在任何一个文件,但是和加密脚本对应的运行时刻文件不匹 配,就会报这个错误。
加密脚本的许可问题¶
The license.lic generated doesn't work¶
通常情况下是因为加密脚本使用的密钥箱和生成许可文件时候使用的密钥箱不一 样,例如在试用版本加密脚本,但是在正式版本下面生成许可文件。
通用的解决方法就是重新把加密脚本生成一下,然后在重新生成许可文件。
打包加密问题¶
No module name pytransform¶
在使用 pyarmor pack 打包的时候报这个错误:
确认命令行指定的脚本是没有加密的
使用选项
--clean
清除缓存的 myscript.spec:pyarmor pack --clean foo.py
PyArmor 注册问题¶
软件许可¶
PyArmor 是一个共享软件。试用版永不过期,试用版的限制是
- 最大可加密的脚本大小(编译成为 .pyc 之后)是 32768 个字节
- 在试用版中中生成的加密脚本不是私有的,也就是说,其他任何人也可以为这 些加密脚本生成新的许可文件。
- 任何人都可以使用本软件加密非商业用途的Python脚本,未经许可不得用于商 业用途。
关于加密脚本的许可文件,可以参考 加密脚本的许可文件
生成私有的加密脚本和加密任意大小的脚本需要购买下列任意一种许可证。
PyArmor 有两种类型的许可证:
个人用户许可。个人用户购买一个许可证可以在自己所有的计算机和相关硬 件设备上使用。
个人用户许可证允许使用本软件加密任何属于自己的 Python 脚本,为加密 脚本生成私有许可文件,发布加密后的脚本和必要的辅助文件到任何其他设 备。
企业用户许可。企业用户购买一个软件许可证可以在同一个产品系列的各个 项目中使用。
企业用户许可证允许使用本软件在任何设备上,加密属于该产品系列的 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 之前的版本,使用下面的方式注册:
- 解压注册文件
- 拷贝解压后的 "license.lic" 到 PyArmor 的安装目录下面
- 拷贝解压后的 ".pyarmor_capsule.zip" 到用户的 HOME 目录
软件注册码永久有效,可以一直使用,但是不能转接或者租用.