標籤:
本文轉載自: http://drops.wooyun.org/tips/9214
Author: @愛博才會贏
本文為烏雲峰會上《Android應用程式通用自動脫殼方法研究》的擴充延伸版。
0x00 背景及意義
Android應用程式相比傳統PC應用程式更容易被逆向,因為被逆向後能夠完整的還原出Java代碼或者smali中繼語言,兩者都具有很豐富的高層語義資訊,理解起來更為容易,讓程式邏輯輕易暴露給技術能力甚至並不需要很高門檻的攻擊者面前。因此Android應用程式加固保護服務隨之應運而生。從一開始只有甲方公司提供服務到現在大型互連網公司都有自己的加固保護服務,同時與金錢相關的Android應用程式例如銀行等也越來越多開始使用加固保護自己,這個市場在不斷的擴大。
一個典型的加固保護服務通常能夠提供如下保護:防逆向,防篡改,反調試,反竊取等功能。加固服務雖然不能夠避免和防止應用程式自身的安全問題和漏洞,但能夠有效保護程式真實邏輯,保護應用程式完整性。但是這些特點同時也容易被惡意程式利用,有資料表明隨著加固保護的流行,加殼惡意程式的比例也在不斷上升。一方面惡意程式分析需要先脫殼,另一方面正常的應用程式如果被輕易脫殼後分析,其面臨的風險也會上升。
0x01 研究對象
通常加固服務提供DEX的整體加固方案和定製化的加固。定製化的加固通常需要與開發更為緊密的結合,可能涉及更深層次加固(如native代碼加固等),而DEX整體加固只需要使用者提供編譯好的Android應用程式APK即可。前者目前缺乏樣本並需要與加固廠商深度合作,而後者被大多數加固服務廠商作為最基本的免費服務提供,因而後者被使用的更為廣泛。本文主要研究對象是針對後者的Android應用程式可執行檔DEX的保護,即DEX檔案加密,旨在研究通用的DEX檔案恢複方法。而定製化的加固服務或針對native代碼的混淆保護等不在本文研究範圍內。
0x02 加固服務特點
我們通過一個靜態逆向加固方法的例子來詳細描述加固服務通常具有的特點。該例子是幾個月前某加固廠商使用的方案,由於加固服務經常變換解密演算法和方案,因此實現細節並不適用於現在的產品,或其他加固服務,但整體的加固思想和方法和使用的保護手段基本上大同小異。
通常當我們用靜態工具分析一個加固後的APP時,AndroidManifest.xml檔案裡會在保留原始的所有資訊,包括定義的組件、許可權等等的基礎上,新增一個進入點類,通常是application。
而DEX的代碼是這樣的。
DEX代碼只包含很少的類和代碼,其主要是做些檢測工作或者準備工作,然後通過載入一個native庫去動態載入原始的DEX檔案。由於使用了動態載入機制,因此加固過的DEX檔案中不會涉及原始DEX的真正代碼(也有一些加固並沒有採取完整DEX的動態載入)。
接著使用IDA去逆向進入點載入啟動並執行native代碼,通常so庫也是被混淆加殼的。手段包括破壞ELF頭部資訊讓IDA解析失敗,如:
通過readelf可以明顯看到ELF頭部的幾個欄位是有問題的。
修複之後,IDA可以正常反組譯碼so檔案了。接著我們從進入點開始分析,會發現F5反編譯成C代碼會有問題,多個函數內容都不能反編譯成正常的C代碼。直接看彙編代碼看到如下的花指令:
這是我們總結的該產品的花指令模式。它會通過壓棧跳轉出棧的方式讓反編譯的函數辨識出現問題,因為反編譯通常會認為一個壓棧操作為函數調用,而其實他通過壓棧,計算寄存器值,跳轉再出棧讓反編譯失效後並平衡棧後,再執行一條真正有用的指令。因此上述例子中只有兩條真正有用的指令。
通過寫指令碼甚至是人工的方式可以把真正的彙編指令提取出來。提取後再逆向代碼,其功能是去解密JNI_OnLoad函數。JNI_OnLoad會從一段資料中再解密出另一個ELF檔案,而此時這個新的ELF檔案還不能正確反組譯碼,後面的代碼會接著對該ELF進行資料的修正。先解壓新ELF檔案中的text端,從text端中提取一個key再去解密rotext,最後才解密出一個真正的對DEX的殼程式,形如:
以上步驟其實是一個ELF檔案的殼。新的被解密修正後的ELF檔案才是真正對DEX殼的解密程式。這個程式並沒有混淆或者加殼,通過逆向後發現,他會取原始DEX後的一段padding資料,擷取一些解密和解壓需要的參數,對整段padding資料解密解壓,就能得到真正原始的DEX檔案了。當然ELF中還包括一些反調試反分析的代碼,由於我們這是靜態分析,不需要顧及這部分代碼,如果是使用調試器去附加進程使用dump等動態分析時就需要考慮怎麼優雅的bypass這些反調試技巧了。
以上例子是一個動態載入DEX的例子,雖然不同的加固服務在很多技術細節包括解密演算法、花指令模式、ELF殼等等上天差地別,但基本上能夠代表絕大多數使用動態載入DEX方式的加固服務的整體解密釋放運行和靜態逆向和破解它的思想方法。我們也是以這個例子來管中窺豹。因為頻繁的變換解密演算法和加固方式也是加固服務的第一大特點。
同時事實上還存在一些加固並沒有使用完整DEX檔案的動態載入機制,而是使用運行時動態自修改,這種機制下加固後的DEX檔案中將存在原始DEX中的部分準確資訊,但受保護的部分代碼還是會選擇其他方式隱藏。另外還有兩者相結合的方式。後面的案例分析中我們將有所涉及。
總結一下,一個加固過的Android應用程式實際上主要是隱藏真正的DEX檔案,其自身也會加入諸多保護措施來防止被輕易逆向。可以看到如果純靜態逆向分析其脫殼演算法會非常耗時耗力,另外不同的加固服務採取不一樣的演算法,而每個本身又會頻繁變換演算法和加固技術讓純靜態逆向脫殼方法短時間內就失效。同時加固服務還會採取除DEX動態載入以外的諸多安卓應用程式保護措施,我們這裡稍作總結,並不展開,因為這部分內容甚至可以單獨寫文章詳細說。
第一大類是完整性檢驗。包含了在運行時對自身的完整性校正,如檢查記憶體中DEX檔案的檢驗值和檢查應用程式認證來檢測是否被重打包及插入代碼。以及對自身環境的檢測,如通過檢查特定裝置檔案等方式檢測模擬器,通過ptrace或者進程狀態等方式檢測是否被調試,hook特定的函數防止代碼記憶體被讀取或dump等。
第二大類是代碼混淆。通常混淆需要基於源碼或位元組碼上修改,其目的是為了讓分析者更難以理解程式的語義。最常見的包括修改變數名,方法名,類名等,加密常量字串,使用Java反射機制調用方法,插入垃圾指令或無效代碼打亂程式控制流程,使用更為複雜的操作替換原始的基本指令,使用JNI方法打斷控制流程等。
第三大類我們定義為防分析或程式碼後置技術,其目的是為了用各種方法防止程式碼被直接暴露,輕易分析。最常見的就是上述的DEX整體加密保護,以及運行時動態自修改。運行時動態自修改主要是在程式運行時當執行到特定的類或方法時才將代碼解密並執行,同時還可能動態之後才修正或修改部分dalvik資料結構讓分析變得困難。另外一些防分析技術需要利用一些小的技巧。例如利用靜態分析工具的bug,或解析時的特性來做對抗,包括曾經出現的manifest cheating,APK偽加密,dex檔案中的方法隱藏,插入非法指令或不存在的類讓靜態分析工具崩潰等等。
0x03 脫殼方法思想
面對加固程式,當前比較流行和常用的脫殼方法主要是兩種方法。一種是靜態逆向分析,其缺點也很明顯,難度大而且無法對抗變換演算法。另一種主要是基於記憶體dump技術的脫殼。缺點在於需要考慮先bypass各種反調試的方法,同時還需要面對日益發展和新的層出不窮的反記憶體dump的各種技巧。例如篡改dex檔案頭防窮搜,動態篡改dalvik資料結構破壞記憶體中的DEX檔案等等,這些對抗技術讓即使dump出DEX檔案後,還需要做大量的通過觀察加固特性後的人工修複工作。
所以我們提出一種通用的自動化脫殼方法,我們的方法基於動態分析,無需關心各個不同的加固保護具體實現,也可以統一繞過各種反調試的手段,同時也不需要做後期大量的修複工作。
首先我們脫殼的對象是Android應用程式中的DEX檔案,因此我們選擇直接修改Android系統中Dalvik虛擬機器的原始碼進行插樁。因為DEX檔案中的代碼都需要在Dalvik虛擬機器上解釋執行,所有的真實行為都能在Dalvik虛擬機器上暴露。Dalvik有多個解釋模式,其中有portable模式是基於C++實現的,而其他模式由於最佳化的緣故使用平台相關的組合語言開發,為了方便實現我們的插樁的代碼,一旦發現開始解釋執行需要被脫殼的APP時,我們先(源碼目錄dalvik/vm/interp/Interp.cpp)將解釋模式改為portable。這麼做的一個好處在於直接修改執行環境可以讓加殼程式更加難以檢測脫殼行為的存在,相比於調試器附加等方法,該方法更為透明。在解譯器上做的另一個好處在於不需要去關心加固程式在哪個階段進行類的載入和初始化以及解密代碼等,直接在運行時就能得到最真實的資料和行為。插樁代碼實現在Dalvik解釋執行的每條指令切換處(dalvik/vm/mterp/out/InterpC-portable.cpp),這樣可以在執行過程中的任意指令處進行脫殼的操作,一邊應對邊運行邊解密的加固程式。最後基於源碼的修改能夠實施真機部署,Android原生源碼可以完美支援所有的Nexus系列手機,也不需要去應對加固程式的檢測模擬器手段。
脫殼的本質是去擷取程式真實的行為,因此插樁代碼其實就是去得到記憶體中的Dalvik資料結構,來反映被執行的真實代碼。在指令執行時可以直接得到該條指令屬於的方法,Method這個結構。而每個被執行的方法中都有該方法屬於的類對象clazz,而clazz(源碼目錄dalvik/vm/oo/Object.h)中又有pDvmDex(dalvik/vm/DvmDex.h)對象,其中有pDexFile(dalvik/libdex/DexFile.h)結構體代表了DEX檔案,也就是說,執行過程中擷取當前方法後,用curMethod->clazz->pDvmDex->pDexFile就能夠得到這個方法屬於的DEX檔案結構。該結構體中包含了所有DEX檔案在解釋其中被執行時的記憶體資訊,通過解析這個DexFile結構體就能恢複出最真實的DEX。
0x04 簡單脫殼實現
至此,我們的第一個反應是有沒有現成的程式,可以去翻譯Dalvik位元組碼的,但是以讀入記憶體中的DexFile結構體為輸入,同時可以直接基於源碼實現,也就是用C/C++實現的,而不是像更多的靜態逆向工具直接以讀入一個靜態DEX檔案為輸入。找了下發現Android系統源碼裡本身就提供了DexDump(dalvik/dexdump/DexDump.cpp)這個工具,直接能滿足這個要求。我們對DexDump代碼稍作修改,插入到解譯器中,如:
讓他去讀取DexFile,預設就直接在一個APP的主Activity處執行這個代碼,主Activity可以通過AndroidManifest.xml檔案擷取,因為該檔案中的進入點類都不會被隱藏。 我們發現這樣幾乎就能夠應對大多數加固程式了,能夠得到加固程式被隱藏的DEX檔案中的真實代碼,輸出如:
但這個方法的缺點也很明顯,就是輸出是dalvik位元組碼的文本形式,一方面無法反組譯碼成Java,另一方面文本形式非常不適合後續的複雜程式的分析,我們的最佳目的是得到一個完整的DEX檔案。
0x05 完善脫殼實現
通常到上一步,許多其他的脫殼工具為了恢複出完整的DEX檔案,會選擇直接讀取pDexFile->baseAddr或者pDvmDex->memMap為起始地址,直接將整個檔案大小的記憶體dump出來。然而我們發現對某些加固軟體,這樣dump出來的代碼裡依然不包含真實的代碼,這是由於DEX檔案中部分真實資訊在運行時被修改和映射到了檔案連續記憶體以外的部分,如,一個DEX檔案被載入記憶體後,理應是在一個連續的記憶體空間中,然後被解析賦值為各個動態執行時Dalvik所需的結構體,而部分索性性質的結構體應該指向連續的data資料區塊。但加固程式可能會做些修改,例如將header的部分資料篡改,以及重新分配不連續的記憶體來存放data資料,並讓那些索引資料區塊指向的新分配的data塊。這樣如果直接用dump的方法,則無法得到完整的DEX檔案。
我們旨在以統一的方法恢複出原始的DEX檔案,不希望還需要針對的不同的殼來做後續的修複,因為這樣又將進入到和靜態逆向加固演算法一樣的困境。因此我們基於上述簡單實現,有了個更加完善的實現方案,稱之為DEX檔案重組。過程非常簡單,就是在程式執行過程中先擷取所有解譯器所需的Dalvik資料結構,這裡都是記憶體中真實的被解釋執行的資料結構,然後再將這些資料結構組合重新寫回成一個新的DEX檔案。如所示,即使記憶體不連續,我們也無需關心他對原始映射記憶體的操作,可以直接擷取每塊不連續的資料,按照一定的規範去把這些資料重組成一個新的DEX檔案。 第一步是去準確擷取每個Dalvik資料機構,為了保證擷取的準確性,我們採取的方式是和運行中解譯器中去執行程式時的擷取方式一致(參考DexFile.h 檔案中的dexGetXXXX方法),因為一個DEX檔案,同一塊資料可能有很多種方式去擷取的,打個比方,常量字串可以去讀檔案頭裡的位移去擷取,也可以通過stringId列表去擷取,等等。正常情況下這些方式都應該是正確的,但是加固程式會去做一些破壞。但它不能去破壞運行時這些資料被擷取時用的資料,因為這個一旦破壞,程式就無法正常運行了。具體的擷取方式如所示:
我們需要遍曆每個數組(如pStringIds,pProtoIds,…,pClassDefs)裡的某些指標和位移,每項中都逐一擷取,將其內容再合并成一個大類(如stringData,typeList,…,ClassData,Code)。接著擷取完重寫的時候,需要注意幾個問題。首先是對擷取這些資料區塊的排列問題,我們參考了dalvik/libdex/DexFile.h裡的map item type codes枚舉的順序進行排列。排列好需要調整每個資料項目裡的位移值為新的位移,如stringDataOff, parametersOff, interfacesOff, classDataOff, codeOff等,接著對於DexHeader, MapList這兩個結構體中的值,我們需要重新計算後填寫,而不是直接取原來的值,對於一些固定的值例如Header裡面的檔案頭等,我們根據已有知識直接填寫。最後需要考慮到記憶體中的資料表達和DEX檔案中的某些資料格式的差異,例如有些資料項目在檔案中是ULEB128編碼的,而在記憶體中就直接是int類型,另外還需要注意4位元組的對齊,以及encoded_method_format裡是field_idx_diff,method_idx_diff而不是簡單的index等。具體細節可參考官方的DEX檔案格式文檔
https://source.android.com/devices/tech/dalvik/dex-format.html
我們在重組的時候忽略了一些資料區塊,例如所有和annotation相關的資料結構,因為這部分真實程式使用不多,而結構又特別複雜,忽略以後對剖析器真實行為影響不大。
0x06 實驗與發現
改完代碼後,我們重新編譯了libdvm模組並將新產生libdvm.so寫入系統目錄/system/lib/下覆蓋掉原始的庫檔案,我們實驗的對象是Galaxy Nexus手機對應Android 4.3版本和Nexus 4手機對應的Android 4.4.2版本。然後我們提交了一個簡單的應用程式,送到各個線上加固服務上擷取加固後的應用程式版本再實施脫殼。實驗發現幾乎能夠針對所有的加固程式恢複出原始的DEX檔案。以下是一些針對加固程式的發現。主要集中在不同的加固所使用的自我保護的手段,這裡有些結果是DexDump的文本,因為有些保護措施用這個方式更好的展示出細節,當然全部都能直接恢複成DEX檔案。
以上兩個例子表明,有些加固程式會將magic number抹去,來隱藏記憶體中的DEX檔案,讓窮搜DEX檔案的方式失效,另外還會篡改header的大小,以及將header中的各種欄位位移值抹去,由於我們用的方法是對header重新計算,因此重組後的DEX不受其影響。
另外有些加固程式會額外插入一些類來破壞正常的反編譯效果,例如這個類就有個方法是能夠讓dex2jar失效。
還有殼將codeOff改成了負的值,這樣代碼就會被映射到檔案記憶體範圍之外。我們的方法可以直接將代碼擷取後重新寫回到正常的位置。
另外還有殼是重寫了某些方法,將代碼放入一個新的方法中,並在執行前去解密,執行後再重新抹去。對於這種情況,由於我們脫殼代碼插樁於每個方法調用處,因此我們只需要調整脫殼點到該方法執行處去實施脫殼就能恢複出代碼了。
除以上例子外,我們還發現某些加固程式會hook進程空間中的write函數,檢測寫的內容如果是特定的資料(如dex檔案頭),則讓write操作失敗,或者擷取記憶體位址空間在映射的DEX檔案地區內,也會讓write失敗等。還有加固程式會將原始的DEX檔案分離出多個DEX,以及修改特定的資料項目如debug_info_off為錯誤值,運行時再動態改回正確值。還有殼會在位元組碼基礎上對原始的程式做代碼混淆。
(註:以上例子都並非最新版本,不保證特定的加固程式現有產品與上述例子依然一致)
0x07 討論與思考
首先我們的方法依然有局限性,一來在研究對象裡說明了我們只針對DEX檔案加密保護,並不做反混淆的工作。其次我們的方法依然是基於動態分析,將面臨動態分析的局限性,如一段加密代碼是運行到才解密,但該方法無法被觸發執行,我們的方法也無法解密這個方法的代碼。最後用該方法雖然難以被加固程式檢測,但用該方法製作的工具在實現上勢必會有某些特徵,這些特徵可能會被加固程式加以利用和對抗。
最後是我想和大家一起探討的關於更好的Android平台應用程式加固的想法。事實上Android平台的加固破解還是相對容易的,然而並不是沒有更難更安全的加固方案,而是在手機平台上商用的加固方案需要考慮到效能損耗和相容性的問題,這是無法避免的。同時綜合這幾個方面,我覺得加固保護的趨勢和做法發展主要集中在以下的幾個點。
一個是我覺得Android混淆和加殼其實可以結合使用。從攻擊者的角度來看,我認為強力的混淆可能要比加殼在保護代碼邏輯方面更加有效。但是好的混淆方案事實上非常難以設計。目前來看國內的加固幾乎不會對原始的代碼做大的變換和混淆,可能是怕修改的代碼在相容性上會有問題。我認為這是一個發展點。我發現國外比較優秀的工具會在深度混淆這個點上做文章,比如dexprotector,他既有加殼,也有混淆,即使脫殼成功,還是需要去面對難以理解的混淆後代碼。
另外我覺得部分加固的效果在安全性上可能要強過整體加固。就像之前的一個例子,一個方法只有在運行時才解密自己,一旦脫離運行則重新加密或抹掉。這個等於是利用了動態執行覆蓋率低的缺陷來進一步保護自己。
第三個就是為了更好的加固效果,加固過程應該儘可能從現在的開發後加固變成開發中的加固。現在有一些加固SDK就是這方面比較好的嘗試。直接在開發的過程中敏感的操作使用一個安全存放庫的介面。這個無論是在效能上還是效果都可以對現在的整體一刀切式的加固做個質的提高。熟悉業務的開發人員會很清楚他們需要保護的代碼是哪一部分,因為一個程式事實上真正需要被保護的邏輯可能只是很小一部分,加固範圍的縮小可以大大提高效能,同時單獨的安全存放庫檔案可以有針對性的保護措施,效果會非常好,另外比起整個APP加固也更容易做的相容性測試。
加固另一個思路是儘可能用Native的代碼,特別是關鍵的程式邏輯,Native代碼逆向本身就比Java困難,加了混淆或者殼後就更難了,同時Native代碼事實上還能在效能上有所提高,是一舉兩得的方案。由此又可以延伸出如何對Android應用程式中native代碼做深度保護的問題,如果敏感操作用深度混淆保護的native代碼做保護,則攻擊成本勢必將極大提升。
最後我覺得加固保護的一個趨勢是盡量少的去利用小trick來做防護,比如那些利用靜態分析工具的BUG或者系統解析APK的BUG來做加固其實意義不是很大,加固保護更應該從整個電腦系統的體繫結構上來考慮和強化,而不應該集中於一些小的技巧。
[轉載]Android應用程式通用自動脫殼方法研究