深入研究Android Dalvik的Dex文件格式
案例研究
在這個案例研究中, 我們將檢查一個 Nexus 銀行木馬惡意樣本(文件 MD5: d87e04db4f4a36df263ecbfe8a8605bd)。Nexus 是在地下論壇上出售的一個框架,它能夠從安卓手機上的許多銀行應用程序中竊取資金。Cyble 發布的一份報告提供了有關該框架的更多詳細信息以及對樣本的徹底分析。
使用 jadx 對樣本進行分析,應用程序中的 AndroidManifest.xml 文件(d87...)顯示它請求訪問設備的短信、聯系人、電話通話等敏感信息。AndroidManifest.xml 中的主要活動在應用程序最初時不會出現,因為它稍后會被解壓,但另一個類被提及為 "com.toss.soda.RWzFxGbGeHaKi" 并且擴展了 Application 類,這意味著它將是應用程序中首個運行的類:
圖片
在 Application 子類 "com.toss.soda.RWzFxGbGeHaKi" 中的 onCreate() 回調引用了兩個額外的方法:melodynight() 和 justclinic(),而后者調用了另一個方法:bleakperfect()。
圖片
bleakperfect() 方法以及應用程序中的其他幾個方法包含大量的死代碼,涉及將值分配給變量并使用多個循環對它們進行算術運算,但最終這些變量從未被使用。
此外,該方法用于解碼在代碼其他位置引用的字符串。這是通過將一個字節數組(編碼字符串)與另一個字節數組(XOR 密鑰)進行異或操作,并將結果存儲在第三個字節數組中,然后將其轉換為字符串。
圖片
諸如此類的修補方法可以刪除冗余代碼并用字符串返回替換冗長的 XOR 操作,可以使應用程序的分析變得更加容易且更高效。為此,我們必須了解此代碼在DEX文件中的呈現方式。
DEX概述
Android應用程序主要是用Java編寫的。為了在Android設備上運行,Java代碼被編譯成Java字節碼,然后被轉換成Dalvik字節碼。Dalvik字節碼可以在APK的DEX(Dalvik可執行)文件中找到。APK(安卓包文件)本質上是一個包含應用程序代碼和所需資源的ZIP文件。可以通過提取APK的內容來檢查DEX文件。
DEX文件分為幾個部分,包括頭部、字符串表、類定義、方法代碼和其他數據。大多數部分被劃分為大小相等的塊,這些塊中包含多個值來定義部分中的項目。為了展示在DEX文件中如何翻譯Java中的常見概念,例如類或字符串,我們將使用class_defs部分作為示例。
圖片
關于類
class_defs部分由class_def_items組成,每個類在應用程序中都是32字節長的。類的名稱以以下方式存儲:class_def_item包含對type_ids部分中的項目的索引(class_idx),而type_ids部分又包含對string_ids中的另一個項目的索引(descriptor_idx)。
string_id_item下的值是從文件開頭的偏移量,它指向包含實際類名字符串(data)的string_data_item的開頭,該字符串前面有其長度(utf16_size)。
圖片
class_def_item還有另一個成員(class_data_off),它是指向一個class_data_item的偏移量,該項代表與類相關聯的數據。它包含了有關類的靜態和虛擬方法、靜態和實例字段的信息,以及每個方法和字段的匹配的encoded_method和encoded_field項。
關于方法
direct_methods和virtual_methods包含一系列encoded_method項目。在每個方法類型的第一個encoded_method項目中,method_idx_diff值持有在method_ids部分中匹配項目的索引。
然而,在后續項目中,這個值是相對于前一個項目的差異,并且要計算method_ids索引,必須將差異增加到前一個method_idx_diff值。
圖片
最后,method_id_item中的方法名稱存儲在name_idx下,類似于type_id_item中的類名稱,并且使用string_id_item索引檢索方法名稱的字符串值。
圖片
在Android應用程序中,每個方法都有一個前言(或者稱為code_item),它指定了有關方法大小、輸入和輸出參數以及異常處理數據的信息。這個前言在DEX文件中的偏移量存儲在前面提到的encoded_method項的code_off值中。
前言的前兩個字節表示寄存器大小,即字節碼使用了多少個寄存器,接著是輸入和輸出參數的字大小,而最后四個字節是字節碼大小(或insns_size)。
字節碼大小以16位指令單元計算,這意味著要計算字節碼中總字節數(8位單位),必須將這個值乘以二。方法的Dalvik字節碼直接在前言之后開始。
圖片
關于字符串
到目前為止,我們已經看到了兩個例子中的string_id_items用于從DEX文件中的字符串表中提取類名和方法名。但是,在Dalvik字節碼中,string_id_item也非常重要,當在應用程序代碼中使用字符串值時,它會被引用。
例如,以下字節碼序列返回"sampleValue"字符串,其中"0xABCD"是在string_ids部分中的"sampleValue"的string_id_item的索引
1A 00 CD AB # const-string v0, "sampleValue" [string@ABCD]
11 00 # return-object v0
這意味著,在對惡意樣本的字節碼進行修補時,一個障礙是,解碼后應該返回的解密字符串并不存在于DEX文件的字符串表中。相反,它們必須在解碼后添加到文件中,以便具有匹配的string_data_item和可以被代碼引用的string_id_item索引。
自然地,添加這些字符串會導致文件的部分大小、索引和偏移量發生變化。這會產生另一個障礙,因為在先前顯示的DEX文件中,不同項之間存在多個依賴關系,改變它們引用的索引或偏移量將導致這些項被錯誤地解析或具有不正確的成員值。這就是為什么在對方法進行修補時,必須確保DEX文件的其余部分保持完整。
關于補丁
為了實現這一點,我們創建了dexmod,這是一個Python輔助工具,根據用戶指定的反混淆邏輯來修補DEX文件。除了修補之外,該工具還支持諸如使用字節碼模式進行方法查找或添加字符串等操作。dexmod下載地址:https://github.com/google/dexmod/
對于Nexus樣本中的混淆方法來說,要使其返回解密后的字符串,必須使用dexmod解碼并將字符串添加到文件中。然后,將在DEX文件中看到的返回字符串的字節碼序列放置在每個混淆方法的字節碼開頭,并與相應的string_id_item索引配對。方法中的任何剩余字節都可以用0x00(NOP)替換,以進行額外的代碼清理,但這并非必要。
還需要更新每個方法的前言以反映這些更改;寄存器大小減小到1,因為只使用了一個寄存器(v0),而字節碼大小更新為3,因為現在它只包含3個16位指令(6字節)。前言中的其他值可以保持不變,因為它們表示的項沒有受到影響。
圖片
在DEX文件的頭部中,校驗和和SHA-1簽名值也必須更新;否則,文件內容的驗證將失敗。在使用dexmod實施了這些步驟之后,可以使用jadx重新檢查DEX文件,一旦混淆的函數現在將會移除所有死代碼并返回解碼后的字符串:
圖片
由于Nexus樣本中的混淆方法是由另一個方法調用而不是直接調用的,另一種可能性是修補調用者方法并返回一個字符串,從而完全跳過混淆方法。這樣做可以節省研究人員在分析過程中重復跳轉方法的時間。
總結
本案例研究展示了Dalvik字節碼修補對研究人員的用處,以及如何使用免費的開源工具來實現。與其他反混淆解決方案面臨的問題類似,打包器和混淆技術經常更新,不幸的是很難找到一個能夠長時間內適用于大量應用程序的修補解決方案。此外,雖然搜索應用程序的字節碼可以高效地識別代碼模式,但嘗試修改DEX文件而不損壞其中某些部分可能是一項挑戰。
附錄(DexMod)
dexmod工具包含以下腳本:
- dexmod.py 主模塊: 接受DEX文件名作為參數,并調用editBytecode.py中的方法來修補文件
- getMethodObjects.py:
創建具有以下屬性的方法對象:
- methodIdx:method_idx值,在Dalvik字節碼中用于調用方法
- offset:方法字節碼的文件偏移量
- name:方法的名稱
- bytecode:方法的字節碼
- searchBytecode.py:在DEX文件中查找字節碼模式并返回匹配的方法對象
- editStrings.py:向DEX文件添加字符串
- editBytecode.py:用于實現自定義修補邏輯,包含空方法
- example/editBytecodeCustom.py :實現了文章中案例研究的修補邏輯
dexmod 工具利用 dexterity(一個解析DEX文件的開源庫),并協助將字符串添加到 DEX文件,同時修復對受影響字符串 ID 和其他部分偏移量的引用。dexterity庫有一些局限性,它不會一次修復字節碼中引用的字符串索引,并且在本案例研究期間對其代碼進行了一些更改以正確添加字符串。
dexterity開源庫地址:https://github.com/rchiossi/dexterity