之前對Android的兩個運行時的源碼做了一些研究,又加上如火如荼的Android加固服務的興起,便產生了打造一個用于脫殼的運行時,于是便有了DexHunter的誕生(源碼:https://github.com/zyq8709/DexHunter/)。今天,我就通過這篇小文聊聊我的一些簡單的思路,供大家參考和討論。
首先,先來看一看Android運行時的一些相關機制,看看我們來怎么搞。
首當其沖,要脫殼少不了研究一下Dex文件的格式,這一點Android的官方文檔寫的已經很清晰了,我這里就簡單再提一下。整個結構便如圖1所示:
圖1 Dex文件結構
其實就是分區段存儲不同的內容,在頭部里有指向各個區段起始的偏移值。當然我們最關心的就是class_defs和data這兩個段了。
class_defs包含了所有的類,用class_def_item來描述。圖2是對class_def_item展開的一個示意圖:
圖2 class_def_item結構
每個class_def_item指向一個class_data_item,每個class_data_item 包含了一個class的數據,每個方法用encoded_method結構來描述,它又指向了一個code_item,這個里面就保存著一個方法的所有指令。
對于ART下,安裝后的dex文件會被編譯為oat文件,這個oat文件其實是一個ELF文件,圖3是它的一個結構:
圖3 OAT文件結構
其中可以看到oatdata指向的部分包含了原有的Dex文件,這個是我們的目標。當然oatexec指向了編譯后的ARM指令,但是對于我們暫時來說沒有什么卵用。
為了脫殼,我們要建立一個概念,就是“時機”。對于非虛擬機殼,從內存中轉儲是一個最為有效和統用的技巧,那么就必須要找到一個時機,保證內存中的數據是完全正確的。
在Android中呢,便有這么四個時機:
打開Dex文件
就是把APK中的dex文件提取并做cache,那么最終打開的其實是odex或oat文件;
加載Class
運行時讀取存儲在Dex中的每個class,并用來填充一個生成的Class對象,其中包含了class的所有成員,這樣一個class才能被使用;圖4表示了ART和DVM下的Class對象的結構
圖4 Class的結構
初始化Class
如果一個class有static塊,那么這個部分就會編譯為類的初始化器,具體看說就是
調用具體的方法
不用多說,就是根據生成的Class對象查找到具體的代碼指令并執行了。
好,那我們怎么做呢?很簡單,我們就從類的加載開始。
總的來說,有兩種可以加載類的方法,一個是顯示加載,主要用于反射,就是通過調用Class.forName()或ClassLoader.loadClass()方法來主動加載一個類;另一個是隱式加載,主要是通過創建第一個class的實例或在類產生前訪問靜態成員時發生。這些操作的背后在運行時中是有相應的函數來真正完成的。
在ART中:
顯式加載:
ClassLoader.loadClass 對應DexFile_defineClassNative
Class.forName 對應Class_classForName
隱式加載:
對應artAllocObjectFromCode
圖5表述了這個關系:
圖5 ART中的實現
在DVM中:
顯式加載:
ClassLoader.loadClass對應Dalvik_dalvik_system_DexFile_defineClassNative
Class.forName對應Dalvik_java_lang_Class_classForName
隱式加載:
對應dvmResolveClass
圖6是DVM中的實現表示:
圖6 DVM中的實現
很清晰看到,我們找到了關鍵點,在ART中是DefineClass,DVM中是Dalvik_dalvik_system_DexFile_defineClassNative,我們就從這里動手,主要的修改就發生在這里。簡單地說就是主動地一次性加載并初始化所有的類。
這樣做是隱含了幾條原則的:
圖7就是DexHunter的一個工作流程:
圖7 DexHunter原理
下面就分這幾個步驟來說:
(1) 定位內存
對于之前提到的入口函數,都有一個參數表示在操作的文件。
ART中,這個參數是DexFile對象,其中有一個location_成員,是一個字符串,可以簡單的理解為此文件的路徑。那么DVM中是DexOrJar,相對的字符串成員是fileName。這下我們就好整了,只要我們指定了目標字符串,我們就可以從可能使用的眾多dex文件中找出我們想要的那個,而且方便的是,通過這兩個對象,我們還能很容易找到操作的文件在內存中的起始地址和長度。
(2) 主動加載并初始化
這個就是遍歷dex文件中class_defs區段里每一個class_def_item,并逐一加載和初始化,在ART里我們使用FindClass函數來加載類,EnsureInitialized進行初始化;在DVM中用dvmDefineClass加載,dvmIsClassInitialized 和dvmInitClass來初始化。
(3) 轉儲并自動修復
最后就是真正抓取dex了。把dex分為三部分:
我們把Part 1存在part1文件里,Part 3存在data文件中,Part 2先不要急。
現在我們要解析class_defs的東東了。不整代碼了,用文字簡單來說,就是模仿Android的過程,我們把每個class_data_item解碼為內存中的對象(有LEB128編碼),便于我們的修復。
下邊就要進行一些判斷看需不需要修復:
看class_def_item中的 class_data_off是不是在之前拿到的dex文件的內存范圍內,如果跑出去了,就需要把這個類的class_data_item給放到dex尾部去,修改class_def_item并保存。
比較解析出來的accessflag、codeoff和運行時生成的方法對象的accessflag、codeoff,如果不一致,以運行時中的為準,并修改保存。
同樣,檢查code_item_off是否出界了,一旦出界,把code_item收回來,繼續向尾部添加,并修改class_def_item的相關內容重新保存。
當然了,所謂放到尾部,只是先保證偏移值從尾部開始的,真正的內容先存在extra文件了。被修改過的class_defs段,就保存在classdef文件中了。
然后我們把四個文件重新拼起來,就得到原始的dex或odex了。
最后聊一下我們看到的一些有趣的現象。
360基本上是把原始的dex加密存在了一個so中,加載之前解密。
阿里把一些class_data_item和code_item拆出去了,打開dex時會修復之間的關系。同時一些annotation_off是無效的的來防止靜態解析。
百度是把一些class_data_item拆走了,與阿里很像,同時它還會抹去dex文件的頭部;它也會選擇個別方法重新包裝,達到調用前還原,調用后抹去的效果。我們可以通過對DoInvoke (ART)和dvmMterp_invokeMethod (DVM)監控來獲取到相關代碼。
梆梆和愛加密與360的做法很像,梆梆把一堆read,write, mmap等libc函數hook了,防止讀取相關dex的區域,愛加密的字符串會變,但是只是文件名變目錄不變。
騰訊針對于被保護的類或方法造了一個假的class_data_item,不包含被保護的內容。真正的class_data_item會在運行的時候釋放并連接上去,但是code_item卻始終存在于dex文件里,它用無效數據填充annotation_off和debug_info_off來實現干擾反編譯。
https://source.android.com/devices/tech/dalvik/dex-format.html
/libcore/libart/src/main/java/java/lang/ClassLoader.java
/libcore/libdvm/src/main/java/java/lang/ClassLoader.java
/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
https://github.com/anestisb/oatdump_plus#dalvik-opcode-changes-in-art