Pyarmor 是一个用于加密和保护 Python 源代码的小工具。它能够在运行时刻保护 Python 脚本的二进制代码不被泄露,设置加密后 Python 源代码的有效期限,绑定加密后的Python 源代码到硬盘、网卡等硬件设备。它的保障机制主要包括
让我们看看一个普通的 Python 脚本 foo.py
加密之后是什么样子。下面是加
密脚本所在的目录 dist
下的所有文件列表
foo.py
pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pyshield.key
pyshield.lic
product.key
license.lic
dist/foo.py
是加密后的脚本,它的内容如下
from pytransfrom import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'\x06\x0f...')
所有其他文件叫做 运行依赖文件
,它们是运行加密脚本所必须的。并且只要这里面的模块pytransform.py
能被正常导入进来,加密脚本 dist/foo.py
就可以像正常脚本一样被运行。这是 Pyarmor 的一个重要特征: 加密脚本无缝替换 Python 源代码
Pyarmor 是怎么加密 Python 源代码呢?
首先把源代码编译成代码块 Code Object
char *filename = "foo.py";
char *source = read_file( filename );
PyCodeObject *co = Py_CompileString( source, "" , 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 pytransfrom 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 );
}
第一个导入的模块是 __main__
, 从现在开始,在整个 Python 解释器的生命周期中
__armor_enter__
, static PyObject *
__armor_enter__(PyObject *self, PyObject *args)
{
// 通过当前执行堆栈得到当前代码块指针
PyFrameObject *frame = PyEval_GetFrame();
PyCodeObject *f_code = frame->f_code;
// 借用 co_names->ob_refcnt 来记录当前代码块
// 的调用次数
PyObject *refcalls = f_code->co_names;
refcalls->ob_refcnt ++;
// 恢复被加密的代码块
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)
{
// 得到当前代码块指针
PyFrameObject *frame = PyEval_GetFrame();
PyCodeObject *f_code = frame->f_code;
// 调用计数器递减
PyObject *refcalls = f_code->co_names;
refcalls->ob_refcnt --;
// 仅当调用计数器为 0 的时候重新加密代码块的代码段 co_code
// 在多线程、递归等很多种情况下,都会出现一个代码段 co_code
// 被多个代码块 Code Object 同时使用的情况
if (refcalls->ob_refcnt == 1) {
obfuscate_byte_code(f_code->co_code);
set_obfuscated_flag(f_code);
}
// 清空当前堆栈的局部变量
clear_frame_locals(frame);
Py_RETURN_NONE;
}
当 引导代码
pyarmor_runtime()
被调用时候,它会检查授权文件dist/license.lic
。
如果存在非授权的使用,就会报错退出。在加密脚本的时候同时会生成一个默认的授权文件,它允许加密脚本运行在任何机器上,并且永不过期。
我们可以在授权文件里面包含一个有效的日期,或者硬盘序列号,网卡的Mac地址等,这样pyarmor_runtime()
就可以检查时间,比对硬件设备,从而确定当前运行环境是否满足条件,选择继续运行或者报错退出。
Pyarmor 使用命令 hdinfo
来获取目标机器的硬件信息
python pyarmor.py hdinfo
然后使用命令 licenses
来生成新的授权文件
python pyarmor.py licenses
--expired 2018-12-31
--bind-disk "100304PBN2081SF3NJ5T"
--bind-mac "70:f1:a1:23:f0:94"
--bind-ipv4 "202.10.2.52"
Customer-Jondy
更多详细信息,请访问 Pyarmor 网站主页