python代码加密方案

为何要对代码加密? python的解释特性是将py编译为独有的二进制编码pyc 文件,然后对pyc中的指令进行解释执行,但是pyc的反编译却非常简单,可直接反编译为源码,当需要将产品发布到外部环境的时候,源码的保护尤为重要。 常见的源码保护手段有如下几种:
  1. 发行.pyc文件  
  2. 代码混淆
  3. 使用py2exe
  4. 使用Cython

一、发行.pyc文件

pyc文件是一种二进制文件,由原生Python文件经过编译后所生成的,py文件编译成pyc文件后加载速度更快而且提高了代码的安全性。pyc的内容与python的版本相关,不同版本编译的pyc文件不一样。

1.1、编译.pyc文件

编译单个py:
python -m compileall xxx.py  # 把单个.py文件编译为字节码文件

pyc会生成在目录下的__pycache__下(前后有双下划线)生成的文件名命名方式:源文件名.cpython-python版本.pyc     编译多个
python -m compileall /path/src/ # 批量生成字节码文件,/path/src/是包含.py文件名的路径
     将.py文件转化为.pyc文件,实现代码隐藏的需要;.pyc文件的使用与.py文件的使用相同。
# 安装
pip install compileall2

import compileall

# compile_file编译单个文件
compileall.compile_file('main.py')

# compile_dir 函数编译文件夹下的py文件
compileall.compile_dir('Lib/', force=True)

# 使用多处理器编译
compileall.compile_dir('Lib/', workers=2)

1.2、反编译.pyc文件 (.pyc -> .py)

# 安装 uncompyle
pip install uncompyle

# 把 name.pyc 反编译成 name.py
uncompyle6 name.pyc > name.py

反编译多个文件:uncompyle6 -o . *.pyc

1.3、优缺点

优点:
  • 简单方便,提高了一点源码破解门槛
  • pyc 文件的加载速度比 py 文件快
  • 平台兼容性好,.py能在哪里运行,.pyc就能在哪里运行
不足:
  • 解释器兼容性差, .pyc只能在特定版本的解释器上运行
  • 有现成的反编译工具, 破解成本低

二、代码混淆

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

可以这样下手:

  • 移除注释和文档。没有这些说明,在一些关键逻辑上就没那么容易明白了。

  • 改变缩进。完美的缩进看着才舒服,如果缩进忽长忽短,看着也一定闹心。

  • 在 tokens 中间加入一定空格。这就和改变缩进的效果差不多。

  • 重命名函数、类、变量。命名直接影响了可读性,乱七八糟的名字可是阅读理解的一大障碍。

  • 在空白行插入无效代码。这就是障眼法,用无关代码来打乱阅读节奏。

实现方法:

  1. 网站混淆:Oxyry Python Obfuscator – The most reliable python obfuscator in the world, 破解: https://www.cnblogs.com/Eeyhan/p/13154217.html

  2. 使用 pyobfuscate 库进行混淆   (2条消息) Python 源码混淆与加密_pyobfuscate_早起的python的博客-CSDN博客

优点:

  • 简单方便,提高了一点源码破解门槛

  • 兼容性好,只要源码逻辑能够做到兼容,混淆代码亦能

不足:

  • 只能对单个文件混淆,无法做到多个互相有联系的源码文件的联动混淆

  • 代码结构未发生变化,也能获取字节码,破解难度不大

三、打包

类似的还有pyinstaller、Nuitka

  • pyinstaller是通过设置key来对源码进行加密的;
  • nuitka则是将python源码转成C++(这里得到的是二进制的pyd文件,防止了反编译),然后再编译成可执行文件。 
  • py2exe 是一款将 Python 脚本转换为 Windows 平台上的可执行文件的工具。其原理是将源码编译为.pyc文件,加之必要的依赖文件,一起打包成一个可执行文件。

3.1、py2exe打包

使用py2exe进行打包的步骤:

1、编写入口文件。本示例中取名为hello.py:
print('Hello World')
 
2、编写setup.py
from distutils.core import setup
import py2exe
 
setup(console=['hello.py'])
 
3、生成可执行文件
python setup.py py2exe

 

优点:

  • 能够直接打包成 exe,方便分发和执行

  • 破解门槛比 .pyc 更高一些

不足:

  • 兼容性差,只能运行在 Windows 系统上

  • 生成的可执行文件内的布局是明确、公开的,可以找到源码对应的.pyc文件,进而反编译出源码

3.2、pyinstaller打包

1、安装pyinstaller

2、在Terminal下输入:“pyinstaller -F -w *.py” 就可以制作出exe。生成的文件放在同目录dist下。

常用选项​​​

-F(注意大写)是所有库文件打包成一个exe。不加-F参数生成一堆文件,但运行快,压缩后比单个exe文件还小一点点。 加-F参数生成一个exe文件,运行起来慢。
-D:默认选项,创建一个目录,包含exe文件以及大量依赖文件;
-a,–ascii   不包含 Unicode 字符集支持;
-d,–debug   产生 debug 版本的可执行文件;
-i: 后接图标文件名,后缀是.ico,表示用自定义图标生成exe程序
-c:默认选项,使用控制台(就是类似cmd的黑框);(仅对 Windows 有效);
-w: 生成的exe程序不带窗口执行(仅对 Windows 有效);
-o DIR,–out=DIR 指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件;
-p 表示你自己自定义需要加载的类路径,一般情况下用不到;
-n NAME,–name=NAME  指定项目(产生的 spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字;
-i  选择图标

示例:

book.py:

import webbrowser

url="https://blog.csdn.net/leiwuhen92?type=blog"
print("hello world")
webbrowser.open_new(url)

打包指令:

pyinstaller -F  -i test.ico.ico -w book.py

 

优点:

  • 隐藏源码
  • 方便一用户使用方便,不用再安装什么python啊,第三方包之类的。

缺点:

  • 打包超级慢,启动超级慢

  • 生成的exe比较大

  • 可以反编译  

    Python可执行文件反编译教程(exe转py)_python_脚本之家 (jb51.net)

     (6条消息) Python 反编译:pycdc工具的使用_小嗷犬的博客-CSDN博客

    在线Python pyc文件编译与反编译 (lddgo.net)

 3.3、Nuitka打包

使用python打包工具nuitka进行编译打包 – 知乎 (zhihu.com)

nuitka的作用是将python程序转换成C语言的可执行elf文件。这样在运行时就可以享受到C语言处理过程中的优化,提高速度。经测试,Nuitka打包后的exe比Pyinstaller打包后的exe运行速度提升30%

对于第三方依赖包较多的项目(比如需要import torch,tensorflow,cv2,numpy,pandas,geopy等等)而言,这里最好打包的方式是只将属于自己的代码转成C++,不管这些大型的第三方包!

指令:

python -m nuitka --standalone --show-memory --show-progress --nofollow-imports --plugin-enable=qt-plugins --follow-import-to=utils,src --output-dir=out --windows-icon-from-ico=./logo.ico demo.py

参数:
--standalone:方便移植到其他机器,不用再安装python
--show-memory --show-progress:展示整个安装的进度过程
--nofollow-imports:不编译代码中所有的import,比如keras,numpy之类的。
--plugin-enable=qt-plugins:我这里用到pyqt5来做界面的,这里nuitka有其对应的插件。
--follow-import-to=utils,src:需要编译成C++代码的指定的2个包含源码的文件夹,这里用,来进行分隔。
--output-dir=out:指定输出的结果路径为out。
--windows-icon-from-ico=./logo.ico:指定生成的exe的图标为logo.ico这个图标,这里推荐一个将图片转成ico格式文件的网站(比特虫)。
--windows-disable-console:运行exe取消弹框。这里没有放上去是因为我们还需要调试,可能哪里还有问题之类的。

 四、使用Cython

原理:先用cython将python语言代码转换为c语言代码,然后用c编译器(gcc)生成可执行文件或动态链接库。

4.1、Cpython介绍

Cython是一个编程语言,它通过类似Python的语法来编写C扩展并可以被Python调用,既具备了Python快速开发的特点,又可以让代码运行起来像C一样快,同时还可以方便地调用C library。
Cython是属于python的超集,用于编写python的c扩展语言。
  
pyx文件由 Cython 编译为.c文件,包含 python 扩展模块的代码。.c文件由 C 编译器编译为.so文件(或 Windows 上的.pyd)。
生成的.so文件或pyd文件是D语言(C/C++综合进化版本)生成的二进制文件,理论上很难反编译。

Note: 纯python源码被cython编译后,因为没有使用类型标注等cython语法,故编译后的动态链接库和可执行文件,主要依赖python的运行时,并不依赖C/C++运行时,主要由python解释器执行,多线程GIL的问题同样存在,性能提升有限,但对for循环、大的列表对象遍历性能有明显优化效果。

4.2、安装

# 安装cython
pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple cython
 
# 安装c编译器(linux需安装python-devel, gcc)
centos: yum install python-devel  gcc
ubuntu: apt-get install build-essential

4.3、使用

先用cython将python语言代码转换为c语言代码,然后用c编译器(gcc)生成可执行文件或动态链接库。
  
通过shell 或 python脚本的方式,将项目启动的入口py编译成可执行文件,将项目的其他.py文件编译成.so(__init__.py除外)

Note: __init__.py文件定义了python的包结构,为了使cython编译后的.so能按照正常路径import,__init__.py不能被编译,故为了保护代码,整个项目的所有__init__.py文件不建议放业务相关代码。

4.3.1、单个文件的编译示例-linux

目录结构如下:
test/
├── test.py
├── main.py

test.py:

def hello():
    print('hello!')

main.py:文件头的#!/usr/bin/python3.8标记是否程序启动文件

#!/usr/bin/python3.8
from test import hello
 
if __name__ == "__main__":
    hello()

4.3.1.1、将启动main.py编译成二进制可执行文件main

 main.py —> main.c —> main.o —> main :

# step1: 将python代码翻译成c代码(main.py -> main.c)
cython -D -3 --directive always_allow_keywords=true --embed main.py
 
# step2: 将c代码编译为目标文件(main.c -> main.o)
gcc -c main.c -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o main.o
 
# step3: 将目标文件编译为二进制可执行文件(main.o -> main)
gcc main.o -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o main

 main.py —> main.c —> main:

# step1: 将python代码翻译成c代码(main.py -> main.c)
cython -D -3 --directive always_allow_keywords=true --embed main.py
 
# step2: 将c代码编译为二进制可执行文件(main.c -> main)
gcc main.c -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o main

4.3.1.2、将test.py编译为动态链接库test.so

test.py —> test.c —> test.so:

# step1: 将python代码翻译成c代码(test.py -> test.c)
cython -D -3 --directive always_allow_keywords=true test.py
 
# step2: 将c代码编译为linux动态链接库文件(test.c -> test.so)
gcc test.c -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o test.so

 cython参数说明:

-D, --no-docstrings, Strip docstrings from the compiled module.
-o, --output-file <filename>   Specify name of generated C file
-2                             Compile based on Python-2 syntax and code semantics.
-3                             Compile based on Python-3 syntax and code semantics.

gcc参数说明:

-shared:
编译动态库时要用到

-pthread:
在Linux中要用到多线程时,需要链接pthread库

-fPIC:
作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),
则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意
位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

-fwrapv:
它定义了溢出时候编译器的行为——采用二补码的方式进行操作

-O参数
这是一个程序优化参数,一般用-O2就是,用来优化程序用的
-O2:
会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。
-O3: 在O2的基础上进行更多的优化

-Wall:
编译时 显示Warning警告,但只会显示编译器认为会出现错误的警告

-fno-strict-aliasing:
“-fstrict-aliasing”表示启用严格别名规则,“-fno-strict-aliasing”表示禁用严格别名规则,当gcc的编译优化参数为“-O2”、“-O3”和“-Os”时,默认会打开“-fstrict-aliasing”。

-I (大写的i):
是用来指定头文件目录
-I /home/hello/include表示将/home/hello/include目录作为第一个寻找头文件的目录,寻找的顺序是:/home/hello/include-->/usr/include-->/usr/local/include

-l:
-l(小写的 L)参数就是用来指定程序要链接的库,-l参数紧接着就是库名,把库文件名的头lib和尾.so去掉就是库名了,例如我们要用libtest.so库库,编译时加上-ltest参数就能用上了

4.3.2、单个文件的编译示例-windows

windows系统使用cython需要确保已安装C/C++编译器且环境变量正确配置,cython能找到编译器。windows系统可使用MSVC(Microsoft Visual C/C++)或者clang编译器。 

将test.py编译为动态链接库test.pyd:
test.py —> test.c —> test.pyd:

# step1: 将python代码翻译成c代码(test.py -> test.c)
cython -D -3 --directive always_allow_keywords=true test.py
 
# step2: 将c代码编译为windows动态链接库文件(test.c -> test.pyd)
cythonize -i test.c

最后得到windows下的动态链接库文件test.cp39-win_amd64.pyd,还需要将文件名重命名为test.pyd。

cythonize参数说明:

-b, --build     build extension modules using distutils
-i, --inplace   build extension modules in place using distutils(implies -b),即将编译后的扩展模块直接放在与test.py同级的目录中。

 4.3.3、python编译可执行文件与动态链接库

 https://github.com/leiwuhen92/cython_test  

main.py:主函数入口

#!/usr/bin/python3.8
from test import hello
from compute import compute
 
if __name__ == "__main__":
    hello()
    compute.is_leap_year(1992)

test.py:

def hello():
    print('hello!')

compute/compute.py:

def is_leap_year(year):
    if year%4==0 and year%100!=0 or year%400==0:
        print(year,"是闰年")
    else:
        print(year,"不是闰年")
 
 
if __name__ == "__main__":
    is_leap_year(1992)

setup.py:

import pathlib
import shutil
from subprocess import Popen, PIPE, STDOUT
 
 
# python编译so
def py_to_so(py_to_so_list):
    """
    :param py_to_so_list:  py文件列表,例如[test.py,]
    :return:
    """
    for py_to_so_item in py_to_so_list:
        # 将python代码翻译成c代码
        basename = py_to_so_item[:-3]
        py_to_c = "cython -D -3 --directive always_allow_keywords=true --embed {}".format(py_to_so_item)
        pl = Popen(py_to_c, shell=True, stdout=PIPE, stderr=STDOUT)
        pl.communicate()[0].decode('utf-8', errors='ignore')
 
        # 将c代码编译为linux动态链接库文件(
        c_to_so = "gcc {} -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o {}".format(basename + ".c", basename + ".so")
        p2 = Popen(c_to_so, shell=True, stdout=PIPE, stderr=STDOUT)
        p2.communicate()[0].decode('utf-8', errors='ignore')
 
 
def py_to_bin(py_to_bin_list):
    """
    :param py_to_bin_list: py文件列表,例如[main.py,]
    :return:
    """
    for py_to_bin_item in py_to_bin_list:
        basename = py_to_bin_item[:-3]
        # 将python代码翻译成c代码
        py_to_c = "cython -D -3 --directive always_allow_keywords=true --embed {}".format(py_to_bin_item)
        p1 = Popen(py_to_c, shell=True, stdout=PIPE, stderr=STDOUT)
        p1.communicate()[0].decode('utf-8', errors='ignore')
 
        # 将c代码编译为二进制可执行文件
        c_to_bin = "gcc {} -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o {}".format(basename + ".c", basename)
        p2 = Popen(c_to_bin, shell=True, stdout=PIPE, stderr=STDOUT)
        p2.communicate()[0].decode('utf-8', errors='ignore')
 
 
def clean_build(project):
    """
    清理编译文件夹中的文件
    :param project:
    :return:
    """
    # 清理py文件
    for i in project.glob("**/*.py"):
        if i.name != "__init__.py" or i.name != "setup.py":
            i.unlink()
 
    # 清理c文件
    for j in project.glob("**/*.c"):
        j.unlink()
 
    # 清理__pycache__文件夹
    for k in project.glob("**/__pycache__"):
        shutil.rmtree(k)
 
 
if __name__ == "__main__":
    # 源文件夹
    project_pathlib = pathlib.Path.cwd()
 
    py_to_bin_list = ["main.py"]
    py_to_bin(py_to_bin_list)
 
    py_to_so_list = ["test.py"]
    for py in project_pathlib.glob("**/*.py"):
        if py.name != "__init__.py" and py.name != "setup.py" and py.name != "main.py":
            py_to_so_list.append(str(py))
    py_to_so(py_to_so_list)
 
    clean_build(project_pathlib)

 4.4、优缺点

优点

  • 生成的二进制so(linux)或.pyd(windows)文件难以破解

  • 性能会提升

不足

  • 兼容性稍差,对于不同版本的操作系统,可能需要重新编译

  • 虽然支持大多数Python代码,但如果一旦发现部分代码不支持,完善成本较高

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年9月12日
下一篇 2023年9月12日

相关推荐