Pyarmor 加密和保護 Python 原始碼的方法和機制
Pyarmor 是一個用於加密和保護 Python 原始碼的小工具。它能夠在執行時刻保護 Python 指令碼的二進位制程式碼不被洩露,設定加密後 Python 原始碼的有效期限,繫結加密後的Python 原始碼到硬碟、網絡卡等硬體裝置。它的保障機制主要包括
- 加密編譯後的程式碼塊,保護模組中的字串和常量
- 在指令碼執行時候動態加密和解密程式碼塊的二進位制程式碼
- 程式碼塊執行完成之後清空堆疊區域性變數
- 通過授權檔案限制加密後腳本的有效期和裝置環境
讓我們看看一個普通的 Python 指令碼 foo.py
加密之後是什麼樣子。下面是加
密指令碼所在的目錄 dist
foo.py
pytransform.py
_pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS
pyshield.key
pyshield.lic
product.key
license.lic
dist/foo.py
是加密後的指令碼,它的內容如下
from pytransfrom import pyarmor_runtime
pyarmor_runtime()
__pyarmor__( __name__, __file__, b'\x06\x0f...')
所有其他檔案叫做 執行依賴檔案
,它們是執行加密指令碼所必須的。並且只要這裡面的模組pytransform.py
能被正常匯入進來,加密指令碼 dist/foo.py
就可以像正常指令碼一樣被執行。這是 Pyarmor 的一個重要特徵: 加密指令碼無縫替換 Python 原始碼
加密 Python 原始碼
Pyarmor 是怎麼加密 Python 原始碼呢?
首先把原始碼編譯成程式碼塊 Code Object
char *filename = "foo.py";
char *source = read_file ( filename );
PyCodeObject *co = Py_CompileString( source, "<frozen foo>", Py_file_input );
接著對這個程式碼塊進行如下處理
- 使用
try...finally
語句把程式碼塊的程式碼段co_code
包裹起來
新新增一個頭部,對應於 try 語句:
LOAD_GLOBALS N (__armor_enter__) N = length of co_consts
CALL_FUNCTION 0
POP_TOP
SETUP_FINALLY X (jump to wrap footer) X = size of original byte code
接著是處理過的原始程式碼段:
對於所有的絕對跳轉指令,運算元增加頭部位元組數
加密修改過的所有指令程式碼
...
追加一個尾部,對應於 finally 塊:
LOAD_GLOBALS N + 1 (__armor_exit__)
CALL_FUNCTION 0
POP_TOP
END_FINALLY
-
新增字串名稱
__armor_enter
,__armor_exit__
到co_consts
-
如果
co_stacksize
小於 4,那麼設定為 4 -
在
co_flags
設定自定義的標誌位 CO_OBFUSCAED (0x80000000) -
按照上面的方式遞迴修改
co_consts
中的所有型別為程式碼塊的常量
然後把改裝後的程式碼塊轉換成為字串,把字串進行加密,保護其中的常量和字串
char *string_code = marshal.dumps( co );
char *obfuscated_code = obfuscate_algorithm( string_code );
最後生成加密後的指令碼,寫入到磁碟檔案
sprintf( buf, "__pyarmor__(__name__, __file__, b'%s')", obfuscated_code );
save_file( "dist/foo.py", buf );
單純加密後的指令碼就是一個正常的函式呼叫語句,長得就像這個樣子
__pyarmor__(__name__, __file__, b'\x01\x0a...')
執行加密指令碼
那麼,一個正常的 Python 直譯器執行加密指令碼 dist/foo.py
的過程是什麼樣呢?
上面我們看到 dist/foo.py
的前兩行是這個樣子
from pytransfrom import pyarmor_runtime
pyarmor_runtime()
這兩行叫做 引導程式碼
,在執行任何加密指令碼之前,它們必須先要被執行。它們
有著重要的使命
- 使用
ctypes
來裝載動態庫_pytransform
- 檢查授權檔案
dist/license.lic
是否合法 - 新增三個內建函式到模組
builtins
__pyarmor__
__armor_enter__
__armor_exit__
最主要的是添加了三個內建函式,這樣 dist/foo.py
的下一行程式碼才不會出錯,
因為它馬上要呼叫函式 __pyarmor__
__pyarmor__(__name__, __file__, b'\x01\x0a...')
__pyarmor__
主要負責匯入加密的模組,實現的原理如下
static PyObject *
__pyarmor__(char *name, char *pathname, unsigned char *obfuscated_code)
{
char *string_code = restore_obfuscated_code( obfuscated_code );
PyCodeObject *co = marshal.loads( string_code );
return PyImport_ExecCodeModuleEx( name, co, pathname );
}
第一個匯入的模組是 __main__
, 從現在開始,在整個 Python 直譯器的生命週期中
- 每一個函式(程式碼塊)一旦被呼叫,首先就會執行函式
__armor_enter__
,
它負責恢復程式碼塊。其實現原理如下所示
static PyObject *
__armor_enter__(PyObject *self, PyObject *args)
{
// 通過當前執行堆疊得到當前程式碼塊指標
PyFrameObject *frame = PyEval_GetFrame();
PyCodeObject *f_code = frame->f_code;
// 借用 co_names->ob_refcnt 來記錄當前程式碼塊
// 的呼叫次數
PyObject *refcalls = f_code->co_names;
refcalls->ob_refcnt ++;
// 恢復被加密的程式碼塊
if (IS_OBFUSCATED(f_code->co_flags)) {
restore_byte_code(f_code->co_code);
clear_obfuscated_flag(f_code);
}
Py_RETURN_NONE;
}
- 因為每一個程式碼塊都被人為的使用
try...finally
塊包裹了一下,所以程式碼
塊執行完之後,在返回上一級之前,就會呼叫__armor_exit__
。它會重新加密程式碼塊,同時清空堆疊內的區域性變數
static PyObject *
__armor_exit__(PyObject *self, PyObject *args)
{
// 得到當前程式碼塊指標
PyFrameObject *frame = PyEval_GetFrame();
PyCodeObject *f_code = frame->f_code;
// 呼叫計數器遞減
PyObject *refcalls = f_code->co_names;
refcalls->ob_refcnt --;
// 僅當呼叫計數器為 0 的時候重新加密程式碼塊的程式碼段 co_code
// 在多執行緒、遞迴等很多種情況下,都會出現一個程式碼段 co_code
// 被多個程式碼塊 Code Object 同時使用的情況
if (refcalls->ob_refcnt == 1) {
obfuscate_byte_code(f_code->co_code);
set_obfuscated_flag(f_code);
}
// 清空當前堆疊的區域性變數
clear_frame_locals(frame);
Py_RETURN_NONE;
}
加密指令碼的授權
當 引導程式碼
pyarmor_runtime()
被呼叫時候,它會檢查授權檔案dist/license.lic
。
如果存在非授權的使用,就會報錯退出。在加密指令碼的時候同時會生成一個預設的授權檔案,它允許加密指令碼執行在任何機器上,並且永不過期。
我們可以在授權檔案裡面包含一個有效的日期,或者硬碟序列號,網絡卡的Mac地址等,這樣pyarmor_runtime()
就可以檢查時間,比對硬體裝置,從而確定當前執行環境是否滿足條件,選擇繼續執行或者報錯退出。
Pyarmor 使用命令 hdinfo
來獲取目標機器的硬體資訊
python pyarmor.py hdinfo
然後使用命令 licenses
來生成新的授權檔案
python pyarmor.py licenses
--expired 2018-12-31
--bind-disk "100304PBN2081SF3NJ5T"
--bind-mac "70:f1:a1:23:f0:94"
--bind-ipv4 "202.10.2.52"
Customer-Jondy
更多詳細資訊,請訪問 Pyarmor 網站主頁