通过adbi,可以对native层的所有代码进行hook。但对于Android系统来说,这还远远不够,因为很多应用都还是在Dalvik虚拟机中运行的。
那么,有没有什么办法可以对Dalvik虚拟机中跑的代码进行hook呢?adbi的作者再接再厉,写了一个叫做ddi(Dynamic Dalvik Instrumentation)的框架,可以从这里获得其源代码:https://github.com/crmulliner/ddi。
在正式开始介绍之前,我们先来讲一些基础知识。
首先,大家知道,在Dalvik虚拟机中每一个方法都由一个称作Method的结构体来表示(包括JNI方法)。ddi其实就是通过修改特定方法所对应的Method结构体中的变量来实现对Dalvik层方法的hook的。
我们先来看看这个结构体:
struct Method { ClassObject* clazz; u4 accessFlags; u2 methodIndex; u2 registersSize; u2 outsSize; u2 insSize; const char* name; DexProto prototype; const char* shorty; const u2* insns; int jniArgInfo; DalvikBridgeFunc nativeFunc; bool fastJni; bool noRef; bool shouldTrace; const RegisterMap* registerMap; bool inProfile; };
大概解释一下这个结构体中几个重要变量的意思:
1)clazz:表示这个方法是定义在哪个类中的;
2)accessFlags:表示该方法对应的一些属性,具体如下表(顺便也列一下Class和Field中accessFlags的具体含义):
AccessFlag比特位 | 类(Class) | 方法(Method) | 域(Field) |
0x00001 | Public | Public | Public |
0x00002 | Private | Private | Private |
0x00004 | Protected | Protected | Protected |
0x00008 | Static | Static | Static |
0x00010 | Final | Final | Final |
0x00020 | N/A | Synchronized | N/A |
0x00040 | N/A | Bridge | Volatile |
0x00080 | N/A | VarArgs | Transient |
0x00100 | N/A | Native | N/A |
0x00200 | Interface | N/A | N/A |
0x00400 | Abstract | Abstract | N/A |
0x00800 | N/A | Strict | N/A |
0x01000 | Synthetic | Synthetic | Synthetic |
0x02000 | Annotation | N/A | N/A |
0x04000 | Enum | N/A | Enum |
0x08000 | N/A | Miranda | N/A |
0x10000 | Verified | Constructor | N/A |
0x20000 | Optimized | Declared_Synchronized | N/A |
3)methodIndex:对于具体已经实现了的虚方法来说,这个是该方法在类虚函数表(vtable)中的偏移;对于未实现的纯接口方法来说,这个是该方法在对应的接口表(假设这个方法定义在类继承的第n+1个接口中,则表示iftable[n]->methodIndexArray)中的偏移;
4)registersSize:该方法总共用到的寄存器个数,包含入口参数所用到的寄存器,还有方法内部自己所用到的其它本地寄存器;
5)outsSize:当该方法要调用其它方法时,用作参数传递而使用的寄存器个数;
6)insSize:作为调用该方法时,参数传递而使用到的寄存器个数;
7)name:方法的名称;
8)prototype:方法对应的协议(也就是对该方法调用参数类型、顺序还有返回类型的描述);
9)shorty:方法对应协议的短表示法,一个字符代表一种类型;
10)insns:如果这个方法不是Native的话,则这里存放了指向方法具体的Dalvik指令的指针(这个变量指向的是实际加载到内存中的Dalvik指令,而不是在Dex文件中的)。如果这个方法是一个Dalvik虚拟机自带的Native函数(Internal Native)的话,则这个变量会是Null。如果这个方法是一个普通的Native函数的话,则这里存放了指向JNI实际函数机器码的首地址;
11)jniArgInfo:这个变量记录了一些预先计算好的信息,从而不需要在调用的时候再通过方法的参数和返回值实时计算了,方便了JNI的调用,提高了调用的速度。如果第一位为1(即0x80000000),则Dalvik虚拟机会忽略后面的所有信息,强制在调用时实时计算;
12)nativeFunc:如果这个方法是一个Dalvik虚拟机自带的Native函数(Internal Native)的话,则这里存放了指向JNI实际函数机器码的首地址。如果这个方法是一个普通的Native函数的话,则这里将指向一个中间的跳转JNI桥(Bridge)代码;
13)registerMap:表示这个方法在每一个GC安全点上,有哪些寄存器其存放的数值是指向某个对象的引用,它主要是给Dalvik虚拟机做精确垃圾收集使用的。如果感兴趣的话,可以参看《Dalvik虚拟机中RegisterMap解析》这篇博客。
通过前面提到的accessFlags可以判断一个方法是不是Native的(和0x00100相与),如果是Native的话,就直接执行nativeFunc所指向的本地代码,如果不是的话就执行insns所指向的Dalvik代码。
关于Dalvik虚拟机的寄存器,还要特别说明一下。当一个方法被调用的时候,如果该方法有N个参数的话,那么该方法的参数将被置于最后N个寄存器中(假设参数中没有long型和double型的参数的话)。而与其它类型不用的是,long型和double型的变量将占用两个寄存器。
同时,对于非静态方法来说(不是static的),其第一个参数总是调用该方法的对象。
举个例子,如果一个非静态方法有2个参数(没有long和double型的),其使用到了5个寄存器(v0-v4),那么参数将置于最后2个寄存器,即v3和v4中,而v2是这个方法所在对象的指针,v0和v1是函数自己所需要的本地寄存器。这时,registersSize的值是5,而insSize的值是3。
其次,在运行时,Dalvik虚拟机的所有功能其实是通过进程内的libdvm.so动态库来提供的。它对外暴露出了很多函数(导出了很多符号),获得这些函数的指针,就可以直接操作Dalvik虚拟机完成很多功能,例如加载一个dex文件、执行指定对象的指定方法等。想要获得这些函数的指针其实很简单,只需要先调用dlopen获得libdvm.so动态库的句柄,然后再调用dlsym同时传输想要找的函数或全局变量的名字(这个名字必须要出现在libdvm.so动态库的符号表中)。
好了,以上就是ddi所用到的所有基础知识,其实非常简单。接下来,我们结合代码以及前面的基础知识来一步步分析ddi的实现机制。
在正式介绍之前,要特别说明一下,这个所谓的ddi是不能单独工作的,它需要和adbi结合起来使用。在前面的《Android平台下hook框架adbi的研究(上)》中,我介绍了adbi是如何注入一个制定进程,让其加载一个指定的.so动态库进来。这部分其实ddi也是需要的,ddi是不包括进程注入的代码的。而在《Android平台下hook框架adbi的研究(下)》中,我介绍了adbi的框架生成的.so动态库是如何查找并篡改被注入进程中指定函数的。对于这部分,如果你只想hook在Dalvik虚拟机内跑的函数,则并不需要;反之,你还想hook在Native层跑的程序,则可以将adbi和ddi结合起来使用。
好了,不多废话,正式开始介绍。其实所谓的ddi框架的核心代码非常简单,主要包含在dexstuff.c和dalvik_hook.c两个C代码源文件中。我们先来看看dexstuff.c,其中有一个dexstuff_resolv_dvm函数,其代码如下:
void dexstuff_resolv_dvm(struct dexstuff_t *d) { d->dvm_hand = dlopen("libdvm.so", RTLD_NOW); log("dvm_hand = 0x%x\n", d->dvm_hand) if (d->dvm_hand) { d->dvm_dalvik_system_DexFile = (DalvikNativeMethod*) mydlsym(d->dvm_hand, "dvm_dalvik_system_DexFile"); d->dvm_java_lang_Class = (DalvikNativeMethod*) mydlsym(d->dvm_hand, "dvm_java_lang_Class"); d->dvmThreadSelf_fnPtr = mydlsym(d->dvm_hand, "_Z13dvmThreadSelfv"); if (!d->dvmThreadSelf_fnPtr) d->dvmThreadSelf_fnPtr = mydlsym(d->dvm_hand, "dvmThreadSelf"); d->dvmStringFromCStr_fnPtr = mydlsym(d->dvm_hand, "_Z32dvmCreateStringFromCstrAndLengthPKcj"); d->dvmGetSystemClassLoader_fnPtr = mydlsym(d->dvm_hand, "_Z23dvmGetSystemClassLoaderv"); d->dvmIsClassInitialized_fnPtr = mydlsym(d->dvm_hand, "_Z21dvmIsClassInitializedPK11ClassObject"); d->dvmInitClass_fnPtr = mydlsym(d->dvm_hand, "dvmInitClass"); ...... } }
而代码中使用到的所谓mydlsym函数的代码如下:
static void* mydlsym(void *hand, const char *name) { void* ret = dlsym(hand, name); log("%s = 0x%x\n", name, ret) return ret; }非常简单,其作用主要是记录一下 log,便于调试,本质上还是调用了 dlsym函数。结合前面的基础知识,很容易就可以看出,这个函数起始就是想获得进程中 libdvm.so动态库的一些有用的函数或者全局变量的地址,并将其保存在一个 dexstuff_t(位于 dexstuff.h源文件中)结构体中:
struct dexstuff_t { void *dvm_hand; dvmCreateStringFromCstr_func dvmStringFromCStr_fnPtr; dvmGetSystemClassLoader_func dvmGetSystemClassLoader_fnPtr; dvmThreadSelf_func dvmThreadSelf_fnPtr; ...... DalvikNativeMethod *dvm_dalvik_system_DexFile; DalvikNativeMethod *dvm_java_lang_Class; void *gDvm; int done; };这个结构体非常简单,只是记录下了 libdvm.so的句柄,以及所有函数和全局变量的地址等信息。所以,综上所述,通过调用 dexstuff_resolv_dvm函数就可以得到所有控制Dalvik虚拟机的重要函数的地址,并将其记录在 dexstuff_t结构体中,方便后面hook的时候使用。
还有一点,不知道大家有没有看到,有些函数的名字好像非常奇怪,例如_Z32dvmCreateStringFromCstrAndLengthPKcj,这是什么?这里稍微解释以下,大家知道C++是支持函数重载的,还有命名空间,并且不同类中可以定义同名的函数,如果函数最终编译之后的名字都是原来的函数名的话,那么将造成严重的名字冲突问题,例如同名函数的重载、不同命名空间内或不同类中的同名函数等,这些都会造成函数重名。那怎么解决这个问题呢,C++中引入了所谓符号改编(Name Mangling)机制,即编译之后的真实函数名除了本来的函数名外,还加入了例如命名空间名字、类名字以及函数参数类型的缩略名等信息。对于_Z32dvmCreateStringFromCstrAndLengthPKcj这个函数名来说,最前面的_Z说明这是一个全局函数(即不属于任何类,并且在顶层命名空间内);32说明真正的函数名有32个字符;接下来就是真实的函数名,即dvmCreateStringFromCstrAndLength,刚好是32个字符;再下来就是参数的类型信息,Pkc代表函数的第一个参数的类型是const char*,j代表第二个参数的类型是u4。如果大家想进一步了解关于符号重编的具体细节,可以参看《GNU C++的符号改编机制介绍》。
好了,让我们接下来看第二个函数,其代码如下:
int dexstuff_loaddex(struct dexstuff_t *d, char *path) { jvalue pResult; jint result; log("dexstuff_loaddex, path = 0x%x\n", path) void *jpath = d->dvmStringFromCStr_fnPtr(path, strlen(path), ALLOC_DEFAULT); u4 args[2] = { (u4)jpath, (u4)NULL }; d->dvm_dalvik_system_DexFile[0].fnPtr(args, &pResult); result = (jint) pResult.l; log("cookie = 0x%x\n", pResult.l) return result; }
这个函数看函数名就知道,其作用是将指定dex文件(dex文件存放的具体路径由参数path指定)加载进Dalvik虚拟机中。由于传进来的path是C的字符串,所以先要使用libdvm.so中的dvmCreateStringFromCstrAndLength将其转换成Dalvik虚拟机可识别的字符串,而这个函数的地址已经在前面的dexstuff_resolv_dvm函数中,被赋给了dexstuff_t结构体中的dvmStringFromCStr_fnPtr变量,所以这里可以直接调用。完成字符转换之后,接下来调用的东西有点奇怪。通过查看前面函数的代码,可以发现dexstuff_t结构体中dvm_dalvik_system_DexFile变量的值就是在libdvm.so动态库中全局变量dvm_dalvik_system_DexFile的地址,而全局变量dvm_dalvik_system_DexFile的定义如下(代码位于dalvik\vm\native\dalvik_system_DexFile.cpp文件中):
const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { { "openDexFileNative", "(Ljava/lang/String;Ljava/lang/String;I)I", Dalvik_dalvik_system_DexFile_openDexFileNative }, { "openDexFile", "([B)I", Dalvik_dalvik_system_DexFile_openDexFile_bytearray }, { "closeDexFile", "(I)V", Dalvik_dalvik_system_DexFile_closeDexFile }, { "defineClassNative", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;", Dalvik_dalvik_system_DexFile_defineClassNative }, { "getClassNameList", "(I)[Ljava/lang/String;", Dalvik_dalvik_system_DexFile_getClassNameList }, { "isDexOptNeeded", "(Ljava/lang/String;)Z", Dalvik_dalvik_system_DexFile_isDexOptNeeded }, { NULL, NULL, NULL }, };
可以看到全局变量dvm_dalvik_system_DexFile其实被定义成一个结构体数组,数组中每个元素都是DalvikNativeMethod类型的,其定义如下(代码位于dalvik\vm\Native.h文件中):
struct DalvikNativeMethod { const char* name; const char* signature; DalvikNativeFunc fnPtr; };这个结构体是联系Dalvik层与Native层的桥梁,其包含三个变量。第一个是该函数在Dalvik层中的名字,第二个是函数在Dalvik层中的签名(包括函数的参数和返回值类型),最后一个是对应的Native层函数的指针。当你调用 DexClassLoader将一个dex加载进来的时候,实际最终都要调用 DexFile类中的 openDexFileNative函数,而这个函数是Native,对应的就是 Dalvik_dalvik_system_DexFile_openDexFileNative函数。而通过前面的代码可以看出,这个函数的具体地址,就记录在全局变量 dvm_dalvik_system_DexFile数组中第一个元素中的 fnPtr变量中。而 Dalvik_dalvik_system_DexFile_openDexFileNative函数的定义(代码位于dalvik\vm\native\dalvik_system_DexFile.cpp文件中)如下:
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult) { ...... }
其第一个参数是u4结构数组,这是入参,第一个元素是转换过后的要加载的dex文件路径名字符串指针,第二个元素是转换过后的要输出文件的路径字符串指针,由于不需要输出这里设置成NULL。其第二个参数其实是用来返回结果的,通过它得到dex加载后的句柄cookie。
dex加载进来后,会得到一个叫做 cookie的东西,它非常的重要,可以理解为是该dex在Dalvik虚拟机的句柄,后面很多地方都需要使用。所以这个dexstuff_loaddex函数的作用就是将指定位置的dex文件加载进当前进程中,并且返回其对应的句柄cookie。
接下来再看第三个,也是此文件中最后一个重要函数,其代码如下:
void* dexstuff_defineclass(struct dexstuff_t *d, char *name, int cookie) { u4 *nameObj = (u4*) name; jvalue pResult; log("dexstuff_defineclass: %s using %x\n", name, cookie) void* cl = d->dvmGetSystemClassLoader_fnPtr(); Method *m = d->dvmGetCurrentJNIMethod_fnPtr(); log("sys classloader = 0x%x\n", cl) log("cur m classloader = 0x%x\n", m->clazz->classLoader) void *jname = d->dvmStringFromCStr_fnPtr(name, strlen(name), ALLOC_DEFAULT); u4 args[3] = { (u4)jname, (u4) m->clazz->classLoader, (u4) cookie }; d->dvm_dalvik_system_DexFile[3].fnPtr( args , &pResult ); jobject *ret = pResult.l; log("class = 0x%x\n", ret) return ret; }这个函数从名字上来看是用来定义(define)一个dex文件中的指定类的,其实就是将指定类加载(load)进来并链接(link)起来。其调用的方法和原理与前面介绍的 dexstuff_loaddex基本一致,大家可以自己分析。
这里笔者还想多提一点,熟悉JNIEnv的人应该知道,它内部也有一个函数指针,可以提供许多有用的JNI函数供Native函数使用,其中就有一个叫做DefineClass的函数(代码位于libnativehelper\include\nativehelper\jni.h文件中):
struct JNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void* reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize); ...... }这个函数看参数,应该是可以直接从二进制数据中解析加载一个类。既然是这样的话,那为什么还要绕一个弯子,不直接用 JNIEnv中已经提供的呢?我们来看看这个所谓 DefineClass的实现(代码位于dalvik\vm\Jni.cpp文件中):
static jclass DefineClass(JNIEnv* env, const char *name, jobject loader, const jbyte* buf, jsize bufLen) { UNUSED_PARAMETER(name); UNUSED_PARAMETER(loader); UNUSED_PARAMETER(buf); UNUSED_PARAMETER(bufLen); ScopedJniThreadState ts(env); ALOGW("JNI DefineClass is not supported"); return NULL; }看见了没有,直接返回 NULL,在Dalvik中通过 JNIEnv直接调用 DefineClass是不支持的。
1)dexstuff_resolv_dvm用来获得libdvm.so动态库中许多hook所需要的函数或全局变量的地址,并将这些函数或全局变量的地址保存dexstuff_t结构体中,这个函数要先于后面两个函数调用;
2)dexstuff_loaddex用来动态加载一个指定路径下的dex文件到当前程序中来;
3)dexstuff_defineclass用来在加载进来的dex文件中找到、加载并链接指定的类。
好了,那我们接着再看dalvik_hook.c中所包含的函数。我们最先来看dalvik_hook函数的实现,这个函数非常重要,真正的hook动作都是在这个函数中完成的,让我们一点点分析:
void* dalvik_hook(struct dexstuff_t *dex, struct dalvik_hook_t *h) { if (h->debug_me) log("dalvik_hook: class %s\n", h->clname) void *target_cls = dex->dvmFindLoadedClass_fnPtr(h->clname); if (h->debug_me) log("class = 0x%x\n", target_cls) if (h->dump && dex && target_cls) dex->dvmDumpClass_fnPtr(target_cls, (void*)1); if (!target_cls) { if (h->debug_me) log("target_cls == 0\n") return (void*)0; } ......先来分析一下入参,第一个参数 dex是一个指向结构体 dexstuff_t的指针,这个结构体的作用在前面分析 dexstuff_resolv_dvm函数的时候就已经介绍过了,包含了各种 libdvm.so动态库中的函数指针和全局变量;第二个参数 h是一个指向 dalvik_hook_t结构体的指针,它包含了hook一个指定函数所需要的基本信息,是在 dalvik_hook_setup函数中都设置好的,具体每项的作用在遇到的时候还会分析。
函数的一开始,主要是调用了dexstuff_t结构体中dvmFindLoadedClass_fnPtr指针指向的函数,也就是libdvm.so中的dvmFindLoadedClass函数。其作用是在所有已经加载进来的类中找到指定名字的类。你要hook的函数其所在的类,如果根本没被加载,那就说明这个类根本就没被使用,那么你hook它还有什么意义呢?所以,包含你要hook函数的类一定已经被加载进来了。要查找的具体类名被包含在传进来的dalvik_hook_t结构体的中的clname变量中。这个函数会返回指向要找的那个类的指针,如果没有找到则返回NULL。
接下来按照逻辑来说,应该是要找到要hook的那个函数的Method结构体了,接着看代码:
...... h->method = dex->dvmFindVirtualMethodHierByDescriptor_fnPtr(target_cls, h->method_name, h->method_sig); if (h->method == 0) { h->method = dex->dvmFindDirectMethodByDescriptor_fnPtr(target_cls, h->method_name, h->method_sig); } if (!h->resolvm) { h->cls = target_cls; h->mid = (void*)h->method; } if (h->debug_me) log("%s(%s) = 0x%x\n", h->method_name, h->method_sig, h->method) ......确实,代码接下来先调用 dexstuff_t结构体中 dvmFindVirtualMethodHierByDescriptor_fnPtr指针指向的函数,也就是 libdvm.so中的 dvmFindVirtualMethodHierByDescriptor函数,来试图在你指定的类中找到你指定名字的那个虚函数。这里所谓的虚函数,指的其实是非静态函数,也就是函数名字前没有 static关键字。如果没有找到的话,那么接下来会调用 dexstuff_t结构体中 dvmFindDirectMethodByDescriptor_fnPtr指针指向的函数,也就是 libdvm.so中的 dvmFindDirectMethodByDescriptor函数,来试图在你指定类中找到你指定名字的那个静态函数。如果在类的非静态和静态函数列表中都找不到你指定的函数,那说明你弄错了,否则会得到你要hook那个函数的 Method结构体。接着,将查找到的指向类结构体和 Method方法结构体的指针都保存在 dalvik_hook_t结构体变量 h中,留作后面使用。
好了,现在要找的所有关键信息全部收集齐了,万事俱备只欠东风,下面正式下手了:
...... if (h->method) { h->insns = h->method->insns; if (h->debug_me) { log("nativeFunc %x\n", h->method->nativeFunc) log("insSize = 0x%x registersSize = 0x%x outsSize = 0x%x\n", h->method->insSize, h->method->registersSize, h->method->outsSize) } h->iss = h->method->insSize; h->rss = h->method->registersSize; h->oss = h->method->outsSize; ......先将那个代表你要hook函数的 Method结构体中的一些变量的当前值保存下来,这些值在后面恢复的时候是要用到的,至于为什么要恢复后面会介绍。
保存完后就要真的动手修改了,接下来的几行代码是hook的核心:
...... h->method->insSize = h->n_iss; h->method->registersSize = h->n_rss; h->method->outsSize = h->n_oss; if (h->debug_me) { log("shorty %s\n", h->method->shorty) log("name %s\n", h->method->name) log("arginfo %x\n", h->method->jniArgInfo) } h->method->jniArgInfo = 0x80000000; if (h->debug_me) { log("noref %c\n", h->method->noRef) log("access %x\n", h->method->a) } h->access_flags = h->method->a; h->method->a = h->method->a | h->af; if (h->debug_me) log("access %x\n", h->method->a) dex->dvmUseJNIBridge_fnPtr(h->method, h->native_func); ......
如果前面基础知识中关于Method结构体中各个域的作用还不是非常清楚的话,请再回过头去看一遍。
要修改的值都是保存在dalvik_hook_t结构体中的,它们都是在函数dalvik_hook_setup中被设置好了的:
...... h->n_iss = ns; h->n_rss = ns; h->n_oss = 0; h->native_func = func; h->af = 0x0100; ......
出去输出日志的代码,其实代码就修改了Method结构体中7域的值,我们一个个分析:
1)accessFlags:
其实就是将原始的值与h->af中的值与了一下,而h->af的值被设置成了0x0100。通过前面的基础知识,大家知道,accessFlags中每一位都表示该方法的一个特性,那么0x0100是什么呢?通过前面的表格可以看到,这一位是表示这个方法是不是Native的。所以,代码其实是将这个函数修改成了Native的。
2)jniArgInfo:
通过前面的介绍,大家知道了这个域其实是方便JNI的调用,提高调用速度的。将其修改成0x80000000就表明不使用这个域中记录的信息,而是在JNI调用的时候重新计算。这个方法原本可能并不是Native的,现在被你偷偷改成了Native的,所以肯定不能使用这个域进行优化。
3)insSize、registersSize、outsSize:
大家知道,所谓的寄存器只存在于Dalvik虚拟机中,在Native的代码中并不存在这种虚拟寄存器的概念。因此,表示函数中用作调用别的函数传递参数的寄存器个数(outsSize)肯定是0。
并且对于Native函数来说,其输入参数寄存器个数(insSize)和所有使用的寄存器个数(registersSize)是相等的(Native函数内部肯定没用到虚拟寄存器)。而insSize和registersSize的值要被设置成Native函数对应的,在Java代码定义中,参数传递所需要的寄存器个数。
4)insns、nativeFunc:
咦,奇怪了,代码中并没有修改这两个域呀?这两个域是通过调用dexstuff_t结构体中dvmUseJNIBridge_fnPtr指针指向的函数,也就是libdvm.so中的dvmUseJNIBridge函数,来修改的。
具体的代码我就不分析了,作用就是将nativeFunc域改成指向一个JNI桥代码(dvmCallJNIMethod),并且将insns域改成指向真正的JNI函数代码(你自己编写的函数)。
好了,做完了这些修改之后,这个函数已经被你修改成一个Native函数了,并且指向的是你自己写的Native代码,hook的目的达到了。
介绍到这里,可以看出,其实ddi的核心思想是,将一个你要hook的Dalvik层的函数,人为修改成一个你自己写的Native的函数。这样,当代码以后调用到这个函数的时候,实际上就变成调用你自己的JNI函数了,你可以在你自己写的JNI函数中实现任何功能,从而达到了hook的目的。
但是,还有一个问题,如果我在自己写的JNI函数中,完成了一些附加的功能之后,还想继续调用原来的那个函数怎么办呢?答案很简单,把那个函数Method结构体中的变量值再恢复回去不就行了嘛。ddi也正是这么做的,通过dalvik_prepare函数来实现:
int dalvik_prepare(struct dexstuff_t *dex, struct dalvik_hook_t *h, JNIEnv *env) { ...... h->method->insSize = h->iss; h->method->registersSize = h->rss; h->method->outsSize = h->oss; h->method->a = h->access_flags; h->method->jniArgInfo = 0; h->method->insns = h->insns; }
很简单,把原来备份下来的原始值再重新写回去,这样修改完成之后hook的代码就不起作用了。而dalvik_postcall函数的作用刚好相反,再修改Method结构体中的变量进行hook:
void dalvik_postcall(struct dexstuff_t *dex, struct dalvik_hook_t *h) { h->method->insSize = h->n_iss; h->method->registersSize = h->n_rss; h->method->outsSize = h->n_oss; h->method->jniArgInfo = 0x80000000; h->access_flags = h->method->a; h->method->a = h->method->a | h->af; dex->dvmUseJNIBridge_fnPtr(h->method, h->native_func); if (h->debug_me) log("patched BACK %s to: 0x%x\n", h->method_name, h->native_func) }
好了,ddi框架的所有代码全部解释清楚了,逻辑其实非常简单。
最后,看一下代码中包含的一个例子,看看这个框架到底怎么用。所有代码在smsdispatch.c源文件中:
void __attribute__ ((constructor)) my_init(void); void my_init(void) { ...... hook(&eph, getpid(), "libc.", "epoll_wait", my_epoll_wait, 0); }
前面也说过了,ddi是要和adbi结合起来使用的,所有hook的代码最终会被编译成一个.so动态库。代码的第一行指定这个动态库被加载进进程后执行的第一个函数是my_init。而在函数my_init中,调用adbi框架的hook函数,完成对进程中libc.so动态库中,epoll_wait函数的hook,将对其的调用编程对自己编写的my_epoll_wait函数的调用:
static int my_epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) { int (*orig_epoll_wait)(int epfd, struct epoll_event *events, int maxevents, int timeout); orig_epoll_wait = (void*)eph.orig; // remove hook for epoll_wait hook_precall(&eph); // resolve symbols from DVM dexstuff_resolv_dvm(&d); // hook dalvik_hook_setup(&dpdu, "Lcom/android/internal/telephony/SMSDispatcher;", "dispatchPdus", "([[B)V", 2, my_dispatch); dalvik_hook(&d, &dpdu); // call original function int res = orig_epoll_wait(epfd, events, maxevents, timeout); return res; }
先是调用dexstuff_resolv_dvm函数获得在libdvm.so动态库中所有hook需要使用的函数和全局变量的地址。
然后调用dalvik_hook_setup函数,初始化后面hook会用到的dalvik_hook_t结构体。
最后,调用dalvik_hook完成对要hook函数Method结构体的修改,从而完成hook。
本例中,作者想要hook在com.android.internal.telephony.SMSDispatcher类中的dispatchPdus函数,将其导向自己写的my_dispatch函数中去:
static void my_dispatch(JNIEnv *env, jobject obj, jobjectArray pdu) { // load dex classes int cookie = dexstuff_loaddex(&d, "/data/local/tmp/ddiclasses.dex"); log("libsmsdispatch: loaddex res = %x\n", cookie) if (!cookie) log("libsmsdispatch: make sure /data/dalvik-cache/ is world writable and delete data@local@[email protected]\n") void *clazz = dexstuff_defineclass(&d, "org/mulliner/ddiexample/SMSDispatch", cookie); log("libsmsdispatch: clazz = 0x%x\n", clazz) // call constructor and passin the pdu jclass smsd = (*env)->FindClass(env, "org/mulliner/ddiexample/SMSDispatch"); jmethodID constructor = (*env)->GetMethodID(env, smsd, "<init>", "([[B)V"); if (constructor) { jvalue args[1]; args[0].l = pdu; jobject obj = (*env)->NewObjectA(env, smsd, constructor, args); log("libsmsdispatch: new obj = 0x%x\n", obj) if (!obj) log("libsmsdispatch: failed to create smsdispatch class, FATAL!\n") } else { log("libsmsdispatch: constructor not found!\n") } // call original SMS dispatch method jvalue args[1]; args[0].l = pdu; dalvik_prepare(&d, &dpdu, env); (*env)->CallVoidMethodA(env, obj, dpdu.mid, args); log("success calling : %s\n", dpdu.method_name) dalvik_postcall(&d, &dpdu); }
这个my_dispatch函数是一个C语言写的Native函数。当然,你可以直接用C语言实现你想要的所有功能,如果想要调用程序中Dalvik层的代码也可以通过JNIEnv实现。但是,这样似乎太麻烦了,要是可以把想实现的功能直接用Java写,然后编译成一个dex,再动态加载进来,最后用JNIEnv调用它,复杂度将减轻很多。
本例中就是这么做的,它预先将要实现的功能用Java代码实现,并编译成了一个dex文件,将其放到/data/local/tmp/ddiclasses.dex位置上。然后调用dexstuff_loaddex函数将这个dex动态加载进来,再调用dexstuff_defineclass函数将这个dex中的org.mulliner.ddiexample.SMSDispatch类加载进来。前面这两步都是使用非常规的做法,一旦类被加载进来后,就可以用JNIEnv来操作了。接着代码调用了org.mulliner.ddiexample.SMSDispatch类的构造函数,具体这个Java函数的代码我就不分析了,感兴趣的大家可以自己看。
再下来,hook函数还想调用原来的那个dispatchPdus函数。做法是先调用dalvik_prepare函数,将Method结构体恢复。再通过JNIEnv中的CallVoidMethodA方法在JNI函数中直接调用Dalvik中的dispatchPdus函数。最后,再调用dalvik_postcall函数,再将这个函数的Method结构体改成指向你的Native函数,再让hook生效。