如何解決惡意APK中常見的Native代碼加殼保護
在分析惡意軟體時偶然會發現受Native代碼加殼保護的APK,大多數情況下,這些樣本只是通過分離出DEX文件中的類/變數名來簡單地進行混淆或者是通過字元串混淆,其中包括:
1. 反調試技術:惡意代碼用它識別是否被調試,或者讓調試器失效。惡意代碼編寫者意識到分析人員經常使用調試器來觀察惡意代碼的操作,因此他們使用反調試技術儘可能地延長惡意代碼的分析時間。Dalvik和本地代碼都有反調試技術(例如檢查JDWP或ptrace的狀態)。
2.反掛鉤(anti-hooking)技術:旨在阻止像Xposed,Cydia substrate(一款強大的插件支持框架(平台),沒有它的支持,大部分的插件是無法工作的,也是必裝的依賴性插件)或Frida等框架,以實現早期檢測。這個想法主要基於檢查由框架引起的副作用或指紋,例如,檢查注入的庫或對框架函數的堆棧進行調用。
3.反模擬(anti-emulation):許多分析人員都會使用Android模擬器,QEMU或Genymotion查看惡意程序如何執行以及是否觸發任何奇怪行為。但許多受保護的或惡意的APK會拒絕在模擬環境中啟動或相應地工作,例如,如果檢測到模擬或分析環境,則惡意軟體可能不會啟動感染常式。
4.編碼或加密:資源文件或可執行代碼可以以壓縮或加密的形式進行傳播,如果惡意行為的保護是由不同的執行存根(execution stub)組成,則每個存根都會執行惡意行為並解碼下一個存根。
5.突破靜態分析工具:這通常需要攻擊者很好地了解用於分析惡意行為的一些工具的缺陷,例如,攻擊者可以利用特製的classes.dex或ELF文件,而這些文件不能被分析工具正確進行分析的。這個反分析的思想既可以應用於文件結構,也可以應用於代碼級,常見的情況有靜態編譯加剝離的ELF文件,格式錯誤的頭文件或應用於代碼的各種反編譯技巧。
6.混淆處理:
6.1 CFG混淆處理:此方法通常應用於本地代碼,大部分時間是將LLVM-Obfuscator應用於代碼的結果。
6.2 虛擬化混淆:這是常見的一種混淆方式,主要原理就是生成一個自定義的位元組碼語言,然後由虛擬機執行。本文會有一部分篇幅,專門介紹如何處理那些應對虛擬機的分析以及如何發現檢測虛擬化混淆的常見指紋。
因此,要想知道如何破解這些反分析的技術,就要知道它們實現的過程,知己知彼,才能百戰百勝。例如,只要在Google上搜索「Android反調試技術」,就可以發現大量這方面的研究。
目前可用的用於分析惡意攻擊的方法
目前,分析人員已經開發了各種用於分析惡意攻擊的方法,以下是一份簡要的研究項目清單,目的是自動從受保護的APK中提取原始代碼,並可分為兩大類:
1.針對Android源代碼的修改,這種分析方案背後的關鍵思想是修改和重新編譯ART和Dalvik運庫(分別為libart.so和libdvm.so)以鉤住用於載入內存中類的特定函數並執行位元組碼。這樣,載入到內存中的數據結構被識別,轉儲並重建與原始類似的classes.dex文件。目前研究人員已經開發了幾個屬於這一類的項目,我會按時間順序列舉如下一些:
1.1 DexHunter:Android脫殼神器DexHunter針對的是Dalvik和 Android Runtime (ART) ,它完全是基於檢測載入時間並初始化內存中的新類,通過轉儲,最終重建一個DEX文件。
1.2 AppSpear:僅針對Dalvik Runtime,它的想法類似於DexHunter,但是它沒有連接到載入類的方法,而是修改libdvm.so代碼以檢測並轉儲Dalvik數據結構,這是一個關鍵的結構,由VM在內部執行位元組碼。
1.3 android-unpacker:這是一款ndk寫的動態Android脫殼的工具,原理簡單來說就是ptrace,然後在內存中匹配dex_file.cc源代碼,且僅注入必要的代碼以便在DEX文件執時轉儲它。
2.輔助腳本:其他時候,有時我們可能無法編譯Android源代碼或不想這樣做,那這時,我們就應該依賴像Xposed框架或Cydia substrate或使用其它調試器,例如GDB/ptrace。這個想法是開發一個掛鉤腳本來控制應用程序的執行,以保存所有動態載入的文件。基於此解決方案的一些可用腳本包括:
2.1 DexHook:這是一個簡單的Xposed模塊,它的主要方法是掛鉤動態載入DEX文件,可以很容易地將它擴展到ART或Dalvik運行時中更詳細的掛鉤。
2.2 gdb腳本:這是一組腳本,它們使用ptrace和gdb來調試進程並轉儲動態載入的DEX文件。
雖然以上列出的解決方案非常適合快速評估要分析的樣本,但有時還是需要深入了解惡意軟體特定的保護工作原理並確認其所有使用的技術。除非我們完全理解用於載入的解包機制,否則我們無法確定轉儲代碼是否完整。舉個例子,只有滿足特定的條件時才會出現特定classes.dex文件,或者一個類可能還沒有在運行時載入,而由於轉儲機制,我們錯過了對它的分析。請注意,以上一些列出的研究方法還採取了進一步改進措施,並提出了可能的解決方案,以解決任意動態載入問題。
我可以認為以上這些方法都是採取完全動態方法所帶來的局限性,這就是為什麼應該在代碼分析中結合靜態逆向方法,比如保護代碼,因為它實際上既可以提供關於內部工作的技巧或鮮為人知的有價值的逆向分析技巧,也可能被野外惡意軟體利用。
這是從AppSpear論文中提取的示意圖,並顯示了DDS的內存結構
示例
接下來,我要分析的樣本是一些中國應用程序,它們已被開發者進行了反分析保護,其中一些應用程序現在已被證實是惡意的。雖然在過去幾個月里,這些應用的保護措施得到更新和加強,但核心技術不變。
這個封裝器不會在smali級別傳遞混淆,但是保護的存根由多個層組成,在執行結束時,它會將原始classes.dex文件載入到內存中。接下里我會描述這個特定保護措施是如何進行反分析的,同時我還會提供一些可能有用的提示,以便你對類似樣本進行逆向分析。
入口點
使用jadx檢查受保護的APK,我可以立即看到AndroidManifest.xml仍然包含所有原始信息(例如許可權,活動,服務,接收者和提供者),並且原始APK資源似乎沒有被壓縮或加密。另外,開發者可能會通過破壞或混淆manifest和各種資源以阻撓分析工具繼續分析。
通過查看已識別的包和反編譯的類,我可以注意到AndroidManifest.xml中介紹的原始入口點都不可用,而一個名為com.qihoo.util.StubApp1868252644的類則會作為新的入口點,或者更恰當地說它構成了保護的存根。該類來源於android.app.Application ,這樣做是為了在創建流程之前確保在任何其他應用程序類之前執行。com.qihoo.util.StubApp1868252644應該在manifest應用程序標籤中聲明,實際上只要稍加留心就可以找到。反編譯的代碼還包含其他兩個類:
我通過jadx查看了反編譯的代碼,但是得出結論的是,雖然manifest和各種資源都存在,但是APK中嵌入的classes.dex文件顯然缺少原始代碼,這些原始代碼是真的消失了嗎?
classes.dex文件大小大約為4.3MB,但它只包含3個類,沒有足夠的代碼來解釋大小,所以下一步就要查看DEX header:
使用DEX模板的010Editor分析DEX header
DEX header顯示了一個6104位元組的data_size和一個2712的data_off值。如果我轉到偏移量8816,就可以清楚地看到我還沒有達到所期望的DEX文件的末尾,所以可以肯定,有些東西是不正確的。該偏移量的第一個位元組看起來並不是真正有意義,但細心的人可能會注意到,前兩個位元組形成了字元串「qh」,這看起來像一個魔術值,以用於識別Qihoo數據段的開始部分。
Qihoo數據標頭分的第一個位元組
顯然,我還無法從位元組序列中獲得更多信息,不過我可以猜測它是經過了某種方式的編碼,例如,0x52的值重複了多次,有些提示可能是簡單的XOR編碼。
現在是分析com.qihoo.util.StubApp1868252644代碼的時候了。
源代碼不會以任何方式混淆,並且可以使用attachBaseContext方法標識入口點:
1.原始上下文被保存;
2.檢測到CPU ABI並生成本地存根庫的名稱;
3.正確的本地庫被複制到文件夾(具有775個許可權);
本地庫最終通過對System.load()的調用載入。
轉換成本地代碼
1.一旦載入了庫,執行就會傳遞給本地代碼,不過,我還需要在繼續之前確認一件重要的事情,即確保在本地庫的載入階段不會遺漏任何代碼。實際上,ELF結構和鏈接器文檔詳細說明了在控制項傳遞到共享庫的入口點之前所運行的鏈接程序採取的步驟(即JNI_OnLoad),這符合「初始化和終止程序」。
.preinit_array,.init_array和.init部分是在構建動態對象時由鏈接編輯器創建的,這些部分分別標記為.dynamic tags DT_PREINIT_ARRAY、DT_INIT_ARRAY和DT_INIT。其地址包含在由DT_PREINIT_ARRAY和DT_INIT_ARRAY指定的數組中的函數,該函數由運行時鏈接程序按照其地址出現在數組中的順序執行。
為了檢查初始化部分的存在,在ELF模板的幫助下我既可以使用010Editor,也可以使用LIEF編寫簡單的腳本來提取所需的信息。
在本地庫上執行腳本將顯示偏移量為0x1a00的終止常式,並且沒有任何初始化常式。顯然,新版本的保護器在.init_array部分有兩個函數偏移量,函數應該用來初始化一些字元串,並在ELF完全載入後清除動態部分,這是用來進行動態分析的一種方法。現在,我就可以安全地從傳統的入口點函數JNI_OnLoad開始進行靜態和動態分析,此時,IDA將成為我進行分析的工具。
libjiagu.so ELF頭文件以及初始化和終止常式
將APK載入到IDA窗口並在另一個窗口上載入本地庫後,我就可以在本地開始我的動態分析了。 像往常一樣,JNI_OnLoad函數會檢索到JNIEnv指針,然後跳轉到一個非常有趣的函數,我會將其重命名為VM_ENTER。
VM_ENTER函數代碼
該函數很有趣,如果你熟悉虛擬機的混淆處理,則可以將代碼片段標識為VM_ENTER函數,然後跳到虛擬機執行循環。操作過程如下:
1.分配一個0x100位元組的虛擬棧或臨時空間(0xC位元組不包含在內,因為它們似乎不屬於虛擬環境的一部分);
2.保存SP寄存器的原始值;
3.將R0的原始值保存到R12,LR和PC寄存器;
4.載入R0中的位元組碼指針和R1中的位元組碼大小;
然後跳轉到另一個已被重命名為EXECUTE_BYTECODE的函數,該函數會執行以下操作:
1.保存原始狀態標誌;
2.推送被稱為VM_MARK的0x1024值,因為它實際上用於識別保存的原始上下文的邊界;
3.執行跳轉到名為VIRTUAL_MACHINE的真正主常式,其目的是解析和執行輸入位元組碼(記住R0和R1)。
EXECUTE_BYTECODE函數代碼
VIRTUAL_MACHINE函數執行圖最初可能看起來有點複雜,但它是由許多單獨的塊組成的,這些塊非常有助於我們理解流程。
VIRTUAL_MACHINE函數執行圖
對虛擬機的分析
現在我將對虛擬機體進行分析,同時我還將詳細分析兩個簡單的虛擬指令。所有對ARM寄存器的引用都將僅適用於以前分析的樣本。這個分析方法的關鍵點是提供了一個關於如何逆向虛擬機的方法,但不能將其視為虛擬機混淆問題的常規解決方案。虛擬機循環的保護實現遵循了一個相當常見的執行順序,但其他解決方案雖然相似,但可能採用完全不同的方法。
執行虛擬機循環的步驟如下:
1.在進入循環之前,堆棧指針回退到VM_MARK,這樣虛擬機就會保存一個指向滾動地址的指針並用它來訪問所謂的VM_REG_CTX;
2.對以下值進行初始化的三個寄存器:
2.1 VM_BYTECODE_SIZE:該寄存器包含位元組碼的大小;
2.2 VM_BYTECODE_PTR:該寄存器包含指向位元組碼的指針;
2.3 VM_BYTECODE_INDEX:該寄存器包含虛擬PC。
如下所示,循環被真正執行並且可以被轉換為更高級別的代碼。
每個虛擬操作碼具有不同的語義,相應地更新VM_REG_CTX和VM_BYTECODE_INDEX
我要看的兩個虛擬機指令分別被命名為VM_NOP和VM_CALL,然後執行上下文分析。
R4 = VM_BYTECODE_PTR
R5 = VM_REG_CTX
R12 = VM_BYTECODE_INDEX
VM_NOP
這是所有指令中最簡單的,正如其名字所包含的意思那樣,它什麼都不做,實際上NOP代表的是無操作的意思,這樣虛擬PC就增加了1並將其進行了保存。
VM_NOP虛擬指令的本地代碼
VM_CALL
這是一個重要的虛擬指令,用於調用虛擬代碼中的所有函數或API,掛鉤下面的代碼將有助於理解反調試技巧和解包階段,然後再跳到保護的第二本地存根。
VM_CALL虛擬指令的本地代碼
編寫de-virtualizer
我採取了以下步驟,來編寫de-virtualizer:
1.在共享庫中標識了所有位元組碼數組和位元組碼大小;
2.執行已經被手動執行,並且ARM代碼已被轉換為更高級別的Python代碼;
3.每個虛擬指令的語義已被轉換為偽ARM指令序列,如果需要的話,可以使用臨時虛擬寄存器的支持;
4.如果將位元組碼及其大小輸入到Python腳本中,將導致一系列ARM塊的輸出;
雖然Python代碼不是足夠好,但足以分析樣本中的虛擬化代碼並用作構建類似de-virtualizer的基礎。真正的挑戰是了解虛擬機如何處理條件控制流程(因為它完全基於虛擬CPSR寄存器的值),並正確實現虛擬機使用的輔助函數的語義,例如,算術,位測試和控制流程函數。
所討論的該樣本具有四個位元組碼虛擬化序列,其中第一個和第三個函數的控制流程圖已經生成。
以上是第一個位元組碼序列(左側)和第三個位元組碼序列(右側)的CFG
可以看到,有很多BLX調用,目標地址已經被識別出來。雖然虛擬化代碼最初可能並不完美,但在早期階段,它會指出有哪些操作正在進行以及哪些函數被調用。
較新的反分析技術
較新的反分析技術就是防止將中文翻譯為英文,目前還不清楚虛擬化機制是否已被刪除或徹底改變。分析清楚地表明,在反調試步驟中,代碼執行了很多跳轉,所以看起來虛擬化仍然存在,但在研究階段可能被忽略了。
反調試技術
就像所有反分析一樣,在早期的解包階段都會依靠反調試檢查。特別是,第一個位元組碼序列會將所有的BLX調用嵌入到反調試器函數中,並相應地修改執行,例如用raise(SIGKILL)來終止進程。
在樣本中有一個簡單的反調試檢查列表:
1.打開proc/self/status並讀取TracerPid的值,確保其值為0;
2.打開linker/system/bin/linker 並讀取函數rtld_db_activity的第一個位元組,如果沒有附加的調試器,那麼該函數只是一個空的存根,而如果連接了調試器,則會在其中放置斷點或未定義的指令,這樣就可以再次保證位元組值為0;
3.監控所有可訪問進程的 /proc//cmdline,以確保某些字元串不存在,例如android_server,gdb,gdbserver等;
5.通過inotify監控進程內存(proc/self/mem和proc/self/pagemap),以檢查在進程空間中是否出現了新的不允許的映射。
在所有反調試檢查都通過驗證後,第二個存根的解包就可以開始了,並且執行過程的步驟如下:
1.對0x2F24D位元組進行內存分配,並將加密和壓縮的位元組流複製到其中;
2.位元組流被解碼,新的內存分配了0x52A88位元組;
3.目標指針(0x52A88)、目標大小、源指針(0x2F24D映射)和源文件大小都準備好了,以用作zlib->解壓縮()函數的參數;
4.未壓縮的位元組流是一個新的ELF文件,這會影響到第二個存根。
內部ELF載入
由於第二個存根ELF文件已在內存中進行了解壓縮,但未被系統正確載入。實際上,本地庫集成了系統ELF載入器和動態鏈接器的一部分,旨在正確初始化第二個存根。該過程可分為以下步驟:
1.libdl.so庫被載入到進程空間中(如果尚未存在);
2.一個0x20位元組的結構由Jiagu360內部分配並用於跟蹤一些重要的值(例如第二存根分配指針,ELF頭指針,ELF程序頭指針,頭大小,程序頭條目數,載入段大小,載入段指針),IDA提供的強大的結構支持;
3.所有的PT_LOAD段都被映射,從文件中填充並正確設置正確的內存訪問屬性;
4.PT_DYNAMIC表將被解析,並且每個動態條目都可以通過基於d_tag類型的big switch-case 正確處理;
5.檢查每個必需共享對象的DT_NEEDED條目,如果庫缺失,則在運行時動態載入它;
6.最後一步是處理重定位表,每個重定位條目意味著一個符號查找,該查找由DT_HASH和DT_GNU_HASH哈希表執行;
7.此時,所有可執行代碼都已在內存中準備好,並且在跳轉到第二個存根的JNI_OnLoad函數之前的最後一步開始執行初始化常式。從初始化函數代碼的圖中,我們可以很容易地看到正在使用的C ++語言的不同指示符,例如,每個函數都會分配一些內存,並將其初始化為構造函數;
8.執行從libjiagu.so傳遞到新載入的ELF文件的JNI_OnLoad條目。
第二個存根ELF標頭以及初始化和終止常式
進行Dalvik的JNI
第二個存根包含的代碼比載入的本地庫多得多。事實上,它的主要目的是識別Android的執行環境(ART或Dalvik),解密並載入原始的classes.dex(如果支持MultiDex,就不止一個)。載入步驟如下:
2.無論ART運行時是否被檢測到一個大小為0x5C位元組的類,如果檢測到Dalvik運行時,則分配一個大小為0x14位元組的類。而每個類則會派生自一個名為Runtime的公共父類,並載入正確的vtable;
3.相比於Dalvik,ART載入要稍微困難一些,這主要是因為在ART載入中,Dalvik位元組碼的文件格式更加複雜,必須在執行之前轉換成優化的格式。這樣在接下來的步驟中,只用解釋Dalvik載入過程既可;
4.在 proc/self/maps中搜索原始(O)DEX文件並進行檢查以確保文件實際上受到了保護,如上說述,使用了「qh」標記,它標識了保護標頭的開始部分和數據部分。(O)DEX映射的起始地址和大小會被保存在一個結構中,另外還保存了指向標標頭分的指針;
5.執行第四個虛擬化函數,其中R0代表指向保護部分的指針的+ 0xC,R1代表保護標頭大小的0x2A0,這樣就會發生一些重要的操作,比如生成一個128位的解密密鑰並且如預期的那樣用0x52 XOR密鑰解碼保護標頭的0x2A0位元組,這樣就會出現以下的元數據序列;
activityName,apk-md5,checkSum和pkg的元數據值已被篡改過
6.元數據列表包含所有類型的信息,這些信息將被載入函數用於重建原始代碼,驗證是否被篡改,並啟用了諸如支持x86代碼或崩潰處理程序的附加功能;
7.所有的元數據都被解析,並插入到一個看起來是radix-tree的地方,其餘代碼將使用密鑰從radix-tree中提取出來;
8.用崩潰報告功能和簽名檢查以驗證.appkey是否發生;
9.解密所有嵌入到保護數據部分(恰好在保護標頭之後)的classes.dex,你可以使用RC4或SM4解碼數據,並使用128位解密密鑰初始化塊密碼,這樣每個classes.dex都會映射到一個新部分,編碼後的數據被複制並解密;
11.在所有classes.dex已經被正確地釋放到內存中之後,是時候修復com.qihoo.util.StubApp1868252644和com.qihoo.util.StartActivity(如果存在)類的某些欄位中包含的虛擬值了:
11.1 包含虛擬值「com.qihoo360.crypt.entryRunApplication」的strEntryApplication欄位被替換為與METADATA key appName相關聯的原始值,在本文中它的值為「com.fake.application.MainApplication」;
11.2 由於本文中的StartActivity類未聲明,所以mEntryActivity欄位不存在。在任何情況下,我都會使用與 METADATA key activityName關聯的原始值來代替mEntryActivity的值;
12.此時,載入過程幾乎完成,getClassNameList函數指針被保護庫提供的本地實現覆蓋。查看com.qihoo.util.StubApp1868252644,我還可以看到許多方法(例如interface5, interface6)標記為native,這意味著它們的實現是通過基於JNI介面的本地代碼提供的。事實上,在返回到Dalvik環境之前,這些介面已被註冊:
12.1 如果啟用了崩潰支持,則為CrashReportDataFactory類的interface9;
12.2 在StubApp1868252644類中,則為interface5、interface6、interface7、interface8;
13.JNI_OnLoad執行結束,又返回到StubApp1868252644類,在該類中創建strEntryApplication的新曆程,並調用內部附加函數來設置由本地代碼製作的類載入器;
14.interface8函數被調用,並負責初始化提供的原始應用程序的內容。之後,本地資源也通過調用initAssetForNative方法進行初始化;
15.執行StubApp1868252644的onCreate方法,並負責Java端崩潰報告功能的初始化。然後調用interface7並載入原始strEntryApplication。最後執行interface5並更新添加到path /data/data/
/files/的assetpath;
16.此時,執行最終回到了原始應用程序類,該類在元數據上的值為com.fake.application.MainApplication。
※恆星幣也被黑客盯上了,價值40多萬美元的恆星幣被盜
※Windows 10 RS3中的EMET ASR功能優劣分析
TAG:嘶吼RoarTalk |