Menu
快讀
  • 旅遊
  • 生活
    • 美食
    • 寵物
    • 養生
    • 親子
  • 娛樂
    • 動漫
  • 時尚
  • 社會
  • 探索
  • 故事
  • 科技
  • 軍事
  • 国际
快讀

如何加密你的 Python 代碼

2021 年 3 月 11 日 娱乐新闻酱

前言

在PyCon China 2018 杭州站分享過 Python 源碼加密 ,講述了如何通過修改 Python 解釋器達到加解密 Python 代碼的目的。然而因爲筆者拖延症發作,一直沒有及時整理成文字版,現在終于戰勝了它,才有了本文。

本文將首先介紹下現有源碼加密方案的思路、方法、優點與不足,進而介紹如何通過定制 Python 解釋器來達到更好地加解密源碼的目的。

現有加密方案

由于 Python 的動態特性和開源特點,導致 Python 代碼很難做到很好的加密。社區中的一些聲音認爲這樣的限制是事實,應該通過法律手段而不是加密源碼達到商業保護的目的;而還有一些聲音則是不論如何都希望能有一種手段來加密。于是乎,人們想出了各種或加密、或混淆的方案,借此來達到保護源碼的目的。

常見的源碼保護手段有如下幾種:

.pyc py2exe Cython

下面來簡單說說這些方案。

發行 .pyc 文件

思路

大家都知道,Python 解釋器在執行代碼的過程中會首先生成 .pyc 文件,然後解釋執行 .pyc 文件中的內容。當然了,Python 解釋器也能夠直接執行 .pyc 文件。而 .pyc 文件是二進制文件,無法直接看出源碼內容。如果發行代碼到客戶環境時都是 .pyc 而非 .py 文件的話,那豈不是能達到保護 Python 代碼的目的?

方法

把 .py 文件編譯爲 .pyc 文件,是件非常輕松地事情,可不需要把所有代碼跑一遍,然後去撈生成的 .pyc 文件。

事實上,Python 標准庫中提供了一個名爲 compileall 的庫,可以輕松地進行編譯。

執行如下命令能夠將遍曆 <src> 目錄下的所有 .py 文件,將之編譯爲 .pyc 文件:

python -m compileall <src>然後刪除 <src> 目錄下所有 .py 文件就可以打包發布了:

$ find <src> -name ‘*.py’ -type f -print -exec rm {} \;

優點

  • 簡單方便,提高了一點源碼破解門檻
  • 平台兼容性好, .py 能在哪裏運行, .pyc 就能在哪裏運行

不足

.pyc

python-uncompyle6 就是這樣一款反編譯工具,效果出衆。

執行如下命令,即可將 .pyc 文件反編譯爲 .py 文件:

$ uncompyle6 *compiled-python-file-pyc-or-pyo*

代碼混淆

如果代碼被混淆到一定程度,連作者看著都費勁的話,是不是也能達到保護源碼的目的呢?

思路

既然我們的目的是混淆,就是通過一系列的轉換,讓代碼逐漸不那麽讓人容易明白,那就可以這樣下手:

  • 移除注釋和文檔。沒有這些說明,在一些關鍵邏輯上就沒那麽容易明白了。
  • 改變縮進。完美的縮進看著才舒服,如果縮進忽長忽短,看著也一定鬧心。
  • 在tokens中間加入一定空格。這就和改變縮進的效果差不多。
  • 重命名函數、類、變量。命名直接影響了可讀性,亂七八糟的名字可是閱讀理解的一大障礙。
  • 在空白行插入無效代碼。這就是障眼法,用無關代碼來打亂閱讀節奏。

方法

方法一:使用 oxyry 進行混淆

http://pyob.oxyry.com/ 是一個在線混淆 Python 代碼的網站,使用它可以方便地進行混淆。

假定我們有這樣一段 Python 代碼,涉及到了類、函數、參數等內容:

# coding: utf-8

class A(object): “”” Description “””

def __init__(self, x, y, default=None): self.z = x + y self.default = default def name(self): return ‘No Name’

def always(): return True

num = 1 a = A(num, 999, 100) a.name() always()

經過 Oxyry 的混淆,得到如下代碼:

class A (object ):#line:4 “”#line:7 def __init__ (O0O0O0OO00OO000O0 ,OO0O0OOOO0000O0OO ,OO0OO00O00OO00OOO ,OO000OOO0O000OOO0 =None ):#line:9 O0O0O0OO00OO000O0 .z =OO0O0OOOO0000O0OO +OO0OO00O00OO00OOO #line:10 O0O0O0OO00OO000O0 .default =OO000OOO0O000OOO0 #line:11 def name (O000O0O0O00O0O0OO ):#line:13 return ‘No Name’#line:14 def always ():#line:17 return True #line:18 num =1 #line:21 a =A (num ,999 ,100 )#line:22 a .name ()#line:23 always ()

混淆後的代碼主要在注釋、參數名稱和空格上做了些調整,稍微帶來了點閱讀上的障礙。

方法二:使用 pyobfuscate 庫進行混淆

pyobfuscate 算是一個頗具年頭的 Python 代碼混淆庫了,但卻是“老當益壯”了。

對上述同樣一段 Python 代碼,經 pyobfuscate 混淆後效果如下:

# coding: utf-8 if 64 – 64: i11iIiiIii if 65 – 65: O0 / iIii1I11I1II1 % OoooooooOO – i1IIi class o0OO00 ( object ) : if 78 – 78: i11i . oOooOoO0Oo0O if 10 – 10: IIiI1I11i11 if 54 – 54: i11iIi1 – oOo0O0Ooo if 2 – 2: o0 * i1 * ii1IiI1i % OOooOOo / I11i / Ii1I def __init__ ( self , x , y , default = None ) : self . z = x + y self . default = default if 48 – 48: iII111i % IiII + I1Ii111 / ooOoO0o * Ii1I def name ( self ) : return ‘No Name’ if 46 – 46: ooOoO0o * I11i – OoooooooOO if 30 – 30: o0 – O0 % o0 – OoooooooOO * O0 * OoooooooOO def Oo0o ( ) : return True if 60 – 60: i1 + I1Ii111 – I11i / i1IIi if 40 – 40: oOooOoO0Oo0O / O0 % ooOoO0o + O0 * i1IIi I1Ii11I1Ii1i = 1 Ooo = o0OO00 ( I1Ii11I1Ii1i , 999 , 100 ) Ooo . name ( ) Oo0o ( ) # dd678faae9ac167bc83abf78e5cb2f3f0688d3a3

相比于方法一,方法二的效果看起來更好些。除了類和函數進行了重命名、加入了一些空格,最明顯的是插入了若幹段無關的代碼,變得更加難讀了。

優點

  • 簡單方便,提高了一點源碼破解門檻
  • 兼容性好,只要源碼邏輯能做到兼容,混淆代碼亦能

不足

  • 只能對單個文件混淆,無法做到多個互相有聯系的源碼文件的聯動混淆
  • 代碼結構未發生變化,也能獲取字節碼,破解難度不大

使用 py2exe

思路

py2exe 是一款將 Python 腳本轉換爲 Windows 平台上的可執行文件的工具。其原理是將源碼編譯爲 .pyc 文件,加之必要的依賴文件,一起打包成一個可執行文件。

如果最終發行由 py2exe 打包出的二進制文件,那豈不是達到了保護源碼的目的?

方法

使用 py2exe 進行打包的步驟較爲簡便。

  1. 編寫入口文件。本示例中取名爲 hello.py :

print ‘Hello World’

  1. 編寫 setup.py :

from distutils.core import setup import py2exe

setup(console=[‘hello.py’])

  1. 生成可執行文件

python setup.py py2exe

生成的可執行文件位于 dist\hello.exe 。

優點

  • 能夠直接打包成 exe,方便分發和執行
  • 破解門檻比 .pyc 更高一些

不足

.pyc

使用 Cython

思路

雖說 Cython 的主要目的是帶來性能的提升,但是基于它的原理:將 .py / .pyx 編譯爲 .c 文件,再將 .c 文件編譯爲 .so (Unix) 或 .pyd (Windows),其帶來的另一個好處就是難以破解。

方法

使用 Cython 進行開發的步驟也不複雜。

  1. 編寫文件 hello.pyx 或 hello.py :

def hello(): print(‘hello’)

  1. 編寫 setup.py :

from distutils.core import setup from Cython.Build import cythonize

setup(name=’Hello World app’, ext_modules=cythonize(‘hello.pyx’))

  1. 編譯爲 .c ,再進一步編譯爲 .so 或 .pyd :

python setup.py build_ext –inplace

執行 python -c “from hello import hello;hello()” 即可直接引用生成的二進制文件中的 hello() 函數。

優點

  • 生成的二進制 .so 或 .pyd 文件難以破解
  • 同時帶來了性能提升

不足

  • 兼容性稍差,對于不同版本的操作系統,可能需要重新編譯
  • 雖然支持大多數 Python 代碼,但如果一旦發現部分代碼不支持,完善成本較高

定制 Python 解釋器

考慮前文所述的幾個方案,均是從源碼的加工入手,或多或少都有些不足。假設我們從解釋器的改造入手,會不會能夠更好的保護代碼呢?

由于發行商業 Python 程序到客戶環境時通常會包含一個 Python 解釋器,如果改造解釋器能解決源碼保護的問題,那麽也是可選的一條路。

假定我們有一個算法,能夠加密原始的 Python 代碼,這些加密後代碼隨發行程序一起,可被任何人看到,卻難以破解。另一方面,有一個定制好的 Python 解釋器,它能夠解密這些被加密的代碼,然後解釋執行。而由于 Python 解釋器本身是二進制文件,人們也就無法從解釋器中獲取解密的關鍵數據。從而達到了保護源碼的目的。

要實現上述的設想,我們首先需要掌握基本的加解密算法,其次探究 Python 執行代碼的方式從而了解在何處進行加解密,最後禁用字節碼用以防止通過 .pyc 反編譯。

加解密算法

對稱密鑰加密算法

對稱密鑰加密(Symmetric-key algorithm)又稱爲對稱加密、私鑰加密、共享密鑰加密,是密碼學中的一類加密算法。這類算法在加密和解密時使用相同的密鑰,或是使用兩個可以簡單地相互推算的密鑰。

對稱加密算法的特點是算法公開、計算量小、加密速度快、加密效率高。

常見的對稱加密算法有:DES、3DES、AES、Blowfish、IDEA、RC5、RC6 等。

對稱密鑰加解密過程如下:

如何加密你的 Python 代碼

明文通過公鑰加密成密文,密文通過與公鑰對應的私鑰解密爲明文。

通過 openssl 工具,我們能夠方便選擇非對稱加密算法進行加解密。下面我們以 RSA 算法爲例,介紹其用法。

生成私鑰、公鑰

# 輔以 AES-128 算法,生成 2048 比特長度的私鑰 $ openssl genrsa -aes128 -out private.pem 2048

# 根據私鑰來生成公鑰 $ openssl rsa -in private.pem -outform PEM -pubout -out public.pem

RSA 加密

# 使用公鑰進行加密 openssl rsautl -encrypt -in passwd.txt -inkey public.pem -pubin -out enpasswd.txt

RSA 解密

# 使用私鑰進行解密 openssl rsautl -decrypt -in enpasswd.txt -inkey private.pem -out passwd.txt

基于加密算法實現源碼保護

對稱加密適合加密源碼文件,而非對稱加密適合加密密鑰。如果將兩者結合,就能達到加解密源碼的目的。

在構建環境進行加密

我們發行出去安裝包中,源碼應該是被加密過的,那麽就需要在構建階段對源碼進行加密。加密的過程如下:

如何加密你的 Python 代碼

  1. Python 解釋器執行加密代碼時需要被傳入指示加密密鑰的參數,通過這個參數,解釋器獲取到了加密密鑰
  2. Python 解釋器使用內置的私鑰,對該加密密鑰進行非對稱解密,得到原始密鑰
  3. Python 解釋器使用原始密鑰對加密代碼進行對稱解密,得到原始代碼
  4. Python 解釋器執行這段原始代碼

可以看到,通過改造構建環節、定制 Python 解釋器的執行過程,便可以實現保護源碼的目的。改造構建環節是容易的,但是如何定制 Python 解釋器呢?我們需要深入了解解釋器執行腳本和模塊的方式,才能在特定的入口進行控制。

腳本、模塊的執行與解密

執行 Python 代碼的幾種方式

爲了找到 Python 解釋器執行 Python 代碼時的所有入口,我們需要首先執行 Python 解釋器都能以怎樣的方式執行代碼。

直接運行腳本

python test.py

直接運行語句

python -c “print ‘hello'”

直接運行模塊

python -m test

導入、重載模塊

python >>> import test # 導入模塊 >>> reload(test) # 重載模塊

直接運行語句的方式接收的就是明文的代碼,我們也無需對這種方式做額外處理。

直接運行模塊和 導入、重載模塊 這兩種方式在流程上是殊途同歸的,所以接下來會一起來看。

因此我們將分兩種情況:運行腳本和加載模塊來進一步探究各自的過程和解密方式。

運行腳本時解密

運行腳本的過程

Python 解釋器在運行腳本時的代碼調用邏輯如下:

main WinMain [Modules/python.c] [PC/WinMain.c] \ / \ / \ / \ / \ / Py_Main [Moduls/main.c]

Python 解釋器運行腳本的入口函數因操作系統而異,在 Linux/Unix 系統上,主入口函數是 Modules/python.c 中的 main 函數,在 Windows系統上,則是 PC/WinMain.c 中的 WinMain 函數。不過這兩個函數最終都會調用 Moduls/main.c 中的 Py_Main 函數。

我們不妨來看看 Py_Main 函數中的相關邏輯:

[Modules/Main.c] ————————————–

int Py_Main(int argc, char **argv) { if (command) { // 處理 python -c <command> } else if (module) { // 處理 python -m <module> } else { // 處理 python <file> … fp = fopen(filename, “r”); … } }

處理 <command> 和 <module> 的部分我們暫且先不管,在處理文件(通過直接運行腳本的方式)的邏輯中,可以看到解釋打開了文件,獲得了文件指針。那麽如果我們把這裏的 fopen 換成是自定義的 decrypt_open 函數,這個函數用來打開一個加密文件,然後進行解密,並返回一個文件指針,這個指針指向解密後的文件。那麽,不就可以實現解密腳本的目的了嗎?

自定義 decrypt_open

我們不妨新增一個 Modules/crypt.c 文件,用來存放一些自定義的加解密函數。

decrypt_open 函數大概實現如下:

[Modules/crypt.c] ————————————–

/* 以解密方式打開文件 */ FILE * decrypt_open(const char *filename, const char *mode) { int plainlen = -1; char *plaintext = NULL; FILE *fp = NULL;

if (aes_passwd == NULL) fp = fopen(filename, “r”); else { plainlen = aes_decrypt(filename, aes_passwd, &plaintext); // 如果無法解密,返回源文件描述符 if (plainlen < 0) fp = fopen(filename, “r”); // 否則,轉換爲內存文件描述符 else fp = fmemopen(plaintext, plainlen, “r”); } return fp; }

這裏的 aes_passwd 是一個全局變量,代表對稱加密算法中的密鑰。我們暫時假定已經獲取該密鑰了,後文會說明如何獲得。而 aes_decrypt 是自定義的一個使用AES算法進行對稱解密的函數,限于篇幅,此函數的實現不再貼出。

decrypt_open 邏輯如下:

  • 判斷是否獲得了對稱密鑰,如果沒獲得,直接打開該文件並返回文件指針
  • 如果獲得了,則嘗試使用對稱算法進行解密如果解密失敗,可能就是一段非加密的腳本,直接打開該文件並返回文件指針如果解密成功,我們通過解密後的內容創建一個內存文件對象,並返回該文件指針

實現了上述這些函數後,我們就能夠實現在直接運行腳本時,解密執行被加密代碼的目的。

加載模塊時解密

加載模塊的過程

加載模塊的邏輯主要實現在 Python/import.c 文件中,其過程如下:

Py_Main [Moduls/main.c] | builtin___import__ RunModule | | PyImport_ImportModuleLevel <—-┐ PyImport_ImportModule | | | import_module_level └——- PyImport_Import | load_next builtin_reload | | import_submodule PyImport_ReloadModule | | find_module <—————————┘

  • 通過 python -m <module> 的方式來加載模塊時,其入口函數是 Py_Main 函數
  • 通過 import <module> 的方式來加載模塊時,其入口函數是 builtin___import__ 函數
  • 通過 reload(<module>) 的方式來加載模塊時,其入口函數是 builtin_reload 函數

但不論是哪種方式,最終都會調用 find_module 函數,我們看看這個函數中是否暗藏乾坤呢?

[Python/import.c] ————————————–

static struct filedescr * find_module(char *fullname, char *subname, PyObject *path, char *buf, size_t buflen, FILE **p_fp, PyObject **p_loader) { … fp = fopen(buf, filemode); … }

我們在 find_module 函數中找到了打開文件的邏輯,如果直接改成前文實現的 decrypt_open ,豈不是就能達成加載模塊時解密的目的了?

總體思路是這樣的,但有個細節需要注意, buf 不一定就是 .py 文件,也可能是 .pyc 文件,我們只對 .py 文件做改動,則可以這麽寫:

[Python/import.c] ————————————–

static struct filedescr * find_module(char *fullname, char *subname, PyObject *path, char *buf, size_t buflen, FILE **p_fp, PyObject **p_loader) { … if (fdp->type == PY_SOURCE) { fp = decrypt_open(buf, filemode); } else { fp = fopen(buf, filemode); } … }

經過上述改動,就實現了加載模塊時解密的目的了。

支持指定密鑰文件

前文中還留有一個待解決的問題:我們一開始是假定解釋器已獲取到了密鑰內容並存放在了全局變量 aes_passwd 中,那麽密鑰內容怎麽獲取呢?

我們需要 Python 解釋器能支持一個新的參數選項,通過它來指定已加密的密鑰文件,然後再通過非對稱算法進行解密,得到 aes_passed 。

假定這個參數選項是 -k <filename> ,則可使用如 python -k enpasswd.txt 的方式來告知解釋器加密密鑰的文件路徑。其實現如下:

[Modules/main.c] ————————————–

/* 命令行選項,注意k:是新增的內容 */ #define BASE_OPTS “3bBc:dEhiJk:m:OQ:RsStuUvVW:xX?” … /* Long usage message, split into parts < 512 bytes */ static char *usage_1 = “\ … -k key : decrypt source file by using key file\n\ … “; … int Py_Main(int argc, char **argv) { … char *keyfilename = NULL; … while ((c = _PyOS_GetOpt(argc, argv, PROGRAM_OPTS)) != EOF) { … case ‘k’: keyfilename = (char *)malloc(strlen(_PyOS_optarg) + 1); if (keyfilename == NULL) Py_FatalError( “not enough memory to copy -k argument”); strcpy(keyfilename, _PyOS_optarg); keyfilename[strlen(_PyOS_optarg)] = ‘\0’; break; … } … if (keyfilename != NULL) { int passwdlen; char *passwd = NULL;

passwdlen = rsa_decrypt(keyfilename, &passwd); set_aes_passwd(passwd); if (passwdlen < 0) { fprintf(stderr, “%s: parsing key file ‘%s’ error\n”, argv[0], keyfilename); free(keyfilename); return 2; } else { free(keyfilename); } } … }

其邏輯如下:

  • k: 中的 k 表示支持 -k 選項; : 表示選項後跟一個參數,即這裏的已加密密鑰文件的路徑
  • 解釋器在處理到 -k 參數時,獲取其後所跟的文件路徑,記錄在 keyfilename 中
  • 使用自定義的 rsa_decrypt 函數(限于篇幅,不列出如何實現的邏輯)對已加密密鑰文件進行非對稱解密,獲得密鑰的原始內容
  • 將該密鑰內容寫入到 aes_passwd 中

由此,通過顯示地指定已加密密鑰文件,解釋器獲得了原始密鑰,進而通過該密鑰解密已加密代碼,再執行原始代碼。但是,這裏面還潛藏著一個 風險 :執行代碼的過程中會生成 .pyc 文件,通過它反編譯出的 .py 文件是未加密的。換句話說,惡意用戶可以通過這種手段繞過限制。所以,我們需要 禁用字節碼

禁用字節碼

不生成 .pyc 文件

首先要做的就是不生成 .pyc 文件,這樣,惡意用戶就沒法直接根據 .pyc 文件來得到源碼。

我們知道,通過 -B 選項可以告知 Python 解釋器不生成 .pyc 文件。既然定制的 Python 解釋器就不生成 .pyc 我們幹脆禁用這個選項:

[Modules/main.c] ————————————–

/* 命令行選項,注意移除了B */ #define BASE_OPTS “3bc:dEhiJm:OQ:RsStuUvVW:xX?” … /* Long usage message, split into parts < 512 bytes */ static char *usage_1 = “\ … //-B : don’t write .py[co] files on import; also PYTHONDONTWRITEBYTECODE=x\n\ … “; … int Py_Main(int argc, char **argv) { … // 不生成 py[co] Py_DontWriteBytecodeFlag++; … }

除此以外,Python 解釋器還會從環境變量中獲取是否不生成 .pyc 文件,因此也需要做處理:

[Python/pythonrun.c] ————————————–

void Py_InitializeEx(int install_sigs) { … f ((p = Py_GETENV(“PYTHONDEBUG”)) && *p != ‘\0’) Py_DebugFlag = add_flag(Py_DebugFlag, p); if ((p = Py_GETENV(“PYTHONVERBOSE”)) && *p != ‘\0’) Py_VerboseFlag = add_flag(Py_VerboseFlag, p); if ((p = Py_GETENV(“PYTHONOPTIMIZE”)) && *p != ‘\0’) Py_OptimizeFlag = add_flag(Py_OptimizeFlag, p); // 移除對 PYTHONDONTWRITEBYTECODE 的處理 if ((p = Py_GETENV(“PYTHONDONTWRITEBYTECODE”)) && *p != ‘\0’) Py_DontWriteBytecodeFlag = add_flag(Py_DontWriteBytecodeFlag, p); … }

禁止訪問字節碼對象 co_code

僅僅是不生成 .pyc 文件還是不夠的,惡意用戶已然可以訪問對象的 co_code 屬性來獲取字節碼,進而通過反編譯的手段獲取到源碼。因此,我們也需要禁止用戶訪問字節碼對象:

[Objects/codeobject.c] ————————————–

static PyMemberDef code_memberlist[] = { {“co_argcount”, T_INT, OFF(co_argcount), READONLY}, {“co_nlocals”, T_INT, OFF(co_nlocals), READONLY}, {“co_stacksize”,T_INT, OFF(co_stacksize), READONLY}, {“co_flags”, T_INT, OFF(co_flags), READONLY}, // {“co_code”, T_OBJECT, OFF(co_code), READONLY}, {“co_consts”, T_OBJECT, OFF(co_consts), READONLY}, {“co_names”, T_OBJECT, OFF(co_names), READONLY}, {“co_varnames”, T_OBJECT, OFF(co_varnames), READONLY}, {“co_freevars”, T_OBJECT, OFF(co_freevars), READONLY}, {“co_cellvars”, T_OBJECT, OFF(co_cellvars), READONLY}, {“co_filename”, T_OBJECT, OFF(co_filename), READONLY}, {“co_name”, T_OBJECT, OFF(co_name), READONLY}, {“co_firstlineno”, T_INT, OFF(co_firstlineno), READONLY}, {“co_lnotab”, T_OBJECT, OFF(co_lnotab), READONLY}, {NULL} /* Sentinel */ };

到此,一個定制的 Python 解釋器完成了。

演示

運行腳本

通過 -k 選項執行已加密密鑰文件,Python 解釋器可以運行已加密和未加密的 Python 文件。

如何加密你的 Python 代碼

禁用字節碼

通過禁用字節碼,我們達到以下效果:

.pyc 如何加密你的 Python 代碼

調試

加密的代碼也是允許調試的,但是輸出的代碼內容會是加密的,這正是我們所期望的。

相關文章:

  • 新加坡房產的集體收購,竟隱藏著這麼多財富
  • 新加坡房産的集體收購,竟隱藏著這麽多財富
  • 帶娃來新加坡玩兒,史上最全攻略!
  • 吳尊的老家文萊,一個低調隱秘的奢華仙境
  • 這樣逛日本古都, 嘗遍地道街頭小吃
  • 去新加坡一定不能錯過的幾件事!
軍事

發佈留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

©2025 快讀 | 服務協議 | DMCA | 聯繫我們