python代码混淆、编译与打包
考虑到生产环境部署, 而python作为解释性语言, 对源码没有任何保护。 此文记录探索如何保护源码的过程。
代码混淆
代码混淆基本上就是把代码编译为字节码。
工具有两种:
- py_compile
- 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简单打包及代码混淆, 都非常容易反编译, 且其还原度较高。