查看原文
其他

Python 源码混淆与加密

Python猫 2021-03-15

The following article is from 360BugCloud Author Collapsar

 △点击上方“Python猫”关注 ,回复“1”领取电子书

作者:Collapsar
来源:360BugCloud

Python 是一种解释型语言,没有编译过程,发布程序的同时就相当于公开了源码,这也是其作为开源语言的一个特性。但在某些场景下,我们的源码是不想被别人看到的,例如开发商业软件、编写 0day 漏洞 POC/EXP、免杀 shellcode 等。


以免杀为例,如果打包的源码没做任何处理,安全研究人员在捕获到样本后连分析的过程都省掉了,直接通过源码锁定特征,很快免杀就会失效,这显然不是我们想看到的。因此对源码做相应的保护还是有必要的。


注:本文代码只为介绍源码保护方法,不涉及免杀姿势。


目前保护 Python 代码主要有以下几种方式:

  • 对代码进行混淆以降低源码可读性
  • 将 py 文件编译为二进制 pyc 文件
  • 使用 Pyinstaller 打包源码为二进制可执行文件
  • 使用 PyArmor 加密脚本
  • 将 py/pyc 文件使用 AES 加密为 pye 文件
  • 将 py 文件转为 c 文件后编译为动态链接库文件


代码混淆

代码混淆是指在不改变代码逻辑的情况下,对代码结构进行变换,通过一些带有混淆性质的命名、注释等,使代码变得晦涩难懂,从而达到保护代码的作用。这里提供两种代码混淆的方式:


代码混淆库 pyobfuscate

pyobfuscate 会对代码中用户定义的类、函数、变量等进行重命名、更改代码缩进(默认1)、移除注释、添加不影响逻辑的代码语句,最终起到混淆的作用。不过 pyobfuscate 使用 Python2 编写,无法解析 Python3 中的 f-string 等特殊语法,因此使用前需要将源码进行一定程度的修改,当然也可以直接修改 pyobfuscate 库,增加对 Python3 版本的支持。


样例 (对 malicious.py 文件进行混淆):

python2 pyobfuscate.py malicious.py > malicious_obfuscated.py



效果如下图所示,左侧为一段从云端获取 shellcode 加载进内存执行的代码,右图为其混淆后的结果。

    


可以看出代码虽然进行了一定程度的变换,但代码结构基本还是原来的样子,并不能很有效的增加破解难度。


利用 AST 混淆源码

AST,即抽象语法树,它可以将源代码以树状结构表示。Python 内置了 ast 模块,该模块通过内置函数 compile() 和 parse() 将  Python 源代码解析为 AST,之后可以利用 ast 模块内的方法对 ast 节点进行相应的操作,混淆处理后使用 codegen 库将 AST 重新生成为 Python 源码。


样例:

python2 astobf.py malicious.py > malicious_astobfed.py



效果如下图所示,AST 混淆后的代码略有修改,以保证 Python3 下脚本可正常执行。



混淆后的代码使用 __import__ 动态导入模块、使用 getattr 调用类方法,这样就可以以字符串方式传入模块名和方法名,借由字符串翻转拼接、数字计算等方式达到混淆目的,相比之下,AST 方式的混淆效果明显要优于 pyobfuscate 库。


编译为 pyc 文件

pyc 文件是 Python 的字节码文件,其存在的意义在于每次调用模块时,不用重新对该模块进行解释,从而提高效率,减少性能损耗。但是在运行一个单独的脚本时,该脚本是不会被编译为 pyc 文件的,这是由于 Python 的解释器认为只有导入的包才会被不断复用,才有编译的价值。不过 Python 提供了 py_compile 库和 compileall 程序用于手动编译 py 文件。


py_compile

import py_compile
py_compile.compile(file="malicious.py")


compileall

python -m compileall ./



编译为字节码文件后,确实没办法直接读取源码了,但是 Python 有 uncompyle6 这么一个跨版本反编译器,可以将 Python 字节码转换回等效的 Python 源代码。

python -m pip install uncompyle6
uncompyle6 malicious.cpython-37.pyc > malicious_Decompiled.py



而且 pyc 还有一个弊端,就是它依赖于 Python 解释器的版本,使用某版本解释器编译的 pyc 文件必须使用相同版本解释器运行才能正常工作, 所以实际上将 py 文件编译为 pyc 文件的实用性并不是很大。


打包为独立可执行程序

通过将 Python 文件打包为独立可执行程序也是一种保护源码的方式。Windows 平台下 ,有 Pyinstaller 、 py2exe 和 cx_Freeze 等多种打包程序可以使用,以 Pyinstaller 为例,打包 malicious.py 命令如下:


python -m pip install pyinstaller
pyinstaller -Fw -i myicon.ico malicious.py


-F 表示生成单文件,-w 表示隐藏控制台窗口,-i 表示为生成的 exe 文件添加图标。



Python 打包的 exe 程序并不是将文件编译为真正的机器码,而是将脚本编译为 pyc 后连同依赖文件、当前的 Python 解释器一同打包起来,根据命令参数生成文件夹或打包成单独的可执行文件。之后运行 exe 时,实际运行的是一个引导加载程序,引导加载程序会创建一个临时的 Python 环境,通过解释器副本来执行 pyc 文件。


由于这种运行方式的特殊性,Pyinstaller 打包的 exe 文件也是可以被还原出源码的。使用 pyinstxtractor 解包 exe:

python pyinstxtractor.py malicious.exe



解包后的文件夹内包含了 malicious.pyc 文件,之后使用 uncompyle6 反编译该文件就可以拿到源码。所以将 Python 打包为 exe 只相当于在编译为 pyc 的基础上添加了一步打包操作,同样不能很有效的对源码进行保护。


使用 PyArmor 加密代码

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


它的保障机制主要包括:

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


PyArmor 的工作原理相对复杂,有兴趣的朋友可以参考官方的说明文档:

https://pyarmor.readthedocs.io/。


使用 PyArmor 默认加密方式加密 malicious.py:

pyarmor obfuscate malicious.py



加密后的文件前两行代码是引导代码,用于加载 pytransform 动态链接库和添加三个内置函数到 builtins 模块,之后调用 __pyarmor__ 导入加密模块执行加密代码。加密后的文件目录还有一个名叫 pytransform 的运行辅助包,它是解密文件所必须的,因此打包加密文件时需要同时将运行辅助包打包进去。

pyinstaller -Fw --add-data "pytransform;pytransform" malicious.py



PyArmor 使用分片式技术来保护 Python 脚本。所谓分片保护,就是单独加密每一个函数,在运行脚本的时候,只有当前调用的函数被解密,其他函数都没有解密。而一旦函数执行完成,就又会重新加密。这种方式相对于混淆来说,效果明显要好的多。


加密为 pye 文件

pyconcrete 是另一个 python 的文件加密库,安装它需要提供一个密钥,用于之后对源码文件进行加密,同时由于过程中涉及 .c 文件的编译,因此 Windows 下需要安装 VC++ build tools,Linux 下需要安装 GCC。


解压出 pyconcrete 库源码后,使用 setup 进行安装。

python setup.py install



安装成功后复制 pyconcrete-admin.py 文件到项目文件夹就可以使用了。


pyconcrete 可以将源码文件夹下所有 py/pyc 文件通过 AES128 加密为 pye 文件,该文件无法被正常的 Pyhon 解释器解释,需要使用 pyconcrete 程序加载运行。当然也可以将函数定义部分提出来作为库文件单独加密,函数调用部分独立出来作为一个入口,如下,将 malicious.py 文件拆分为 malicious_func.py 和 malicious_enter.py。



对 malicious_func.py 文件单独加密。

python pyconcrete-admin.py compile --source=malicious_func.py --pye



加密后在只有 malicious_enter.py 和 malicious_func.pye 两个文件的情况下运行 malicious_enter.py 文件,脚本是可以在本地正常运行的,因为解释器在导入 pye 文件时会自动调用环境变量中的 pyconcrete 进行解密。



如果想将其打包为 exe 在其他机器上运行,还需要进行一些修改:

  1. 修改 malicious_enter.py 文件,在首部导入 pyconcrete,以及加密脚本中需要用到的库,这是为了在调用 Pyinstaller 时,将 pyconcrete 解密程序和脚本依赖的库同时打包进 exe。pyconcrete 库需要在其他库之前导入,它会自动和其他模块挂钩,在其目录寻找 pye 文件,然后通过 _pyconcrete.pyd 对 pye 文件进行解密。
  2. Pyinstaller 不会将 pye 文件主动打包进 exe,需要在打包时通过 --add-data 添加。



这样打包出的 exe 就可以执行了。



使用 pyconcrete 加密的源码在运行时会调用 _pyconcrete.pyd 文件进行解密,该文件内存储了用于解密源码的密钥。由于其密钥隐藏在二进制数据中,无法通过十六进制编辑器直接看到,因此想要解密源码,就必须对 _pyconcrete.pyd 文件进行逆向分析,提取密钥。


编译为 pyd 文件

上面提到的 pyd 文件是 Python 的动态链接库,类似 Windows 下的 DLL 和 Linux 下的 SO,它是 Cython 结合 C 的编译器编译而来,涉及 C 的编译, 因此同样需要 VC++ build tools 或 GCC。


实际上,Cython 是一门单独的语言,专门用来写 Python 的 C 扩展。原本是为了解决 Python 语言的效率问题,但由于其有专门的转换器可以将 .py 文件转换为 .c 文件 (自动加入大量 C-Python API ) 后编译为 pyd,因此也可以利用这个特点来保护 Python 源码,下面为编译方法:


pyd 的文件为库文件,所以也需要一个 py 文件进行调用,这里还是使用 malicious_enter.py 和 malicious_func.py 作为示例。


创建一个 py 文件,用于将 malicious_func.py 编译为 pyd。

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

from distutils.core import setup
from Cython.Build import cythonize

setup(
   ext_modules = cythonize(['malicious_func.py',]),
  )


cythonize 方法会将 malicious_func.py 的 Python 代码转换为 Cython 代码,之后调用 setup 将 .c 文件编译为 pyd。

python build_pyd.py build_ext --inplace



可以直接运行。



也可以打包为 exe,Pyinstaller 会自动将 pyd 文件作为依赖导入。



编译为 pyd 后,想要了解源码的逻辑就必须通过逆向来分析,相较于从 _pyconcrete.pyd 中提取密钥解密 pye,这种直接将完整逻辑代码编译为二进制文件的方式更不容易被逆向出来,逻辑写的越复杂,逆向分析的代价就越高。


最后,上面说的这些 Python 源码保护方法其实正常情况下很少会用得到,既然使用了 Python,一般也不会有人刻意去隐藏自己的代码。不过对于安全领域,这些方法还是有一定价值的。拿免杀来说,复杂的加解密流程,配合上面某些方法,说不定就能很好的隐藏自己的特征,养出一匹低调的马儿。

Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~

近期热门文章推荐:

Python最会变魔术的魔术方法,我觉得是它!
饿了么交易系统 5 年演化史
Python 任务自动化工具 tox 教程
Python 为什么会有个奇怪的“...”对象?

感谢创作者的好文

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存