江秋語(yǔ),殷芙萍
(1.四川大學(xué)計(jì)算機(jī)學(xué)院,成都610065;2.上海理工大學(xué)管理學(xué)院,上海200082)
Android 平臺(tái)的開(kāi)源特性使它在全球智能手機(jī)市場(chǎng)占據(jù)主導(dǎo)地位[1]。然而,近90%的Android 應(yīng)用程序中存在重打包的現(xiàn)象,嚴(yán)重侵犯了開(kāi)發(fā)者的知識(shí)產(chǎn)權(quán)。由于Java 類(lèi)文件和DEX 文件保留了大部分的語(yǔ)義信息,攻擊者使用逆向工具可以輕而易舉地進(jìn)行破解以獲取程序的源代碼。
為解決此問(wèn)題,常用混淆技術(shù)對(duì)Android 應(yīng)用程序進(jìn)行加固。代碼混淆技術(shù)是一種代碼轉(zhuǎn)換機(jī)制,通過(guò)將易讀的代碼或數(shù)據(jù)進(jìn)行保留語(yǔ)義的轉(zhuǎn)換、重組和整理來(lái)加大代碼分析難度,達(dá)到保護(hù)軟件安全的目的。但是對(duì)Java 代碼進(jìn)行混淆會(huì)大大降低程序的性能,為了解決這個(gè)問(wèn)題,本文提出了一種基于LLVM 的Android應(yīng)用程序的混淆方法,利用LLVM 將DEX 文件中的Java 函數(shù)轉(zhuǎn)化為本地代碼,相比于Java 代碼,本地代碼具有更少的語(yǔ)義信息和更快的加載速度,能有效增加攻擊者分析的難度,減少混淆帶來(lái)的性能損失。
近年來(lái),由于本地代碼具有更少的語(yǔ)義信息和更快的加載速度的特點(diǎn),Android 應(yīng)用保護(hù)的研究方向也逐漸轉(zhuǎn)向本地代碼的保護(hù),研究人員開(kāi)始結(jié)合代碼混淆技術(shù)和虛擬化保護(hù)方法來(lái)保護(hù)Android 應(yīng)用中的本地代碼。
Vivek Balachandran 等人[2]提出一種針對(duì)Java 層代碼的混淆保護(hù)方法,利用切片技術(shù)將函數(shù)劃分為多個(gè)代碼片段,通過(guò)pack 和switch 指令將代碼片段的控制流平展,同時(shí)插入垃圾代碼擴(kuò)展分支語(yǔ)句來(lái)增強(qiáng)其控制流復(fù)雜程度,為了防止符號(hào)執(zhí)行方法恢復(fù)混淆后的控制流,使用try 和catch 指令來(lái)隱藏控制流的跳轉(zhuǎn)。但僅僅增加控制流的復(fù)雜程度不能很好地增加攻擊者分析的難度,而且對(duì)APK 內(nèi)所有基本塊進(jìn)行切片并進(jìn)行混淆會(huì)導(dǎo)致很大的性能開(kāi)銷(xiāo),因此只能選擇部分關(guān)鍵函數(shù)進(jìn)行保護(hù)。
Kyeonghwan Lim 等人[3]進(jìn)一步提出針對(duì)Android 本地代碼的保護(hù)方法,通過(guò)將Android 字節(jié)碼以更安全和高效的形式保存在本地代碼中并使用OLLVM 來(lái)對(duì)其進(jìn)行混淆,以增強(qiáng)代碼最終的安全性,但是近年來(lái)有很多針對(duì)OLLVM 的反混淆研究,僅僅使用OLLVM 進(jìn)行混淆保護(hù)不能很好的增加攻擊者分析的難度,因此需要增加混淆方式的多樣性和復(fù)雜性以便更好的保護(hù)Android 應(yīng)用程序。
趙貝貝等人[4]提出了針對(duì)DEX 文件和SO 文件的多重虛擬化保護(hù)方法,通過(guò)將DEX 文件中的部分函數(shù)轉(zhuǎn)化為本地層函數(shù),然后分別對(duì)其和SO 文件進(jìn)行指令的虛擬化,大大增加的逆向分析的難度。由于虛擬化保護(hù)技術(shù)需要將被保護(hù)代碼全部轉(zhuǎn)化為自定義的虛擬指令,然后在運(yùn)行的時(shí)候根據(jù)自定義的解釋器逐條翻譯虛擬指令,這樣會(huì)大大降低Android 應(yīng)用程序的執(zhí)行效率。
綜上所述,現(xiàn)有的Android 應(yīng)用程序保護(hù)方法能夠在一定程度上提高程序的安全性,但是仍然存在性能開(kāi)銷(xiāo)過(guò)大和無(wú)法有效抵御動(dòng)態(tài)攻擊的問(wèn)題。
圖1 描述了本文所提Android 應(yīng)用程序加固方法的五個(gè)步驟:首先是關(guān)鍵函數(shù)的提取,通過(guò)一個(gè)決策模型選擇被保護(hù)函數(shù),將其反匯編為Smali 代碼;再對(duì)Smali 代碼進(jìn)行解析,將其轉(zhuǎn)化為同語(yǔ)義的C++代碼;接著由LLVM[5]前端Clang 解析、驗(yàn)證和診斷輸入的C++代碼;然后通過(guò)編寫(xiě)LLVM 中端的優(yōu)化模塊完成對(duì)LLVMIR 的優(yōu)化、轉(zhuǎn)換、混淆[6]等一系列自定義操作,將優(yōu)化后的指令轉(zhuǎn)化為本地代碼;最后將修改后的DEX文件和LLVM 后端生成的本地代碼重新打包為一個(gè)新的APK 文件。下面介紹一些關(guān)鍵技術(shù)的具體實(shí)現(xiàn),如選擇關(guān)鍵函數(shù)、關(guān)鍵函數(shù)的轉(zhuǎn)化、控制流混淆、不透明謂詞混淆、整型數(shù)和字符串混淆。
圖1 本文所提加固方法的主要流程
對(duì)所有Java 層函數(shù)進(jìn)行加固會(huì)造成巨大開(kāi)銷(xiāo)[7],部分函數(shù)的程序框架,如窗口操作和界面渲染,已經(jīng)被業(yè)內(nèi)人士所熟知。相比于核心加密和通信邏輯的函數(shù),這些函數(shù)本身調(diào)用了許多其他函數(shù),無(wú)需浪費(fèi)額外的資源對(duì)其進(jìn)行加固,因此本文構(gòu)建一個(gè)決策模型識(shí)別DEX 文件中的關(guān)鍵函數(shù):
首先深度優(yōu)先遍歷反匯編后的Smali 文件,生成每個(gè)函數(shù)的函數(shù)調(diào)用樹(shù);其次選擇樹(shù)中的葉子節(jié)點(diǎn),也就是不存在函數(shù)調(diào)用的函數(shù),將函數(shù)名添加到被保護(hù)函數(shù)列表中,修改函數(shù)屬性為Native 類(lèi)型;最后刪除原函數(shù)體并添加無(wú)用的垃圾指令來(lái)影響攻擊者逆向分析。
圖2 函數(shù)F調(diào)用樹(shù)
將關(guān)鍵函數(shù)轉(zhuǎn)化為C++代碼通過(guò)指令逐條翻譯和程序語(yǔ)義恢復(fù)實(shí)現(xiàn)。
指令逐條翻譯:圖3 是一個(gè)函數(shù)反匯編生成的Smali 代碼,Smali 是基于寄存器的指令,由于同一個(gè)寄存器一般儲(chǔ)存多個(gè)不同類(lèi)型的變量,因此在翻譯每一條指令時(shí)要對(duì)寄存器里的值進(jìn)行區(qū)分。將Smali 指令逐條翻譯后如圖4 所示:為每一個(gè)寄存器操作分配一個(gè)新的臨時(shí)變量來(lái)翻譯指令,當(dāng)該寄存器進(jìn)行賦值操作后,用一個(gè)新的臨時(shí)變量替代之前的臨時(shí)變量。
圖3 Smali 指令
圖4 C++代碼
程序語(yǔ)義恢復(fù):構(gòu)建一個(gè)以每一條指令作為一個(gè)節(jié)點(diǎn)的指令圖,深度優(yōu)先搜索圖中的每一個(gè)節(jié)點(diǎn),根據(jù)每個(gè)節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)和后繼節(jié)點(diǎn)以及節(jié)點(diǎn)的出度和入度,刪除不必要的變量并增加新的變量關(guān)聯(lián),恢復(fù)程序正常的邏輯關(guān)系和上下文。
圖5 C++代碼的指令圖
圖6 優(yōu)化后的C++代碼
控制流圖[8]是程序的抽象表現(xiàn),圖中每一個(gè)節(jié)點(diǎn)代表一個(gè)基本塊??刂屏髌教够瘜⒊绦虻目刂屏鲌D轉(zhuǎn)化為一個(gè)有很多分支的switch 結(jié)構(gòu),通過(guò)替換程序中的循環(huán)判斷條件和計(jì)數(shù)器,將程序的跳轉(zhuǎn)過(guò)程集中在switch 的判斷語(yǔ)句。一個(gè)函數(shù)的原始控制流圖,包含一些變量的賦值和一個(gè)循環(huán),如圖7 所示。圖8 是對(duì)其進(jìn)行控制流平坦化后的控制流圖,通過(guò)變量V 來(lái)控制程序的執(zhí)行,每執(zhí)行完一個(gè)基本塊后,對(duì)V 進(jìn)行賦值操作來(lái)決定下一個(gè)要執(zhí)行的基本塊。
圖7 函數(shù)的原始控制流圖
實(shí)現(xiàn)流程如下面的偽代碼所示:遍歷函數(shù)中所有的基本塊,選擇第一個(gè)基本塊作為函數(shù)的入口,若是有條件的基本塊,則將其轉(zhuǎn)為一個(gè)無(wú)條件的基本塊和一個(gè)有條件的基本塊,并將無(wú)條件的基本塊作為程序的入口;為每一個(gè)基本塊分配一個(gè)唯一的編號(hào),并修改其結(jié)尾的跳轉(zhuǎn)指令,若當(dāng)前基本塊為非條件跳轉(zhuǎn),將變量V 的值賦值為要跳轉(zhuǎn)的基本塊的編號(hào);若當(dāng)前基本塊為條件跳轉(zhuǎn),則需要修改為兩個(gè)條件賦值指令。最后,將基本塊插入到switch 結(jié)構(gòu)中。
圖8 控制流平坦化后的控制流圖
圖9 偽代碼
不透明謂詞[9]是一種布爾表達(dá)式,其結(jié)果為真或者為假。攻擊者只有在程序執(zhí)行到一定階段才能利用逆向工具對(duì)它進(jìn)行分析,因此不透明謂詞混淆可以抵御大部分靜態(tài)攻擊,本文選擇數(shù)論的結(jié)論來(lái)生成不透明謂詞。
實(shí)現(xiàn)過(guò)程如下:對(duì)基本塊進(jìn)行拆分,直到每個(gè)基本塊都無(wú)法再被拆分,向前驅(qū)基本塊插入一個(gè)無(wú)條件跳轉(zhuǎn)指令指向其后繼基本塊,以保證程序的正確運(yùn)行;生成一組不透明謂詞(pi,i=1,2,…,n,其中任意一個(gè)pi 的值為真時(shí),其他所有不透明謂詞的值都為真)將其插入到拆分后的基本塊中,如圖10 所示。
圖10 不透明謂詞混淆
整型數(shù)替換相比于加密優(yōu)勢(shì)在于,加密后的數(shù)據(jù)在內(nèi)存中運(yùn)行時(shí)解密,很容易被攻擊者發(fā)現(xiàn)并利用,而混淆后的數(shù)據(jù)在內(nèi)存中保持原有的形式,攻擊者難以理解。整型數(shù)替換的原理是將一個(gè)整型數(shù)拆分為高位和低位長(zhǎng)度相同的兩部分,分別儲(chǔ)存在寄存器中并對(duì)整型數(shù)的基本運(yùn)算進(jìn)行替換。例如,可以將一個(gè)長(zhǎng)為2N 的整型數(shù)A 拆分為長(zhǎng)度均為N 的Ah(高位)和Al(低位)。
對(duì)于加法運(yùn)算A=B+C,
Al=(Bl+Cl)mod(1< 對(duì)于減法運(yùn)算A=B-C, Al=(Bl-Cl)mod(1< 對(duì)于比較運(yùn)算,只需要先比較高位,若不相等則不需要再比較低位。 對(duì)于位運(yùn)算,只需要同時(shí)對(duì)高位或低位進(jìn)行相同的位運(yùn)算操作即可。 對(duì)于移位運(yùn)算A=B>>x, 其中0 對(duì)于除法和乘法運(yùn)算,LLVM 在編譯器優(yōu)化過(guò)程會(huì)對(duì)乘除法指令進(jìn)行替換,轉(zhuǎn)化為由移位和加法等指令等價(jià)表示的形式,由于乘法和除法的拆分會(huì)造成很大的開(kāi)銷(xiāo),因此不對(duì)存在乘法和除法的代碼進(jìn)行整型數(shù)的拆分。 LLVMIR[10]使用了一個(gè)由%字符命名的無(wú)限寄存器集合,而不是一個(gè)固定的命名寄存器集合。整型變量的表示一般以i 為起始標(biāo)志,i32 表示32 位整型數(shù),all?oca i32 指令用于分配32 位長(zhǎng)度的整型數(shù)變量。例如加法運(yùn)算A=B+C,%1、%2、%3 分別用于儲(chǔ)存A、B、C的值,然后將B 和C 的值讀取出來(lái),相加后儲(chǔ)存到A中;其進(jìn)行整型數(shù)拆分的結(jié)果如圖11 所示,分別使用兩個(gè)寄存器儲(chǔ)存A、B、C 的值,首先是低位相加,再對(duì)65536 取模,完成低位相加,然后是高位相加的結(jié)果和低位和右移16 位的結(jié)果相加,最后對(duì)65536 取模完成高位相加。 圖11 混淆前的LLVMIR 圖12 混淆后的LLVMIR 代碼中hashcode 的字符串可以在生成的binary 中查找到,因此增加破解的難度對(duì)字符串進(jìn)行混淆很重要。常用方法是在編譯過(guò)程中使用密鑰對(duì)原字符串異或加密,然后在函數(shù)頭部插入解密代碼在運(yùn)行時(shí)進(jìn)行字符串解密[11],文件加載后,字符串在內(nèi)存中仍是解密后的形式,因此,需要對(duì)字符串的解密進(jìn)行隱藏。 實(shí)現(xiàn)過(guò)程如下:首先生成隨機(jī)密鑰對(duì)字符串異或加密,在函數(shù)要使用加密的字符串時(shí)調(diào)用解密函數(shù)。解密函數(shù)會(huì)首先申請(qǐng)一個(gè)與原字符串等長(zhǎng)的??臻g,用于保存解密后的字符串,并修改原字符串的引用地址到當(dāng)前棧的地址;字符串使用后會(huì)釋放棧的空間,解密后的字符串不會(huì)一直存在內(nèi)存中,能一定程度上抵抗動(dòng)態(tài)攻擊。 從手動(dòng)分析、通用脫殼工具攻擊和運(yùn)行性能三個(gè) 方面進(jìn)行測(cè)試。編譯時(shí)系統(tǒng)使用Ubuntu 16.04 和LLVM 5.0 版本;測(cè)試端使用Android 5.0。 本部分主要以攻擊者的角度,使用一些逆向工具來(lái)分析加固后的代碼。首先,使用JEB[12]的工具反編譯Android 應(yīng)用程序。如圖13 所示,可以看到未加固代碼的邏輯是清晰可見(jiàn),而加固后的代碼在Java 層中只有方法的聲明。攻擊者再進(jìn)一步的分析,結(jié)合Java 文件中的函數(shù)聲明和從IDA[13]輸出的偽代碼,可以快速找到函數(shù)的實(shí)現(xiàn),但是經(jīng)過(guò)字符串和整型變量混淆以后,它們都沒(méi)有完整的語(yǔ)義,導(dǎo)致攻擊者很難理解。所以,攻擊者想要獲取應(yīng)用程序的原始邏輯需要大量的時(shí)間和精力。 圖13 原始代碼反編譯結(jié)果 圖14 加固后代碼反編譯結(jié)果 本部分主要選擇了六個(gè)常見(jiàn)的Android 脫殼工具,并使用它們來(lái)分析加固后的應(yīng)用程序。DEXExtractor[14]原理是修改系統(tǒng)DVM 虛擬機(jī)模塊代碼DEXFile.cpp文件的DEXFileParse 函數(shù),在系統(tǒng)調(diào)用DEXFileParse函數(shù)之前將原始DEX 文件從內(nèi)存dump 出來(lái)。但是本文的方法已經(jīng)將DEX 文件中的關(guān)鍵函數(shù)本地化,即使在內(nèi)存中dump 出DEX 文件依然得不到函數(shù)的實(shí)現(xiàn)方式。Drizzledumper[15]原理是在root 環(huán)境下,通過(guò)ptrace附加需要脫殼的APK 進(jìn)程,然后在脫殼的APK 進(jìn)程的內(nèi)存中進(jìn)行DEX 文件頭的特征搜索,當(dāng)搜索到DEX文件時(shí)進(jìn)行DEX 文件的內(nèi)存dump。由于函數(shù)經(jīng)過(guò)本地化保護(hù)后在內(nèi)存中找不到DEX 頭文件,因此這個(gè)工具對(duì)于本方法是無(wú)效的。ZjDroid[16]基于Xposed 框架,通過(guò)hook 每個(gè)應(yīng)用進(jìn)程在Java 級(jí)別獲取DEX 文件并利用獲得的mcookie dump 出DEX 文件,因此它無(wú)法在本地層還原代碼。DEXHunter[17]原理是在Android 系統(tǒng)代碼調(diào)用函數(shù)dvmDefineClass 進(jìn)行類(lèi)加載之前,主動(dòng)地一次性加載并初始化DEX 文件所有的類(lèi)。但它不能處理帶有代碼混淆和垃圾指令的加殼程序。Packer?Grind[18]從運(yùn)行時(shí)跟蹤、系統(tǒng)跟蹤和指令跟蹤對(duì)加殼的App 進(jìn)行監(jiān)控和分析,最后進(jìn)行還原。本文對(duì)代碼的控制流和數(shù)據(jù)都進(jìn)行了混淆,不透明謂詞產(chǎn)生的虛假控制流使它無(wú)法決定哪個(gè)代碼是正確的。DROIDUN?PACK 可以對(duì)JNI 接口進(jìn)行檢查,進(jìn)而知道哪些函數(shù)已經(jīng)本地化,本文的方法對(duì)數(shù)據(jù)進(jìn)行了混淆,使得它很難將其還原為正確的指令執(zhí)行。 結(jié)果如表1 所示,DROIDUNPACK 是目前最強(qiáng)大的脫殼工具,但是對(duì)本文提出的方法無(wú)效。這是因?yàn)楸疚牡姆椒▽EX 字節(jié)碼編譯時(shí)混淆成本機(jī)代碼,在運(yùn)行代碼時(shí)動(dòng)態(tài)還原并在使用后再次加密。實(shí)驗(yàn)結(jié)果表明,本文提出的加固方法克服了傳統(tǒng)加固方法容易被還原的缺點(diǎn),對(duì)通用逆向工具有良好的抵抗能力。 表1 脫殼工具以及對(duì)應(yīng)的脫殼結(jié)果 本部分從CPU 占用率、大小和運(yùn)行時(shí)內(nèi)存使用率三個(gè)方面來(lái)評(píng)估加固前后程序的運(yùn)行性能。選擇了六個(gè)常見(jiàn)的Android 應(yīng)用,每一個(gè)應(yīng)用程序都要運(yùn)行10次,取其結(jié)果的平均值作為最后的實(shí)驗(yàn)結(jié)果。在測(cè)試過(guò)程中,通過(guò)Monkey[19]觸發(fā)隨機(jī)點(diǎn)擊、幻燈片、文本或字符輸入模擬用戶(hù)的行為,隨機(jī)事件設(shè)置間隔為1s,每次數(shù)據(jù)收集的時(shí)間為10 分鐘。最后使用騰訊的開(kāi)源性能測(cè)試工具GT[20]獲得了相應(yīng)的實(shí)驗(yàn)數(shù)據(jù),如表2所示。 CPU 使用率:可以看出,加固后程序的CPU 使用率與原應(yīng)用程序幾乎相同。 大小:從表中可以看出,所有加固后的應(yīng)用程序比原應(yīng)用程序約增加20%,原因是加固后的應(yīng)用程序包含一個(gè)修改過(guò)的DEX 文件和一個(gè)新生成的SO 文件。雖然DEX 文件中的一些函數(shù)是在本機(jī)層中實(shí)現(xiàn)的,但它在原函數(shù)的指令中添加了JNI 注冊(cè)信息以及一些垃圾指令,因此它比原函數(shù)要占用更大的內(nèi)存空間,并且在一定程度上增加了攻擊者逆向分析的難度。 內(nèi)存使用:加固后,程序總內(nèi)存使用量呈上升趨勢(shì),但增幅不大,增加的內(nèi)存消耗主要來(lái)自本機(jī)代碼的消耗。 綜上所述,由于本機(jī)層代碼直接執(zhí)行效率更高、更直觀的CPU 指令,使得函數(shù)的本地化提高了程序的運(yùn)行效率、抵消了部分混淆帶來(lái)的性能開(kāi)銷(xiāo),因此加固前后程序的性能未受到很大影響。 表2 加固前后程序運(yùn)行性能對(duì)比 本文提出了一種基于LLVM 的Android 應(yīng)用加固方法,該方法將被保護(hù)程序中的Java 函數(shù)轉(zhuǎn)化為本地代碼并對(duì)其進(jìn)行混淆保護(hù),能夠有效抵抗逆向軟件的靜態(tài)分析和攻擊者的動(dòng)態(tài)調(diào)試。將關(guān)鍵函數(shù)反匯編為Smali 指令后轉(zhuǎn)化為C++代碼,利用LLVM 框架的特點(diǎn)對(duì)C++代碼進(jìn)行分析得到LLVM 中間表示,然后對(duì)LL?VMIR 指令進(jìn)行優(yōu)化和混淆,最后輸出機(jī)器碼。實(shí)驗(yàn)結(jié)果表明,本文提出的方法具有較高的隱蔽性,相比其他混淆保護(hù)方法使用更少的性能消耗和代碼體積,且能夠抵御大部分通用脫殼工具的攻擊,因此本文的方法具有更優(yōu)越的性能。 本文僅選擇不存在函數(shù)調(diào)用的函數(shù)作為保護(hù)的對(duì)象,在具有函數(shù)調(diào)用的函數(shù)中可能存在比較重要、需要被保護(hù)的函數(shù)。在以后的工作中,將考慮保護(hù)更多函數(shù)時(shí)應(yīng)用程序性能的變化,以及如何在保證高安全性的前提下克服代碼混淆帶來(lái)的性能開(kāi)銷(xiāo)。2.6 字符串混淆
3 實(shí)驗(yàn)
3.1 手動(dòng)攻擊
3.2 通用脫殼工具攻擊
3.3 運(yùn)行性能
4 結(jié)語(yǔ)