Changelogs » Pyarmor

PyUp Safety actively tracks 232,000 Python packages for vulnerabilities and notifies you when to upgrade.

Pyarmor

0.5

不可接受的,也迫使我重新思考加密的机制和方法。
  
  有一天想到了`C`代码的加密保护方式,
  
  * 首先编译成可执行文件或者动态链接库
  
  * 然后替换二级制文件中的函数代码块,并在每一个函数入口处插入一条跳转指
  令,跳转到自己定义的包裹函数
  
  * 在包裹函数里面,进行反编译侦测,没有问题的话恢复原来的函数代码,并跳
  转到真正的函数入口执行
  
  那么,在Python中是不是可以借鉴这种方式,不是在源代码层,而是在可执行文
  件层,直接通过修改汇编指令来进行保护呢?这世界上的事情只有想不到的,没
  有做不到了。有了这样的思路,一种全新的加密机制马上就出来了。在Python中,
  汇编指令对应的就是伪代码(byte code),模仿`C`代码保护方式的实现方式如
  下:
  
  1. 首先是编译Python源文件为代码对象(Code Object)
  
  c
  char * filename = "xxx.py";
  char * source = read_file( filename );
  PyObject *co = Py_CompileString( source, filename, Py_file_input );
  
  
  2. 然后遍历代码对象的所有子代码对象,并对每一个代码对象进行如下处理
  
  * 使用 `try...finally` 语句包裹代码对象的伪代码(co_code),改造后的
  伪代码如下
  
  
  LOAD_GLOBALS    N (__armor_enter__)
  CALL_FUNCTION   0
  POP_TOP
  SETUP_FINALLY   X (jump to wrap footer)
  
  以上是额外增加的包裹头部,调用 __armor_enter__,然后开始一个 try...finally 块
  
  中间是处理过的原始伪代码,主要进行了如下处理
  
  修改所有的绝对跳转指令(例如 JUMP_ABSOLUTE)的目标地址,增加包裹头部的大小
  然后加密修改后的伪代码
  
  以下是额外增加的包裹尾部,是 try...finally 块的执行代码,调用 __armor_exit__
  
  LOAD_GLOBALS    N + 1 (__armor_exit__)
  CALL_FUNCTION   0
  POP_TOP
  END_FINALLY
  
  
  
  * 在代码对象的常量列表(co_consts)的最后面增加两个函数名称(字符串)
  * `__armor_enter__`
  * `__armor_exit__`
  
  * 代码对象的堆栈(co_stacksize)大小增加2
  
  刚开始的时候没有增加堆栈,结果在64位机器上工作正常,在32位机器
  上出现各种崩溃问题。更令人头疼的是,使用 gdb 进行跟踪又不崩溃。
  足足花了我几乎半个月的时间,最终才发现是堆栈出了问题。我有时候
  就想,当你发现真正的原因的时候,总感觉为什么这么简单的解决方式,
  也就增加了两行代码,却花费了你如此多的时间。如何才能高效快速的
  定位原因所在呢,这个问题值得深思。
  
  3. 把改造后的代码对象转换成为字符串,并进行加密,保护里面的常量和字符串
  
  c
  char *original_code = marshal.dumps( co );
  char *obfuscated_code = obfuscate_algorithm( original_code  );
  
  
  
  4. 创建最终的加密脚本,生成一个和原来文件同名的 `.py` 文件
  
  
  sprintf( buf, "__pyarmor__(__name__, __file__, b'%s')", obfuscated_code );
  save_to_file( "/path/to/output/xxx.py", buf );
  
  
  
  最终的加密脚本也是一个合法的Python源文件,长的就像这个样子
  
  
  __pyarmor__(__name__, __file__, b'\x01\x0a...')
  
  
  
  这个加密的脚本可以像普通的Python脚本一样被使用,但是在使用之前,必须添
  加三个自定义的函数到内置模块 `builtins` 里面
  
  * `__pyarmor__`
  * `__armor_enter__`
  * `__armor_exit__`
  
  这样,当加密脚本被Python解释器执行的时候
  
  1. `__pyarmor__` 首先被调用,负责导入加密的模块,它的原型和实现如下
  
  c
  int __pyarmor__(char *modname, char *pathname, unsigned char *obfuscated_code) {
  
  char *original_code = resotre_obfuscated_code( obfuscated_code );
  PyObject *co = marshal.loads( original_code );
  PyObject *m = PyImport_ExecCodeModuleEx( modname, co, pathname );
  
  }
  
  
  2. `__armor_enter__` 会在每一个代码块执行的时候被调用,它的原型和实现如下
  
  c
  static PyObject*
  enter_armor(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_OBFUSCATE(f_code->co_flags)) {
  restore_byte_code(f_code->co_code);
  clear_obfuscate_flag(f_code);
  }
  
  Py_RETURN_NONE;
  }
  
  
  
  3. `__armor_exit__` 会在每一个代码块执行完成的时候被调用,它的原型和实现如下
  c
  static PyObject*
  exit_armor(PyObject *self, PyObject *args)
  {
  // 得到对应的代码块
  PyFrameObject *frame = PyEval_GetFrame();
  PyCodeObject *f_code = frame->f_code;
  
  // 代码块调用计数器减去一
  PyObject *refcalls = f_code->co_names;
  refcalls->ob_refcnt --;
  
  // 仅当代码块调用计数器为 1 的时候,重新加密伪代码
  if (refcalls->ob_refcnt == 1) {
  obfuscate_byte_code(f_code->co_code);
  set_obfuscate_flag(f_code);
  }
  
  // 清空局部变量
  clear_frame_locals(frame);
  
  Py_RETURN_NONE;
  }
  
  
  
  这种Python仿真版的保护机制在性能和安全性方面都有了质的变化,PyArmor 也
  终于变得成熟。
  
  从安全级别上来说,使用 Python 语言提供的任何机制是无法突破 PyArmor 的保
  护的,例如,访问出现异常的`traceback`等。即便是使用调试器(例如 `gdb`),
  设置断点在 `PyEval_EvalFrameEx`,PyArmor 也可以在 `__armor_enter__` 中
  进行反侦测,一旦发现调试器存在,或者Python解释器经过了改造,就拒绝工作。
  当然,如何进行反侦测就是加密和破解两条阵线的较量,也是性能和安全之间综
  合平衡的问题。不管怎么说,这种安全性已经到了`C`语言的层面,是和如何保护
  二进制的可执行文件是相同的了。
  
  回顾**PyArmor**的发展历程,最终的实现方式和保护`C`代码如此类似,使我想
  到了《老子》中的一句话 **大道归一**,有感而写下这篇日志。
  
  如果你有保护Python源码方面的需求,PyArmor可能是你的一个选择: https://github.com/dashingsoft/pyarmor

Links

Releases