Android Proguard混淆對抗之我見

2022-06-01 21:00:47

關於何為Proguard,可以參考GuardSquare官網其優化業務Wikipedia相關條目.
Proguard:https://www.guardsquare.com/proguard
Wikipedia:https://en.wikipedia.org/wiki/ProGuard

區域性敏感雜湊與Proguard混淆對抗

  2017年6月,Richard Baumann發表了標題為"Anti-ProGuard: Towards Automated Deobfuscation of Android Apps"(DOI:10.1145/3099012.3099020)的論文,其旨在利用SimHash演演算法實現對Apk的自動化反混淆.關於何為SimHash,簡要來講可以理解為可用之計算兩個文字片段相似度的演演算法,此處不再進行具體闡述,可以參考Google在2007年發表的論文"Detecting Near-Duplicates for Web Crawling"(DOI:10.1145/1242572.1242592).

  論文第二部分中,其強調Proguard對Apk進行Obfuscating的過程是多個Transformation的集合,論文設欲混淆工程為P,而P可看作同時包含多個物件的物件集合,物件集合中包含的物件與未混淆之工程中的類,方法,成員一 一存在對映關係,作者統一稱這些物件為S.即P={S1,S2,S3...}
經過Proguard混淆後的工程設之為P',而Proguard的混淆過程可看作一個能夠傳入任意量的Transformation函數T(x),即P'={T(S1),T(S2),T(S3)...}.可見論文強調整個混淆過程為對整個未混淆工程中各個元素進行轉換的總和(事實上也確實如此),而該思想在接下來的分析中也會多次得到體現.

  而論文中的第三部分正式進入到自動化反混淆的實現部分,但由於論文中闡述的實現思路略去了很多細節,故下文不以論文進行分析,而Richard Baumann已經將自動化方案落地,在其Github賬戶ohaz下即可找到對應的POC專案(論文中並未給出其實現的專案地址,而該專案的首次commit時間最早為2016年,即早於論文釋出時間).接下來以該專案具體分析自動化反混淆的實現思路.
https://github.com/ohaz/antiproguard

  作者在其專案README檔案上簡述了工具的基本使用,需要引起注意的是其工具的--sb -t引數與-d引數,其前者用於指定一個未被混淆的Apk,並將未混淆Apk之包,類,方法名及其對應的方法實體逐一抽離提取,存入資料庫,後者為反混淆的進行指定一個欲反混淆的Apk.

  由於篇幅限制,下文僅對專案最核心的演演算法部分進行分析,並在分析前假設一個前提:資料庫中已經填充了足夠多的未混淆Apk.
  專案中antiproguard.py申明瞭一個關鍵方法compare,該方法用於將傳入的欲分析方法實體(被混淆)與資料庫中儲存的各個方法實體(未混淆)分別進行相似度對比,並依據相似度對比結果判斷是否生成一個hint,該生成的hint將作為輔助分析其他被混淆方法實體的依據.

可見在compare方法內,程式分別對被混淆方法實體分別生成了三個不同的SimHash值,而經過後續驗證,這三個SimHash的產生均與方法實體所對應的操作碼串有高度關聯,則此三個SimHash值的關係可以下圖進行表達.

由上圖不難得知這三個SimHash值與被混淆方法實體的關聯強度有關,並按照關聯強度以由大到小的順序排列.
接著該三值分別與資料庫中的未混淆方法實體對應產生的此三個SimHash值以論文中提到的如下方式分別進行一次計算:

  SimHash結合漢明距離,該計算方式得出的結果可抽象理解為兩個方法實體的相似程度,接著程式判斷計算出的相似程度是否大於90%,若超過該值,則判斷該被混淆方法實體與正在與之進行對比的未混淆方法實體高度相似,此時即可為下一次compare的呼叫產生一個hint,根據該hint以加快識別其他方法實體相似程度的速度,此處不再對hint更進一步分析.
  而最終程式將對所有經過compare方法確定的與資料庫中未混淆的方法實體產生對應關係的被混淆的方法實體進行批次重新命名,將其方法名'還原'為資料庫中對應的未混淆方法實體的對應方法名,簡而言之,compare方法負責確定被混淆方法與資料庫中的那個未混淆方法具有強相關的關係.
  如果已經理解了compare方法,不難看出compare方法與反混淆之精細程度有著直接關係,同時也不難得出該反混淆方案的本質.既已分析過關鍵方法,剩下的分析流程我僅以一圖概之.

  雖方案可行,但仍有侷限之處,且看該論文的第四部分.

  可見其選擇的被測試物件均為F-Droid上開放原始碼的專案,且這些專案至少使用了一個或多個開源的第三方庫,這些開源第三方庫將在正式測試前被匯入至資料庫中,以啟用Proguard優化的情況下編譯專案,以反混淆工具提供的方案處理之,雖然最終該反混淆方案正確還原的包超過了50%,但該方案依然在很大程度上無法勝任真正的逆向工程實戰.

  • 其一,回顧上文對關鍵方法compare的分析,不難發現一個需求與方案實現上的衝突,即逆向工程的本質是分析與剝離被分析物件最有價值的核心程式碼,而根據compare的實現可知其對方法實體的分析基於資料庫中資料量的多少,而能夠輸入資料庫的資料也僅限於第三方的開源支援庫(你總不能輸入一些能夠還原未開原始碼的資料吧,這形成了悖論).
  • 其二,論文第一部分明確指出,其所提出的基於相似性演演算法的混淆程式碼還原方案基於資料庫中的已知程式碼,而如第一點所言,能輸入資料庫的程式碼主要來源於第三方的開源支援庫,故該論文提出的所謂通用性方案僅能對本就開源但被混淆的部分進行還原.

  基於以上兩點假設一個理想情況,方案能夠還原所有被混淆的第三方開源庫程式碼,但需要明確的是,逆向工程的主要物件仍是針對軟體的業務程式碼,而市面上投放的軟體其業務程式碼量均十分龐大(想象一下如今使用者能夠從已知渠道下載的軟體,其臃腫程度導致的程式碼量可見一斑),即使能還原所有第三方開源庫程式碼,對逆向工程的幫助也是微乎其微.


DataFlow分析與Proguard混淆對抗

  不論是嘗試'還原'被混淆為短位元組的方法名還是以其他方式處理被Proguard混淆的Apk工程,不難發現這些工作的本質都是嘗試輔助逆向工程人員'理解'被混淆方法實體,排除無意義短位元組的干擾,以減少逆向工程中抽離出有價值程式碼的時間成本.
  基於該思想,我曾於21年4月份編寫並開源過一個專案,該專案的主旨是通過分析被混淆的成員與成員,方法,類之間的關係,讓逆向工程人員快速判斷被混淆成員是否具有被分析價值,但由於當時該專案不與任何現有工具聯動,且分析物件僅針對成員,職能單一,在逆向工程中發揮的作用不大,故在今年(22年)5月對部分程式碼重構,拓展了專案職能,目前可以與著名逆向工程工具JADX進行聯動,且分析物件由單一的成員拓展至方法,可通過分析欲分析方法中的引數在被標記為汙點的情況下的向下傳播方向,即DataFlow分析.
https://github.com/MG1937/AntiProguard-KRSFinder

  而該專案的實現本質無疑是分析整個Apk工程的Dalvik操作碼,為操作碼分配相應的控制程式碼以具體處理操作碼的操作物件,並依據處理結果進一步分析Apk內各個類的成員與方法,生成對應的分析報告用以聯動JADX.(關於Dalvik操作碼,可以參考Gabor Paller於2009年提供的Dalvik Opcodes).

像Dalvik虛擬機器器一樣思考

  分析Apk無不分析其Dex,分析Dex無不Dump其IR,得其IR,何如?
  正如前文所述,該專案旨在通過分析成員間關係與DataFlow以輔助逆向工程,然則不論如何分析,深入到Dex檔案其操作碼總是必要的.雖從Dex中獲取Dalvik操作碼的方法有千百種,但論如何處理操作碼及其處理思想,固然是要引Google之鑑的.然則自Android5.0開始,Google就棄用Dalvik轉而以ART虛擬機器器處理Dex,但ART絕大情況下使用AOT技術,僅於技術需求而言,該專案對操作碼的處理思想必然要取JIT之鑑,故該部分將分析Dalvik虛擬機器器處理位元組碼的部分實現,進以指導專案實現.

  以Android-4.0.1_r1分支下的Dalvik虛擬機器器原始碼為例,Dex範例將流入Frontend.cpp檔案下dvmCompileTrace函數,該函數正是將Dalvik位元組碼轉換為機器碼的入口函數.

  該函數不斷遍歷包含位元組碼的基本塊,最終將攜帶Dalvik位元組指令的cUnit成員參照傳入compileLoop函數下,而compileLoop函數最終將參照傳入CodegenDriver.cpp檔案下的關鍵函數 dvmCompilerMIR2LIR,顧名思義,此函數將Dalvik位元組指令處理為LIR,最終由其他方法處理LIR為機器碼.

  dvmCompilerMIR2LIR函數下cUnit成員參照中攜帶的Dalvik位元組碼被傳入dexGetFormatFromOpcode函數下處理為dalvikFormat成員,最終該成員被傳入一個巨大的switch-case塊中進行處理,該switch-case塊根據dalvikFormat成員為cUnit成員分配對應的處理控制程式碼(handle)以具體處理其攜帶的Dalvik位元組碼即其操作物件(即暫存器).

  至此,Android4.0 Dalvik虛擬機器器處理位元組碼的大致流程已經分析完畢,如上圖流程圖所示,該流程及其Dalvik位元組碼處理思想將被運用到接下來的專案結構中.

專案實現

專案已經開源,可至AntiProguard-KRSFinder下檢視專案具體程式碼.


  基於前文對Dalvik虛擬機器器處理位元組碼的流程分析,基於Dalvik思想繪製如上流程圖,該圖所示流程在專案正式開始前被作為理想流程框架用以指導專案進行,並且在已經完成的專案中其對位元組碼的大致處理流程也接近該理想流程框架.


  由Dalvik虛擬機器器原始碼下docs資料夾中檔案的部分描述可知Dalvik虛擬機器器是基於暫存器的.故專案也需要一個元件用以儲存暫存器,該元件即為專案中TempRegisterMap.cs檔案下的TempRegisterMap類.而該元件本質上是一個Dictionary物件,該元件其鍵為Dalvik操作碼具體操作的暫存器其名(即p0,v0...),其值為TempRegister物件,該物件即代表一個具體的暫存器,此暫存器目前僅需要儲存字串與方法.下圖為該元件大致結構.

  下面部分為專案對單個Dalvik位元組碼處理部分,該部分模仿了Dalvik虛擬機器器處理位元組碼的模式.

  上圖為專案下MethodCodeAnalyseModule.cs檔案中MethodCodeAnalyseModule類的methodAnalyse 函數,該函數為專案處理操作碼的正式入口,可以看到該部分我效仿Dalvik虛擬機器器處理單個位元組碼的流程模式,為即將處理的操作碼分類,併為之分配處理控制程式碼以具體解析操作碼及其攜帶的暫存器,值得一提的是該函數的實現並不完全與Dalvik虛擬機器器處理操作碼的形式相同,在正式為操作碼分配處理控制程式碼前,我將操作碼的處理優先順序以 MUST,CHECK,PASS 劃分,被賦予MUST優先順序的操作碼將被優先分配處理控制程式碼,賦予CHECK優先順序的操作碼將在檢查其具體操作的暫存器是否有必要處理後再為操作碼分配處理控制程式碼,賦予PASS優先順序的操作碼將不進行任何解析,以此略去部分不必要解析的操作碼,以加快整個流程的處理速度.

  在正式進入下個部分前,有必要闡述清楚函數中方法區塊(以下稱為)的含義.
  Dex中以MIR形式儲存的函數不僅以多個位元組碼組成的串形式儲存,單個函數也可被分為多個,即以多個塊組成一個函數,欲簡單理解,可以ASM中的JMP指令為參考,JMP指令可以跳躍到記憶體中的任意地址,而在Dalvik中則以為跳躍物件,即在Dalvik中以為基本單位組成執行流程.以市面上常見的工具抽離Dex其操作碼串,輸出的結果中大多以高階語言中Label的形式表示函數中的塊,如下圖為帶有多個塊的函數片段.

  下面部分為專案對單個函數的處理部分.

  先假設專案即將對一個函數進行處理,且該函數被分為數量未知的多個,而上圖所示流程即為專案在該情況下的大致處理流程,假設一個理想情況,此時塊與塊之間沒有任何指令使得程式跨塊跳躍,即執行流程從塊頂部向下執行直到方法結束,那麼專案將在每個塊執行結束時,立刻擷取當前塊的暫存器集(即TempRegisterMap元件)快照,並且記錄當前的執行路線,以暫存器集快照為引索儲存每次執行路線,那麼在該執行流程下,暫存器集將以如下圖情況儲存.

  那麼此時假設塊與塊之間出現了一個或多個流程控制指令(如if,goto).

  • 若流程控制指令為 goto 指令,專案將標記其操作物件為強行跳轉目標,並繼續向下執行,但不具體解析非強行跳轉目標的塊,直到目標塊被找到才具體對塊中操作碼進行解析.
  • 若為 if 類指令,專案將僅對其操作物件進行標記,而不令專案強行尋找目標塊,正常向下執行並解析塊.

  以如上兩種流程控制方法儘可能覆蓋到大多數由流程控制指令導致的未知數量的解析路線.

  假設塊與塊之間出現了數量未知的goto指令,此時處理流程將為下圖所示.

  從流程上來看似乎處理過程沒有太大變化,但此時構造一個在Dalvik執行流程上能夠影響暫存器內容的函數片段,此時且看下圖處理結果.

  專案對流程的控制及暫存器集快照儲存的作用就在此體現,從上圖給出的函數片段可知塊L1中的v1暫存器為執行流程所改變,若不儲存暫存器集快照,就不能夠完全記錄暫存器的前後變化.
  上文即為專案對單個函數的大致處理流程,該部分也是專案最核心的部分,不論是成員關係分析還是DataFlow分析,其結果的精確度都依賴於此,更多細節不再在此文寫出,至此,本文完結.