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簡單打包及程式碼混淆, 都非常容易反編譯, 且其還原度較高。
參考
- py_compile --- 編譯 Python 原始檔
- Bundling to One Folder
- Cython Users Guide