指令碼程式碼混淆-Python篇-pyminifier(1)
前言
最近研究了一下指令碼語言的混淆方法,比如 python,javascript
等。指令碼語言屬於動態語言,程式碼大多無法直接編譯成二進位制機器碼,發行指令碼基本上相當於暴露原始碼,這對於一些商業應用是無法接受的。因此對指令碼程式碼進行加固,成為很多應用的首選。程式碼加固的一項措施是程式碼混淆,增加逆向人員閱讀程式碼邏輯的難度,拖延被破解的時間。
今天講解一下Python程式碼的混淆方法,Python程式碼一般用作web,提供服務介面,但也有一些桌面的應用,這一部分就需要對程式碼進行混淆保護。以一個開源專案pyminifier (https://github.com/qiyeboy/pyminifier)來說明混淆的技巧方法,這個專案已經有4年沒更新,有一些bug,但是依然值得我們學習和入門。
專案結構
框架詳情:
analyze.py - 用於分析Python程式碼 compression.py - 使用壓縮演算法壓縮程式碼 minification.py - 用於簡化Python程式碼 obfuscate.py - 用於混淆Python 程式碼 token_utils.py - 用於收集Python Token
從專案程式碼中,可以看到pyminifier的混淆方法是基於Token的,即基於詞法分析,假如大家之前做過混淆的話,這應該屬於混淆的初級方案,因為這樣的混淆並不會修改程式碼原有的邏輯結構。
提取Token
如何提取Python語言的Token呢?Python中提供了專門的包進行詞法分析: tokenize
def listified_tokenizer(source): """Tokenizes *source* and returns the tokens as a list of lists.""" io_obj = io.StringIO(source) return [list(a) for a in tokenize.generate_tokens(io_obj.readline)]
首先讀取原始檔,然後通過tokenize.generate_tokens生成token列表。咱們就將這個提取token的函式儲存起來,然後讓他自己提取自己,看一下token列表的結構。
[[1, 'def', (1, 0), (1, 3), 'def listified_tokenizer(source):\n'], [1, 'listified_tokenizer', (1, 4), (1, 23), 'def listified_tokenizer(source):\n'], [53, '(', (1, 23), (1, 24), 'def listified_tokenizer(source):\n'], [1, 'source', (1, 24), (1, 30), 'def listified_tokenizer(source):\n'], [53, ')', (1, 30), (1, 31), 'def listified_tokenizer(source):\n'], [53, ':', (1, 31), (1, 32), 'def listified_tokenizer(source):\n'], [4, '\n', (1, 32), (1, 33), 'def listified_tokenizer(source):\n'], ......
每一個Token對應一個list,以第一行 [1,'def',(1,0),(1,3),'def listified_tokenizer(source):\n']
為例子進行解釋:
-
1代表的是token的型別
-
def是提取的token字串
-
(1, 0)代表的是token字串的起始行與列
-
(1, 3)代表的是token字串的結束行與列
-
'def listified_tokenizer(source):\n' 代表所在的行
Token還原始碼
能從原始檔中提取token 列表,如何從token列表還原為原始碼呢?其實很簡單,因為提取token 列表裡面有位置資訊和字串資訊,所以進行字串拼接即可。
def untokenize(tokens): """ Converts the output of tokenize.generate_tokens back into a human-readable string (that doesn't contain oddly-placed whitespace everywhere). .. note:: Unlike :meth:`tokenize.untokenize`, this function requires the 3rd and 4th items in each token tuple (though we can use lists *or* tuples). """ out = "" last_lineno = -1 last_col = 0 for tok in tokens: token_string = tok[1] start_line, start_col = tok[2] end_line, end_col = tok[3] # The following two conditionals preserve indentation: if start_line > last_lineno: last_col = 0 if start_col > last_col and token_string != '\n': out += (" " * (start_col - last_col)) out += token_string last_col = end_col last_lineno = end_line return out
精簡與壓縮程式碼
在pyminifier中,有兩個縮小Python程式碼的方法:一個是精簡方式,另一個是使用壓縮演算法的方式。
精簡
在minification.py中使用的是精簡方式,具體程式碼如下:
def minify(tokens, options): """ Performs minification on *tokens* according to the values in *options* """ # Remove comments remove_comments(tokens) # Remove docstrings remove_docstrings(tokens) result = token_utils.untokenize(tokens) # Minify our input script result = multiline_indicator.sub('', result) result = fix_empty_methods(result) result = join_multiline_pairs(result) result = join_multiline_pairs(result, '[]') result = join_multiline_pairs(result, '{}') result = remove_blank_lines(result) result = reduce_operators(result) result = dedent(result, use_tabs=options.tabs) return result
上面的程式碼總共使用了9種方法來縮小指令碼的體積:
remove_comments
去掉程式碼中的註釋,但是有兩類要保留:1.指令碼直譯器路徑 2. 指令碼編碼
#!/usr/bin/env python # -*- coding: utf-8 -*-
remove_docstrings
去掉doc所指定的內容,example:
__doc__ = """\ Module for minification functions. """
fix_empty_methods
修改空函式變成pass
def myfunc(): '''This is just a placeholder function.'''
轉化為:
def myfunc():pass
join_multiline_pairs
(1) 第一種情況:
test = ( "This is inside a multi-line pair of parentheses" )
轉化為:
test = ( "This is inside a multi-line pair of parentheses")
(2)第二種情況:
test = [ "This is inside a multi-line pair of parentheses" ]
轉化為:
test = [ "This is inside a multi-line pair of parentheses"]
(3)第三種情況:
test = { "parentheses":"This is inside a multi-line pair of parentheses" }
轉化為:
test = { "parentheses":"This is inside a multi-line pair of parentheses"}
remove_blank_lines
移除空白行。
test = "foo" test2 = "bar"
轉化為:
test = "foo" test2 = "bar"
reduce_operators
移除操作符之間的空格。
def foo(foo, bar, blah): test = "This is a %s" % foo
修改為:
def foo(foo,bar,blah): test="This is a %s"%foo
dedent
替換程式碼間的縮排,比如替換成單個空格
def foo(bar): test = "This is a test"
修改為:
def foo(bar): test = "This is a test"
壓縮
在這個專案中的compression.py,提供了4種程式碼壓縮的方法,其中3個原理是一樣,只不過使用的壓縮演算法不一樣。
bz2,gz,lzma 壓縮執行原理
假如新建一個1.py,並儲存如下內容:
if __name__=="__main__": print(__name__)
以bz2為例子,首先使用bz2演算法壓縮程式碼,然後轉化成base64編碼。
code=''' if __name__=="__main__": print(__name__) ''' import bz2,base64 compressed_source = bz2.compress(code.encode("utf-8")) print(base64.b64encode(compressed_source).decode('utf-8'))
輸出:
QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==
程式碼壓縮完成後,如何執行呢?其實就用到了exec這個函式/關鍵字。將編碼好的內容,先base64解碼,再使用bz2演算法解壓縮,最後獲得真實的程式碼,並使用exec執行
import bz2, base64 exec(bz2.decompress(base64.b64decode("QlpoOTFBWSZTWdfQmoEAAAHbgEAQUGAAEgAAoyNUACAAIam1NNGgaaFNMjExMQ2Za0TTvJepAjgXb2pDBBGoliFIT04+LuSKcKEhr6E1Ag==")))
這段程式碼就代表了最原始的程式碼,而使用gz,lzma壓縮方式,將bz2包換成zlib 或者lzma即可。
zip執行原理
可能很多朋友不知道,Python是可以直接執行zip檔案的(特別的),主要是為了方便開發者管理和釋出專案。Python能直接執行一個包含 __main__.py的目錄或者zip檔案。
舉個例子:
|—— ABC/ |—— A.py |—— __main__.py
示例程式碼:
# A.py def echo(): print('ABC!') # __main__.py if __name == '__main__': import A A.echo()
可以直接將多個檔案壓縮成一個zip檔案,直接執行zip檔案就可以。目錄結構:
|—— ABC.zip/ |—— A.py |—— __main__.py
執行情況:
$ python ABC.zip ABC!
未完待續。。。
最後
關注公眾號:七夜安全部落格
- 回覆【1】:領取 Python資料分析 教程大禮包
- 回覆【2】:領取 Python Flask 全套教程
- 回覆【3】:領取 某學院 機器學習 教程
- 回覆【4】:領取 爬蟲 教程
- 回覆【5】:領取 編譯原理 教程
- 回覆【6】:領取 滲透測試 教程
- 回覆【7】:領取 人工智慧數學基礎 教程
&n