python代码混淆与编译

python代码混淆、编译与打包

考虑到生产环境部署, 而python作为解释性语言, 对源码没有任何保护。 此文记录探索如何保护源码的过程。

代码混淆

代码混淆基本上就是把代码编译为字节码。

工具有两种:

  1. py_compile
  2. pyarmor

py_compile示例:

py_compile.compile(src_pyfile, dst_pyfile, dfile=os.path.relpath(dst_pyfile, target_directory), optimize=2)

给定一个输入文件, 及输出路径即可进行混淆。 当然还可以设置其优化级别。其中dfile用于报错时展示的名称。

而不是内置库, 需要额外安装:

pip install pyarmor

其混淆形式为:

pyarmor obfuscate test.py,

结果将会在dist目录中生成。

对当前目录进行递归执行:

pyarmor obfuscate --recursive test.py,

打包为exe

首先安装pyinstaller:

pip install pyinstaller

命令选项解释

  • -F, –onefile 打包单个文件, 适用于只有一个py脚本的情况
  • -D, –onedir 打包多个文件, 适用于多层目录的项目
  • --uac-admin 编译出来的exe需要管理员权限执行
  • -w 程序为GUI, 仅仅适用Windows
  • -c 程序为CUI, 仅仅适用Windows

如果你只需打包一个脚本, 如app.py, 执行如下命令:

pyinstaller -F app.py

如果你的项目有多个package, 多层目录。 其入口脚本为app.py。则执行如下命令:

pyinstaller -D app.py

编译为运行库

pyd即是python动态模块, 英文为Python Dynamic Module。其本质上是共享库。 所以编译过程需要依赖特定的编译器Windows上为msvc, linux为gcc。

首先安装编译器, 安装哪个版本却决于你当前python版本。 通过如下代码查看python 对应的编译器版本:

import sys
print(sys.version)

可以看到类似msvc或gcc相关描述, 其中包括版本信息。

通过cython可以将.py文件编译为.pyd。首先安装:

pip install cython

编写python脚本:

# add.py

def add(lhs, rhs):
    return lhs + rhs

编写setup.py脚本:

from setuptools import setup
from Cython.Build import cythonize

setup(
    name='addapp',
    ext_modules=cythonize("add.py")
)

执行编译脚本:

python setup.py build_ext --inplace

会在当前目录下生成add.c以及add.cp310-win_amd64.pyd。 其pyd命名是自动根据python版本与操作系统生成。

在其他目录中将pyd拷贝过去, 然后新建test.py测试脚本:

from add import add

print(add(1, 2))

最终输出结果为3

或者不编写setup.py, 对单个文件进行编译。 可通cython的命令行工具cythonize:

cythonize -i add.py

也会在目录下生成相同的文件。

编译多个文件:

setup(
    name='addapp',
    ext_modules=cythonize("*.py"),
)

如果要针对不同的模块设置不同的编译选项, 其目录结构如下:

│  setup.py
└─utils
        add.py
        subtract.py

其setup需要这样写:

from setuptools import setup
from Cython.Build import cythonize
from setuptools import Extension, setup


extensions = [
    Extension("utils.add", ["utils/add.py"]),
    Extension("utils.subtract", ["utils/subtract.py"]),
    # Extension("utils.__init__", ["utils/__init__.py"]),
]

setup(
    name='addapp',
    ext_modules=cythonize(extensions),
)

注意其中Extension的第一个参数其路径必须与文件结构一致。 第二个参数列表中放置当前py文件即可,一个python模块对应一个py文件。 如果将utils本身作为package, 可以取消对__init__.py的注释。

如何编译到其他目录?

因为上述编译命令都带了选项--inplace顾名思义即编译到当前目录。 可以通过编译选项--build-lib指定目录。

自定义编译流程

通过设置cmdclass进行实现:

from distutils.command.clean import clean
from Cython.Distutils import build_ext

class MyBuildExtCommand(build_ext):
  
    def run(self):
        super().run()
        clean_command = clean(self.distribution)
        clean_command.all = True
        clean_command.finalize_options()
        clean_command.run()

setup(
    name='addapp',
    ext_modules=cythonize(extensions),
    cmdclass={"build_ext": MyBuildExtCommand}
)

实际编译时执行的命令为基类build_ext的run方法。代码中通过super().run()进行调用。 调用完后执行clean, 此清理仅仅是清理build目录, 主要是编译过程生成的中间文件。 而在目录中生成的.c文件并未清理。 此时需要手动清理:

def clear(srcdir, exts=(".c")):

    for r,d,fs in os.walk(srcdir):
        for f in fs:
            _,ext = os.path.splitext(f)
            if not ext in exts:
                continue
            
            os.remove(os.path.join(r, f))

class MyBuildExtCommand(build_ext):
  
    def run(self):
        # ...
        # ...
        clear(".")

不通过命令行工具执行, 直接执行setup.py?

秩序添加参数script_args即可:

setup(
    name='addapp',
    ext_modules=cythonize(extensions),
    cmdclass={"build_ext": MyBuildExtCommand},
    script_args=['build_ext', '--build-lib', "dist"]
)

此时直接python setup.py即可。 可将setup.py改名为compile.py。

将打包后的pyd通过pyinstaller打包成exe?

可以打包成exe, 但是pyinstaller无法获取pyd的依赖包。 只有.py文件,pyinstaller才能获取其依赖, 并打包依赖。

结论

打包为动态链接库其保密程度最高, pyinstaller简单打包及代码混淆, 都非常容易反编译, 且其还原度较高。

参考

  1. py_compile --- 编译 Python 源文件
  2. Bundling to One Folder
  3. Cython Users Guide
posted @ 2024-08-08 16:15  汗牛充栋  阅读(55)  评论(0编辑  收藏  举报