王小強(qiáng),顧乃杰
(1.中國科學(xué)技術(shù)大學(xué) a.計算機(jī)科學(xué)與技術(shù)學(xué)院; b.先進(jìn)技術(shù)研究院,合肥 230027;2.安徽省計算與通信軟件重點實驗室,合肥 230027)
Python是一種面向?qū)ο蟮慕忉屝陀嬎銠C(jī)程序設(shè)計語言,由于語法簡潔清晰、簡單易學(xué)、免費、開源、擴(kuò)展性強(qiáng)等優(yōu)點,自從20世紀(jì)90年代誕生以來,就廣受歡迎和使用,如網(wǎng)絡(luò)文件同步工具Dropbox、社交新聞?wù)军cReddit、網(wǎng)絡(luò)問答社區(qū)知乎,都是由Python編程語言所開發(fā)。Python字節(jié)碼文件(.pyc)由Python編程語言編寫的程序編譯而成,與傳統(tǒng)的針對特定處理器和操作系統(tǒng)的二進(jìn)制文件相比,Python字節(jié)碼文件保留了Python源碼文件中的全部信息,是針對Python虛擬機(jī)的具有特定結(jié)構(gòu)和特征的文件,具有可跨平臺使用的特性。正是因為字節(jié)碼文件的這種結(jié)構(gòu)和特征,導(dǎo)致其極易被攻擊者反編譯出其中的源碼,如使用uncompyle2[1]、Decompyle++[2]、Easy Python Decompiler[3]等工具就可以將Python字節(jié)碼文件反編譯為源碼文件,這不僅會造成重要數(shù)據(jù)結(jié)構(gòu)、算法、業(yè)務(wù)邏輯的暴露,而且會給開發(fā)者帶來巨大的經(jīng)濟(jì)損失,有時甚至具有嚴(yán)重的安全隱患。
針對上述問題,本文提出一種基于操作碼合并的Python程序防逆轉(zhuǎn)算法。在不影響Python程序正確執(zhí)行的前提下,引入新操作碼對原Python操作碼集進(jìn)行擴(kuò)充,并在編譯生成字節(jié)碼文件時使用新操作碼來代替操作碼序列中的2個或多個操作碼,從而縮短Python字節(jié)碼文件中操作碼序列的長度,改變操作碼序列的結(jié)構(gòu)和語義。
代碼混淆旨在通過布局混淆、數(shù)據(jù)混淆、控制流混淆[4-5]等措施,將一個程序轉(zhuǎn)換為能夠妨礙攻擊者理解其中算法和數(shù)據(jù)結(jié)構(gòu)或能夠阻止攻擊者從程序文本中提取有價值信息的另一種形式[6-7]。
雖然Java、Python等腳本語言經(jīng)代碼混淆后編譯生成的字節(jié)碼文件可以跨平臺使用,但是其仍然遵守原來的文件格式和指令集,因此,代碼混淆技術(shù)對Python字節(jié)碼文件的保護(hù)能力不足。此外,代碼混淆在處理多文件項目中的導(dǎo)入模塊和對象名稱時有局限性,且該方法會給代碼執(zhí)行效率帶來一定的影響。
本地編譯是一種將Python腳本程序和解釋器一起編譯為平臺相關(guān)程序的技術(shù),其工作步驟為[8]:首先編寫Python源碼程序,然后借助特定的工具(如py2exe[9])將目標(biāo)腳本程序進(jìn)行轉(zhuǎn)換并且將Python解釋器編譯為平臺相關(guān)的動態(tài)鏈接庫,最后運(yùn)用一個額外的可執(zhí)行程序來運(yùn)行該動態(tài)鏈接程序和轉(zhuǎn)換后的文件。
雖然本地編譯技術(shù)對Python程序起到了一定的保護(hù)作用,但是采用這種方式,Python程序在發(fā)布時需要同時發(fā)布解釋器對應(yīng)的動態(tài)鏈接程序和腳本文件所依賴的所有庫文件,導(dǎo)致目標(biāo)程序所占用的空間大大增加。
數(shù)字水印通常是永久鑲嵌在宿主數(shù)據(jù)中的具有可鑒別性的數(shù)字信號或模式,其可以用來標(biāo)識作者、所有者、發(fā)行者、使用者等信息[10-11],且不影響宿主數(shù)據(jù)的可用性。使用數(shù)字水印技術(shù)并不能阻止字節(jié)碼文件被反編譯[12],但是能夠阻止數(shù)字產(chǎn)品被偷竊,或者當(dāng)偷竊發(fā)生時為使用者提供數(shù)字產(chǎn)品的擁有權(quán)證明[13]。
雖然理論上水印能夠作為軟件所有權(quán)的有力憑證,但實際中無論是靜態(tài)水印還是動態(tài)水印,都很容易通過混淆變換和優(yōu)化等措施從軟件中被移除[14-15],因此,水印技術(shù)并不能為Java、Python等字節(jié)碼文件提供有效保護(hù)。
除上述防逆轉(zhuǎn)方法外,研究人員在操作碼替換領(lǐng)域也進(jìn)行了研究。操作碼替換技術(shù)是將字節(jié)碼文件中的每個操作碼替換為其他操作碼,使得編譯生成的字節(jié)碼文件對于標(biāo)準(zhǔn)的解釋器而言包含非法的操作碼序列,以此達(dá)到防逆轉(zhuǎn)的目的。
操作碼替換技術(shù)已分別被基于虛擬機(jī)Dropbox[16]和基于PC的應(yīng)用,在操作碼隨機(jī)化的安全[17]中實現(xiàn),且網(wǎng)絡(luò)上已有工具python-obfuscation[18]對Python操作碼進(jìn)行隨機(jī)替換。但是,該技術(shù)的實質(zhì)是利用密碼學(xué)中的單表代替密碼[19],如果攻擊者了解采取的保護(hù)方式,就可以利用操作碼的一些統(tǒng)計學(xué)規(guī)律進(jìn)行分析,如采用頻率分析進(jìn)行攻擊,因此,該技術(shù)的安全性不足。
Python字節(jié)碼文件易被反編譯的主要原因是字節(jié)碼文件保留了Python源碼文件的所有信息,且字節(jié)碼文件中操作碼序列的結(jié)構(gòu)和每個操作碼的含義極易被攻擊者分析和理解。因此,改變操作碼序列的結(jié)構(gòu)或隱藏操作碼的含義,是保護(hù)Python字節(jié)碼文件的重要方法,但是,這類方法又會對虛擬機(jī)執(zhí)行字節(jié)碼文件的結(jié)果造成影響。鑒于此,本文將探索如何利用窺孔優(yōu)化策略,在不影響字節(jié)碼文件正確執(zhí)行的前提下,通過引入新操作碼來對Python操作碼集進(jìn)行擴(kuò)充,進(jìn)而改變Python字節(jié)碼文件中操作碼序列的結(jié)構(gòu),最終達(dá)到防逆轉(zhuǎn)的目的。
定義1操作碼
操作碼即Python虛擬機(jī)中的指令碼,占1 Byte的長度,其規(guī)定了Python虛擬機(jī)需要執(zhí)行哪一條指令。目前,Python 2.7.9版本的虛擬機(jī)規(guī)范中定義了110個操作碼,其中不帶參數(shù)的操作碼共61個,帶一個參數(shù)的操作碼共49個,每個參數(shù)占2 Byte的長度。
定義2操作碼序列
一個字節(jié)碼文件的操作碼序列S由操作碼和其所帶的參數(shù)構(gòu)成,操作碼序列結(jié)構(gòu)如圖1所示,其中,OPi為操作碼,ARGij為OPi的參數(shù)。OPi、ARGij均為正整數(shù),1≤i≤n,1≤j≤2,n為一個字節(jié)碼文件所包含的操作碼總數(shù)。

圖1操作碼序列示意圖
Python虛擬機(jī)執(zhí)行字節(jié)碼文件的本質(zhì)是利用循環(huán)依序讀取操作碼序列中的一個操作碼OPi和2個字節(jié)參數(shù)ARGi1與ARGi2,然后查找并執(zhí)行OPi對應(yīng)的解釋程序,修改虛擬機(jī)內(nèi)部眾多的狀態(tài)值,接著再讀取下一個操作碼OP(i+1)和字節(jié)參數(shù)ARG(i+1)1與ARG(i+1)2,重復(fù)上述步驟直至操作碼序列讀取結(jié)束。
定義3基本塊
基本塊是由S中順序執(zhí)行的若干個操作碼構(gòu)成的序列。通常情況下,若S中沒有出現(xiàn)條件跳轉(zhuǎn)、絕對跳轉(zhuǎn)操作,則這些操作碼處于同一個基本塊中。
定義4基本塊信息
字節(jié)碼文件的基本塊信息B是一個由正整數(shù)構(gòu)成的單調(diào)不減序列,其長度與操作碼序列S相同,且其中的每個元素與S中的每個操作碼一一對應(yīng),元素值為對應(yīng)的操作碼在S中所在的基本塊號。圖2所示為一個基本塊信息,從圖中可以看出,S中第1個操作碼處于第1個基本塊,第2個~第5個操作碼處于第2個基本塊,依此類推。

圖2基本塊信息示意圖
本文提出的操作碼合并算法流程如圖3所示。其中,操作碼序列提取與頻率統(tǒng)計的目的是尋找待合并的操作碼序列;基本塊信息提取是為了從大量的待合并操作碼序列中選出可以合并的序列,減少對操作碼集擴(kuò)充的干擾;操作碼集擴(kuò)充是添加新操作碼的定義與解釋執(zhí)行。

圖3操作碼合并算法流程
2.2.1 操作碼序列提取

本文提取Python/Lib下的1 950個庫文件源程序?qū)?yīng)的1 950個操作碼序列。這些程序通常提供接口給其他模塊進(jìn)行導(dǎo)入,其功能多樣且不會隨意發(fā)生改變,選它們進(jìn)行操作碼序列提取較為合理。1 950個操作碼序列中,最長的操作碼序列包含10 000多個操作碼,最短的也包含60個操作碼,共有操作碼240 263個。
2.2.2 頻率統(tǒng)計
頻率統(tǒng)計的目的是獲取候選合并對象。在2.2.1節(jié)獲取多個操作碼序列Sx的基礎(chǔ)上,統(tǒng)計出Sx中所有長度為len的操作碼子序列出現(xiàn)的次數(shù),并按出現(xiàn)次數(shù)從高到底進(jìn)行排序,其中,2≤len,1≤x≤k。
2.2.3 基本塊信息提取
基本塊信息的提取是在眾多的候選子序列中篩選出可以合并的子序列。在2.2.2節(jié)的排序結(jié)果中,靠前的對象并不一定都是可以進(jìn)行合并的操作碼子序列,原因是有些子序列中的操作碼處于不同的基本塊中,如果將該類操作碼進(jìn)行合并,將會破壞程序的執(zhí)行邏輯。因此,需要進(jìn)一步根據(jù)程序執(zhí)行邏輯來提取上述操作碼序列Sx的基本塊信息Bx,計算表達(dá)式如下:
其中,n為Sx中操作碼的個數(shù),1≤i 表1 部分長度為2 Byte的操作碼子序列以及其出現(xiàn)次數(shù) 2.2.4 操作碼集擴(kuò)充 操作碼集擴(kuò)充是利用窺孔優(yōu)化策略將出現(xiàn)頻率較高且處于同一個基本塊中的操作碼子序列(OP1,OP2,…,OPlen)合并為一個新的操作碼OPnew,并在Python虛擬機(jī)中添加對OPnew的解釋執(zhí)行過程,使得對OPnew的解釋執(zhí)行效果與依次執(zhí)行(OP1,OP2,…,OPlen)的效果等價。 圖4 操作碼合并示意圖 因為在Python字節(jié)碼序列中任意一個操作碼僅占1 Byte的大小,所以最多可以定義28個操作碼。而在Python 2.7.9版本的虛擬機(jī)規(guī)范中僅使用了其中的110個操作碼,因此,可以利用剩余的146個操作碼來定義新操作碼,從而在利用操作碼合并縮短序列的同時隱藏操作碼序列的含義。本文對表1中的5個子序列進(jìn)行合并,并在Python 2.7.9中利用剩余的146個操作碼中的5個來表示合并后的OPnew。 Python 3在Python 2的基礎(chǔ)上對某些語法和模塊進(jìn)行調(diào)整和改動,因為同樣遵循操作碼僅占1 Byte長度的特性和操作碼解釋執(zhí)行的機(jī)制,所以操作碼合并算法可以在Python 3上實現(xiàn),如Python 3.6.1使用了256個操作碼中的119個,只能利用剩余的137個操作碼來定義新操作碼。 在操作碼僅占1 Byte長度的基礎(chǔ)上進(jìn)行操作碼合并,會大大限制操作碼合并的擴(kuò)展空間。若要生成和使用更多的操作碼,則必須增加操作碼所占存儲空間的大小(如將操作碼大小全部設(shè)為2 Byte或?qū)⒉糠植僮鞔a設(shè)為2 Byte),但該操作會因多次讀取操作碼而導(dǎo)致程序執(zhí)行效率下降。 2.3.1 安全性 Ox=min(256nx+2mx,f) (1) 2.3.2 執(zhí)行效率 當(dāng)Python虛擬機(jī)執(zhí)行字節(jié)碼文件時,每次只能讀取一個操作碼和一個參數(shù),并調(diào)用相應(yīng)的解釋執(zhí)行過程。操作碼合并前后Python虛擬機(jī)執(zhí)行字節(jié)碼文件運(yùn)行過程如圖5所示。 圖5 操作碼合并前后執(zhí)行過程對比 如圖5(a)所示,由于操作碼序列中包含ADD、SUB列操作,因此需要讀取2次操作碼和參數(shù)以對棧頂?shù)?個元素a和b出棧、求和,將結(jié)果r1壓棧,然后對棧頂元素r1和c出棧、求差,將結(jié)果r再壓棧。式(2)為操作碼合并前Python虛擬機(jī)執(zhí)行源碼文件filex.py的時間。 (2) 如圖5(b)所示,操作碼合并后用新操作碼ADD_SUB代替原先的2個操作碼,用新操作碼一次完成棧頂3個元素a、b、c出棧,并將a+b-c的結(jié)果r運(yùn)算完之后再壓棧。該操作雖然增加了編譯源碼文件所需的時間,但減少了冗余的讀取操作及修改Python虛擬機(jī)內(nèi)部狀態(tài)的次數(shù),且編譯后的字節(jié)碼文件在以后的運(yùn)行過程中無需再次編譯,從而提升了字節(jié)碼文件的執(zhí)行效率。 式(3)為操作碼合并后Python虛擬機(jī)執(zhí)行源碼文件filex.py的時間。 (3) 操作碼合并算法的整體性能與合并前后的字節(jié)碼文件執(zhí)行時間有關(guān)。式(4)為操作碼合并前后字節(jié)碼文件執(zhí)行時間的變化率。 (4) 2.3.3 存儲空間 操作碼合并將操作碼序列Sx中連續(xù)出現(xiàn)的多個操作碼合并為一個新操作碼,在不減少Sx中參數(shù)個數(shù)的同時減少了操作碼的個數(shù),從而縮短了操作碼序列的長度,如原本需占用(nx+2mx) Byte的操作碼序列,現(xiàn)只需占用(nx+2mx-gx(num)) Byte即可,字節(jié)碼文件的大小縮小了(gx(num)) Byte。在需要存儲和導(dǎo)入大量模塊時,該方法有利于節(jié)約計算機(jī)磁盤和內(nèi)存。同時,基于操作碼合并生成的字節(jié)碼文件需要新的解釋器來解釋執(zhí)行,這就需要客戶安裝指定的Python解釋器。雖然在首次使用時會給客戶帶來一些不便(以后無需再次安裝),但該過程也是為了確保程序的安全性。 本文基于Python 2.7.9版本,實驗測試平臺為Intel Xeon E5-2690,主頻2.6 GHz,252 GB內(nèi)存,操作系統(tǒng)為CentOS 6.7 64位,實驗對象為表1中的5對操作碼。采用microbenchmark[20]來對新生成運(yùn)行環(huán)境的防逆轉(zhuǎn)效果、執(zhí)行效率、文件大小進(jìn)行檢驗。microbenchmark包含數(shù)字運(yùn)算、字符處理等多個方面的Python源碼文件,是一個較好的測試集。 3.2.1 安全性 本文通過操作碼合并改變了原操作碼序列的結(jié)構(gòu),對原操作序列信息進(jìn)行了較好的隱藏。傳統(tǒng)的反編譯工具uncompile2、Decompyle++等無法識別合并后的操作碼序列結(jié)構(gòu),且無法理解新操作碼所包含的語義信息,因此,無法對新的字節(jié)碼文件進(jìn)行反編譯。 3.2.2 執(zhí)行效率 表2 操作碼合并前后文件執(zhí)行時間比較 從表2中的實驗數(shù)據(jù)可以看出,使用操作碼合并算法在參數(shù)個數(shù)不變的前提下,減少了字節(jié)碼文件中操作碼序列的操作碼個數(shù),即減少了CPU讀取操作碼的次數(shù)和多個操作碼執(zhí)行之間的跳轉(zhuǎn)次數(shù),從而縮短了程序的執(zhí)行時間。 從表2實驗數(shù)據(jù)中還可以看出,操作碼合并后test.pyc和pydigits.pyc文件執(zhí)行時間較之前有明顯的減少,通過分析發(fā)現(xiàn)其原因為,test.pyc操作碼序列中包含的操作碼個數(shù)較少,只需要合并若干個操作碼就可以使其個數(shù)有明顯的減少;合并前的pydigits.pyc操作碼包含在循環(huán)中,因為循環(huán)中的操作碼被多次執(zhí)行,每次執(zhí)行的時間較合并前都有所減少,所以使得整個操作碼序列的執(zhí)行時間有明顯減少。 3.2.3 存儲空間 表3 操作碼合并前后文件大小比較 從表3可以看出,操作碼合并后所有字節(jié)碼文件的大小都有所減小。 本文提出基于操作碼合并的Python字節(jié)碼文件防逆轉(zhuǎn)算法,與文獻(xiàn)[21]基于隱藏參數(shù)的方法相同,都是通過擴(kuò)充操作碼集來改變操作碼序列的內(nèi)容和結(jié)構(gòu),最終達(dá)到防逆轉(zhuǎn)的目的。雖然目前兩者都受限于操作碼僅占1 Byte長度的特性,存在擴(kuò)展空間不足的問題,但本文算法建立在對操作碼序列特征統(tǒng)計分析的基礎(chǔ)上,其不會因程序運(yùn)行時參數(shù)的變化而產(chǎn)生影響,通用性較強(qiáng)。實驗結(jié)果表明,在字節(jié)碼文件執(zhí)行效率和文件大小兩方面,本文算法在操作碼合并前后都有明顯提升。為進(jìn)一步提升字節(jié)碼文件的安全性和算法的適用性,下一步將考慮通過抽象語法樹的變換和優(yōu)化,對多基本塊之間的操作碼進(jìn)行合并操作。 [1] Mysterie.A Python 2.7 byte-code decompiler[EB/OL].[2017-03-10].https://github.com/wibiti/uncompyle2. [2] Niwit.Decompyle-Python decompiler[EB/OL].[2017-03-10].https://sourceforge.net/projects/decompyle/?source=typ_redirect. [3] Extremecoders.Python 1.0-3.4 bytecode decompiler[EB/OL].[2017-03-10].https://sourceforge.net/projects/easypytho ndecompiler/. [4] 蔣 華,劉 勇,王 鑫.基于控制流的代碼混淆技術(shù)研究[J].計算機(jī)應(yīng)用研究,2013,30(3):897-899. [5] 楊 樂,周強(qiáng)強(qiáng),薛錦云.基于垃圾代碼的控制流混淆算法[J].計算機(jī)工程,2011,37(12):23-25. [6] KUZURIN N,SHOKUROV A,VARNOVSKY N,et al.On the concept of software obfuscation in computer security[C]//Proceedings of International Conference on Information Security.Washington D.C.,USA:IEEE Press,2007:281-298. [7] 徐海銀,雷植洲,李 丹.代碼混淆技術(shù)研究[J].計算機(jī)與數(shù)字工程,2007,35(10):4-7. [8] 鮑福良,彭俊艷,方志剛.Java類文件保護(hù)方法綜述[J].計算機(jī)系統(tǒng)應(yīng)用,2007,16(6):124-126. [9] Py2exe:A Python distutils extension which converts Python scripts into executable windows programs[EB/OL].[2017-03-10].http://www.py2exe.org/. [10] 陳明奇,鈕心忻.數(shù)字水印的研究進(jìn)展和應(yīng)用[J].通信學(xué)報,2001,22(5):71-79. [11] 孫圣和,陸哲明.數(shù)字水印處理技術(shù)[J].電子學(xué)報,2000,28(8):85-90. [12] 陳 晗,趙軼群,繆亞波.Java字節(jié)碼的水印嵌入[J].計算機(jī)應(yīng)用,2003,23(9):96-98. [13] COLLBERG C S,THOMBORSON C.Watermarking tamper-proofing and obfuscation[J].IEEE Transactions on Software Engineering,2000,28(8):735-746. [14] HAMILTON J,DANICIC S.An evaluation of static Java bytecode watermarking[EB/OL].[2017-02-25].https://jameshamilton.eu/sites/default/files/JavaBytecodeWatermar kingSurvey.pdf. [15] KUMAR K,KEHAR V,KAUR P.An evaluation of dynamic Java bytecode software watermarking algori-thms[J].International Journal of Security and Its Applications,2016,10(7):147-156. [16] KHOLIA D,WEGRZYN P.Looking inside the (Drop) box[EB/OL].[2017-03-01].https://www.usenix.org/system/files/conference/woot13/woot13-kholia.pdf. [17] J.C.斯普拉德林.通過操作碼隨機(jī)化的安全:CN 102592082 A[P].2012-07-18. [18] Omab.Python-obfuscation[EB/OL].[2017-03-10].https://github.com/citrusbyte/python-obfuscation. [19] STALLINGS M.Cryptography and network security[M].王章宜,楊 敏,杜瑞穎,等,譯.北京:電子工業(yè)出版社,2012. [20] MODZELEWSKI K.Microbenchmarks[EB/OL].[2017-03-10].https://github.com/dropbox/pyston/tree/master/micro benchmarks. [21] GUELTON S.Building an obfuscated Python interpreter:we need more opcodes[EB/OL].[2017-03-10].https://blog.quarkslab.com/building-an-obfuscated-python-interpreter-we-need-more-opcodes.html.



2.3 算法性能分析







3 實驗結(jié)果與分析
3.1 實驗環(huán)境
3.2 結(jié)果分析




4 結(jié)束語