衆所周知,在計算機領域中全部的軟件分析方法均可以歸爲靜態分析和動態分析兩大類,在Android平臺也不例外。而隨着軟件加固、混淆技術的不斷改進,靜態分析愈來愈難以知足安全人員的分析要求,所以天生對軟件加固、混淆免疫的動態分析技術應運而生。雖然動態分析技術自己有不少侷限性,諸如:代碼覆蓋率低,執行效率低下等等,可是瑕不掩瑜,我的認爲熟悉各類動態分析技術的核心原理也應當是安全從業人員的必備要求。
下圖1-1展現了部分工業界和學術界在android平臺動態分析技術上的成果,有興趣的同窗根據需求進一步瞭解:html
圖1-1 當前工業界和學術界在android平臺動態分析技術部分紅果java
ps: 這張圖是去年總結的,因此未能將一些最新的系統、工具歸入,歡迎各位大牛補充。linux
在上述衆多優秀的動態分析系統、工具中,我的以爲基於污點跟蹤技術的TaintDroid必定是其中最重量級的成果之一,截止今天,該論文的引用次數已經達到了驚人的1788次。雖然不少人都用過TaintDroid,甚至大牛們進行過二次開發,可是目前市面上並無對TaintDroid進行深刻剖析文章。所以本系列文章將會詳細分析TaintDroid的具體實現,從源碼層深瞭解TaindDroid的優缺點,希冀能跟你們一塊兒開發出檢測效果更好、運行效率更高的污點跟蹤系統。android
首先讀者須要詳細閱讀TaintDroid的那篇論文 (爲方便英文很差的同窗,咱們已經將其翻譯成了中文,因爲論文翻譯太費時,其中不免有不對的地方,建議你們對照着原文看^_^),該論文詳細講解了TaintDroid的核心技術以及其設計模型等等,充分理解這篇論文對咱們後續深刻了解TaintDroid的具體實現頗有幫助。git
本文主要以Android4.1版本的TaintDroid爲分析對象(其最新爲android4.3版本,主要是添加對selinux的適配,核心內容並無改變),爲了便於讀者進行對照分析或測試,建議讀者按照官網http://appanalysis.org/download_4.1.html 的提示下載源碼。固然,若是讀者僅僅是想閱讀污點跟蹤相關的代碼,能夠去github中按照本身的須要下載對應部分源碼便可。如實現變量級、Native級污點跟蹤的代碼基本都在dalvik目錄下,因此能夠在:
https://github.com/TaintDroid/android_platform_dalvik
下載dalvik相關源碼。github
因爲TaintDroid的變量級和方法級污點跟蹤是創建在其對DVM棧和Native棧的修改之上的,因此咱們必須熟悉系統棧幀的概念,如圖1-2所示:數組
圖1-2 系統棧幀分佈安全
單個函數調用操做所使用的棧部分被稱爲棧幀(stack frame)結構,其通常結構如上圖所示。棧幀結構的兩端由兩個指針來指定。寄存器ebp一般用作幀指針(frame pointer),而esp則用做棧指針(stack pointer)。在函數執行過程當中,棧指針esp會隨着數據的入棧和出棧而移動,所以函數中對大部分數據的訪問都基於幀指針ebp進行。數據結構
以下圖所述:app
鑑於TaintDroid有四種粒度的污點跟蹤機制,且這四種污點跟蹤機制實現邏輯相對獨立,因此本系列文章將會分章講解各個粒度污點跟蹤機制的實現原理、方法,而後再從某些具體的情境出發,詳細分析TaintDroid是如何綜合利用這4種跟蹤機制,以及爲了無縫融合這些機制其所做的一些輔助性修改。
嚴格來講,應該叫作「DVM中interpreted方法的變量級污點跟蹤分析」。從論文中咱們得知:DVM 有 5 種類型的變量須要進行污點存儲:方法的本地變量,方法的參數,類的靜態域,類的實例域,數組。鑑於方法的本地變量和方法的參數是存儲在方法的執行棧幀中;而類的靜態域、實例域卻以指針的方式進行存儲;至於數組又有本身獨特的數據結構ArrayObject。因此爲了分析邏輯更加清晰,咱們將TaintDroid變量級污點跟蹤分析分爲上下兩篇:上篇主要講解方法本地變量與方法參數的污點跟蹤,下篇主要介紹類的靜態域、實例域以及數組的污點跟蹤。
TaintDroid爲了實現此種機制以及後面章節將介紹的Native方法級污點跟蹤機制,它對棧進行了一次大手術!至於這個手術的複雜度和難度係數具體如何,請聽咱們娓娓道來。
衆所周知,在4.4以前的整個Android系統共存在兩種類型的方法:
①Interpreted method: 在DVM虛擬機中解釋執行的方法;須要注意的是,DVM中存在兩種解釋器:標準的可移植解釋器dvmInterpretStd以及對某個特定平臺優化後的解釋器dvmMterpStd,前者由C代碼實現,後者由彙編實現。
②Native method: 直接執行的C/C++/彙編代碼,又可細分爲Internal VM Method(如System.arraycopy)和JNI method。
這兩類方法有各自的棧幀結構(Interpreted Stack和Native Stack),可是能夠互相調用,即存在瞭如下4種狀況:
一、interpreted → interpreted
同一個類方法之間直接經過GOTO_invoke系列宏進行跳轉。不一樣類的話根據具體狀況而定。一直在interpreted stack中執行。二、interpreted → native
若是目標函數是jni調用那麼就判斷method的NATIVE標誌位,經過native調用橋dvmCallJniMethod進行跳轉。常見狀況就是JNI調用。
若是目標函數是Internal VM
Method,那麼就能夠經過interpted代碼直接調用,只是須要傳遞一個指向32位寄存器參數的指針以及一個指向返回值的指針便可。常見形式以下:
InternalVMfunc(const u4 args, JValue rResult){……} 由interpreted
stack轉到native stack。三、native → native 這裏主要說明由Internal VM
Method或反射調用跳轉到JNI Method的狀況。在這種狀況下最終會調用dvmPushJNIFrame爲目標函數分配一個JNI幀。四、native → interpreted
反射的話經過dvmInvokeMethod系列函數進行跳轉;非反射的JNI調用就經過Jni.cpp中定義的CALL_XXX系列宏經過dvmCallMethodV/A進行跳轉,均走dvmPushInterpFrame分支;非反射的Internal
VM Method直接返回。常見狀況就是jni調用。由native stack轉到interpreted stack。
具體的棧幀結構會在後文進行詳細說明,這裏主要說一下TaintDroid對棧結構修改的代碼位置。
它對棧結構的修改代碼在dalvik/vm/interp/Stack.cpp文件中。按照常識,修改棧幀須要完成兩個功能:1)分配新結構的棧幀;2)初始化新結構的棧幀。分配棧幀主要涉及到兩個函數:1)dvmPushInterpFrame;2)dvmPushJNIFrame。而初始化棧幀一樣涉及到兩類函數:1) dvmCallMethodV/A;2) dvmInvokeMethod。
用於分配棧幀的兩個函數均且只在callPrep函數中被調用:
if (dvmIsNativeMethod(method)) { /* native code calling native code the hard way */ if (!dvmPushJNIFrame(self, method)) { …… } } else { /* native code calling interpreted code */ if (!dvmPushInterpFrame(self, method)) { …… } }
而callPrep函數會在dvmCallMethodV/A以及dvmInvokeMethod中被調用。dvmCallMethodV/A會在jni.cpp中定義的CALL_XXX系列宏中被調用,dvmInvokeMethod會在java.lang.reflect的反射函數中被調用,即前2者用於jni,後者用於反射調用。
同時須要注意的是,TaintDroid也對dvmInvokeMethodV/A以及dvmInvokeMethod函數進行了修改以便正確地對棧幀進行初始化。另外還須要注意的是,上文中的dvmIsNativeMethod方法是用於判斷即將被調用的方法是native仍是dvm方法,而不是調用此方法的方法是native仍是dvm。
鑑於兩種棧幀的使用場景和佈局大相庭徑,且在TaintDroid中修改後的DVM棧幀主要用於實現變量級的污點跟蹤,而Native棧幀主要用於實現方法級的污點跟蹤,因此本章先分析執行在DVM中的interpreted棧幀,至於Native棧幀,在分析Native方法級污點跟蹤的時候再詳細說明。下面開始分析Interpreted棧幀的分配函數。
當從DVM內部的函數或經過反射調用一個interpreted method時,系統會爲之分配一個棧幀,爲了方便,後文將這種棧幀統稱爲DVM棧幀。注意此方法只有在「由native代碼調用一個interpreted代碼」的時候纔會被調用。主要的更改代碼以下:
#ifdef WITH_TAINT_TRACKING /* taint tags are interleaved, plus "native hack" spacer for args */ stackReq = method->registersSize * 8 + 4 // params + locals +sizeof(StackSaveArea) * 2 // break frame + regular frame +method->outsSize * 8 + 4; // args to other methods # else stackReq = method->registersSize * 4 // params + locals +sizeof(StackSaveArea) * 2 // break frame + regular frame +method->outsSize * 4; // args to other methods #endif … #ifdef WITH_TAINT_TRACKING /* interleaved taint tracking plus "native hack" spacer for args */ stackPtr -= method->registersSize * 8 + 4 + sizeof(StackSaveArea); #else stackPtr -= method->registersSize * 4 + sizeof(StackSaveArea); … /* debug -- memset the new stack, unless we want valgrind's help */ #ifdef WITH_TAINT_TRACKING memset(stackPtr - (method->outsSize*8+4), 0xaf, stackReq); #else memset(stackPtr - (method->outsSize*4), 0xaf, stackReq);
顯然TaintDroid在爲interpreted方法分配DVM棧幀時對method->registersSize和method->outsSize的內存空間進行了倍增。不過這裏有一點奇怪的地方,那就是method->registersSize倍增以後還加了4。其實這個加4對於interpreted方法來講是無用的,只在native方法的棧幀纔有用,這裏僅僅是爲了後續代碼的複用(由於對兩種棧幀的初始化操做均在dvmCallMethodV/A函數中實現)。
結合論文第4章以及前面的分析咱們就能夠理解下圖的意思了:
對於Interpreted方法,TaintDroid在變量(locals and ins)之間交叉存儲各個變量的污點信息(taint tag)。不過細心的朋友可能會發如今DVM棧幀中,其幀指針(frame pointer)所指向的位置跟咱們以前在1.3節中所描述的系統棧幀結構並不相同——前者指向的是第一個本地變量(local0)的地址,然後者卻指向被保存的ebp的位置(若是咱們將ebp與返回地址也看作一種輸入參數的話,那麼就能夠理解爲系統棧幀的幀指針指向的是第一個輸入參數in0的位置)。原來對於DVM而言因爲它在每一個方法執行以前都預先肯定好了該方法中全部本地變量會用到本地寄存器的個數(這就是smali代碼裏面每一個方法前都指定了P與V寄存器個數的做用),所以它在分配棧空間的時候,就一次性將輸入參數和本地變量共佔用的寄存器個數分配完畢,這樣fp就直接指向了本地變量以後的位置。
瞭解DVM棧幀與傳統意義的系統棧幀之間的異同點,對咱們後續分析TaintDroid如何初始化新的DVM棧幀結構極其有用。
如前文所述,DVM棧幀的初始化工做在Stack.cpp的dvmCallMethodV/A函數中。雖然此函數的代碼較多,可是邏輯功能並不複雜,只是須要注意幀指針的位置以及interpreted方法和native方法之間的不一樣處理策略便可(當前只須要關心interpreted方法的棧幀初始化)。
下面簡要分析其初始化過程。
首先:
#ifdef WITH_TAINT_TRACKING int slot_cnt = 0; bool nativeTarget = dvmIsNativeMethod(method); #endif
在代碼起初部分添加了上述代碼,slot_cnt表示跳過的用於變量污點標記的個數(每一個變量一個);nativeTarget表示目的方法是否爲Native方法,由於TaintDroid對native方法是不會爲每一個變量交叉存儲污點的(tag interleaving),因此這就須要根據目的方法的種類來進行相應的指令偏移計算。
而後調用callPrep爲目的方法分配棧幀,其大體實現是:判斷方法是否爲native,若是是,則調用dvmPushJNIFrame爲之分配一個JNI幀,不然調用dvmPushInterpFrame。兩個方法涉及到TaintDroid對DVM棧幀的修改,前面已經分析過,這裏再也不細說,其最終的實現結果就是改變self->interpSave.curFrame,即此時的curFrame已經指向了目的方法的幀結構。
若是是Interp幀的話,它的幀結構的寄存器部分包含參數和本地變量,可是對於JNI幀,它的幀結構的寄存器部分只包含參數變量,是不涉及到本地變量的!因此幀結構的不一樣,對後續的處理思路也不一樣。這裏先分析Interp幀。
分配完方法所需的棧幀以後:
/* "ins" for new frame start at frame pointer plus locals */ #ifdef WITH_TAINT_TRACKING if (nativeTarget) { /*對於native方法後面再單獨分析*/ } #else ins = ((u4*)self->interpSave.curFrame) + (method->registersSize - method->insSize); #endif
而後就是根據參數的shorty描述符依次處理各個參數。在TaintDroid中的主要處理就是,將每一個參數的tag變爲TAINT_CLEAR。須要特別注意的是,TaintDroid在處理完參數後就調用新的dvmInterpret函數:
#ifdef WITH_TAINT_TRACKING u4 rtaint; /* not used */ dvmInterpret(self, method, pResult, &rtaint); #else dvmInterpret(self, method, pResult); #endif
該函數用於解釋執行interpreted方法,對於TaintDroid而言,這個函數有4個參數,而本來卻只有3個參數。此函數的詳細功能會在後文加以分析。
至此DVM棧幀的初始化工做就分析完畢了,下一步就是分析TaintDroid是如何在已經被作過大手術的DVM棧幀上正確執行interpreted方法,也就是分析dvmInterpret的實現機制。
該函數定義在dalvik/vm/interp/Interp.cpp文件中,核心代碼以下:
#ifdef WITH_TAINT_TRACKING void dvmInterpret(Thread* self, const Method* method, JValue* pResult, u4* rtaint) //TaintDroid添加了一個參數 #else void dvmInterpret(Thread* self, const Method* method, JValue* pResult) #endif { InterpSaveState interpSaveState; ExecutionSubModes savedSubModes; …… #ifdef WITH_TAINT_TRACKING self->interpSave.rtaint.tag = TAINT_CLEAR; #endif self->interpSave.method = method; self->interpSave.curFrame = (u4*) self->interpSave.curFrame; self->interpSave.pc = method->insns; …… typedef void (*Interpreter)(Thread*); //申明一個函數指針,參數爲Thread*,函數名字爲Interpreter Interpreter stdInterp; if (gDvm.executionMode == kExecutionModeInterpFast) stdInterp = dvmMterpStd; #if defined(WITH_JIT) else if (gDvm.executionMode == kExecutionModeJit) stdInterp = dvmMterpStd; #endif else stdInterp = dvmInterpretPortable; // Call the interpreter (*stdInterp)(self); //這表示調用該函數 *pResult = self->interpSave.retval; #ifdef WITH_TAINT_TRACKING *rtaint = self->interpSave.rtaint.tag; #endif ……
顯然這裏關鍵就是stdInterp函數的執行,它在不一樣的執行模式下對應不一樣的函數(還記得前面提到的DVM虛擬機中存在兩種解釋器麼?),這裏以dvmMterpStd爲例。此函數定義在dalvik/vm/mterp/Mterp.cpp中,代碼以下:
void dvmMterpStd(Thread* self) { /* configure mterp items */ self->interpSave.methodClassDex = self->interpSave.method->clazz->pDvmDex; …… /* *Handle any ongoing profiling and prep for debugging */ if (self->interpBreak.ctl.subMode != 0) { TRACE_METHOD_ENTER(self, self->interpSave.method); self->debugIsMethodEntry = true; // Always true on startup } dvmMterpStdRun(self); #ifdef LOG_INSTR ALOGD("|-- Leaving interpreter loop"); #endif }
它經過執行dvmMterpStdRun函數以真正地執行方法指令,此函數由彙編實現,代碼定義在dalvik/vm/mterp/arm*/entry.S中,注意因爲TaintDroid更改了棧結構以及爲了實現污點傳播,因此它對絕大部分opcode的彙編實現均進行了修改,下面就以簡單的加法指令爲例進行分析:
ps:全部opcode的彙編實現,都在vm/mterp/armv*_taint目錄中。
1)首先,須要理解DVM是如何分配CPU寄存器的(不是DVM的虛擬寄存器VREG!):
reg nick purpose r4 rPC interpreted program counter, used for fetching instructions r5 rFP interpreted frame pointer, used for accessing locals and args r6 rSELF self (Thread) pointer r7 rINST first 16-bit code unit of current instruction r8 rIBASE interpreted instruction base pointer, used for computed goto
2)再看op_add_int_2addr指令的具體實現,此指令定義在OP_ADD_INT_2ADDR.s中:
%verify "executed" %include "armv5te_taint/binop2addr.S" {"instr":"add r0, r0, r1"}
3)顯然真正的實如今binop2addr.s:
/* binop/2addr vA, vB */ mov r9, rINST, lsr #8 @ r9<- A+ A,B表示dvm寄存器編號 mov r3, rINST, lsr #12 @ r3<- B and r9, r9, #15 GET_VREG(r1, r3) @ r1<- vB vB,vA表示dvm寄存器的值 GET_VREG(r0, r9) @ r0<- vA .if $chkzero cmp r1, #0 @ is second operand zero? beq common_errDivideByZero .endif // begin WITH_TAINT_TRACKING bl .L${opcode}_taint_prop // end WITH_TAINT_TRACKING FETCH_ADVANCE_INST(1) @ advance rPC, load rINST $preinstr @ optional op; may set condition codes $instr @ $result<- op, r0-r3 changed GET_INST_OPCODE(ip) @ extract opcode from rINST SET_VREG($result, r9) @ vAA<- $result GOTO_OPCODE(ip) @ jump to next instruction /* 10-13 instructions */ %break .L${opcode}_taint_prop: SET_TAINT_FP(r10) @以r10爲基本偏移值,後續的taint系列宏都以這個r10爲基準。 GET_VREG_TAINT(r3, r3, r10) @r3 <- vB的污點 GET_VREG_TAINT(r2, r9, r10) @r2 <- vA的污點 orr r2, r3, r2 @相或,r2 = r2 | r3 SET_VREG_TAINT(r2, r9, r10) @將最終的污點存儲在vA的污點中,由於vA是返回值。 bx lr
4)GET_VREG之類的宏定義在header.s中:
#ifdef WITH_TAINT_TRACKING #define SET_TAINT_FP(_reg) add _reg, rFP, #4 //fFP+4 #define SET_TAINT_CLEAR(_reg) mov _reg, #0 #define GET_VREG(_reg, _vreg) ldr _reg, [rFP, _vreg, lsl #3] //表示乘以8 #define SET_VREG(_reg, _vreg) str _reg, [rFP, _vreg, lsl #3] #define GET_VREG_TAINT(_reg, _vreg, _rFP) ldr _reg, [_rFP, _vreg, lsl #3] #define SET_VREG_TAINT(_reg, _vreg, _rFP) str _reg, [_rFP, _vreg, lsl #3] #else #define GET_VREG(_reg, _vreg) ldr _reg, [rFP, _vreg, lsl #2] //表示乘以4 #define SET_VREG(_reg, _vreg) str _reg, [rFP, _vreg, lsl #2] #endif /*WITH_TAINT_TRACKING*/
簡而言之,因爲TaintDroid將DVM棧幀的變量進行了倍增(由原來的4字節擴充到8字節),且交叉存儲各個變量的污點信息,因此,爲了可以正確地取得各個DVM虛擬寄存器VREG的數據,它將GET_VREG宏中的偏移值由之前的乘以4擴大爲乘以8,以及爲了設置和獲取各個變量(VREG)所對應的污點信息,它還添加了SET_VRER_TAINT和GET_VREG_TAINT系列宏定義。
至此關於TaintDroid中針對各個平臺優化後的由彙編代碼實現的dvmMterpStd解釋器如何實現對方法參數和方法變量的變量級污點跟蹤機制就分析完畢了,讀者可按照一樣的方式自行分析TaintDroid中可移植模式的由C代碼實現的dvmInterptStd解釋器,這個更簡單。咱們將在下一篇文章中進一步分析TaintDroid對類的靜態域、實例域以及數組的污點跟蹤機制。
做者:簡行、走位@阿里聚安全,更多技術文章,請訪問阿里聚安全博客