如何加密你的 Python 代碼

前言

在PyCon China 2018 杭州站分享過 Python 源碼加密

,講述瞭如何通過修改 Python 解釋器達到加解密 Python 代碼的目的。然而因為筆者拖延症發作,一直沒有及時整理成文字版,現在終於戰勝了它,才有了本文。

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

現有加密方案

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

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

<code>.pyc
py2exe
Cython
/<code>

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

發行 .pyc 文件

思路

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

方法

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

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

執行如下命令能夠將遍歷 目錄下的所有 .py 文件,將之編譯為 .pyc 文件:

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

<code>$ find  -name '*.py' -type f -print -exec rm {} \\;/<code>

優點

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

不足

<code>.pyc
/<code>

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

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

<code>$ uncompyle6 *compiled-python-file-pyc-or-pyo*/<code>

代碼混淆

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

思路

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

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

方法

方法一:使用 oxyry 進行混淆

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

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

<code># 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()/<code>

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

<code>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 ()/<code>

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

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

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

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

<code># 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/<code>

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

優點

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

不足

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

使用 py2exe

思路

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

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

方法

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

  1. 編寫入口文件。本示例中取名為 hello.py :
<code>print 'Hello World'/<code>
  1. 編寫 setup.py :
<code>from distutils.core import setup
import py2exe

setup(console=['hello.py'])/<code>
  1. 生成可執行文件
<code>python setup.py py2exe/<code>

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

優點

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

不足

<code>.pyc
/<code>

使用 Cython

思路

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

方法

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

  1. 編寫文件 hello.pyx 或 hello.py :
<code>def hello():
print('hello')/<code>
  1. 編寫 setup.py :
<code>from distutils.core import setup
from Cython.Build import cythonize


setup(name='Hello World app',
ext_modules=cythonize('hello.pyx'))/<code>
  1. 編譯為 .c ,再進一步編譯為 .so 或 .pyd :
<code>python setup.py build_ext --inplace/<code>

執行 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 工具,我們能夠方便選擇對稱加密算法進行加解密。下面我們以 AES 算法為例,介紹其用法。

AES 加密

<code># 指定密碼進行對稱加密
$ openssl enc -aes-128-cbc -in test.py -out entest.py -pass pass:123456

# 指定文件進行對稱加密
$ openssl enc -aes-128-cbc -in test.py -out entest.py -pass file:passwd.txt

# 指定環境變量進行對稱加密
$ openssl enc -aes-128-cbc -in test.py -out entest.py -pass env:passwd/<code>

AES 解密

<code># 指定密碼進行對稱解密
$ openssl enc -aes-128-cbc -d -in entest.py -out test.py -pass pass:123456

# 指定文件進行對稱解密
$ openssl enc -aes-128-cbc -d -in entest.py -out test.py -pass file:passwd.txt

# 指定環境變量進行對稱解密
$ openssl enc -aes-128-cbc -d -in entest.py -out test.py -pass env:passwd/<code>

非對稱密鑰加密算法

密鑰加密(英語:public-key cryptography,又譯為公開密鑰加密),也稱為非對稱加密(asymmetric cryptography),一種密碼學算法類型,在這種密碼學方法中,需要一對密鑰,一個是私鑰,另一個則是公鑰。這兩個密鑰是數學相關,用某用戶公鑰加密後所得的信息,只能用該用戶的私鑰才能解密。

非對稱加密算法的特點是算法強度複雜、安全性依賴於算法與密鑰但是由於其算法複雜,而使得加密解密速度沒有對稱加密解密的速度快。

常見的對稱加密算法有:RSA、Elgamal、揹包算法、Rabin、D-H、ECC 等。

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

如何加密你的 Python 代碼

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

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

生成私鑰、公鑰

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

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

RSA 加密

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

RSA 解密

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

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

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

在構建環境進行加密

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

如何加密你的 Python 代碼

  1. 隨機生成一個密鑰。這個密鑰實際上是一個用於對稱加密的密碼。
  2. 使用該密鑰對源代碼進行對稱加密,生成加密後的代碼。
  3. 使用公鑰(生成方法見 非對稱密鑰加密算法)對該密鑰進行非對稱加密,生成加密後的密鑰。

不論是加密後的代碼還是加密後的密鑰,都會放在安裝包中。它們能夠被用戶看到,卻無法被破譯。而 Python 解釋器該如何執行加密後的代碼呢?

Python 解釋器進行解密

假定我們發行的 Python 解釋器中內置了與公鑰相對應的私鑰,有了它就有了解密的可能。而由於 Python 解釋器本身是二進制文件,所以不需要擔心內置的私鑰會被看到。解密的過程如下:

如何加密你的 Python 代碼

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

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

腳本、模塊的執行與解密

執行 Python 代碼的幾種方式

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

直接運行腳本

<code>python test.py/<code>

直接運行語句

<code>python -c "print 'hello'"/<code>

直接運行模塊

<code>python -m test/<code> 

導入、重載模塊

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

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

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

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

運行腳本時解密

運行腳本的過程

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

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

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

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

<code>[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");
...
}
}/<file>/<module>/<command>/<code>

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

自定義 decrypt_open

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

decrypt_open 函數大概實現如下:

<code>[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;
}/<code>

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

decrypt_open 邏輯如下:

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

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

加載模塊時解密

加載模塊的過程

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

<code>Py_Main
[Moduls/main.c]
|
builtin___import__ RunModule
| |
PyImport_ImportModuleLevel | | |
import_module_level └------- PyImport_Import
|
load_next builtin_reload
| |
import_submodule PyImport_ReloadModule
| |
find_module /<code>
  • 通過 python -m <module> 的方式來加載模塊時,其入口函數是 Py_Main 函數/<module>
  • 通過 import <module> 的方式來加載模塊時,其入口函數是 builtin___import__ 函數/<module>
  • 通過 reload(<module>) 的方式來加載模塊時,其入口函數是 builtin_reload 函數/<module>

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

<code>[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);
...
}/<code>

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

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

<code>[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);
}
...
}/<code>

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

支持指定密鑰文件

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

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

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

<code>[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);
}
}
...
}/<code>

其邏輯如下:

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

由此,通過顯示地指定已加密密鑰文件,解釋器獲得了原始密鑰,進而通過該密鑰解密已加密代碼,再執行原始代碼。但是,這裡面還潛藏著一個

風險 :執行代碼的過程中會生成 .pyc 文件,通過它反編譯出的 .py 文件是未加密的。換句話說,惡意用戶可以通過這種手段繞過限制。所以,我們需要 禁用字節碼

禁用字節碼

不生成 .pyc 文件

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

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

<code>[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++;
...

}/<code>

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

<code>[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);
...
}/<code>

禁止訪問字節碼對象 co_code

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

<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 */
};/<code>

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

演示

運行腳本

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

如何加密你的 Python 代碼

加載模塊

可以通過 -m <module> 的方式加載已加密和未加密的模塊,也可以通過 import <module> 的方式來加載已加密和未加密的模塊。/<module>/<module>

如何加密你的 Python 代碼

禁用字節碼

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

<code>.pyc
/<code>
如何加密你的 Python 代碼

異常堆棧信息

儘管代碼是加密的,但是不會影響異常時的堆棧信息。

如何加密你的 Python 代碼

調試

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

如何加密你的 Python 代碼

思考

  1. 如何防止通過內存操作的方式找到對象的co_code?
  2. 如何進一步提升私鑰被逆向工程探知的難度?
  3. 如何能在調試並希望看到源碼的時候看到?


分享到:


相關文章: