pyc是編譯py之后生成的二進(jìn)制文件。當(dāng)我們發(fā)布系統(tǒng)的時(shí)候不想讓別人看到源代碼,就需要將py文件編譯生成pyc文件,對外只提供pyc文件。同樣,如果拿到一個(gè)python程序,只有pyc文件,我們就無法看到源碼,希望有辦法反編譯pyc文件以獲得源碼。
既然有反編譯的需求,為了保護(hù)pyc文件不被反編譯,我們又希望找到一個(gè)方法來防止pyc文件被反編譯。
本文介紹了如何將py文件編譯成pyc和pyo文件,也介紹了如何反編譯pyc文件, 又給出了一種字節(jié)碼混淆方法來防止pyc文件被反編譯。
pyc是編譯py之后生成的二進(jìn)制文件,由python虛擬機(jī)來執(zhí)行的。當(dāng)我們發(fā)布系統(tǒng)的時(shí)候不想讓別人看到源代碼,就需要將py文件編譯生成pyc文件,對外只提供pyc文件。
同時(shí),在模塊被加載時(shí),.pyc文件比.py文件更快
但是pyc的內(nèi)容跟python的版本相關(guān),不同的版本編譯后的pyc文件不同,2.5編譯的pyc文件不能到3.5上執(zhí)行.
發(fā)布python軟件的過程為:
1. 生產(chǎn)pyc文件: python -m compileall .
2. 刪除py文件: find . -name “*.py” |xargs rm -rf
3. 刪除pycache目錄: find . -name “pycache” |xargs rm -rf
命令:
python -m py_compile file.py
python -m py_compile {file1,file2}.py
編譯完成后,生成的 .pyc文件在當(dāng)前目錄。
可以使用-O或者-OO轉(zhuǎn)換python命令來減少編譯模塊的大小
-O轉(zhuǎn)換會(huì)幫你去掉assert語句
-OO轉(zhuǎn)換會(huì)幫你去掉assert語句和__doc__文檔字符串
由于一些程序可能依賴于assert語句或文檔字符串,應(yīng)該在確認(rèn)需要的情況下使用這些選項(xiàng)。
例如 python –O -m py_compile file.py 會(huì)在本目錄生成 file.pyo
Python3的編譯和python2一樣,只是在python3之后編譯后的文件在pycache目錄下,而不是與源文件同一目錄。
那么如何讓python3實(shí)現(xiàn)生成的pyc與源代碼在同一目錄呢?可以加上-b參數(shù),例如
Python3 -m py_compile -b file.py
python -m compileall DIR
python3 -m compileall –b DIR
DIR為需要編譯的目錄
例如: python -m compileall .
參數(shù)說明:
usage: python compileall.py [-l] [-f] [-q] [-d destdir] [-x regexp] [-i list] [directory|file ...]
arguments: zero or more file and directory names to compile; if no arguments given,
defaults to the equivalent of -l sys.path
options:
-l: don't recurse into subdirectories
-f: force rebuild even if timestamps are up-to-date
-q: output only error messages
-d destdir: directory to prepend to file paths for use in compile-time tracebacks and in
runtime tracebacks in cases where the source file is unavailable
-x regexp: skip files matching the regular expression regexp; the regexp is searched for
in the full path of each file considered for compilation
-i file: add all the files and directories listed in file to the list considered for
compilation; if "-", names are read from stdin
單文件編譯:
import py_compile
py_compile.compile('path') //path是包括.py文件名的路徑
批量編譯:
import compileall
compileall.compile_dir('$dir')
如果只有pyc文件,我們是無法直接查看內(nèi)容的,這時(shí),就需要反編譯工具將pyc文件反編譯成py源程序。 這里介紹uncompyle6反編譯工具。Pyc文件能夠反編譯的前提是文件沒有被加密或者代碼混淆。
> git clone https://github.com/rocky/python-uncompyle6.git
> git checkout python-2.4
> sudo python setup.py install
如果沒有報(bào)錯(cuò),則安裝成功了。
執(zhí)行命令:
>uncompyle6 -o . *.pyc
這時(shí)會(huì)在當(dāng)前目錄生成.py源文件。
如何防止pyc文件被反編譯呢? 這里介紹字節(jié)碼混淆的方法,此方法能夠抵擋低端的反編譯手段,例如第2章的方法,但是,要到高手,還是抵擋不住。
字節(jié)碼混淆可以非常容易的欺騙通常的反匯編器和反編譯器,同時(shí)不影響代碼的正常執(zhí)行。下面這個(gè)例子展示了如何欺騙Uncompyle6反編譯器以及dis反匯編器:
#一個(gè)簡單的Python應(yīng)用 sample1.py
print 'Hello World'
對其進(jìn)行編譯:
python -m py_compile sample1.py
對編譯后的sample1.pyc使用Python內(nèi)置dis模塊反匯編:
>>> import marshal,dis
>>> fd = open('sample1.pyc', 'rb')
>>> fd.seek(8)
>>> sample1_code_obj = marshal.load(fd)
>>> fd.close()
>>> dis.dis(sample1_code_obj)
1 0 LOAD_CONST 0 ('Hello World')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 1 (None)
8 RETURN_VALUE
>>>
以上的匯編代碼筆者肉眼反匯編的結(jié)果如下:
0 LOAD_CONST 0 ('Hello World') #加載co_consts[0]到棧頂,co_consts[0]存儲著常量字符串'Hello World'
3 PRINT_ITEM #打印棧頂?shù)?/span>sys.stdout,即print 'Hello World'
4 PRINT_NEWLINE #打印新行到sys.stdout,此指令因print語句而由編譯器自動(dòng)生成
5 LOAD_CONST 1 (None) #加載co_consts[1]到棧頂,co_consts[1]存儲著None
8 RETURN_VALUE #將棧頂返回給調(diào)用者,此兩條指令為編譯器自動(dòng)生成
現(xiàn)在我們修改sample1.pyc,在程序入口增加一條絕對跳轉(zhuǎn)指令(可以使用UltraEdit 16進(jìn)制插入功能修改pyc文件,”JUMP_ABSOLUTE 3”在Python 2.7中對應(yīng)的字節(jié)碼為 0x71 0x03 0x00。修改code string內(nèi)容的同時(shí)應(yīng)修改code string的長度,此處增加了一個(gè)3字節(jié)指令),使用內(nèi)置dis模塊反匯編的結(jié)果如下:
1 0 JUMP_ABSOLUTE 3 #自行添加
>> 3 LOAD_CONST 0 ('Hello World')
6 PRINT_ITEM
7 PRINT_NEWLINE
8 LOAD_CONST 1 (None)
11 RETURN_VALUE
如果讀者對匯編代碼有一定認(rèn)識,就會(huì)明白此處的絕對跳轉(zhuǎn)對Python虛擬機(jī)執(zhí)行此程序基本沒有影響(除了增加一個(gè)指令執(zhí)行周期),然而這個(gè)絕對跳轉(zhuǎn)將成功欺騙反編譯器。使用Uncompyle6反編譯的結(jié)果如下:
<<< Error: Decompiling stopped due to <class 'uncompyle6.semantics.pysource.ParserError'>
如果一個(gè)pyc文件無法被反編譯,初級的破解者可能就會(huì)止步于此了,但對于有經(jīng)驗(yàn)的工程師來說這還遠(yuǎn)遠(yuǎn)不夠。同樣的,我們還要讓通常的反匯編器也無法工作才行。按下面的匯編代碼繼續(xù)加工上面的sample1.pyc。
| 1 0 JUMP_ABSOLUTE [71 06 00] 6
| 3 LOAD_CONST [64 FF FF] 65535 (FAKE!)
| >> 6 LOAD_CONST [64 00 00] 0 (Hello World)
| 9 PRINT_ITEM [47 -- --]
| 10 PRINT_NEWLINE [48 -- --]
| 11 LOAD_CONST [64 01 00] 1 (None)
| 14 RETURN_VALUE [53 -- --]
以上第二條指令的意思是加載code object常量表的第65535項(xiàng)到棧頂。在上述sample1.pyc中,常量表的長度為2,下標(biāo)65535已超出常量表的范圍,所以這是條非法指令。但由于第一條絕對跳轉(zhuǎn)的存在,第二條指令永遠(yuǎn)都不會(huì)被執(zhí)行。通常的反匯編器如dis會(huì)盡全力列舉有用的信息,但并不能理解實(shí)際執(zhí)行的控制流,當(dāng)反匯編器嘗試反匯編第二條指令時(shí),會(huì)試著去讀取code object常量表的第65535項(xiàng)并且拋出一個(gè)’tuple index out of range’的意外。Python內(nèi)置dis模塊的出錯(cuò)信息如下:
>>> fd = open('sample1.pyc', 'rb')
>>> fd.seek(8)
>>> import marshal,dis
>>> sample1_code_obj = marshal.load(fd)
>>> dis.dis(sample1_code_obj)
1 0 JUMP_ABSOLUTE 6
3 LOAD_CONST 65535
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python27\lib\dis.py", line 43, in dis
disassemble(x)
File "C:\Python27\lib\dis.py", line 96, in disassemble
print '(' + repr(co.co_consts[oparg]) + ')',
IndexError: tuple index out of range
>>>
現(xiàn)在Uncompyle6和dis都被欺騙了,代碼得到了有效的保護(hù)。
https://blog.csdn.net/ir0nf1st/article/details/61650984
https://www.cnblogs.com/nickchen121/p/10802465.html
https://www.cnblogs.com/dkblog/archive/2009/04/16/1980757.html