TaintDroid剖析之Native方法级污点跟踪分析
作者:简行、走位@阿里聚安全
1、Native方法的污点传播
在前两篇文章中我们详细分析了TaintDroid对DVM栈帧的修改,以及它是如何在修改之后的栈帧中实现DVM变量级污点跟踪的。现在我们继续分析其第二个粒度的污点跟踪——Native方法级跟踪。
回顾前文,我们知道Native方法执行在Native栈帧中,且Native栈帧由dvmPushJNIFrame函数分配栈空间,再由dvmCallMethodV/A或者dvmInvokeMethod对栈帧进行初始化,所以我们也按照之前的方式进行Native方法级跟踪机制分析。
TaintDroid深入剖析系列目录:
1.1 dvmPushJNIFrame分析
该函数与dvmPushInterpFrame基本相同,也是对寄存器所占用的空间进行了倍增。但是,在此栈帧中,变量与污点的位置却与DVM栈帧大相庭径!Native栈帧的结构如下图所示:
显然在Native栈帧中,并没有为变量交叉存储污点信息,而是跟传统的系统栈帧一样(必须一样,因为这是ARM通用的标准调用约定),将参数依次存储在栈帧开始部分。只是,在最后一个参数之后分配了4字节的空间用于存储return taint(用于存储方法的返回污点信息),然后又紧邻这个return taint依次为每个参数分配了4字节大小的空间用于存储各个参数的污点信息。所以虽然两种栈帧都是对method->registers进行了倍增,但是其倍增后的栈帧的布局却是完全不同的,并且在这里我们就能理解为什么在进行倍增的时候会额外多分配4字节的空间了——原来是用于存储return taint!
只要理解了Native方法的栈帧结构,就不难以分析Native方法中Internal VM method的污点跟踪机制了,所以我们主要讲解JNI方法的污点跟踪——因为它额外使用了较为复杂的method profile policy。
1.2 JNI方法的污点跟踪分析
所有的JNI方法都通过dvmCallJniMethod方法调用,TaintDroid在其代码中添加如下语句:
#ifdef WITH_TAINT_TRACKING
// Copy args to another array, to ensure correct taint propagation in case args change
int nArgs = method->insSize * 2 + 1;
u4* oldArgs = (u4*)malloc(sizeof(u4)*nArgs);
memcpy(oldArgs, args, sizeof(u4)*nArgs);
#endif /*WITH_TAINT_TRACKING*/
首先将所有的参数存储下来,注意红色部分代码,对于native 栈帧来说,insSize就是参数的个数,由于TaintDroid对栈进行了扩展,所以这里也要对应的进行扩展。
然后就是在JNI方法执行结束之后调用如下代码:
#ifdef WITH_TAINT_TRACKING
dvmTaintPropJniMethod(oldArgs, pResult, method);
free(oldArgs);
#endif /*WITH_TAINT_TRACKING*/
这个dvmTaintPropJniMethod方法定义在dalvik/vm/tprop/TaintProp.cpp中,此函数结合dvmCallMethod对JNI帧的赋值,解释了为何可以以及如何实现NATIVE层污点传播。
1.3 dvmTaintPropJniMethod分析
此方法用于JNI方法的污点传播,这里共包含两种污点传播类型(同时使用):
1、基于参数的简单保守的污点传播;
2、基于函数剖析策略(function profile policies)的污点传播;
1.3.1基于参数的简单保守的污点传播分析
下面详细分析dvmTaintPropJniMethod函数的代码。
第一部分,参数准备:
const DexProto* proto = &method->prototype;
DexParameterIterator pIterator;
int nParams = dexProtoGetParameterCount(proto);
int pStart = (dvmIsStaticMethod(method)?0:1); /* index where params start */
/* Consider 3 arguments. [x] indicates return taint index
* 0 1 2 [3] 4 5 6
*/
int nArgs = method->insSize;
u4* rtaint = (u4*) &args[nArgs]; /* The return taint value */
int tStart = nArgs+1; /* index of args[] where taint values start */
int tEnd = nArgs*2; /* index of args[] where taint values end */
u4 tag = TAINT_CLEAR;
int i;
这部分代码很简单,结合上文的Native栈帧,不难理解其中各个变量的含义。
现在回到dvmTaintPropJniMethod的第二部分:
for (i = tStart; i <= tend;="" i++)="" {="" tag="" |="args[i];" }="" *="" if="" not="" static,="" pull="" any="" taint="" from="" the="" "this"="" object="" (!dvmisstaticmethod(method))="" method-="">clazz->descriptor);
}
/* Union taint from Objects we care about */
dexParameterIteratorInit(&pIterator, proto);
for (i=pStart; ; i++) {
const char* desc = dexParameterIteratorNextDescriptor(&pIterator);
if (desc == NULL) {
break;
}
if (desc[0] == '[' || desc[0] == 'L'] {
tag |= getObjectTaint((Object*) args[i], desc); //当前只支持array和string对象的污点获取!
}
if (desc[0] == 'J' || desc[0] == 'D') {
/* wide argument, increment index one more */
i++;
}
}
/* Look at the taint policy profiles (may have return taint) */
tag |= propMethodProfile(args, method);
/* Update return taint according to the return type */
if (tag) {
const char* desc = dexProtoGetReturnType(proto);
setReturnTaint(tag, rtaint, pResult, desc);
}
这部分代码的功能简要概括为:
1. 将所有参数的污点都集合到返回tag中;2. 对于非静态方法,将this指针的tag集合到返回tag中;
3. 将参数中所有arrayObject和object对象的tag集合到返回tag中;getObjectTaint的实现并不复杂,结合第3章的讲解,读者可以很容易地理解其实现逻辑。
4. 通过函数propMethodProfile对参数的污点进行profile,如果该函数有返回值的话就将这个值(其实就是一个tag值)集合到返回tag中, 后文会详细分析其实现机制;
5. 最后通过函数setReturnTaint方法将返回tag放置到返回值中。
这里有几个概念需要说明:
1. 虽然每个参数在栈帧中都有一个专门的空间存储其污点,但是这并不意味着参数的污点数据就一定存储在这个空间了,因为对于ArrayObject/StringObject之类的参数其污点是存储在自己的存储空间的(如第3章所述,TaintDroid对它们的数据结构进行了修改——添加了一个Taint成员)。2. setReturnTaint对不同类型参数的污点处理需要注意。比如,对ArrayObject与StringObject,以及对Object的引用(等同于普通的变量)。这里就涉及到TaintDroid对基于参数的简单保守的污点传播的一些定义与限制了,TaintDroid将其称之为启发式污点传播补丁。结合前文对DVM的变量级污点跟踪分析,TaintDroid仅仅对原始类型数据和ArrayObject以及类的静态域、实例域进行了污点传播,它并不关心其他object类型的污点,另外它还有一个TODO:考虑String的派生类的污点。
1.3.2 基于函数剖析策略的污点传播分析
上文主要分析了JNI污点传播中基于参数的简单保守污点传播方式,下面继续分析其基于函数剖析策略(function profile policies)的污点传播,其实现接口就是前文提到的propMethodProfile函数。
在分析Taint method Profile之前,需要先了解其所需的各种结构体,这些结构体定义在dalvik/vm/tprop/TaintPolicyPriv.h中:
typedef enum {
kTaintProfileUnknown = 0,
kTaintProfileClass,
kTaintProfileParam,
kTaintProfileReturn
} TaintProfileEntryType;
typedef struct {
const char* from; //格式大概为:[class/param/argX/return].[xxx].[xxx]
const char* to;
} TaintProfileEntry;
#define TAINT_PROFILE_TABLE_SIZE 8 /* per class */
#define TAINT_POLICY_TABLE_SIZE 32 /* number of classes */
typedef struct {
const char* methodName;
const TaintProfileEntry* entries;
} TaintProfile;
typedef struct {
const char* classDescriptor;
const TaintProfile* profiles;
HashTable* methodTable; /* created on startup */
} TaintPolicy;
三者的相互关系如下图所示:
TaintProfileEntry的[from, to]数据对,用于记录变量之间(包括方法参数、类变量以及返回值)的数据流。显然,这三种结构体构成了一个完整的数据流链表。了解了这些结构体之后就可以继续分析TaintDroid是如何部署、实施这种策略的了。为了便于理解,我们将method profile policy的整个实现分为三个阶段:1)初始化阶段;2)策略执行之搜索阶段;3)策略执行之更新阶段。
1)初始化阶段
首先我们进入jni.cpp中dvmJniStartup()函数,发现其添加了如下代码:
#ifdef WITH_TAINT_TRACKING
dvmTaintPropJniStartup();
#endif
顾名思义dvmJniStartup用于启动整个dvm的jni功能(可能有误,待确定),TD将dvmTaintPropStartup添加到此函数中,表示整个Taint method Profile是对所有JNImethods起作用的。dvmTaintPropStartup定义在TaintProp.cpp文件中:
/* Code called from dvmJniStartup()
* Initializes the gPolicyTable for fast lookup of taint policy
* profiles for methods.
*/
void dvmTaintPropJniStartup()
{
TaintPolicy* policy;
u4 hash;
/* Create the policy table (perfect size) */
gPolicyTable = dvmHashTableCreate(
dvmHashSize(TAINT_POLICY_TABLE_SIZE),
freeTaintPolicy);
for (policy = gDvmJniTaintPolicy; policy->classDescriptor != NULL; policy++) {
const TaintProfile *profile;
/* Create the method table for this class */
policy->methodTable = dvmHashTableCreate(
TAINT_PROFILE_TABLE_SIZE, freeTaintProfile);
/* Add all of the methods */
for (profile = &policy->profiles[0]; profile->methodName != NULL; profile++) {
hash = dvmComputeUtf8Hash(profile->methodName);
dvmHashTableLookup(policy->methodTable, hash,(void *) profile,
hashcmpTaintProfile, true); //最后一个参数表示在hash表中找不到目标item时,是否将这个item添加到hash表中。
}
/* Add this class to gPolicyTable */
hash = dvmComputeUtf8Hash(policy->classDescriptor);
dvmHashTableLookup(gPolicyTable, hash, policy,
hashcmpTaintPolicy, true);
}
#ifdef TAINT_JNI_LOG
/* JNI logging for debugging purposes */
gJniLogSeen = dvmHashTableCreate(dvmHashSize(50), free);
#endif
}
1. 通过dvmHashTableCreate创建一个全局hash表gPolicyTable;
2. 遍历全局变量gDvmJniTaintPolicy,这是一个TaintPolicy结构体数组,定义在tprop/TaintPolicy.cpp中:
TaintPolicy gDvmJniTaintPolicy[] = {
{"Llibcore/icu/NativeConverter;", libcore_icu_NativeConverter_methods, NULL},
{"Lfoo/bar/name2;", foo_bar_name2_methods, NULL},
{NULL, NULL, NULL}
};
由于起初NativeConverter与name2类的methodTable指针为空,它又是一个HashTable指针成员,所以需要通过dvmHashTableCreate为其创建hash表;然后将该类的所有方法(也定义在tprop/TaintPolicy.cpp中)加入到这个hash表中;最后将该类加入到全局hash表gPolicyTable中。至于为何只定义了这两个类,见后面分析。
至此jni method profile的初始化工作就做完了。以后就是根据具体的jni method对TaintPolicy, TaintProfile以及TaintProfileEntry进行更新了。
2)策略执行之搜索阶段
由于整个策略通过hash表实现,所以在开始分析具体的JNI method执行的时候TaintDroid是如何对TaintProfile等结构进行更新之前,我们需要了解TaindDroid对以上三种结构是如何进行搜索的。
涉及到搜索的方法主要有getPolicyProfile以及getEntryTaint。这里主要分析getEntryTaint的实现。函数代码如下:
/*
entry = entry->from
*/
u4 getEntryTaint(const char* entry, const u4* args, const Method* method)
{
u4 tag = TAINT_CLEAR;
char *pos;
/* Determine split point if any */
pos = index((char *) entry, '.'); //这里涉及到entry的命名方式
switch (getEntryType(entry)) {
case kTaintProfileClass: //如果是类的话就获取该类由entry指定的filed的tag
if (dvmIsStaticMethod(method)) {
tag = getFieldEntryTaint(pos+1, method->clazz, NULL);
} else {
tag = getFieldEntryTaint(pos+1, method->clazz, (Object*)args[0]);
}
break;
case kTaintProfileParam:
tag = getParamEntryTaint(entry, args, method);
break;
default:
ALOGW("TaintPolicy: Invalid from type: [%s]", entry);
}
return tag;
}
函数逻辑还是很简单的,概括如下:
1. 通过getEntryType函数获取entry->from所对应的变量的类型;
2. 根据变量的具体类型调用不同的处理方法获取该变量的taint,如getFieldEntryTaint、getParamEntryTaint。
不过要想理解getFieldEntryTaint之类的函数,需要先了解entry->from与entry->to的命名规则:
其命名有三种方式(这段话有点问题,待修正):
1) class.field1[.field2[…]]。class只能用于第一个参数arg0,即this或静态方法的当前class;
2) param.num[.field1[.field2[…]]]。Num表示参数的序列号,另外如果某变量的tag并不是保存在栈帧中与参数相邻的tag中,就可能继续添加字段名;
3) return。 Ununsed。
现在再分析getFieldEntryTaint就简单了:
u4 getFieldEntryTaint(const char* entry, ClassObject* clazz, Object* obj)
{
u4 tag = TAINT_CLEAR;
FieldRef fRef;
fRef = getFieldFromEntry(entry, clazz, obj); //此时的entry已经去掉了’class.’ ,‘argX.’, ‘return.’ 前缀
if (fRef.field != NULL) {
tag = getTaintFromField(fRef.field, fRef.obj);
}
return tag;
}
对于静态域来说,obj为Null。首先,发现有一个新的结构体FieldRef :
/* 这是一个封装结构体,在处理嵌套的实例域entry的时候会用到*/
typedef struct {
Field *field;
Object *obj;
} FieldRef;
继续分析getFieldFromEntry,此函数递归地查找某个class对象的某个field,最后返回由object和field构成的封装结构体FieldRef.
如果fRef不为空的话,就通过getTaintFromField函数获取该field的tag。其代码如下:
u4 getTaintFromField(Field* field, Object* obj)
{
u4 tag = TAINT_CLEAR;
if (dvmIsStaticField(field)) {
StaticField* sfield = (StaticField*) field;
tag = dvmGetStaticFieldTaint(sfield);
} else {
InstField* ifield = (InstField*) field;
if (field->signature[0] == 'J' || field->signature[0] == 'D') {
tag = dvmGetFieldTaintWide(obj, ifield->byteOffset);
} else {
tag = dvmGetFieldTaint(obj, ifield->byteOffset);
}
}
return tag;
}
这里用到的dvmGetFieldTaintXXX系列函数都是inline函数,定义在oo/ObjectInlines.h中。
搜索完毕,下面就开始进行更新了。
3)策略执行之更新阶段
首先,看propMethodProfile方法的实现:
/* Returns a taint if the profile policy indicates propagation
* to the return
*/
u4 propMethodProfile(const u4* args, const Method* method)
{
u4 rtaint = TAINT_CLEAR;
TaintProfile* profile = NULL;
const TaintProfileEntry* entry = NULL;
profile = getPolicyProfile(method); //根据method结构体获取该方法对应的TaintProfile结构体(此函数很耗时)
if (profile == NULL) {
return rtaint; //若为空,表示当前profile链表中没有此方法,那么就直接返回空tag
}
//LOGD("TaintPolicy: applying policy for %s.%s",
// method->clazz->descriptor, method->name);
/* Cycle through the profile entries */
for (entry = &profile->entries[0]; entry->from != NULL; entry++) {
u4 tag = TAINT_CLEAR;
tag = getEntryTaint(entry->from, args, method);
if (tag) {
//LOGD("TaintPolicy: tag = %d %s -> %s",
// tag, entry->from, entry->to);
rtaint |= addEntryTaint(tag, entry->to, args, method);
}
}
return rtaint;
}
其功能简要概括如下:
1. 根据method结构体获取该方法对应的TaintProfile结构体;
2. 遍历该TaintProfile包含的所有TaintProfileEntry结构体,通过getEntryTaint获取每个entry->from所对应的变量的污点,如果其污点不为空的话,就将这个污点通过addEntryTaint函数添加到entry->to所对应的变量中,并将addEntryTaint的返回值添加给rtaint。这里涉及到addEntryTaint的处理逻辑:如果entry->to对应的变量的类型为kTaintProfileReturn的话,就说明这是一个返回函数,那么我们就不需要再存储其tag,只需要将它返回给上层函数就行,否则就存储tag到entry->to对于的变量中,且返回给上层函数的tag为空。
通过上面的处理,就实现了taint profile的污点传播了,但是枚举所有JNI方法的数据流是一件及其耗时的任务,所以最好能通过源码分析工具来离线地、自动化地实现数据流更新(这项工作TD并没有完成)。
2、dvmInvokeMethod分析
之所以要单独分析此方法是因为我们在对方法进行hook后,需要通过此方法来调用原来的java method。鉴于origionMethod的accessFlags不是native的,所以对于dvmInvokeMethod分析而言,我们只考虑非native分支。
代码如下:
Object* dvmInvokeMethod(Object* obj, const Method* method,
ArrayObject* argList, ArrayObject* params, ClassObject* returnType,
bool noAccessCheck)
{
ClassObject* clazz;
Object* retObj = NULL;
Thread* self = dvmThreadSelf();
s4* ins;
int verifyCount, argListLength;
JValue retval;
bool needPop = false;
#ifdef WITH_TAINT_TRACKING
u4 rtaint = TAINT_CLEAR;
int slot_cnt = 0;
bool nativeTarget = dvmIsNativeMethod(method);
/* For simplicity, argument tags for native targets
* are unioned. This may cause false positives, but
* it is the easiest way to handle this for now.
*/
u4 nativeTag = TAINT_CLEAR;
#endif
/* verify arg count */
if (argList != NULL)
argListLength = argList->length;
else
argListLength = 0;
……
clazz = callPrep(self, method, obj, !noAccessCheck); //分配一个interpreted栈帧
if (clazz == NULL)
return NULL;
needPop = true;
/* "ins" for new frame start at frame pointer plus locals */
#ifdef WITH_TAINT_TRACKING
if (nativeTarget) {
/* native target, no taint tag interleaving */
ins = ((s4*)self->interpSave.curFrame) + (method->registersSize - method->insSize);
} else {
/* interpreted target, taint tags are interleaved */
ins = ((s4*)self->interpSave.curFrame) +
((method->registersSize - method->insSize) << 1);
}
#else
ins = ((s4*)self->interpSave.curFrame) +
(method->registersSize - method->insSize);
#endif
verifyCount = 0;
//ALOGD(" FP is %p, INs live at >= %p", self->interpSave.curFrame, ins);
/* put "this" pointer into in0 if appropriate */
if (!dvmIsStaticMethod(method)) {
assert(obj != NULL);
*ins++ = (s4) obj; //非静态方法第一个参数为this
#ifdef WITH_TAINT_TRACKING
if (!nativeTarget) {
*ins++ = TAINT_CLEAR; //交叉存储的污点为空
}
slot_cnt++;
#endif
verifyCount++;
}
/*
* Copy the args onto the stack. Primitive types are converted when
* necessary, and object types are verified.
*/
DataObject** args = (DataObject**)(void*)argList->contents;
ClassObject** types = (ClassObject**)(void*)params->contents;
for (int i = 0; i < argListLength; i++) {
#ifdef WITH_TAINT_TRACKING
int tag = dvmGetPrimitiveTaint(*args, *types); //取得基本类型变量的污点,对于非基本类型,统一返回空
#endif
int width = dvmConvertArgument(*args++, *types++, ins); //获取变量的长度,4字节为单位。
if (width < 0) {
dvmPopFrame(self); // throw wants to pull PC out of stack
needPop = false;
throwArgumentTypeMismatch(i, *(types-1), *(args-1));
goto bail;
}
#ifdef WITH_TAINT_TRACKING
/* dvmConvertArgument() returns -1, 1, or 2 */
if (nativeTarget) {
nativeTag |= tag; /* TODO: is there a better way to do this?*/
ins += width;
} else { //对于基本类型的参数,其污点交叉存储,对于非基本类型的参数,由于其width为1,tag为空(其污点存储在自己的结构体中),所以在这里这么处理不会出错。
if (width == 2) {
ins[2] = ins[1];
ins[1] = tag;
ins[3] = tag;
ins += 4;
} else if (width == 1) {
ins[1] = tag;
ins += 2;
} else { /* error condition duplicated from above */
dvmPopFrame(self);
needPop = false;
dvmThrowExceptionFmt(gDvm.exIllegalArgumentException, "argument type mismatch");
goto bail;
}
}
slot_cnt += width;
#else
ins += width;
#endif
verifyCount += width;
}
#ifdef WITH_TAINT_TRACKING
/* native hack spacer */
*ins++ = TAINT_CLEAR; /* if nativeTarget, this is return taint */
{
int i;
if (nativeTarget) {
for (i = 0; i < slot_cnt; i++) {
*ins++ = nativeTag; /* TODO: better way? */
}
}
}
#endif
#ifndef NDEBUG
if (verifyCount != method->insSize) {
ALOGE("Got vfycount=%d insSize=%d for %s.%s", verifyCount,
method->insSize, clazz->descriptor, method->name);
assert(false);
goto bail;
}
#endif
if (dvmIsNativeMethod(method)) {
TRACE_METHOD_ENTER(self, method);
/*
* Because we leave no space for local variables, "curFrame" points
* directly at the method arguments.
*/
(*method->nativeFunc)((u4*)self->interpSave.curFrame, &retval,
method, self);
TRACE_METHOD_EXIT(self, method);
#ifdef WITH_TAINT_TRACKING
rtaint = ((u4*)self->interpSave.curFrame)[slot_cnt];
#endif
} else {
#ifdef WITH_TAINT_TRACKING
dvmInterpret(self, method, &retval, &rtaint); //解释执行,方法的返回污点存储在rtaint中
#else
dvmInterpret(self, method, &retval);
#endif
}
/*
* Pop the frame immediately. The "wrap" calls below can cause
* allocations, and we don't want the GC to walk the now-dead frame.
*/
dvmPopFrame(self);
needPop = false;
/*
* If an exception is raised, wrap and replace. This is necessary
* because the invoked method could have thrown a checked exception
* that the caller wasn't prepared for.
*
* We might be able to do this up in the interpreted code, but that will
* leave us with a shortened stack trace in the top-level exception.
*/
if (dvmCheckException(self)) {
dvmWrapException("Ljava/lang/reflect/InvocationTargetException;");
} else {
/*
* If this isn't a void method or constructor, convert the return type
* to an appropriate object.
*
* We don't do this when an exception is raised because the value
* in "retval" is undefined.
*/
if (returnType != NULL) {
retObj = (Object*)dvmBoxPrimitive(retval, returnType);//对返回值进行box
#ifdef WITH_TAINT_TRACKING
dvmSetPrimitiveTaint((DataObject*)retObj, returnType, rtaint); //将方法执行的返回值的污点存储在retObj对于的污点区域中。
#endif
dvmReleaseTrackedAlloc(retObj, NULL);
}
}
bail:
if (needPop) {
dvmPopFrame(self);
}
return retObj;
}
逻辑很简单,这里主要说一下hook造成的污点丢失问题。
首先,由于在hook方法的dvmBoxPrimitive方法中在为基本类型数据构建object的时候,只考虑了它的值,而没考虑它的taint tag,所以:hook后,所有新生产的基本类型的object参数的tag都丢失掉了(其真正的tag存储在Native栈帧的rtaint变量之后的合适位置)。那么在之后调用dvmInvokeMethod函数,其中执行for(int i; i < argListLength;i++)的时候发现其是通过dvmGetPrimitiveTaint来依次获取各个参数的污点的,但此时对于基本类型的参数,其tag都是空的,所以这里获取的所有tag都是空!这必然造成污点丢失。
作者:简行、走位@阿里聚安全,更多技术文章,请访问阿里聚安全博客