陳耀陽,陳 偉
南京郵電大學(xué) 計(jì)算機(jī)學(xué)院,南京 210023
近年來,隨著計(jì)算機(jī)技術(shù)的發(fā)展,針對軟件本身的保護(hù)逐漸引起人們的關(guān)注。對于軟件的攻擊形式有很多種,一般而言,軟件保護(hù)技術(shù)主要對以下三種類型[1]的攻擊進(jìn)行防護(hù):軟件盜版、逆向工程以及軟件篡改。在這幾種攻擊中,逆向工程是相對比較基礎(chǔ)同時也是危害性最大的一種攻擊形式。
逆向工程[2]是指對一項(xiàng)目標(biāo)產(chǎn)品進(jìn)行逆向分析和研究,在無法輕易獲得必要產(chǎn)品信息的情況下,直接從成品分析,推導(dǎo)產(chǎn)品的設(shè)計(jì)原理。在計(jì)算機(jī)領(lǐng)域中,特別是針對程序的二進(jìn)制代碼進(jìn)行的逆向工程也稱“代碼逆向工程”。一次成功的逆向,對于競爭者而言可以輕易獲取產(chǎn)品中的敏感數(shù)據(jù)、算法以及設(shè)計(jì)思想;而對于惡意攻擊者來說,則可以獲取到軟件中漏洞,從而加以利用,造成嚴(yán)重后果。
靜態(tài)分析往往是逆向分析的第一步。以Java為代表的語言,需要編譯成與平臺無關(guān)的字節(jié)碼形式,所以可以通過解析字節(jié)碼文件輕易還原出源程序的實(shí)現(xiàn)信息。而對于以C語言為代表的需要編譯成機(jī)器碼的語言,也可以通過翻譯機(jī)器碼從而轉(zhuǎn)化為匯編指令來進(jìn)行代碼分析。所以,通過靜態(tài)分析可以輕易地構(gòu)建軟件的控制流信息,未經(jīng)保護(hù)的控制流信息暴露了包括源程序靜態(tài)結(jié)構(gòu)、代碼邏輯在內(nèi)的大量信息,因此對于許多逆向工具而言,控制流圖都是必須構(gòu)造的數(shù)據(jù)結(jié)構(gòu)。本文研究的主要目的是抵御靜態(tài)代碼分析。
抵抗靜態(tài)代碼分析特別是控制流分析的主要技術(shù)是代碼混淆,這個概念最早由Collerg等[3]提出并加以分類及實(shí)現(xiàn)。代碼混淆技術(shù)是將一些結(jié)構(gòu)良好、易于理解的代碼轉(zhuǎn)換為難以閱讀分析的代碼,但是還能完整地保留其所實(shí)現(xiàn)的功能,著名的國際C語言混亂代碼大賽[4]就是典型的例子。一般而言,混淆技術(shù)可以分為4類:結(jié)構(gòu)混淆、數(shù)據(jù)混淆、控制流混淆以及動態(tài)混淆。其中控制流混淆的主要目的就是抵御逆向工具對代碼控制流信息的分析,目前應(yīng)用比較廣泛的控制流混淆技術(shù)有指令替換、虛假控制流、控制流平坦化等。但是目前這些技術(shù)都存在一些缺點(diǎn),如通用性差、性能消耗過大、控制流信息隱藏不徹底等。
鑒于這些現(xiàn)狀,本文提出一種通用的、輕量級的、細(xì)粒度代碼保護(hù)方案。通過對代碼的控制流信息進(jìn)行分析并建立原始控制流圖,之后從基本塊跳轉(zhuǎn)、函數(shù)調(diào)用與變量引用入手,隱藏這些敏感信息,減少靜態(tài)分析時所能獲取到的有價(jià)值信息,實(shí)現(xiàn)對程序的保護(hù)。同時通過構(gòu)建狀態(tài)轉(zhuǎn)移模型和實(shí)施基于環(huán)境密鑰的兩階段加密方案,解決了密鑰安全和密文重復(fù)出現(xiàn)問題,彌補(bǔ)了其他方案的不足,提高了軟件保護(hù)的能力。
代碼混淆的概念由來已久,在20世紀(jì)90年代末這個概念就已經(jīng)被提出。它的形式有很多,其中最簡單的一種形式是符號混淆,它通過將源碼中的函數(shù)名、類名以及變量名替換成一些無意義的符號,來增加攻擊者的理解難度,具有代表性的項(xiàng)目是ProGuard[5],由于其并不改變原代碼的結(jié)構(gòu),其抗逆向能力非常差,所以現(xiàn)在多作為一種優(yōu)化工具或者其他混淆技術(shù)的輔助工具來使用。
由于簡單的符號混淆并不能滿足代碼保護(hù)的要求,一些研究人員又提出了結(jié)構(gòu)混淆的概念。結(jié)構(gòu)混淆是從代碼的類、函數(shù)等組成入手,破壞其整體結(jié)構(gòu),增加靜態(tài)分析的復(fù)雜度。這方面比較典型的是Foket等人[6]的研究成果,他們以Java語言為例闡述并實(shí)現(xiàn)了包括類繼承結(jié)構(gòu)平坦化、方法合并、接口合并以及對象創(chuàng)建工廠化在內(nèi)的多種結(jié)構(gòu)混淆技術(shù)。雖然結(jié)構(gòu)混淆能起到一定的抗逆向效果,但是其混淆的粒度較大,實(shí)際的逆向工程中,攻擊者很少會逆向全部代碼,一般都是從一部分關(guān)鍵代碼入手,也被稱為“局部攻擊”,這時就需要更細(xì)粒度的混淆。
控制流混淆是目前較為流行的方案,它通過修改程序控制流來阻礙攻擊者對代碼進(jìn)行靜態(tài)分析。常用的控制流混淆技術(shù)主要有同義指令替換、不透明分支以及控制流平坦化以及等。
同義指令替換[7]多是將一些簡單的指令替換為有相同功能但形式非常復(fù)雜的指令,來增大代碼理解難度,如公式(1)所示;但是這種混淆方法有很大的局限性:首先并不是所有的指令都可以替換,多用在算術(shù)運(yùn)算上;其次一般所替換的指令都比較復(fù)雜,也降低了程序的性能。
虛假控制流又稱為插入不透明謂詞[8],它通過在程序的控制流中插入一些結(jié)果恒定的布爾表達(dá)式來引入虛假分支,如圖1所示;由于被插入的布爾表達(dá)在運(yùn)行過程中值恒等不變,所以虛假分支并不影響程序運(yùn)行,所以這是一種低成本高收益的混淆技術(shù)。生成虛假控制流的不透明謂詞可以分為靜態(tài)不透明謂詞[9]、上下文不透明謂詞[10]以及動態(tài)不透明謂詞[11]等。但是由于不透明謂詞插入后的特征比較明顯,現(xiàn)在已有很多方案[12]可以去識別和去除。虛假控制流混淆雖然改變了代碼的控制流信息,但只是在局部上的修改,難以全局應(yīng)用,并且過多的不透明謂詞也會大幅降低程序性能。
圖1 虛假控制流Fig.1 Bogus control flow
控制流平坦化[13]混淆如圖2所示,是通過構(gòu)造分發(fā)器,將原本控制流重構(gòu)為一個類似switch-case的結(jié)構(gòu),由分發(fā)器在運(yùn)行時決定下一個將要執(zhí)行的基本塊。這樣可以徹底打亂原有的控制流圖,具有很強(qiáng)的反逆向能力,但是這種方法只是將控制流信息從形式上打亂,其實(shí)際的控制流信息還是不變,通過分析case的值,也可以局部或者整體還原代碼的控制流信息,重構(gòu)原始的控制流圖。
圖2 控制流平坦化Fig.2 Control flow flattening
前面介紹的控制流混淆方案大多都是通過引入額外基本塊來修改源程序控制流圖中基本塊之間的關(guān)系,從而實(shí)現(xiàn)代碼混淆。除此之外,還可以通過修改基本塊的跳轉(zhuǎn)形式來實(shí)現(xiàn)這個目的,也就是將顯示跳轉(zhuǎn)改為隱式跳轉(zhuǎn)。隱式跳轉(zhuǎn)又稱間接跳轉(zhuǎn),是將源程序中的直接跳轉(zhuǎn)形式改為間接形式,這樣無論是反編譯工具還是人為分析,都不能輕易分析任意基本塊的前后關(guān)系,這樣也就阻止了靜態(tài)分析,實(shí)現(xiàn)了程序保護(hù)。這方面比較典型的是Hikari[14]項(xiàng)目,它通過將程序中跳轉(zhuǎn)指令的目標(biāo)地址由明文改為數(shù)組匹配的方式來實(shí)現(xiàn)隱式跳轉(zhuǎn)。但作為一種混淆策略,其實(shí)現(xiàn)形式過于簡單而導(dǎo)致安全性較差(詳見2.1節(jié)分析),同時也僅處理了基本塊之間的跳轉(zhuǎn),對于其他敏感信息并未做相關(guān)的安全保護(hù),所以在實(shí)際應(yīng)用中價(jià)值較低。
前一章介紹的幾種代碼保護(hù)技術(shù)中,都只是在宏觀上對控制流進(jìn)行修改,實(shí)際各個基本塊之間的跳轉(zhuǎn)關(guān)系還會保留,而且也不能保護(hù)函數(shù)調(diào)用以及變量引用這些敏感信息。因此,本文通過隱藏基本塊之間的跳轉(zhuǎn)關(guān)系,并在此基礎(chǔ)上對程序其他敏感信息如函數(shù)的調(diào)用以及變量引用也加以隱藏,從而實(shí)現(xiàn)程序的控制流混淆,以此來保護(hù)程序。
控制流圖是一個程序的抽象表示,最早由Frances E.Allen提出,它利用圖的形式將一個程序的所有可能執(zhí)行路徑表示出來,不僅是靜態(tài)分析的重要工具,也是現(xiàn)代編譯器所需要維護(hù)的一種重要數(shù)據(jù)結(jié)構(gòu)。一個控制流圖可以表示為
圖3 控制流圖示例Fig.3 Example of control flow graph
在控制流圖中,控制流從一個基本塊到另外一個基本塊的轉(zhuǎn)移多是通過一系列跳轉(zhuǎn)指令實(shí)現(xiàn),所要跳轉(zhuǎn)的目標(biāo)地址在原始情況下是以明文方式作為指令的操作數(shù)出現(xiàn)的,這樣的跳轉(zhuǎn)形式稱之為顯示跳轉(zhuǎn),以圖3(b)為例,一共出現(xiàn)了兩個跳轉(zhuǎn)指令,分別是地址為040056B處的條件跳轉(zhuǎn)指令和地址為0400585處的無條件跳轉(zhuǎn)指令,這兩條指令的操作數(shù)都是直接的明文地址(圖中顯示的是LABEL偽指令,表示的是某個地址,目的是便于閱讀),由于是明文地址,無論從哪一個基本塊開始都可以輕松地推斷出程序接下來的流程。
與顯示跳轉(zhuǎn)對應(yīng)的是隱式跳轉(zhuǎn),這種形式下,跳轉(zhuǎn)的目的地址需要通過一系列計(jì)算得出,圖4所示的是圖3(a)中的代碼經(jīng)過Hikari的間接分支處理后的部分匯編代碼,對應(yīng)的就是圖3(c)中的基本塊A,但是與之不同的是圖4中的基本塊最后的跳轉(zhuǎn)指令的操作數(shù)并不是一個直接地址,而以一個寄存器表示,根據(jù)寄存器中存儲的值,去跳轉(zhuǎn)的相應(yīng)地址,至于寄存器中所存儲的地址則是在跳轉(zhuǎn)前計(jì)算出來的,這就是隱式跳轉(zhuǎn)的一種形式。這里所做的處理是將某個基本塊的所有可能后繼基本塊插入一個全局跳轉(zhuǎn)表中,然后修改原始基本塊的內(nèi)容,將原來的明文地址替換為數(shù)組尋址的方式,如圖4中地址0400575處所示。這種隱式跳轉(zhuǎn)方法,由于隱藏了目標(biāo)地址,所以一些工具如IDA Pro[15]就不能通過靜態(tài)分析去構(gòu)建程序的控制流圖,僅能顯示如圖4所示的控制流圖的第一個基本塊。
雖然這種形式的隱式跳轉(zhuǎn)雖然能在一定程度上阻礙控制流圖被構(gòu)建,但是若對跳轉(zhuǎn)前部分指令的含義進(jìn)行簡單分析還是可以推斷出該基本塊的后繼。以圖4為例,在輸入值與10做減法之后,根據(jù)EFLAGS寄存器的ZF標(biāo)志位的情況,在_LcondInBrTable數(shù)組中取不同的值存入rsi寄存器中進(jìn)行跳轉(zhuǎn),由此知道當(dāng)輸入等于10時,取數(shù)組中第二個元素,否則取第一個元素。而_LcondInBrTable數(shù)組中存儲的都是明文地址,由此還是可以構(gòu)建出局部或者全局的控制流圖。
前一節(jié)中Hikari實(shí)現(xiàn)了較為初級的隱式跳轉(zhuǎn),但是依舊是將地址明文存儲到全局跳轉(zhuǎn)表中,這樣的效果等同于做了一次稍微復(fù)雜的間接尋址,雖然可以欺騙一些靜態(tài)分析工具,但是并沒有達(dá)到理想的保護(hù)效果。本文通過對地址加密來增強(qiáng)保護(hù),但是由于對軟件的靜態(tài)分析是處于一種MATE[16](Man At The End)環(huán)境下,此時攻擊者完全掌握軟件運(yùn)行環(huán)境,可以對軟件進(jìn)行徹底的反編譯和監(jiān)控,傳統(tǒng)的加密并不能明顯提升保護(hù)效果,所以本文通過構(gòu)建狀態(tài)轉(zhuǎn)移模型來確保密鑰安全,利用二者的結(jié)合來實(shí)現(xiàn)代碼保護(hù)。
S:狀態(tài)集合;
S0:初始狀態(tài);
I:輸入集合;
O:輸出集合;
T:轉(zhuǎn)移函數(shù),描述系統(tǒng)中各個狀態(tài)之間轉(zhuǎn)移的規(guī)則;
G:輸出函數(shù),描述系統(tǒng)在狀態(tài)轉(zhuǎn)移后的輸出。
首先定義一個程序控制流圖由n個基本塊構(gòu)成:,控制流圖的入口為BB0。假設(shè)控制流內(nèi)部的跳轉(zhuǎn)是根據(jù)基本塊的地址定位的,第i個基本塊的地址表示為Ai,該地址對應(yīng)的加密后密文表示為,其對應(yīng)的密鑰表示為key i,密鑰是在編譯期隨機(jī)生成的,不直接存儲在代碼中。
系統(tǒng)的各部分定義如下:系統(tǒng)的狀態(tài)集合即為全體密鑰的集合S={}key i|0≤i≤n,當(dāng)程序要從BB i跳轉(zhuǎn)到BB j時,系統(tǒng)的當(dāng)前狀態(tài)即為k eyi,將來狀態(tài)為key j,系統(tǒng)的初始狀態(tài)為key0;系統(tǒng)某次狀態(tài)轉(zhuǎn)移時的輸入由兩部分組成,一部分用于計(jì)算新狀態(tài),定義為k ij,該值直接存儲在BB i中;另一部分用于計(jì)算輸出,即BB j的密文;故系統(tǒng)的輸入可以用如下二元組的集合表示,系統(tǒng)在轉(zhuǎn)移后的輸出即基本塊地址的集合,O={A j|0≤i≤n}。
系統(tǒng)的輸出函數(shù)描述的就是狀態(tài)轉(zhuǎn)移后系統(tǒng)產(chǎn)生的一個輸出,這里以輸入的密文以及當(dāng)前狀態(tài)為參數(shù)并輸出基本塊地址,所以輸出函數(shù)就是解密函數(shù),它和編譯時的加密函數(shù)互為反函數(shù)。若加密函數(shù)定義為如下形式:
則輸出函數(shù)的形式為:
關(guān)于加解密函數(shù)的具體實(shí)現(xiàn)沒有特殊要求,但是由于需要在指令層面進(jìn)行運(yùn)行時解密,為了保證運(yùn)行時性能,要盡量選擇運(yùn)算量少的算法,如異或運(yùn)算。
系統(tǒng)的轉(zhuǎn)移函數(shù)描述了狀態(tài)轉(zhuǎn)移規(guī)則,其形式如下:
轉(zhuǎn)移函數(shù)實(shí)際上是以迭代的形式描述了如何從一個狀態(tài)轉(zhuǎn)移到下一個狀態(tài),這種形式的定義可以保證在靜態(tài)分析時中間狀態(tài)的隱蔽性,從而間接保證目標(biāo)地址的安全。攻擊者在對程序進(jìn)行靜態(tài)分析構(gòu)建控制流圖時,由于完整的控制流圖一般都比較龐大,構(gòu)建起來耗時耗力,所以攻擊者一般更愿意從某個感興趣的基本塊入手,構(gòu)建局部的控制流圖,此時需要完成的目標(biāo)有如下兩點(diǎn):
(1)確定某個基本塊的所有直接后繼塊;
(2)確定某個基本塊的所有直接前驅(qū)塊。
在未處理之前,指令中所有目標(biāo)地址都是明文出現(xiàn)的,所以目標(biāo)1可以通過分析基本塊中的跳轉(zhuǎn)指令及相鄰基本塊的邏輯關(guān)系而實(shí)現(xiàn),目標(biāo)2可以通過匹配全局的絕對跳轉(zhuǎn)地址和相對跳轉(zhuǎn)地址來實(shí)現(xiàn)。在使用本文方案之后,聯(lián)合轉(zhuǎn)移函數(shù)與輸出函數(shù),若從BBi轉(zhuǎn)移到BB j,目標(biāo)基本塊地址需要經(jīng)過如下計(jì)算:
當(dāng)攻擊者從控制流圖中某一基本塊開始靜態(tài)分析時,所能獲取到的信息只有該基本塊的地址A i,以及所有可能的k ij和。由公式(5)可知,還缺少參數(shù)keyi,而keyi是系統(tǒng)的當(dāng)前狀態(tài),它在程序中的表現(xiàn)是一個可讀寫的全局變量,它隨著控制流的轉(zhuǎn)移而更新,而直接從中間某一基本塊入手時,是無法直接獲得當(dāng)前基本塊對應(yīng)狀態(tài)值,所以不能計(jì)算出下一基本塊的地址,這樣目標(biāo)1就很難完成。另外,由于在基本塊中,原始狀態(tài)下所有目標(biāo)地址都是以密文形式呈現(xiàn)的,當(dāng)攻擊者知道某一基本塊的明文地址,而要計(jì)算對應(yīng)的密文地址時需要進(jìn)行如下式的運(yùn)算(假設(shè)BB k是BBi的一個前驅(qū)塊):
根據(jù)系統(tǒng)的狀態(tài)轉(zhuǎn)移模型,key k是在執(zhí)行到基本塊BB k時的狀態(tài),k ki的值也僅存在于BB k內(nèi)部的,所以在前驅(qū)塊未知的情況下,想要計(jì)算當(dāng)前基本塊對應(yīng)的密文信息,以及以此為基礎(chǔ)去尋找所有前驅(qū)塊是比較困難的,所以目標(biāo)2也難以實(shí)現(xiàn)。
前文分析了模型在抵御靜態(tài)分析時的能力,證明了當(dāng)從控制流圖的非起始位置入手時,并不能有效地還原出程序的控制流圖。但是,當(dāng)從控制流入口開始靜態(tài)分析時,系統(tǒng)狀態(tài)能否安全地初始化決定了程序控制流圖能否被順利構(gòu)建。目前,程序中類似的安全初始化問題依舊是亟待解決的,不過一些現(xiàn)有的方法可以在一定程度上解決這個問題,如白盒加密、外部輸入以及從環(huán)境元素生成狀態(tài)信息等。即使在無法保證安全初始化的情況下,在經(jīng)過本方案處理過之后,攻擊者也只能從控制流圖的入口開始分析,當(dāng)程序足夠龐大時,這將是一個漫長的過程。另外本方案關(guān)注于隱藏控制流的跳轉(zhuǎn)信息,所以可以通過增加冗余的虛假基本塊、對基本塊進(jìn)行分裂或者結(jié)合已有的控制流混淆技術(shù)來增大靜態(tài)分析的難度。
除了控制流信息是抵抗靜態(tài)分析時需要保護(hù)的信息,函數(shù)的調(diào)用與變量的引用也是一類需要保護(hù)敏感信息。例如通過對關(guān)鍵函數(shù)的調(diào)用及特殊字符串的搜索可以快速定位到關(guān)鍵部分,提高靜態(tài)分析的效率。傳統(tǒng)的關(guān)于這兩類信息的保護(hù)都注重于內(nèi)容的保護(hù),如對函數(shù)進(jìn)行包裝、對字符串內(nèi)容加密等。這樣的處理不僅有較大性能損耗,同時也并不能達(dá)到理想的效果。本文在隱式跳轉(zhuǎn)的基礎(chǔ)上從對象與指令間的關(guān)系入手對這兩類信息進(jìn)行保護(hù)。
與基本塊的跳轉(zhuǎn)類似,在指令層面函數(shù)的調(diào)用與變量的引用都是以地址為基礎(chǔ)。如圖3(b)中的幾處call指令和包括0400571地址在內(nèi)的幾處變量引用指令,都是把目標(biāo)對象的地址直接作為操作數(shù)放在指令后。所以可以推廣隱式跳轉(zhuǎn)的思想,將對應(yīng)的地址加密隱藏,實(shí)現(xiàn)函數(shù)的隱式調(diào)用和變量的隱式引用。同樣在上一節(jié)構(gòu)建的狀態(tài)轉(zhuǎn)移系統(tǒng)的基礎(chǔ)上,依賴系統(tǒng)當(dāng)前狀態(tài)與輸入?yún)?shù)在運(yùn)行時解密出真實(shí)地址,而不直接存儲密鑰,使得靜態(tài)分析時無法確定真實(shí)地址,以此切斷相關(guān)對象和程序的關(guān)聯(lián),也就無法利用特殊的函數(shù)和變量去定位關(guān)鍵邏輯。另外,也可以插入相似的冗余函數(shù)和變量,進(jìn)一步干擾攻擊者的靜態(tài)分析。
前文介紹的模型雖能較好在靜態(tài)分析時保護(hù)程序各種敏感信息,但是各種加密行為都是在編譯期完成的,所以在后續(xù)的指令修改中,同樣的密文可能多次出現(xiàn),這樣做是有一定風(fēng)險(xiǎn)的。如在基本塊的隱式跳轉(zhuǎn)中,若BB j是BB i的直接后繼,雖然在BB i中無法直接計(jì)算出BB j的實(shí)際地址,但是將會出現(xiàn)在所有BB j的前驅(qū)塊中,此時通過搜索可以找到BB j的所有前驅(qū)節(jié)點(diǎn)。同樣的問題也出現(xiàn)在函數(shù)隱式調(diào)用與全局變量隱式引用的處理中,利用對密文的全局搜索可以找到調(diào)用相同函數(shù)或引用相同變量的所有指令,再結(jié)合一些統(tǒng)計(jì)學(xué)的知識,還是可以獲取一些有價(jià)值信息。
為了解決這個問題,這里引入環(huán)境密鑰與兩階段加密的概念,基本思路是在不同環(huán)境中(即不同的基本塊中)表示出不同的密文。以基本塊隱式跳轉(zhuǎn)為例,除了使用主密鑰keyi,還需要一個環(huán)境密鑰ckeyi,每個基本塊對應(yīng)一個環(huán)境密鑰,為了保證唯一性,比較簡單的就是取基本塊的地址作為環(huán)境密鑰。在編譯期進(jìn)行加密時使用兩個加密函數(shù)進(jìn)行兩階段加密,分別是使用環(huán)境密鑰進(jìn)行的預(yù)加密和使用主密鑰進(jìn)行的正式加密,這里定義預(yù)加密函數(shù)如下:
預(yù)加密函數(shù)需要保證在給出相同的A i,對于任意兩個ckey j,其結(jié)果一定不相同。此時結(jié)合2.2節(jié)中定義的加密函數(shù),完整的加密運(yùn)算如下所示(表示BBi的地址在BB j環(huán)境下所對應(yīng)的密文):
注意這里選取的預(yù)加密函數(shù)P可以和F一樣,但要保證函數(shù)對解密順序敏感,也就是解密時若先用環(huán)境密鑰再用主密鑰時不能獲得正確結(jié)果,以防靜態(tài)分析時可以對密文進(jìn)行預(yù)處理。在公式(8)中對于同一明文A j,對應(yīng)不同的ckey j則有不同的結(jié)果,也就實(shí)現(xiàn)同一個基本塊在不同的環(huán)境下展示不同的密文。
同樣對于函數(shù)的隱式調(diào)用和全局變量的隱式引用,也可以使用環(huán)境密鑰和兩階段解密實(shí)現(xiàn)密文不重復(fù)出現(xiàn)。但是對于這兩者,還存在一種特殊情況,也就是在同一個基本塊內(nèi)多次調(diào)用同一個函數(shù)或引用同一個全局變量,這樣仍然會出現(xiàn)密文重復(fù),有效的解決方法是對基本塊進(jìn)行分裂,在兩個相同的函數(shù)調(diào)用或者變量引用之間插入無條件跳轉(zhuǎn)指令,將一個基本塊分裂為多個基本塊,保證兩次相同的調(diào)用或引用不出現(xiàn)在一個基本塊內(nèi),再進(jìn)行隱式轉(zhuǎn)換,就可以保證密文的唯一。
利用環(huán)境密鑰的兩階段加密沒有必要對全部對象都使用,在使用前可以統(tǒng)計(jì)對象在源程序中的出現(xiàn)次數(shù),若僅出現(xiàn)一次,就可以省去兩階段加密以減少性能損耗。
為了評估本文所提出的方案,這里選取了一些樣本進(jìn)行測試,并與原始程序和其他已有方案進(jìn)行比較從而得出結(jié)論,本文的方案實(shí)現(xiàn)是基于OLLVM[17]實(shí)現(xiàn)的。
本文從編譯后程序的空間開銷和時間開銷兩個維度進(jìn)行測試。這兩個維度使用不同的樣本集,分別是GNU Coreutils[18]內(nèi)的部分模塊以及排序、散列、加密以及壓縮等領(lǐng)域中有代表性的程序,樣本源碼語言都為C語言。另外關(guān)于方案的正確性與通用性可以通過觀察樣本是否能正確編譯以及正確執(zhí)行來判斷。具體樣本信息如表1所示。
表1 樣本詳情Table 1 Sample details
在實(shí)驗(yàn)中分別對每個樣本都進(jìn)行如表2所述的幾組實(shí)驗(yàn)。
表2 實(shí)驗(yàn)內(nèi)容Table 2 Experiment details
其中實(shí)驗(yàn)B、C因?yàn)榭梢詫?shí)現(xiàn)類似效果,所以有較直觀的對比。本次實(shí)驗(yàn)的環(huán)境如表3所示。
表3 實(shí)驗(yàn)環(huán)境Table 3 Experimental environment
按表2描述的四種處理方法對10個樣本進(jìn)行編譯,首先各個樣本均能通過編譯并正常運(yùn)行,這一點(diǎn)說明本文方案對一般源代碼程序的處理具有較好的通用性和正確性,具體編譯后可執(zhí)行文件的大小如表4所示。
表4 可執(zhí)行文件大小Table 4 Executable file size Byte
通過表4可以看出,任何一種處理手段都會伴隨出現(xiàn)文件體積增大現(xiàn)象。其中,僅做控制流隱式跳轉(zhuǎn)處理的對照組中,文件體積平均增大29%左右,在此基礎(chǔ)上若對函數(shù)調(diào)用與變量引用也做處理,文件體積再增加15%左右。
通過分析編譯后程序的匯編指令,可以得出文件增大的原因。以實(shí)驗(yàn)B為例,若要實(shí)現(xiàn)一個簡單的無條件跳轉(zhuǎn)指令從顯式到隱式的轉(zhuǎn)換,需要在基本塊中添加:當(dāng)前系統(tǒng)狀態(tài)讀取、輔助變量讀取與密鑰計(jì)算、密文讀取與解密、系統(tǒng)狀態(tài)更新以及跳轉(zhuǎn)指令替換等操作,在實(shí)際轉(zhuǎn)換中,這些操作將在原基本塊中增加約10條指令。另外,對于有條件跳轉(zhuǎn)指令及函數(shù)調(diào)用指令等一些較為復(fù)雜的指令,在實(shí)際轉(zhuǎn)換將會增加更多指令。其次,隨著基本塊數(shù)量的增多,控制流圖中邊的數(shù)量也越多,要轉(zhuǎn)換的指令也越多,所以編譯后文件體積會有一定的增加。
雖然本文方案最后文件體積會有一定增加,但相比控制流平坦化而言,在達(dá)到類似效果的基礎(chǔ)上,實(shí)際實(shí)驗(yàn)中二者文件的體積上并沒有明顯的差異。而且原始的控制流平坦化方案只是在形式上打亂了控制流,若要更加安全地保護(hù)控制流信息則需要對分發(fā)器的邏輯復(fù)雜化,這樣只會進(jìn)一步增加文件體積。
實(shí)驗(yàn)的第二項(xiàng)內(nèi)容是測試經(jīng)過處理后程序的性能對比,關(guān)于性能可以通過程序運(yùn)行時間得到直觀的表現(xiàn)。這里對后四種樣本進(jìn)行測試,其中每組實(shí)驗(yàn)之后得到的二進(jìn)制文件都執(zhí)行如表5所示的一定操作并記錄執(zhí)行時間,重復(fù)執(zhí)行100次,最后取平均時間。
表5 操作詳情Table 5 Operation details
各個樣本的最終運(yùn)行時間如圖5所示。
圖5 運(yùn)行時間Fig.5 Execution time
通過圖5可知,在經(jīng)過控制流扁平化處理之后的程序耗時是最多的,本文所提供的方案中,若只進(jìn)行控制流隱式跳轉(zhuǎn),則平均增加了44%的耗時,若全部處理,則再增加約15%的耗時,雖然最后運(yùn)行時間相對原始程序都有一定增加,但是相對于控制流平坦化,執(zhí)行耗時上平均減少約23%,有較大的提升。
通過前面的分析可知由于增加了額外指令,所以在運(yùn)行時不可避免地增加了運(yùn)行時間。雖然本文所提方案與控制流平坦化方案都是通過增加額外指令實(shí)現(xiàn)控制流混淆,但是本文所增加的額外指令都直接插入到原始的基本塊中,而且與原基本塊內(nèi)的指令在地址上是相鄰的,同時大多數(shù)是一些簡單的數(shù)據(jù)存儲指令。而控制流平坦化方案中由于匯編指令無法直接實(shí)現(xiàn)類似switch結(jié)構(gòu)的分發(fā)器,所以只能通過增加大量子分發(fā)器將控制流圖轉(zhuǎn)為一個多層的樹型結(jié)構(gòu),這樣控制流從主分發(fā)器出發(fā),要通過大量子分發(fā)器經(jīng)過層層比較與跳轉(zhuǎn)才能到達(dá)源程序的有效邏輯塊,另外這些子分發(fā)器在地址上多數(shù)都是不相鄰的,也會造成一定影響。所以相比較來看控制流平坦化方案比本文的方案會造成更大的影響。
通過上面兩方面的實(shí)驗(yàn)可以得出,不管何種保護(hù)方法都是有一定負(fù)面效應(yīng)的,不過這也是不可避免的,由于在實(shí)驗(yàn)中默認(rèn)對全部代碼都進(jìn)行保護(hù)處理,所以影響比較明顯,實(shí)際過程中可以有選擇性地對關(guān)鍵代碼進(jìn)行保護(hù)處理,從而降低對性能的影響。
本文提出并實(shí)現(xiàn)了一種采用隱式跳轉(zhuǎn)的控制流混淆方案,它不僅能隱藏程序的控制流信息,同時也能保護(hù)包括函數(shù)調(diào)用及變量引用在內(nèi)的多種敏感信息,增強(qiáng)程序的抗靜態(tài)分析能力。由于是建立在抽象的控制流圖基礎(chǔ)上,所以該方案不受具體編程語言及代碼結(jié)構(gòu)的限制,也不需要對源碼進(jìn)行任何修改,是一種通用的控制流混淆方案。并且還能結(jié)合已有的混淆方案保護(hù)實(shí)現(xiàn)更好的保護(hù)效果。未來的研究工作將從如下兩點(diǎn)進(jìn)行:首先是優(yōu)化狀態(tài)轉(zhuǎn)移系統(tǒng)進(jìn)一步減小編譯后文件體積的大小;其次是優(yōu)化轉(zhuǎn)移函數(shù)和輸出函數(shù),使性能損耗降到最低。