2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

Editor發表於2020-10-28

Dex檔案是程式碼加固的保護核心,也是App安全的核心所在。


然而目前常用的DexVmp虛擬保護技術尚存在不足之處。虛擬化後的位元組碼透過對映表,和系統原始位元組碼對應。透過逆向仍然有可能讓攻擊者找到對映表或者對映關係,從而還原原始指令。


為此,很有必要對現有的Dex加固方案進行革新,擺脫對系統位元組碼的依賴,用更完善的保護技術穩定地對Dex檔案進行高效保護,才能防止App被逆向破解而洩露原始碼,最大限度地保障App的安全性。


下面就讓我們來回顧看雪2020第四屆安全開發者峰會上《 DexVmp最新進化:流式編碼》的精彩內容。



演講嘉賓



2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼


曹陽, 360加固保團隊負責人,畢業於北京郵電大學。從事移動安全十年,深耕移動應用安全領域,主導研發360加固多項核心技術。擅長移動端程式碼保護、逆向破解和黑灰產對抗等。

目前專注於移動平臺新型vmp引擎(dex、so)的研究、自動化合規檢測平臺搭建和基於加固的黑灰產防禦技術演進。



演講內容



以下為速記全文:

大家好,我是來自於360加固保的曹陽。今天帶來我們團隊最新的研究,就是流式編碼。首先我會介紹一下背景知識,然後會介紹一下目前各代的Dex殼的特點和弱點,接下來是針對它DexVmp的設計和弱點進行介紹,最後會引出我們對DexVmp的改進。

一.背景知識


首先,目前Dex這塊的保護方向分兩塊,一個是Java,一個是Native。先來看Java,它的一些破解工具和指令碼越來越多,要破解是一件比較簡單的事情。從最根本上來說,如果我們修改直譯器的話,是可以做到針對Java的指令級別的跟蹤的。對於Native的話,它會有一些缺點,首先它保護函式的話會使保護的點比較分散,其次保護越多的函式它的體積也會增大。

Dex格式的話就是我們搞安卓的小夥伴非常熟悉的,主流的Dex格式分三部分,Dex頭,各種常量索引表,還有data區。從後面的分析指令就可以看到,Dex索引表在指令中主要都是透過這個值來訪問它的下標,data區基本上都是透過偏移來指向。我們這次的改進方案主要是解決在指令中如何處理偏移的問題。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼


這裡的展示是以二進位制的形式來展示一下Dex主要的分佈。我們可以看到,從中間開始都是以陣列形式存在的各種索引表,指令透過檢索的話就是檢索這些具體的結構體。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

至於Dex指令的話,這兩張圖是我從谷歌官網上摘抄下來的,它儲存格式有16種,編碼方式有32種,指令總數有256個。例如mov指令,它的編碼方式是12x,對應左邊這個編碼方式就是B|A|op,它是按位元組去倒著排列的。

舉一個例子,第一個是最簡單的指令move,是把v0的指令移動到v1。第二個,移動v1的內容到v0的暫存器,如果說v3等於v11的話,就跳轉到x66的位置。第三條指令也是比較典型的,它會牽涉到一個物件引用,意思就是說我們會檢查v1暫存器當中的物件並引用,然後去我們當時說的DexType這個表中找到相應的型別,檢視是否可以轉化。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

二. Dex攻防演進


接下來我會介紹一下每代Dex殼的特點和弱點。先來做一個總結的話就是,加固方案在不斷演進的過程中,是原生資訊不斷消亡的過程

從檔案粒度來看,檔案加密、記憶體載入、格式對抗和檔案打散都是不斷演進的,一開始就是加密,記憶體載入更進了一步,就是我們在使用的時候不會生成檔案,這兩種方式透過記憶體dump都可以很輕鬆地繞過。而格式對抗是一些小技巧,就是會加一些無用的索引。這裡面比較有效的就是檔案打散,Dex在記憶體中,包括檔案加密、載入、破解的核心都有一個前提,就是Dex在記憶體中一定是連續存放的。但如果說我們把它打散,即使把記憶體都拿下來,還需要做各種各樣的拼接和重組,這種的話難度也比較高。

接下來看函式粒度的保護,函式粒度也分兩種。第一種是屬性偽造,之前出現過類似這種的函式保護方案,就是把它的屬性進行偽裝,等執行的時候再還原。第二種方法就是目前更主流的方法體抽空,包括類初始化回填、呼叫時回填以及永不回填。其中Dexhunter負責主動呼叫、記憶體重組;Fupk3解決主呼叫問題;Fart解決ART等高版本問題,並結合frida達到更精細的效果;Youpk則會深入直譯器。

接下來就是指令粒度的保護,分為Dex2C、Java2C以及DexCFG。其中,Java2C的適配性是比較有問題的。Dex的控制流轉化,是在控制流的粒度上做轉換,其實控制流的整個邏輯是可以千變萬化的,這當中也會造成一些資訊的缺失,目前也有一些開源的工程,但是BUG是比較多的。

Dex2C它的核心思想和vmp是一樣的,這個其實很簡單,相當於我們把整個控制流都轉變為更細粒度的概念:跳偏移、查引用、使用暫存器陣列,可以更方便地實現指令粒度的保護。得益於此,目前也有一些比較成熟穩定的產品。但Dex2C的缺點我剛才也提過,就是加固的函式越多它的體積就越大,而且它的底層也是要透過env函式來承接的。如果說我們用vmp的話,相當於可以把所有的防禦點都集中在vmp解釋引擎上,可以加上更多更高階的防護。

三.DexVmp虛擬化設計


2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

下面來說說虛擬化目前的一個核心概念,左邊是系統的一個Opcode和指令,它還是要依賴系統,也不算是一個完全的vmp。核心的步驟也就是三個,取指、譯碼和執行。

目前的取指也是按照系統的標準,以16位長度為單位,Opcode和指令的對映其實是打散的,現在可以做到每一個版本、每一次加固,它的Opcode和指令都是不一樣的。

譯碼的話也是256個種Opcode,其所對應的指令不同,但相同指令的譯碼方式和系統是相同的,所以說這裡面都有很多可以破解的點。

至於執行的話就是最典型四種:索引暫存器、索引Dex中的Type、Field、String等表、16位為單元的偏移跳轉以及呼叫函式。

接下來我會講一下DexVmp的虛擬化設計,以及為什麼它不太安全。

雖然它能防止很多自動化的破解,但還是有一些弱點的。這是一個整體流程,首先我們會把Java函式轉變為Native函式,就是把函式解釋所需要的資訊提前準備好,並把引數同時抽取出來,分配一個當前函式所需的解釋暫存器的陣列,還會有一些資訊,例如程式碼、名稱等等。如果直譯器準備好了,我們就會進入迴圈的操作,取指、譯碼、執行。在譯碼這塊,最典型的就是譯引用,不管是對Dex這種常量索引表進行解析還是去呼叫JNI函式,最終它的出口其實都是JNI函式。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

這裡我把譯碼分成三類,暫存器、引用和偏移。暫存器的話在指令中是以idx值存在的,在暫存器陣列中檢索。每個函式它的暫存器陣列的數量都是提前定好的,個數為registerSize。引用中指令也是以idx值存在的,在IDs陣列中去檢索,解析Dex結構,找到相應資訊,為初始化jclass/jfieldId等做準備。最難的是偏移,在指令中它是以常量存在的,而且是按2位元組為基本單位的,在Dex指令中的IF、Switch、Go流程等等中,都會涉及到偏移的操作。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

接下來就是一些譯碼的方式,首先看函式呼叫,這個函式需要4個引數,0521就是當前的v5、v0、v1、v2,來表示指標和函式所需要呼叫的值。解釋到這塊指令的時候,核心的操作就是首先找到相應的表和對應的類,把其讀出來,然後透過一些流程對其進行呼叫。如果我們呼叫的這個函式也是被虛擬化的呢?同樣在直譯器的入口也要把一些東西準備好。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

下面我們再來看流程控制。這個是switch指令,這裡我主要說說偏移是怎麼回事。首先00代表的是V0暫存器,變數值要從V0暫存器當中找,以其為索引,在陣列中找到偏移,再用當前的PC加上這個偏移,就會跳轉到相應的地方,再繼續取指、譯碼、解釋等等。

異常處理這塊的話,也是比較典型的,首先我們針對大部分指令的話,在解釋之後都會檢查當前是不是發生了異常。如果發生了異常,我們就會記錄當前的PC,然後遍歷DexTry陣列,找到某一個塊的Handler。找到之後會記錄,跳過去繼續執行流程。如果沒有找到的話,就會把這個異常丟擲來。

這個是在Dex異常的一個結構,首先DexTry是放在insns指令之後,以陣列形式儲存的,最重要的是,它Try塊對應的Handler發生之後怎麼去處理。

Handler的每一項都是一個陣列,可以表示處理多個異常,如果說捕獲到相應的異常的話,就會到對應的地址,我們就會把PC移動到這塊繼續來處理。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

上面是幾個典型譯碼的過程,其實透過我們剛才的分析可以看到它主要有以下幾個核心缺點。第一個就是它存放快取程式碼、引用等很多都要依賴Dex結構,例如解析DexTry等等。第二個就是對映關係,它是要依賴於系統的操縱碼來實現整個的執行的。最後就是Opcode、暫存器、引用等等,它的編碼長度都是固定的。

這些其實都是很多破解者依賴的資訊,所以我們如何能把所有的這些缺點給克服呢?

核心就是我們要能夠自己定義操作碼,不依賴Dex,不依賴系統,自己定義操縱碼之後,Opcode等等這些都是可以自定義的。


四.改進與展望


我們的改進方案大概是這樣的。我們來看直譯器的入口和出口,在它的入口我們要準備args,還要準備當前的位元組碼資料流,並針對每個直譯器要分配好暫存器陣列。直譯器出口分兩類,一個是直接把整個函式退出,還有一個是env函式。

在直譯器、入口出口都準備好之後,中間的這部分過程都是我們可以自己控制的,例如取指令、讀寫暫存器、譯碼暫存器idx、譯碼引用id、譯碼偏移。如果說把指令給改了的話,偏移是最難處理的一塊。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

首先取指令的話,因為取指令是我們直譯器自己控制的,opcode無固定長度,可以自定義變長指令,跟系統指令無對映。

讀寫暫存器我們也是自己控制,因為暫存器陣列都是我們分配的。

譯碼暫存器idx的話,因為每個函式它的暫存器最多也就是registerSize個數,最高的編碼位數16位,基本不需要那麼多位數的編碼。所以在指令中,idx沒必要按照系統編碼,只用於內部解釋。

譯碼引用的話涉及到我們會去查詢系統當中的String表等等,查詢也是我們自己查詢的,沒必要一定要去Dex這個結構中去查詢。我們把要虛擬化的位元組資料流已經扣出來了,相關的結構都可以自己去設計,自己查詢並按照自己的設計原則,怎麼方便怎麼來。

但影響最大的是譯碼偏移這塊。我們剛才也看到了,在真正跳轉的過程中,它其實是一個硬編碼,會跳轉到以當前PC為基準的某個偏移,或者直接跳轉到相應的地址做一個偏移。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼


怎麼處理偏移呢?左邊這塊是Dex所有受偏移影響的指令,包括goto還有流程控制、異常處理等等。以goto為例,這裡有一個內定前提,偏移必定不會截斷指令。如果我們跳轉到某一個地址,這個地址不會跳轉到指令中間,這個也是正常的。

所以說在這個大前提下,我們可以把偏移轉變為陣列項的索引。當我解釋的時候,我其實會預先把當前要解釋函式的所有指令都讀進來,每一個指令都是有它自己的譯碼方式的,雖然是變長,但是從邏輯上我會把這些指令都放在一個陣列中,跳轉也就自然而然地轉變成我要跳轉到哪一條指令。

之前的話可能就是地址,轉化之後其實就是我要向後或者向前移動多少條指令。這條解決之後,我們的變長問題就解決了。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

解決變長問題之後,可以自定義各種各樣的編碼。這塊我們實現的就是流式編碼,所謂流式編碼安卓這邊有一個比較典型的8位的流式編碼,這塊就相當於它會把它要編碼的資訊分散到七個bit位元組當中。

我們設計這種編碼主要是因為它比較方便、靈活,可以用多種位數編碼。另外,剛才我也提過虛擬函式是有入口和出口的。由於每個虛擬化的函式在邏輯上是隔離的,所以說這樣就可以達到每一個虛擬化函式編碼的bit個數都是不同的。

2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

總結一下剛才我說的改進方案。首先我們的指令是轉化了原先的指令,指令肯定是變長的。其次,指令的編碼是不固定的,每個版本不同,不同版本的函式也不同。另外,不會再用Dex容器去儲存相關的快取程式碼和相關的常量池,也不會再用相關的這類結構,因為這些都是公開的。最後,指令Opcode對應的Handler也是不固定的,如果有感興趣的小夥伴可以加微信要樣本,謝謝大家!


本屆峰會議題回顧



2020看雪SDC議題回顧 | 逃逸IE瀏覽器沙箱:在野0Day漏洞利用復現

2020 看雪SDC議題回顧 | LightSpy:Mobile間諜軟體的狩獵和剖析

……

更多議題回顧盡情期待!!




注意:關注看雪學院公眾號(ikanxue)回覆“SDC”,即可獲得本次峰會演講ppt!

其他議題演講PPT,經講師同意後會陸續放出,請大家持續關注看雪論壇及看雪學院公眾號!




2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼
- End -




2020 看雪SDC議題回顧 | DexVmp最新進化:流式編碼

相關文章