TaintDroid深入剖析之啟動篇

美人遲暮發表於2017-05-02

TaintDroid深入剖析之啟動篇

 

 

 

 

 

1 背景知識


1.1 Android平臺軟體動態分析現狀

眾所周知,在計算機領域中所有的軟體分析方法都可以歸為靜態分析和動態分析兩大類,在Android平臺也不例外。而隨著軟體加固、混淆技術的不斷改進,靜態分析越來越難以滿足安全人員的分析要求,因此天生對軟體加固、混淆免疫的動態分析技術應運而生。雖然動態分析技術本身有很多侷限性,諸如:程式碼覆蓋率低,執行效率低下等等,但是瑕不掩瑜,個人認為熟悉各種動態分析技術的核心原理也應當是安全從業人員的必備要求。

下圖1-1展示了部分工業界和學術界在android平臺動態分析技術上的成果,有興趣的同學根據需求進一步瞭解:

圖1-1 當前工業界和學術界在android平臺動態分析技術部分成果


ps: 這張圖是去年總結的,所以未能將一些最新的系統、工具納入,歡迎各位大牛補充。


在上述眾多優秀的動態分析系統、工具中,個人覺得基於汙點跟蹤技術的TaintDroid一定是其中最重量級的成果之一,截止今天,該論文的引用次數已經達到了驚人的1788次。雖然很多人都用過TaintDroid,甚至大牛們進行過二次開發,但是目前市面上並沒有對TaintDroid進行深入剖析文章。因此本系列文章將會詳細分析TaintDroid的具體實現,從原始碼層深瞭解TaindDroid的優缺點,希冀能跟大家一起開發出檢測效果更好、執行效率更高的汙點跟蹤系統。


1.2 閱讀論文

首先讀者需要詳細閱讀TaintDroid那篇論文(為方便英文不好的同學,我們已經將其翻譯成了中文,由於論文翻譯太費時,其中難免有不對的地方,建議大家對照著原文看^_^),該論文詳細講解了TaintDroid的核心技術以及其設計模型等等,充分理解這篇論文對我們後續深入瞭解TaintDroid的具體實現很有幫助。


1.3 下載原始碼

本文主要以Android4.1版本的TaintDroid為分析物件(其最新為android4.3版本,主要是新增對selinux的適配,核心內容並沒有改變),為了便於讀者進行對照分析或測試,建議讀者按照官網http://appanalysis.org/download_4.1.html 的提示下載原始碼。當然,如果讀者僅僅是想閱讀汙點跟蹤相關的程式碼,可以去github中按照自己的需要下載對應部分原始碼即可。如實現變數級、Native級汙點跟蹤的程式碼基本都在dalvik目錄下,所以可以在:

https://github.com/TaintDroid/android_platform_dalvik

下載dalvik相關原始碼。


1.4 系統棧幀概念

由於TaintDroid的變數級和方法級汙點跟蹤是建立在其對DVM棧和Native棧的修改之上的,所以我們必須熟悉系統棧幀的概念,如圖1-2所示:

圖1-2 系統棧幀分佈

單個函式呼叫操作所使用的棧部分被稱為棧幀(stack frame)結構,其一般結構如上圖所示。棧幀結構的兩端由兩個指標來指定。暫存器ebp通常用做幀指標(frame pointer),而esp則用作棧指標(stack pointer)。在函式執行過程中,棧指標esp會隨著資料的入棧和出棧而移動,因此函式中對大部分資料的訪問都基於幀指標ebp進行。


1.5 內容安排

如下圖所述:

鑑於TaintDroid有四種粒度的汙點跟蹤機制,且這四種汙點跟蹤機制實現邏輯相對獨立,所以本系列文章將會分章講解各個粒度汙點跟蹤機制的實現原理、方法,然後再從某些具體的情境出發,詳細分析TaintDroid是如何綜合利用這4種跟蹤機制,以及為了無縫融合這些機制其所作的一些輔助性修改。

2 TaintDroid變數級汙點跟蹤分析之上篇


嚴格來說,應該叫做“DVMinterpreted方法的變數級汙點跟蹤分析”。從論文中我們得知: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 StackNative Stack),但是可以互相呼叫,即存在了以下4種情況:


a. interpreted → interpreted

同一個類方法之間直接通過GOTO_invoke系列巨集進行跳轉。不同類的話根據具體情況而定。一直在interpreted stack中執行。

b. interpreted native

如果目標函式是jni呼叫那麼就判斷methodNATIVE標誌位,通過native呼叫橋dvmCallJniMethod進行跳轉。常見情況就是JNI呼叫。

如果目標函式是Internal VM Method,那麼就可以通過interpted程式碼直接呼叫,只是需要傳遞一個指向32位暫存器引數的指標以及一個指向返回值的指標即可。常見形式如下:

InternalVMfunc(const u4* args, JValue* rResult){……}

interpreted stack轉到native stack

c. native native

這裡主要說明由Internal VM Method或反射呼叫跳轉到JNI Method的情況。在這種情況下最終會呼叫dvmPushJNIFrame為目標函式分配一個JNI幀。

d. 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)dvmPushInterpFrame2)dvmPushJNIFrame。而初始化棧幀同樣涉及到兩類函式:1) dvmCallMethodV/A2) 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棧幀的分配函式。


2.1 dvmPushInterpFrame分析

當從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->registersSizemethod->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程式碼裡面每個方法前都指定了PV暫存器個數的作用),因此它在分配棧空間的時候,就一次性將輸入引數和本地變數共佔用的暫存器個數分配完畢,這樣fp就直接指向了本地變數之後的位置。


瞭解DVM棧幀與傳統意義的系統棧幀之間的異同點,對我們後續分析TaintDroid如何初始化新的DVM棧幀結構極其有用。


2.2 初始化DVM棧幀

如前文所述,DVM棧幀的初始化工作在Stack.cppdvmCallMethodV/A函式中。雖然此函式的程式碼較多,但是邏輯功能並不複雜,只是需要注意幀指標的位置以及interpreted方法和native方法之間的不同處理策略即可(當前只需要關心interpreted方法的棧幀初始化)


下面簡要分析其初始化過程。

首先:


#ifdef WITH_TAINT_TRACKING

int slot_cnt = 0;

bool nativeTarget = dvmIsNativeMethod(method);

#endif


在程式碼起初部分新增了上述程式碼,slot_cnt表示跳過的用於變數汙點標記的個數(每個變數一個)nativeTarget表示目的方法是否為Native方法,因為TaintDroidnative方法是不會為每個變數交叉儲存汙點的(tag interleaving),所以這就需要根據目的方法的種類來進行相應的指令偏移計算。


然後呼叫callPrep為目的方法分配棧幀,其大致實現是:判斷方法是否為native,如果是,則呼叫dvmPushJNIFrame為之分配一個JNI幀,否則呼叫dvmPushInterpFrame。兩個方法涉及到TaintDroidDVM棧幀的修改,前面已經分析過,這裡不再細說,其最終的實現結果就是改變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的實現機制。


2.3 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*/


簡而言之,由於TaintDroidDVM棧幀的變數進行了倍增(由原來的4位元組擴充到8位元組),且交叉儲存各個變數的汙點資訊,所以,為了能夠正確地取得各個DVM虛擬暫存器VREG的資料,它將GET_VREG巨集中的偏移值由以前的乘以4擴大為乘以8,以及為了設定和獲取各個變數(VREG)所對應的汙點資訊,它還新增了SET_VRER_TAINTGET_VREG_TAINT系列巨集定義。


至此關於TaintDroid中針對各個平臺優化後的由彙編程式碼實現的dvmMterpStd直譯器如何實現對方法引數和方法變數的變數級汙點跟蹤機制就分析完畢了,讀者可按照同樣的方式自行分析TaintDroid中可移植模式的由C程式碼實現的dvmInterptStd直譯器,這個更簡單。我們將在下一篇文章中進一步分析TaintDroid對類的靜態域、例項域以及陣列的汙點跟蹤機制。

 

 

 

 

 

本文來自合作伙伴“阿里聚安全”,發表於2016年07月04日 10:56.


相關文章