去年11月在PyCon China 2018 杭州站分享了 Python 源碼加密麻蹋,講述了如何通過修改 Python 解釋器達(dá)到加解密 Python 代碼的目的。然而因?yàn)楣P者拖延癥發(fā)作扮授,一直沒有及時(shí)整理成文字版刹勃,現(xiàn)在終于戰(zhàn)勝了它,才有了本文伍宦。
本系列將首先介紹下現(xiàn)有源碼加密方案的思路次洼、方法掌呜、優(yōu)點(diǎn)與不足,進(jìn)而介紹如何通過定制 Python 解釋器來達(dá)到更好地加解密源碼的目的势篡。
由于 Python 的動(dòng)態(tài)特性和開源特點(diǎn)禁悠,導(dǎo)致 Python 代碼很難做到很好的加密碍侦。社區(qū)中的一些聲音認(rèn)為這樣的限制是事實(shí),應(yīng)該通過法律手段而不是加密源碼達(dá)到商業(yè)保護(hù)的目的站玄;而還有一些聲音則是不論如何都希望能有一種手段來加密株旷。于是乎尔邓,人們想出了各種或加密、或混淆的方案齿尽,借此來達(dá)到保護(hù)源碼的目的循头。
常見的源碼保護(hù)手段有如下幾種:
發(fā)行.pyc文件
代碼混淆
使用py2exe
使用Cython
下面來簡(jiǎn)單說說這些方案贷岸。
1 發(fā)行 .pyc 文件
1.1 思路
大家都知道磷雇,Python 解釋器在執(zhí)行代碼的過程中會(huì)首先生成.pyc文件唯笙,然后解釋執(zhí)行.pyc文件中的內(nèi)容崩掘。當(dāng)然了苞慢,Python 解釋器也能夠直接執(zhí)行.pyc文件英妓。而.pyc文件是二進(jìn)制文件,無法直接看出源碼內(nèi)容辑畦。如果發(fā)行代碼到客戶環(huán)境時(shí)都是.pyc而非.py文件的話腿倚,那豈不是能達(dá)到保護(hù) Python 代碼的目的?
1.2 方法
把.py文件編譯為.pyc文件,是件非常輕松地事情箩言,可不需要把所有代碼跑一遍陨收,然后去撈生成的.pyc文件畏吓。
事實(shí)上菲饼,Python 標(biāo)準(zhǔn)庫中提供了一個(gè)名為 compileall 的庫列赎,可以輕松地進(jìn)行編譯包吝。
執(zhí)行如下命令能夠?qū)⒈闅v<src>目錄下的所有.py文件,將之編譯為.pyc文件:
python -m compileall<src>然后刪除<src>目錄下所有.py文件就可以打包發(fā)布了:
1$ find ?-name?'*.py'?-type?f?-print?-exec?rm {} \;
1.3 優(yōu)點(diǎn)
簡(jiǎn)單方便诗越,提高了一點(diǎn)源碼破解門檻
平臺(tái)兼容性好砖瞧,.py能在哪里運(yùn)行,.pyc就能在哪里運(yùn)行
1.4 不足
解釋器兼容性差嚷狞,.pyc只能在特定版本的解釋器上運(yùn)行
有現(xiàn)成的反編譯工具块促,破解成本低
python-uncompyle6 就是這樣一款反編譯工具,效果出眾床未。
執(zhí)行如下命令竭翠,即可將.pyc文件反編譯為.py文件:
1$ uncompyle6?*compiled-python-file-pyc-or-pyo*
2 代碼混淆
如果代碼被混淆到一定程度,連作者看著都費(fèi)勁的話薇搁,是不是也能達(dá)到保護(hù)源碼的目的呢斋扰?
2.1 思路
既然我們的目的是混淆,就是通過一系列的轉(zhuǎn)換啃洋,讓代碼逐漸不讓人那么容易明白损离,那就可以這樣下手:- 移除注釋和文檔窟勃。沒有這些說明亚斋,在一些關(guān)鍵邏輯上就沒那么容易明白了赖瞒。- 改變縮進(jìn)。完美的縮進(jìn)看著才舒服民逼,如果縮進(jìn)忽長(zhǎng)忽短,看著也一定鬧心燕侠。- 在tokens中間加入一定空格械巡。這就和改變縮進(jìn)的效果差不多。- 重命名函數(shù)贷祈、類粟耻、變量戈泼。命名直接影響了可讀性,亂七八糟的名字可是閱讀理解的一大障礙巨坊。- 在空白行插入無效代碼暂题。這就是障眼法言津,用無關(guān)代碼來打亂閱讀節(jié)奏。
2.2 方法
方法一:使用 oxyry 進(jìn)行混淆
http://pyob.oxyry.com/ 是一個(gè)在線混淆 Python 代碼的網(wǎng)站,使用它可以方便地進(jìn)行混淆访雪。
假定我們有這樣一段 Python 代碼锣杂,涉及到了類、函數(shù)企锌、參數(shù)等內(nèi)容:
# coding: utf-8
clas?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()
經(jīng)過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 ()
混淆后的代碼主要在注釋柳击、參數(shù)名稱和空格上做了些調(diào)整蹬叭,稍微帶來了點(diǎn)閱讀上的障礙饥悴。
方法二:使用 pyobfuscate 庫進(jìn)行混淆
pyobfuscate 算是一個(gè)頗具年頭的 Python 代碼混淆庫了瓣铣,但卻是“老當(dāng)益壯”了禽绪。
對(duì)上述同樣一段 Python 代碼循捺,經(jīng)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
相比于方法一,方法二的效果看起來更好些。除了類和函數(shù)進(jìn)行了重命名正罢、加入了一些空格回还,最明顯的是插入了若干段無關(guān)的代碼蝗柔,變得更加難讀了。
2.3 優(yōu)點(diǎn)
簡(jiǎn)單方便,提高了一點(diǎn)源碼破解門檻
兼容性好,只要源碼邏輯能做到兼容,混淆代碼亦能
2.4 不足
只能對(duì)單個(gè)文件混淆蒋得,無法做到多個(gè)互相有聯(lián)系的源碼文件的聯(lián)動(dòng)混淆
代碼結(jié)構(gòu)未發(fā)生變化,也能獲取字節(jié)碼,破解難度不大
3 使用 py2exe
3.1 思路
py2exe 是一款將 Python 腳本轉(zhuǎn)換為 Windows 平臺(tái)上的可執(zhí)行文件的工具。其原理是將源碼編譯為.pyc文件,加之必要的依賴文件,一起打包成一個(gè)可執(zhí)行文件渠抹。
如果最終發(fā)行由py2exe打包出的二進(jìn)制文件篮幢,那豈不是達(dá)到了保護(hù)源碼的目的搜锰?
3.2 方法
使用py2exe進(jìn)行打包的步驟較為簡(jiǎn)便狐胎。
1)編寫入口文件歌馍。本示例中取名為hello.py:
1print?'Hello World'
2)編寫setup.py:
from?distutils.core?import?setup
import?py2exe
setup(console=['hello.py'])
3)生成可執(zhí)行文件
1python setup.py py2exe
生成的可執(zhí)行文件位于dist\hello.exe。
3.3 優(yōu)點(diǎn)
能夠直接打包成 exe砚哆,方便分發(fā)和執(zhí)行
破解門檻比 .pyc 更高一些
3.4 不足
兼容性差关炼,只能運(yùn)行在 Windows 系統(tǒng)上
生成的可執(zhí)行文件內(nèi)的布局是明確、公開的吏砂,可以找到源碼對(duì)應(yīng)的.pyc文件匈织,進(jìn)而反編譯出源碼
4 使用 Cython
4.1 思路
雖說Cython的主要目的是帶來性能的提升,但是基于它的原理:將.py/.pyx編譯為.c文件掸哑,再將.c文件編譯為.so(Unix) 或.pyd(Windows),其帶來的另一個(gè)好處就是難以破解逾条。
4.2 方法
使用Cython進(jìn)行開發(fā)的步驟也不復(fù)雜。
1)編寫文件hello.pyx或hello.py:
def?hello():
????print('hello')
2)編寫setup.py:
from?distutils.core?import?setup
from?Cython.Build?import?cythonize
setup(name='Hello World app',
?????ext_modules=cythonize('hello.pyx'))
3)編譯為.c墩崩,再進(jìn)一步編譯為.so或.pyd:
1python setup.py build_ext?--inplace
執(zhí)行python -c "from hello import hello;hello()"即可直接引用生成的二進(jìn)制文件中的hello()函數(shù)。
4.3 優(yōu)點(diǎn)
生成的二進(jìn)制 .so 或 .pyd 文件難以破解
同時(shí)帶來了性能提升
4.4 不足
兼容性稍差赠制,對(duì)于不同版本的操作系統(tǒng)政恍,可能需要重新編譯
雖然支持大多數(shù) Python 代碼,但如果一旦發(fā)現(xiàn)部分代碼不支持叽讳,完善成本較高