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