1. 程式人生 > >Pyarmor 加密和保護 Python 原始碼的方法和機制

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 網站主頁