python程式碼混淆與編譯

汗牛充栋發表於2024-08-08

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

相關文章