通过adbi,可以对native层的所有代码进行hook。但对于Android系统来说,这还远远不够,因为很多应用都还是在Dalvik虚拟机中运行的。
那么,有没有什么办法可以对Dalvik虚拟机中跑的代码进行hook呢?
adbi的作者再接再厉,写了一个叫做ddi(Dynamic Dalvik Instrumentation)的框架,可以从这里获得其源代码:https://github.com/crmulliner/ddi。
首先,大家知道,在Dalvik虚拟机中每一个方法都由一个称作Method的结构体来表示(包括JNI方法)。ddi其实就是通过修改特定方法所对应的Method结构体中的变量来实现对Dalvik层方法的hook的。
我们先来看看这个结构体:
大概解释一下这个结构体中几个重要变量的意思:
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所指向的本地代码,如果不是Native的话,就执行insns所指向的Dalvik代码。
I、关于Dalvik虚拟机的寄存器,还要特别说明一下。当一个方法被调用的时候,如果该方法有N个参数的话,那么该方法的参数将被置于最后N个寄存器中(假设参数中没有long型和double型的参数的话)。而与其它类型不同的是,long型和double型的变量将占用两个寄存器。同时,对于非静态方法来说(不是static的),其第一个参数总是调用该方法的对象。
举个例子:
如果一个非静态方法有2个参数(没有long和double型的),其使用到了5个寄存器(v0-v4),那么参数将置于最后2个寄存器,即v3和v4中,而v2是这个方法所在对象的指针,v0和v1是函数自己所需要的本地寄存器。这时,registersSize的值是5,而insSize的值是3。
II、其次,在运行时,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函数,其代码如下:
而代码中使用到的所谓mydlsym函数的代码如下:
非常简单,其作用主要是记录一下log,便于调试,本质上还是调用了dlsym函数。结合前面的基础知识,很容易就可以看出,这个函数其实就是想获得进程中libdvm.so动态库的一些有用的函数或者全局变量的地址,并将其保存在一个dexstuff_t(位于dexstuff.h源文件中)结构体中:这个结构体非常简单,只是记录下了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++的符号改编机制介绍》。[函数名称粉粹]
好了,让我们接下来看第二个函数,其代码如下:
这个函数看函数名就知道,其作用是将指定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的定义如下(代码位于dalvikvm ativedalvik_system_DexFile.cpp文件中):
可以看到全局变量dvm_dalvik_system_DexFile其实被定义成一个结构体数组,数组中每个元素都是DalvikNativeMethod类型的,其定义如下(代码位于dalvikvmNative.h文件中):
这个结构体是联系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函数的定义(代码位于dalvikvm ativedalvik_system_DexFile.cpp文件中)如下:
其第一个参数是u4结构数组,这是入参,第一个元素是转换过后的要加载的dex文件路径名字符串指针,第二个元素是转换过后的要输出文件的路径字符串指针,由于不需要输出这里设置成NULL。其第二个参数其实是用来返回结果的,通过它得到dex加载后的句柄cookie。
所以这个dexstuff_loaddex函数的作用就是将指定位置的dex文件加载进当前进程中,并且返回其对应的句柄cookie。
接下来再看第三个,也是此文件中最后一个重要函数,其代码如下:
这个函数从名字上来看是用来定义(define)一个dex文件中的指定类的,其实就是将指定类加载(load)进来并链接(link)起来。其调用的方法和原理与前面介绍的dexstuff_loaddex基本一致,大家可以自己分析。这里笔者还想多提一点,熟悉JNIEnv的人应该知道,它内部也有一个函数指针,可以提供许多有用的JNI函数供Native函数使用,其中就有一个叫做DefineClass的函数(代码位于libnativehelperinclude ativehelperjni.h文件中):
这个函数看参数,应该是可以直接从二进制数据中解析加载一个类。既然是这样的话,那为什么还要绕一个弯子,不直接用JNIEnv中已经提供的呢?我们来看看这个所谓DefineClass的实现(代码位于dalvikvmJni.cpp文件中):看见了没有,直接返回NULL,在Dalvik中通过JNIEnv直接调用DefineClass是不支持的。
好了,到这里dexstuff.c中的代码就已经分析完了。其实很简单,它一共提供了三个重要的函数:
1)dexstuff_resolv_dvm用来获得libdvm.so动态库中许多hook所需要的函数或全局变量的地址,并将这些函数或全局变量的地址保存dexstuff_t结构体中,这个函数要先于后面两个函数调用;
2)dexstuff_loaddex用来动态加载一个指定路径下的dex文件到当前程序中来;
3)dexstuff_defineclass用来在加载进来的dex文件中找到、加载并链接指定的类。
好了,那我们接着再看dalvik_hook.c中所包含的函数。我们最先来看dalvik_hook函数的实现,这个函数非常重要,真正的hook动作都是在这个函数中完成的,让我们一点点分析:
先来分析一下入参,第一个参数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它还有什么意义呢?
A、所以,包含你要hook函数的类一定已经被加载进来了。要查找的具体类名被包含在传进来的dalvik_hook_t结构体的中的clname变量中。这个函数会返回指向要找的那个类的指针,如果没有找到则返回NULL。-------------[查找目标类的步骤完成]
接下来按照逻辑来说,应该是要找到要hook的那个函数的Method结构体了,接着看代码:
确实,代码接下来先调用dexstuff_t结构体中dvmFindVirtualMethodHierByDescriptor_fnPtr指针指向的函数,也就是libdvm.so中的dvmFindVirtualMethodHierByDescriptor函数,来试图在你指定的类中找到你指定名字的那个虚函数。这里所谓的虚函数,指的其实是非静态函数,也就是函数名字前没有static关键字。如果没有找到的话,那么接下来会调用dexstuff_t结构体中dvmFindDirectMethodByDescriptor_fnPtr指针指向的函数,也就是libdvm.so中的dvmFindDirectMethodByDescriptor函数,来试图在你指定类中找到你指定名字的那个静态函数。如果在类的非静态和静态函数列表中都找不到你指定的函数,那说明你弄错了,否则会得到你要hook那个函数的Method结构体。
B、接着,将查找到的指向类结构体和Method方法结构体的指针都保存在dalvik_hook_t结构体变量h中,留作后面使用。------[查找目标类的目标方法的步骤完成]
好了,现在要找的所有关键信息全部收集齐了,万事俱备只欠东风,下面正式下手了:
# 先将那个代表你要hook函数的Method结构体中的一些变量的当前值保存下来,这些值在后面恢复的时候是要用到的,至于为什么要恢复后面会介绍。保存完后就要真的动手修改了,接下来的几行代码是hook的核心:
如果前面基础知识中关于Method结构体中各个域的作用还不是非常清楚的话,请再回过头去看一遍。
# 要修改的值都是保存在dalvik_hook_t结构体中的,它们都是在函数dalvik_hook_setup中被设置好了的:
除去输出日志的代码,其实代码就修改了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函数代码(你自己编写的函数)。
C、好了,做完了这些修改之后,这个函数已经被你修改成一个Native函数了,并且指向的是你自己写的Native代码,hook的目的达到了。------[Hook指定类的目标函数的步骤完成了]
#介绍到这里,可以看出,其实ddi的核心思想是,将一个你要hook的Dalvik层的函数,人为修改成一个你自己写的Native的函数。这样,当代码以后调用到这个函数的时候,实际上就变成调用你自己的JNI函数了,你可以在你自己写的JNI函数中实现任何功能,从而达到了hook的目的。#
D、但是,还有一个问题,如果我在自己写的JNI函数中,完成了一些附加的功能之后,还想继续调用原来的那个函数怎么办呢?
答案很简单,把那个函数Method结构体中的变量值再恢复回去不就行了嘛。ddi也正是这么做的,通过dalvik_prepare函数来实现:
#很简单,把原来备份下来的原始值再重新写回去,这样修改完成之后hook的代码就不起作用了。而 dalvik_postcall函数 的作用刚好相反,再修改Method结构体中的变量进行hook:
好了,ddi框架的所有代码全部解释清楚了,逻辑其实非常简单。
最后,看一下代码中包含的一个例子,看看这个框架到底怎么用。所有代码在smsdispatch.c源文件中:
前面也说过了,ddi是要和adbi结合起来使用的,所有hook的代码最终会被编译成一个.so动态库。代码的第一行指定这个动态库被加载进进程后执行的第一个函数是my_init。而在函数my_init中,调用adbi框架的hook函数,完成对进程中libc.so动态库中epoll_wait函数的hook,将对其的调用自己编写的my_epoll_wait函数的调用:
先是调用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函数中去:
这个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生效。
转载于:http://blog.csdn.net/roland_sun/article/details/38640297 作者大牛的博客写很细致,说的很明白,值得学习。