Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> TaintDroid深入剖析之啟動篇

TaintDroid深入剖析之啟動篇

編輯:關於Android編程

?1背景知識

1.1Android平台軟件動態分析現狀

眾所周知,在計算機領域中所有的軟件分析方法都可以歸為靜態分析和動態分析兩大類,在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變量級污點跟蹤分析之上篇

嚴格來說,應該叫做“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種情況:

 

a. interpreted →interpreted

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

b. interpreted→native

如果目標函數是jni調用那麼就判斷method的NATIVE標志位,通過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)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棧幀的分配函數。

 

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->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棧幀結構極其有用。

 

2.2初始化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的實現機制。

 

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<- vBvB,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對類的靜態域、實例域以及數組的污點跟蹤機制。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved