使用Cython编译运行保护Python源代码

一、背景

Python语言被广泛使用,在数据处理、机器学习等领域有统治地位。Python作为解释型语言,其主要优势就是跨平台、代码简洁,然而这也带来负面的影响,那就是解释型语言的运行效率低。因此,针对Python语言的编译运行有很多解决方案,其中一种是Cython。Cython可以将Python代码转换为C语言并编译为动态链接库,然后Python代码中可直接调用这些动态链接库。如此虽然提升了速度,但也带来一系列问题。一是编译后的动态链接库失去了跨平台特性,每个硬件架构平台(如X86_64和ARM)、不同操作系统版本(Glibc版本差异)的动态链接库是不通用的,都需要重新编译。二是要达到和C相近的性能,Python代码需要做优化才可以完全按C语言编译执行,由此带来代码改动的工作量,即需要在代码中按照Cython语言的要求对变量添加类型声明。如果不做这些优化,那么使用Cython比直接运行原Python效率提升有限,有提升但没有上升一个数量级,远不及C的运行速度。

虽然有着这两点缺点,不过使用Cython还有一个额外的收获,那就是编译后的动态链接库可封装代码,在一定程度上起到保护源代码的作用,让原始代码不再直接可读。由于反编译C代码动态链接库的门槛较高,因此这个方式在一定程度上提升了获取源代码的难度,实现了比较初步的源代码保护。因此本文不探讨Cython编译后的加速效果,而是介绍编译后保护代码明文的效果。

本文即讲述这种场景。准备一个访问存储桶的代码作为示例,假设这个业务场景需要列存储桶目录,并进行了一定的业务逻辑处理例如过滤特定文件并显示,然后将其命名为file_access.py,其中包含函数list_my_file。假设这个程序需要被保护,稍后将被编译为file_access.so。在运行环境中,只需要import引用这个动态链接库,即可调用其中的函数。现在开始。

二、编译环境

文本以Ubuntu 24.04 LTS环境为例。CentOS或者Amazon Linux同理替换下包管理,从apt改为yum即可。

1、开发环境需要的依存性包

开发环境需要有Python的pip工具,还要安装Linux下的GCC编译环境。运行环境不需要安装。执行如下命令。

apt install awscli pipx gcc cython3 -y

准备依赖环境,安装如下扩展库。因为本demo是访问AWS S3存储桶,因此开发环境还需要安装boto3。运行环境就不用装这些库了,因为逻辑都打包到动态链接库了。

pipx install boto3 cython setuptools

注:以上用的命令是pipx,这是因为Ubuntu系统采用系统级别的Python包管理,也就是Python-pip3,要安装任何扩展包都要通过OS级别的apt。如果不希望走系统的apt,那么可以绕开系统的apt包管理,直接在Python层面安装扩展库,就仿佛MacOS和其他Linux一样。因此这里使用了pipx,就可以直接安装了。

2、开发环境业务代码准备

环境安装好之后,准备业务代码如下,将其保存为file_access.py

import boto3

def list_my_file():
    s3 = boto3.client('s3')
    response = s3.list_buckets()
    bucket_list = [bucket['Name'] for bucket in response['Buckets']]
    # you may add your code here
    return(bucket_list)

if __name__ == "__main__":
    result=list_my_file()
    print(result)

以上文件保存为file_access.py。以上代码会检查~/.aws/credentials获取AKSK,然后正常访问AWS。本机选择AWS S3服务的调用,使用AWS SDK的boto3库,其主要延迟发生在网络上访问S3 Endpoint,因此是否被Cython编译为动态链接库,还是直接运行Python解释型脚本,其性能基本上没有提升。本文选这个逻辑来模拟业务代码,主要是为了演示Cython编译后可以保护源代码,让运行环境不能直接看到原始代码的明文。

现在执行这个代码,验证业务代码编写正常。接下来,就是要实现对业务逻辑file_access.py代码的保护,这个文件将被编译为动态链接库。

3、编译环境

准备另一个文件名叫setup.py,内容如下。

from setuptools import setup
from Cython.Build import cythonize

setup(ext_modules = cythonize("file_access.py"))

在这个文件中,传递给cythonize函数的是要编译的文件名。然后将file_access.pysetup.py放在同一个目录下,执行如下命令:

python3 setup.py build_ext --inplace

由此,即可在当前目录下,获得名为file_access.cpython-312-x86_64-linux-gnu.so的动态链接库。

最后,将file_access.cpython-312-x86_64-linux-gnu.so复制出来,上传到另一台作为运行环境的机器上(注意需要相同硬件架构、相同OS版本)。

三、运行环境

由于编译为C代码需要环境兼容,因此运行环境也需要相同硬件架构、相同版本的操作系统,主要是Glibc版本相同即可运行。

在运行环境上,只需要有Python即可,无须安装Cython,也无须安装运行实际业务代码需要的Python3 AWS SDK(即不用pip install boto3)。因为运行实际业务代码file_access.py的过程已经被封装到动态连结库了,所以就不需要引用相关库了(即调用代码不用import boto3)。

准备一个调用动态连结库的代码,开头部分引用这个动态连结库,并引用里边预先定义好的函数list_my_file。将其保存为run.py,其内容如下。

from file_access import list_my_file 

result=list_my_file()
print(result)

将以上代码保存为run.py。确保file_access.cpython-312-x86_64-linux-gnu.sorun.py在同一个目录下之后,运行如下命令:

python3 run.py

可看到程序正常运行。

四、结论

这里需要注意,本演示中模拟业务逻辑部分的代码使用了AWS S3的SDK,其主要运行耗时在与AWS S3的Endpoint建立连接的网络通信方面,因此这样代码被编译后,没有什么加速效果。Cython加速效果最好的是用于无外部网络交互、100%在本机执行的Python数学运算类代码,它们被编译为C将会真正获得巨大的性能提升。另外要完全让所有代码都以C方式运行,还需要调整代码。有兴趣可以继续研究通过开启Cython的cythonize命令的Annonate=True标签,将编译过程输出为HTML格式的文件,并追踪动态连结库中以C运行的、以及Python运行的部分,可进一步提升运行速度。

以上过程可以看到,通过Cython将Python程序打包为动态连结库,即降低了在运行环境上的各种依赖包的复杂度,又在一定程度上保护了原始代码内容。如果没有一定的反编译技巧,不能立刻获取原始代码,实现了代码保护的目的。

五、参考文档

Cython官网文档

https://cython.readthedocs.io/en/latest