前言
在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 進行打包的步驟較為簡便。
- 編寫入口文件。本示例中取名為 hello.py :
print 'Hello World'
- 編寫 setup.py :
from distutils.core import setup
import py2exe
setup(console=['hello.py'])
- 生成可執行文件
python setup.py py2exe
生成的可執行文件位于 disthello.exe 。
優點
- 能夠直接打包成 exe,方便分發和執行
- 破解門檻比 .pyc 更高一些
不足
.pyc
使用 Cython
思路
雖說 Cython 的主要目的是帶來性能的提升,但是基于它的原理:將 .py / .pyx 編譯為 .c 文件,再將 .c 文件編譯為 .so (Unix) 或 .pyd (Windows),其帶來的另一個好處就是難以破解。
方法
使用 Cython 進行開發的步驟也不復雜。
- 編寫文件 hello.pyx 或 hello.py :
def hello():
print('hello')
- 編寫 setup.py :
from distutils.core import setup
from Cython.Build import cythonize
setup(name='Hello World App',
ext_modules=cythonize('hello.pyx'))
- 編譯為 .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 等。
對稱密鑰加解密過程如下:
明文通過密鑰加密成密文,密文也可通過相同的密鑰解密為明文。
通過 openssl 工具,我們能夠方便選擇對稱加密算法進行加解密。下面我們以 AES 算法為例,介紹其用法。
AES 加密
# 指定密碼進行對稱加密
$ 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
AES 解密
# 指定密碼進行對稱解密
$ 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
非對稱密鑰加密算法
密鑰加密(英語:public-key cryptography,又譯為公開密鑰加密),也稱為非對稱加密(asymmetric cryptography),一種密碼學算法類型,在這種密碼學方法中,需要一對密鑰,一個是私鑰,另一個則是公鑰。這兩個密鑰是數學相關,用某用戶公鑰加密后所得的信息,只能用該用戶的私鑰才能解密。
非對稱加密算法的特點是算法強度復雜、安全性依賴于算法與密鑰但是由于其算法復雜,而使得加密解密速度沒有對稱加密解密的速度快。
常見的對稱加密算法有:RSA、Elgamal、背包算法、Rabin、D-H、ECC 等。
非對稱密鑰加解密過程如下:
明文通過公鑰加密成密文,密文通過與公鑰對應的私鑰解密為明文。
通過 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 解釋器該如何執行加密后的代碼呢?
Python 解釋器進行解密
假定我們發行的 Python 解釋器中內置了與公鑰相對應的私鑰,有了它就有了解密的可能。而由于 Python 解釋器本身是二進制文件,所以不需要擔心內置的私鑰會被看到。解密的過程如下:
- Python 解釋器執行加密代碼時需要被傳入指示加密密鑰的參數,通過這個參數,解釋器獲取到了加密密鑰
- Python 解釋器使用內置的私鑰,對該加密密鑰進行非對稱解密,得到原始密鑰
- Python 解釋器使用原始密鑰對加密代碼進行對稱解密,得到原始代碼
- 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 filen
...
";
...
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)] = '';
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' errorn", 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=xn
...
";
...
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 != '')
Py_DebugFlag = add_flag(Py_DebugFlag, p);
if ((p = Py_GETENV("PYTHONVERBOSE")) && *p != '')
Py_VerboseFlag = add_flag(Py_VerboseFlag, p);
if ((p = Py_GETENV("PYTHONOPTIMIZE")) && *p != '')
Py_OptimizeFlag = add_flag(Py_OptimizeFlag, p);
// 移除對 PYTHONDONTWRITEBYTECODE 的處理
if ((p = Py_GETENV("PYTHONDONTWRITEBYTECODE")) && *p != '')
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 文件。
加載模塊
可以通過 -m <module> 的方式加載已加密和未加密的模塊,也可以通過 import <module> 的方式來加載已加密和未加密的模塊。
禁用字節碼
通過禁用字節碼,我們達到以下效果:
.pyc
異常堆棧信息
盡管代碼是加密的,但是不會影響異常時的堆棧信息。
調試
加密的代碼也是允許調試的,但是輸出的代碼內容會是加密的,這正是我們所期望的。
思考
- 如何防止通過內存操作的方式找到對象的co_code?
- 如何進一步提升私鑰被逆向工程探知的難度?
- 如何能在調試并希望看到源碼的時候看到?