徐玄驥,董帥克,張智斌
(1.昆明理工大學,云南 昆明 650600;2.螞蟻科技集團股份有限公司,浙江 杭州 310000)
代碼對于普通開發者而言是非常重要的知識產權。為了保護代碼資源,防止其被隨意剽竊,開發者通常會使用代碼混淆這一技術。然而,代碼混淆無異于一把雙刃劍,雖然能有效防止開發者的代碼被惡意使用,但也極大程度上增加了安全分析師的分析成本。此外,惡意軟件開發者還可以利用代碼混淆來隱藏自己的真實意圖,從而逃脫自動分析工具的掃描。與傳統領域的應用相比,因為Java 語言天生的易于逆向的特征,代碼混淆技術對于安卓(Android)應用具有更重要的意義。
Android 操作系統是使用人數最多的移動操作系統。統計信息[1]顯示,截至2020 年12 月,谷歌官方應用程序商店(Google Play)共有超過350 萬款應用可供下載,月平均下載人次超過500 萬。因此,對Android 應用的代碼混淆技術使用情況進行調研具有很重要的意義。一方面,開發者可以了解到目前主流的代碼混淆技術,并判斷是否要為自己的應用添加相應的保護。另一方面,安全分析師可以了解到不同種類的應用在代碼混淆技術的使用方式上的異同,從而設計出更好的代碼分析 工具。
目前,學術界已經有部分針對Android 代碼混淆機制的研究。文獻[2]提出了針對Dalvik 字節碼的混淆方法,并針對市面上主流的反病毒軟件進行了測試,觀察其對混淆的抵抗能力。文獻[3]提出了一種開源的黑盒混淆工具,并提供了包括重簽名、隨機清單、重命名等多種混淆方法,從而可以幫助開發者方便快捷地為應用添加代碼混淆。文獻[4]提出了一種針對Android 應用的控制流混淆方法,通過插入多余控制流以及對原有控制流進行壓扁操作,從而提升了代碼被逆向破解的難度。文獻[5]提出了一種基于Dalvik 字節碼抽取以及重新映射的混淆方法,通過將Dalvik 操作碼重新映射成新操作碼進而封裝成SO 文件,可以顯著提高逆向工程的成本。以往的學術文獻,往往關注如何對Android應用的現有混淆技術進行修改,而忽略了從全局的角度去發掘Android 應用整體對代碼混淆使用情況的分析。
基于此,設計并實現了一套針對Android 應用的代碼混淆掃描框架,并對標識符重命名、字符串加密、Java 反射以及應用加殼4 種常見的代碼混淆手段設計了相應的檢測算法。將檢測算法運行在超過10萬款Android應用上,并對統計結果進行了分析。
Android應用程序包(Android Aapplication Package,APK)文件包含了一款Android 應用的全部內容,通常由res、assets、lib、META-INF 這4 個路徑和AndroidManifest.xml、classes.dex、resources.arsc 這3 個文件組成。它們的具體用途如下。
(1)res:此路徑下存放了Android 的資源文件,這些資源文件將會被映射到.R 文件當中。
(2)assets:此路徑與res 路徑功能類似,用來存放APK 的靜態資源文件。與res 路徑不同的是,開發者可以在此路徑下創建任意深度的子路徑,并且存放任意文件類型的文件。
(3)lib:此路徑下存放著為不同CPU 平臺編譯的文件,通常是各種庫文件,如.so 文件等。
(4)META-INF:此路徑用來存放一款應用的簽名信息,可以用來校驗應用的完整性。
(5)AndroidManifest.xml:Android 應用的清單文件,用來存放應用的基本信息,包括名稱,版本,所需權限以及組件等。每個Android 應用都只包含一個清單文件。
(6)classes.dex:存放Android 應用中的所有類信息。此文件可以被Android Dalvik 虛擬機理解并執行。
(7)resources.arsc:此文件存放應用中的資源文件以及其對應的ID。
1.2.1 標識符重命名
在軟件開發過程中,開發者通常賦予變量名較多的語義信息,以保證程序的較高可讀性。然而,充足的語義也為逆向者提供了方便,在變量名的幫助下,逆向者可以輕易理解原作者的編程意圖,竊取其中重要的實現,從而對開發者的知識產權產生威脅。
標識符重命名通過將變量中的語義信息抹除,可以有效增加逆向者的攻擊成本,從而被廣泛使用。如下代碼片段表明,經過標識符重命名之后,在沒有上下文的環境下,逆向者很難弄清類a 的實際含義。

1.2.2 字符串加密
由于應用中的字符串包含了非常多的語義信息,逆向者通常會將其作為理解程序語義的突破口。如1.2.1 小節程序片段表明,即便混淆了程序中出現的標識符名稱,有經驗的逆向者仍然可以依靠字符串來猜測函數的功能。因此,字符串加密被作為一種有效的混淆手段。在Android 應用開發的過程中,字符串加密可應用在多個階段,包括Java 代碼編譯階段、Java 字節碼轉Dex 文件階段等。
對于惡意代碼開發者來說,字符串加密能夠有效地抹除程序語義信息,抵御部分基于硬編碼特征的掃描工具的檢測。假如加密算法實現得足夠復雜,還能夠有效地增加逆向過程的時間成本。下面的代碼展示了字符串加密的例子。

1.2.3 Java 反射
Java 反射機制是一種具有與類進行動態交互能力的一種機制,是Java 語言的常用手段,能夠使得開發者在程序運行時了解類、方法和變量的信息,動態地創建類的實例或調用方法。反射強調動態交互,通過運行時加載,在需要時可以隨時隨意的利用反射這種機制來進行操作。在Android 程序開發領域,Java 反射通常在需要訪問隱藏屬性或者調用方法來改變原來程序的邏輯中使用,常用來調用標注有hidden 注解的應用程序編程接口(Application Programming Interface,API)。
以下為使用Java 反射的一個簡單示例:

反射在普通軟件中的使用與惡意軟件存在較大的差異。普通軟件通常會利用反射進行Java 本地接口(Java Native Interface,JNI)調用或后向兼容性的檢測,惡意軟件則會利用反射去隱藏控制流,從而抵抗靜態分析工具的檢測,或使用反射將原本正常的函數調用變得十分臃腫,加強對逆向者的 干擾。
1.2.4 應用加殼
加殼是一種廣泛應用的代碼保護技巧。在加殼之后,原應用將會被加密并被稱為“殼”的新應用所替代。當用戶在屏幕點擊應用時,新應用將被啟動,在啟動的過程中同時完成解密與還原操作,還原完成后再將程序控制權轉移給原應用。在實際生產環境中,“殼”的解密過程通常較為復雜,難以通過靜態分析的方式將加密后的原應用解密,進而可以大大增加逆向工程的成本與開銷。
圖1 展示了本文所使用的掃描框架的設計原理。為了增加掃描結果的覆蓋性,從3 種不同的數據源中爬取Android 應用程序,使用Androguard 工具[6]對其進行解包。無法解包成功的應用實例將被剔除。在解包之后,使用自行設計的4 種混淆手段檢測方法對應用進行掃描,并對掃描結果進行手工分析與匯總。

圖1 掃描框架設計
通過對多種Android 代碼混淆工具進行調研,研究發現,使用了標識符重命名的Android 程序在標識符的分布上會與未混淆的Android 程序產生較大差異。為了準確地表示這種差異,設計了基于3-Gram[7]詞頻的標識符重命名檢測算法,其具體流程如下。
(1)數據預處理:針對某Android 應用,提取其全部標識符名稱,組成集合Set_id。
(2)特征生成:針對Set_id 中的所有標識符,使用3-Gram 方法抽取其所有元組,統計各元組的出現頻次,組成固定長度的向量并作歸一化。
(3)模型訓練:從開源倉庫F-Droid 下載3 147 款未經混淆的Android 應用程序組成集合Set_app,使用兩種不同的混淆器(ProGuard 和DashO)對Set_app 中的應用程序的標識符進行重命名,進而生成了未經混淆與經過混淆的兩種正負樣本集合。對樣本集合中的應用程序進行數據預處理與特征生成得到特征集合Set_feat,再依靠特征集合Set_feat訓練支持向量機(Support Vector Machine,SVM)分類器。
訓練出來的SVM 分類器可以判斷某Android 應用是否進行了標識符重命名混淆。
由1.2.2 小節可知,加密能夠將Android 應用中的字符串變得隨機,并且難以理解。而字符串的隨機性可以用信息熵來描述。加密后的字符串較之普通字符串,往往擁有更高的信息熵。
信息熵的通用計算公式為:

式中,N表示事件的個數,Pi表示事件i的發生概率。
在本系統的實現中,抽取了一個Android 應用中出現的所有字符串并將其合并。假設合并后的字符串為S中出現的字符種類共為N,Pi表示第i個字符Xi出現的概率,則,其中S.count(Xi)表示Xi在S中出現的次數,S.length表示字符串的長度。最終通過式(1)計算獲得信息熵。
接下來,進一步復用了2.1 小節展示的思路,針對一款Android 應用抽取其全部字符串并組合,計算其信息熵,并使用信息熵來訓練SVM 分類器。
Java 提供了多種反射API 來實現不同的目的,這是Java語言的一種高級特征。本文主要關注[Class.forName()→getMethod()→invoke()]操作序列,一方面因為這種操作序列的Java 反射操作中最常見的模式,另一方面這種操作序列能夠隱性地轉移程序的控制流,從而作為一種混淆手段躲避靜態掃描工具的檢測。在檢測到Java 反射的使用后,對解包產生的Smali 文件進行了進一步的分析,通過后向切片的方法獲取Java 反射調用的真實對象,并分析其調用意圖。
應用加殼通常有較高的開發門檻,因此開發者往往選擇專門廠商提供的應用加殼服務,如梆梆加固、360 加固等。由于應用加殼功能通常作為一種云服務出售,使用某廠商加殼功能的應用在代碼層面通常具有相似的特征。基于此,人工分析了若干應用加殼廠商,并從中抽取了相應的靜態特征進行掃描,若某應用命中了這條特征,則可以認為它使用了該廠商的應用加殼服務。本文選取的特征如 表1 所示。

表1 常用應用加殼服務及其對應的靜態特征
為了使掃描的結果更具有代表性,從3 種不同渠道收集了共114 560 款Android 應用。數據集囊括了來自Google 官方市場和中國第三方市場的Android 應用以及惡意軟件。具體的數量信息如表2所示。

表2 數據集詳情
表3 展示了數據集中標識符重命名的掃描結果。可以看出,中國第三方市場和惡意軟件中的Android 應用使用標識符重命名的頻率更高。而Google 官方市場中僅有不到一半的Android 應用使用了標識符重命名。對此現象的解釋是,Google 官方市場針對軟件剽竊提采取了更為嚴格的監管措施,從而使得對于軟件開發者來說代碼混淆并不是必須的工作。

表3 標識符重命名使用情況
在進一步人工判斷了惡意軟件與正常軟件針對標識符重命名的不同策略后發現,惡意軟件傾向于使用更復雜的命名策略,比如使用相似的字母組成不同的字符串,如Ill1II 與ll1II1 中使用了l、I、1這3 種不同的字符。除此之外,惡意軟件還常常利用Java 的重載特性,用毫無關聯的詞匯替代原有的標識符。這種情況對安全分析師造成了較強的干擾。
表4 展示了數據集中字符串加密的掃描情況。可以看到,Google 官方市場與中國第三方應用市場中均極少使用字符串加密,而惡意軟件中使用字符串加密的樣例占比超過5&。

表4 字符串加密使用情況
通過進一步手工分析了使用字符串加密的惡意軟件樣例,發現字符串加密通常與標識符重命名結合起來,通過將字符串的加解密函數名更改為毫無關聯的詞組,從而誤導安全分析師。比如,在軟件com.solodroid.materialwallpaper 中,字符串的解密函數被命名為NavigationItem;->getDrawable()。從字面分析,其意義可能為獲取圖像資源的函數。
表5 展示了數據集中的Android 應用對Java 反射的使用情況。可以看出,反射作為一種Java 語言的高級用法,在各種類型的應用中被使用的比例較為接近。

表5 Java 反射使用情況
進一步分析不同類型的Android 應用使用反射調用的真實目標,發現惡意應用傾向于使用Java 反射調用敏感的API,或使用Java 反射將普通的調用臃腫化,從而增加逆向分析師的分析成本。而普通應用則通常只考慮應用程序的兼容性。這種方法的差異可以從表6 中清晰地看出。

表6 Java 反射調用頻率最高的前兩位使用情況
掃描結果顯示,在114 560 款Android 應用當中,共有7 408 款應用使用了加殼技術,占比百分之6.5&。具體到各廠商提供的加殼服務的數據如表7 所示。

表7 應用加殼使用情況
在進一步的人工分析當中發現,使用了加殼技術的應用非常難于自動化分析,往往需要通過虛擬機或人工脫殼,才能夠獲取原應用文件,此舉通常能夠比較好地保護開發者的代碼資源。
在本文中,為了了解代碼混淆在Android 應用上的使用情況。采集了來自3 種不同來源的Android應用程序,并構建了一個擁有10 余萬不同類型Android 應用的數據庫。除此之外,設計了一套代碼混淆掃描框架,并將其部署于收集的數據集上。針對標識符重命名、字符串加密、Java 反射以及應用加殼4 種不同的代碼混淆技巧,進行自動化掃描和人工分析。掃描結果顯示,不同來源的Android應用在代碼混淆的使用上差別較大。在實際應用中,開發者應該考慮更多地使用更高級的代碼混淆技巧,如字符串加密與應用加殼等,從而更好地保護自己的代碼資源。