宅吉便


如何保護Python原始碼

python

這個問題其實常常被提及
但是沒有一個比較完整的答案
以下我針對概念性問題技術性問題這兩個大方向來探討

概念性問題

不管是用哪一種語言寫的
我們永遠無法防範別人把商業模式或是創意偷走
其實透過申請專利,建立License等
才是保護產品的最佳途徑

啊不過我就只是不想給我的朋友看清楚我的程式碼
不想要給我的同事看我在幹嘛而已
誰給你去申請什麼專利膩!

以下我們就來看看技術上
我們能做些什麼

技術性問題

在往下探討之前
我先以底下這段程式碼當作範例
檔名: getip.py

import sys 
import json
import logging

python_version = sys.version_info
if python_version.major == 2:
    from urllib import urlopen
elif python_version.major == 3:
    from urllib.request import urlopen

logging.basicConfig(level=logging.DEBUG)

logger = logging.getLogger(__name__)


def getip():
    """Get current ip."""
    resp = urlopen('http://api.ipify.org?format=json')
    logger.debug('status code: %d' % resp.code)
    raw = resp.read()
    info = json.loads(raw.decode('utf-8'))
    logger.info('current ip is: %s' % info['ip'])


if "__main__" == __name__:
    getip()

Minification

根據Wikipeida, minification指的是

極簡化(另稱縮小化),在程式語言的範疇裡,指的是在不影響功能的情況下,移除所有非功能性必要之原始碼字元(如:空白、換行、註解、以及些許的區塊辦識子),因為雖然它們有助於提昇原始碼的易讀性,但在實際運行時卻不是必要的部份。

在Javascript裡面我常用的是 UglifyJS
或是Python的pyminifier
這些都算是算是minification的工具
不過這些工具也帶有obfuscation的功能

以 pyminifier 為例

pyminifier getip.py

即可產生

import sys
import json
import logging
python_version=sys.version_info
if python_version.major==2:
 from urllib import urlopen
elif python_version.major==3:
 from urllib.request import urlopen
logging.basicConfig(level=logging.DEBUG)
logger=logging.getLogger(__name__)
def getip():
 resp=urlopen('http://api.ipify.org?format=json')
 logger.debug('status code: %d'%resp.code)
 raw=resp.read()
 info=json.loads(raw.decode('utf-8'))
 logger.info('current ip is: %s'%info['ip'])
if "__main__"==__name__:
 getip()

其實也不過就把docstring還有一些空格拿掉
在Python這種高易讀性的語言,minification幫助並不大

Obfuscation (代碼混淆)

根據Wikipedia,obfuscation 指的是

... 將電腦程式的代碼,轉換成一種功能上等價,但是難於閱讀和理解的形式的行為。

慣用的手法為:

• 將代碼中的各種元素,如變數,函式,類的名字覆寫成無意義的名字。比如覆寫成單個字母,或是簡短的無意義字母組合,甚至覆寫成「__」這樣的符號,使得閱讀的人無法根據名字猜測其用途。
• 重寫代碼中的部分邏輯,將其變成功能上等價,但是更難理解的形式。比如將for迴圈覆寫成while迴圈,將迴圈覆寫成遞迴,精簡中間變數,等等。
• 打亂代碼的格式。比如刪除空格,將多行代碼擠到一行中,或者將一行代碼斷成多行等等。
• 添加花指令,通過特殊構造的指令來使得反組譯器出錯,進而干擾反編譯工作的進行。

pyminifier -O getip.py

即可產生

import sys
P=sys.version_info
import json
j=json.loads
import logging
B=logging.getLogger
X=logging.DEBUG
x=logging.basicConfig
o=P
if o.major==2:
 from urllib import urlopen
elif o.major==3:
 from urllib.request import urlopen
x(level=X)
c=B(__name__)
def C():
 n=urlopen('http://api.ipify.org?format=json')
 c.debug('status code: %d'%n.code)
 D=n.read()
 t=j(D.decode('utf-8'))
 c.info('current ip is: %s'%t['ip'])
if "__main__"==__name__:
 C()

恩...變數名稱都被改掉了
看起來亂七八糟的,但還是可以窺其一二

compilation

compile(編譯)可能是「保護程式碼」最有效的方式

我們在run Python的時候,一定會產生一些 .pyc 的檔案
不過說穿了這些也只是bytecode,功能只是用來加速用的
並不是用來保護程式碼的作用
要decompile的話,網路上有一堆工具可以使用

要反編譯可以夠過python-uncompyle6輕鬆達成目的

pip install uncompyle6
uncompyle6 getip.pyc

大家可以試試看,根本就是完全還原...

那有沒有其他compile的方法呢?有
轉成cython是一個不錯的選擇
不過這裡我介紹另外一個工具: Nuitka
Nuitka 是什麼呢?根據官方解釋:

Nuitka is a Python compiler.

對,它就也只是一個 Python的compiler
目前它支援的版本有 2.6, 2.7, 3.2, 3.3, 3.4, 3.5

實際來操作看看

我這邊是把getip當作一個module來用
所以我用到Nuitka的 --module 這個參數

sudo pip install nuitka
nuitka --module getip.py

然後會產生一個 getip.so
這樣就完成了!

在測試之前,記得把 getip.py 刪掉
以免不小心import 到 getip.py

$ python
>>> import getip
>>> getip.getip()
DEBUG:getip:status code: 200
INFO:getip:current ip is: xxx.xxx.xxx.xxx

成功,而且 getip.so 是編譯後的東西,根本不可讀!

不過別忘了:
有compiler,就有decompiler
沒有什麼事不能反編譯的。

結論

不管是哪一種手段
或是三種手段混用
都只是在增加被破解的難度
而且對於有逆向工程 (Reverse Engineering)的經驗的人來說
真正的問題恐怕只剩下時間的問題
所以
沒有一種方法可以真正的保護原始碼