* 本文原創作者:漁村安全,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載
本文主要講解Android虛擬機動態調試背后涉及到的技術原理,除了JDWP協議細節,還包括任意位置斷點、堆棧輸出、變量值獲取等基礎調試功能的具體實現。另外本文提供了一款新的android動態調試工具——AVMDBG,提供調試API接口,支持python腳本擴展。作為android調試技術研究過程中的實驗項目,AVMDBG功能尚不完善,開源出來僅供參考,如過有bug或其他疑問反饋歡迎提交issue。
一、Android動態調試方案
在講解android動態調試實現之前,先回顧下針對apk進行動態調試常用的幾種方法,比較下不同解決方案的特點和存在的問題:
1)smali插樁大法,反編譯apk后在關鍵位置插入smali代碼,通過打印日志的形式進行調試跟蹤。但是這個方法非常繁瑣,除了需要編寫smali代碼外,每次新增日志輸出點都需要重新解包重打包,而且經常會遇到apk保護導致解包失敗的問題需要解決。
2)IDA在6.6版本以后支持dex文件的動態調試,但是部分功能并不完善,比如監視寄存器變量的值,需要通過watch窗口手動添加并指定類型,而且單步過程中需要留意類型變化,可能導致虛擬機意外崩潰,導致這個問題的原因在后續文章中會分析到。
3)JEB,堪稱apk靜態分析的神器,在2.0版本以后開始加入動態調試的功能,最新版本同時支持smali代碼和native代碼的調試,功能可以說非常強大,如果說缺點可能只有一個,和IDA一樣都是商業軟件,收費不菲。
4)andbug開源項目,5年前的項目原作者已經不再維護,原項目只支持linux環境,動態調試關鍵功能也有缺失,例如不支持任意代碼位置斷點等。使用過程中也碰到過一些bug,類似shell的交互方式使用起來并不是很順手。后來有國內開發者anbc對該項目進行了完善改造,新增函數調用監控功能支持配置文件。
5)android studio+smalidea插件,也是一個非常不錯的調試解決方案,作者通過IDE調試器插件的形式支持smali代碼的動態調試,使用過程中偶爾會遇到跳行的bug。smalidea插件同樣是開源項目,作者的作品還有非常知名的dex反匯編工具baksmali等。
6)xposed類的安卓代碼Hook框架,可以通過在函數入口和出口處hook進行調試輸出,但同樣無法對函數內部代碼進行斷點輸出。類似的項目還有cydia_substrate、frida、ddi等,也都是非常優秀的開源項目。
二、JDWP協議簡述
JDWP是JavaDebug Wire Protocol (Java調試線協議)的縮寫,簡單來講,就是調試器和目標虛擬機進行調試交互的通信協議,調試器通過JDWP協議來獲取JAVA虛擬機中程序的信息或控制目標程序運行狀態,比如函數斷點設置,線程狀態、變量值獲取等,而目標虛擬機也在指定事件觸發時通過JDWP協議通知調試器進行處理,比如斷點事件消息、線程創建消息等。
[1] JDWP協議交互簡圖
Android虛擬機雖然和普通java虛擬機存在不少差異,但是它的調試接口同樣是基于JDWP協議的,Dalvik虛擬機JDWP服務端的實現源碼位于:./android/dalvik/vm/jdwp。androidstudio、eclipse 等IDE的調試功能和DDMS的監控功能也都是基于JDWP協議實現的。
JDWP協議通信要求首先進行握手會話來表明互相的身份,調試器端發送的與虛擬機返回的數據包內容一樣,內容為“JDWP-Handshake”,通過驗證以后才可以繼續后續的會話。
[2] JDWP協議通信前需要進行握手會話
Android虛擬機的JDWP實現支持adb和socket兩種通信方式,可以使用adb的jdwp命令進行端口轉發綁定,這樣就可以通過socket和調試目標進程進行通訊,命令格式為”adb forwardtcp:[port] jdwp:[pid]”。
JDWP協議的通信會話主要包含2類數據包,分別為Command packet(命令包)與Reply packet(回復包):
1)Command packet:調試器發送給虛擬機用于獲取程序狀態信息或控制程序運行;虛擬機發送給調試器用于通知事件觸發消息。
2)Reply packet:虛擬機發送給調試器用于回復命令包的請求或者執行結果。
JDWP數據包主要包含包頭和和數據兩部分,包頭部分格式長度固定,為11字節,數據部分為可變長度,字段結構由其包頭指定的類型決定。下面詳解包頭各字段的含義:
[3] JDWP數據包頭結構簡圖
從上圖可以看出,命令包和回復包的包頭結構基本一致,區別在于最后2個字節,命令包拆分為2個單字節分別表示命令分組和命令序號,在回復包中則用于表示錯誤碼,非0表示命令執行存在錯誤。
以java7為例說明,JDWP協議按功能劃分為18組命令,總計91個不同命令請求,包含了虛擬機、引用類型、對象、線程、方法、堆棧、事件等不同類型的操作命令。JDWP協議支持的命令細節可以參考官方文檔,這里不再贅述。
Android虛擬機對JDWP協議的支持實現并不完整,當然調試需要的絕大部分關鍵命令都是支持的,具體信息可以參考安卓dalvik虛擬機源碼:./android/dalvik/vm/jdwp/JdwpHandler.cpp,下圖是dalvik虛擬機(以android4.2版本為準)支持的命令請求類型表:
[4] JDWP協議命令表簡圖(以android4.2版本支持為準)
雖然上面圖表中的命令類型比較多,但JDWP協議本身并不算復雜,按協議標準進行組包請求即可,下面以獲取目標虛擬機版本信息的命令“VirtualMachine:version”為例,演示一次JDWP協議的交互通信過程:
[5] “VirtualMachine:version”命令返回數據的注解
從上圖的命令注解中可以看出,“VirtualMachine:version”請求命令沒有附加數據,回復包數據中包含5個字段,通信數據包解析過程如下圖:
[6]“VirtualMachine:version”命令通信數據包解析
從上圖的解析示意中,我們最終獲取到目標虛擬機的版本信息,這中間有2點需要注意:
1)數據包中數據使用大端模式;
2)基本數據類型的內存結構,例如string,使用[長度]+[字符數據]的形式;
下面我們整理下JDWP協議中使用到的其他基本數據類型,在后續的命令請求與數據包解析中都會頻繁使用到。其中有些數據類型的長度是由虛擬機實現定義的,比如ObjectID等,可以通過“VirtualMachine:IDSizes”命令進行獲取。下圖整理的數據類型說明以Android虛擬機的實現為標準:
[7] JDWP協議使用到基本數據類型
三、動態調試的核心 —— 任意代碼位置斷點
針對簡單apk進行逆向時靜態分析就足夠勝任,但是碰到下面幾種情況的時候,“任意代碼位置斷點”的動態調試分析則更加合適:
1)需要對代碼中的關鍵參數、變量值進行觀察,例如加密后的字符串。
2)對偏底層的公共函數庫進行hook輸出,快速篩選定位可疑調用,例如API調用監控等。
3)在代碼規模比較龐大、調用邏輯比較復雜的情況下,需要使用堆棧跟蹤對可疑關鍵點的調用路徑梳理確認。
“任意代碼位置斷點”功能不單指代碼斷點的調試事件通知,還會涉及到虛擬機棧結構、寄存器使用、參數變量值獲取以及堆棧跟蹤等方面的功能,但是其中很多知識點限于篇幅無法在本文中全部講解透徹,這里推薦兩篇關于Dalvik虛擬機的文章可以作為擴展閱讀:《深入理解Android之Java虛擬機Dalvik》、《Dalvik虛擬機進程和線程的創建過程分析》。
下面重點講解斷點功能的實現,其中的關鍵點可以歸結為以下幾個問題:
1) 如何設置和處理斷點事件?
斷點設置需要用到的命令是“EventRequest:Set”,該命令支持多種事件請求,包括斷點、單步、類加載、方法進出、字段訪問、線程、異常等多種事件,設置成功以后目標虛擬機會返回RequestID,并且在事件觸發時會發送相應的事件信息給調試器請求處理(Event:Composite),各類事件命令的具體格式可以參考官方文檔。下面以斷點為例進行講解事件設置,首先看下事件命令請求的結構字段:
[8] 斷點命令請求的數據包字段結構
上面的斷點事件我們只設置了一個過濾器,就是斷點位置Location,這個結構體用于指明觸發事件的代碼位置,包含類型標記,ClassID,MethodID以及代碼偏移DexPc。
1.其中的ClassID和MethodID兩個字段的值可以通過“VirtualMachine: ClassesBySignature”命令與“ReferenceType: MethodsWithGeneric”命令獲取。
[9] 獲取ClassID與MethodId的演示代碼
2.代碼偏移位置DexPc值可以使用反匯編工具baksmali的“-l”參數獲取。下圖紅圈中的數字就是代碼偏移位置。
[10] backsmali反匯編結果中的代碼偏移標記
當虛擬機運行到我們設置的斷點位置以后,會發送“Event: Composite”命令給調試器,目標虛擬機發送的命令中會包含線事件類型、請求序號、線程序號以及代碼位置等信息,其中請求序號和斷點命令返回的請求序號是對應的,調試器可以根據請求序號進行相應后續處理,例如獲取線程堆棧、變量值等,最后需要恢復虛擬機的運行狀態。斷點事件報告的字段結構如下圖:
[11] 斷點事件報告的數據包字段結構
2) 如果獲取當前函數調用棧信息
通過獲取當前線程的調用棧信息可以定位目標函數的調用路徑和源頭,對于邏輯層次復雜的逆向分析非常有用。對應的JDWP命令為“ThreadReference:Frames”,該命令的請求包與返回包字段比較簡單,結構如下:
[12] “ThreadReference:Frames”命令相關包的字段結構
返回數據為棧幀數組,每一層棧幀信息包含棧幀ID和棧幀位置2個字段,第一層棧幀一般為當前函數,其棧幀ID在后面堆棧獲取變量值命令中需要使用。棧幀信息解析后輸出的例子如下:
[13] “ThreadReference:Frames”返回數據的解析結果
3) 如何獲取函數參數值與變量值
dalvik虛擬機和普通java虛擬機最大的區別之一應該是dalvik是基于寄存器架構的,可以觀察dex反匯編后的smali代碼內容,參數傳遞、變量賦值全部都是對寄存器的操作。關于smali語法與寄存器變量等方面的基礎知識,推薦幾篇文章作為前置閱讀:《smali-Registers》、《smali-TypesMethodsAndFields》、《Smali學習筆記》,本文就不再贅述。
從當前堆棧中獲取寄存器值需要使用“StackFrame:GetValues”命令,下面我們解析下該命令的請求包、返回包的字段結構:
[14] “StackFrame: GetValues”命令的注解
該命令請求中的有幾個關鍵字段需要詳細解釋:
1)ThreadId,返回包中觸發斷點事件被掛起的線程ID。
2)FrameId,可以通過“ThreadReference: Frames”命令獲取,從返回的棧列表中取第一層棧幀,即可獲得的我們需要的當前棧幀ID。
3)Slot,變量偏移位置,這個字段是“StackFrame: GetValues”命中最為關鍵的字段,對于Debug版的apk可以通過“Method: VariableTable”命令獲取,但是逆向分析遇到的幾乎都是Release版本,是不包含這些調試輔助信息的,關于參數和變量對應偏移位置slot的計算方法,我們可以嘗試從dalvik虛擬機源碼中尋找答案。
dalvik虛擬機處理“StackFrame: GetValues”命令的函數為handleSF_GetValues,handleSF_GetValues函數主要負責解析請求包的字段信息,擴展返回數據的內存空間,最后調用dvmDbgGetLocalValue獲取變量值。下面我們跟蹤slot參數的傳遞使用過程來分析它的含義。
[15] dvmDbgGetLocalValue函數的代碼解析
從上圖的dvmDbgGetLocalValue函數代碼解析過程中,我們有以下幾點發現:
1) frameId值就是當前棧指針的內存地址,函數調用過程中使用的寄存器(參數與局部變量)相當于此處的內存映射,而slot值就相當于映射偏移索引號。參數使用最后的N個寄存器(內存段高地址),局部變量使用從v0開始的前(M-N)個寄存器(注意參數占用2個寄存器的情況)。
2) Slot偏移值會經過untweakSlot函數處理,這可能是針對Eclipse的變通方案。1000偏移被重定向到0,而0偏移被重定向到參數偏移起始處,在計算slot索引值時需要注意到這一點。
3) 獲取變量值支持的幾種類型細節如下:
boolean、byte、short、char、int、float類型的變量直接根據slot偏移取值,大小為4字節,占用1個寄存器;
array、object類型根據slot偏移取值為Object指針,查表獲得ObjectId返回,大小為4字節,占用1個寄存器;
double、long類型根據slot偏移取值,大小為8字節,占用2個寄存器。
4) IDA的watch窗口指定變量類型取值后可能導致崩潰的原因在這里也可以找到,在進入其他函數調用過程時,沒有及時手動修正watch窗口指定的寄存器類型,取值過程由于讀取異常拋出導致虛擬機退出。
以下為2個函數的參數與局部變量slot偏移結果的對比,其中一個是普通成員函數,另外一個為靜態成員函數,可以通過這兩個例子加深對slot計算的理解:
[16] 普通成員函數參數與變量slot解析
[17] 靜態成員函數參數與變量slot解析
四、全新的android動態調試工具——AVMDBG
AVMDBG是android虛擬機調試技術研究過程中一個實驗項目,目前只支持dalvik虛擬機,已實現代碼斷點、堆棧輸出、參數變量值獲取等基礎功能。
項目地址:https://github.com/cheetahsec/avmdbg
項目說明:AVMDBG的目標是打造一款輕量級的的android虛擬機調試器,底層使用C++編寫,通過Python擴展的方式提供調試接口,可以通過編寫python腳本實現對安卓app的快速動態調試,當前版本主要提供以下API:
1.bool attach(string&processName);
功能: 附加到目標進程
2.void waitLoop();
功能: 循環等待調試事件
3.bool setBreakPoint(py::dict&breakPoint);
功能: 設置斷點事件
4.py::list getStackFrames(ObjectIdthreadId);
功能: 獲取當前線程堆棧,可在斷點回調函數中使用
5.py::dictgetRegisterValue(py::dict& Context, string& regName, u1varType);
功能: 獲取寄存器變量的值,需要指定寄存器名稱(V命名或P命名法)以及變量類型
6.py::listgetObjectFieldValues(ObjectId objectId);
功能: 獲取對象的成員變量信息,參數為對象ID
7.py::dict getArrayObjectValue(ObjectIdobjectId);
功能: 獲取數組類型變量的信息
8.py::str getStringValue(ObjectIdobjectId);
功能:獲取字符串String類型變量的值
詳細的使用說明可以參考項目文檔和測試demo,以下為部分測試代碼展示:
[18] AVMDBG功能測試代碼
[19] AVMDBG測試輸出結果
五、參考文檔
1)《深入Java調試體系》
2)《Java(tm) Debug WireProtocol》
3)《深入理解Android之Java虛擬機Dalvik》
4)《Dalvik虛擬機進程和線程的創建過程分析》
5)《smali-Registers》、《smali-TypesMethodsAndFields》
6)《Android dalvik虛擬機源代碼》
* 本文原創作者:漁村安全,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載