Android平台下Dalvik层hook框架ddi的研究
阅读原文时间:2023年07月09日阅读:1

通过adbi,可以对native层的所有代码进行hook。但对于Android系统来说,这还远远不够,因为很多应用都还是在Dalvik虚拟机中运行的。

那么,有没有什么办法可以对Dalvik虚拟机中跑的代码进行hook呢?

adbi的作者再接再厉,写了一个叫做ddi(Dynamic Dalvik Instrumentation)的框架,可以从这里获得其源代码:https://github.com/crmulliner/ddi

首先,大家知道,在Dalvik虚拟机中每一个方法都由一个称作Method的结构体来表示(包括JNI方法)。ddi其实就是通过修改特定方法所对应的Method结构体中的变量来实现对Dalvik层方法的hook的。

我们先来看看这个结构体

[cpp] view plain copy

大概解释一下这个结构体中几个重要变量的意思

1)clazz:表示这个方法是定义在哪个类中的;

2)accessFlags:表示该方法对应的一些属性,具体如下表(顺便也列一下ClassFieldaccessFlags的具体含义):

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个参数(没有longdouble型的),其使用到了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.cdalvik_hook.c两个C代码源文件中。我们先来看看dexstuff.c,其中有一个dexstuff_resolv_dvm函数,其代码如下:

[cpp] view plain copy

而代码中使用到的所谓mydlsym函数的代码如下:

[cpp] view plain copy

非常简单,其作用主要是记录一下log,便于调试,本质上还是调用了dlsym函数。结合前面的基础知识,很容易就可以看出,这个函数其实就是想获得进程中libdvm.so动态库的一些有用的函数或者全局变量的地址,并将其保存在一个dexstuff_t(位于dexstuff.h源文件中)结构体中

[cpp] view plain copy

这个结构体非常简单,只是记录下了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++的符号改编机制介绍》。[函数名称粉粹]

好了,让我们接下来看第二个函数,其代码如下:

[cpp] view plain copy

这个函数看函数名就知道,其作用是将指定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文件中):

[cpp] view plain copy

可以看到全局变量dvm_dalvik_system_DexFile其实被定义成一个结构体数组,数组中每个元素都是DalvikNativeMethod类型的,其定义如下(代码位于dalvik\vm\Native.h文件中):

[cpp] view plain copy

这个结构体是联系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文件中)如下:

[cpp] view plain copy

第一个参数u4结构数组,这是入参,第一个元素转换过后的要加载的dex文件路径名字符串指针,第二个元素是转换过后的要输出文件的路径字符串指针,由于不需要输出这里设置成NULL。其第二个参数其实是用来返回结果的通过它得到dex加载后的句柄cookie

# 当dex加载进来后,会得到一个叫做cookie的东西,它非常的重要,可以理解为是该dex在Dalvik虚拟机的句柄,后面很多地方都需要使用。

所以这个dexstuff_loaddex函数的作用就是将指定位置的dex文件加载进当前进程中,并且返回其对应的句柄cookie

接下来再看第三个,也是此文件中最后一个重要函数,其代码如下:

[cpp] view plain copy

这个函数从名字上来看是用来定义(define)一个dex文件中的指定类的,其实就是将指定类加载(load)进来并链接(link)起来。其调用的方法和原理与前面介绍的dexstuff_loaddex基本一致,大家可以自己分析。

这里笔者还想多提一点,熟悉JNIEnv的人应该知道,它内部也有一个函数指针,可以提供许多有用的JNI函数供Native函数使用,其中就有一个叫做DefineClass的函数(代码位于libnativehelper\include\nativehelper\jni.h文件中):

[cpp] view plain copy

这个函数看参数,应该是可以直接从二进制数据中解析加载一个类。既然是这样的话,那为什么还要绕一个弯子,不直接用JNIEnv中已经提供的呢?我们来看看这个所谓DefineClass的实现(代码位于dalvik\vm\Jni.cpp文件中):

[cpp] view plain copy

看见了没有,直接返回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动作都是在这个函数中完成的,让我们一点点分析:

[cpp] view plain copy

先来分析一下入参,第一个参数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结构体了,接着看代码:

[cpp] view plain copy

确实,代码接下来先调用dexstuff_t结构体中dvmFindVirtualMethodHierByDescriptor_fnPtr指针指向的函数,也就是libdvm.so中的dvmFindVirtualMethodHierByDescriptor函数,来试图在你指定的类中找到你指定名字的那个虚函数。这里所谓的虚函数,指的其实是非静态函数,也就是函数名字前没有static关键字。如果没有找到的话,那么接下来会调用dexstuff_t结构体中dvmFindDirectMethodByDescriptor_fnPtr指针指向的函数,也就是libdvm.so中的dvmFindDirectMethodByDescriptor函数,来试图在你指定类中找到你指定名字的那个静态函数。如果在类的非静态和静态函数列表中都找不到你指定的函数,那说明你弄错了,否则会得到你要hook那个函数的Method结构体

B、接着,将查找到的指向类结构体Method方法结构体的指针存在dalvik_hook_t结构体变量h中,留作后面使用。------[查找目标类的目标方法的步骤完成]

好了,现在要找的所有关键信息全部收集齐了,万事俱备只欠东风,下面正式下手了:

[cpp] view plain copy

# 先将那个代表你要hook函数的Method结构体中的一些变量的当前值保存下来这些值在后面恢复的时候是要用到的,至于为什么要恢复后面会介绍。

保存完后就要真的动手修改了,接下来的几行代码是hook的核心:

[cpp] view plain copy

如果前面基础知识中关于Method结构体中各个域的作用还不是非常清楚的话,请再回过头去看一遍。

# 要修改的值都是保存在dalvik_hook_t结构体中的,它们都是在函数dalvik_hook_setup中被设置好了的:

[cpp] view plain copy

除去输出日志的代码,其实代码就修改了Method结构体中7域的值,我们一个个分析:

1)accessFlags

其实就是将原始的值与h->af中的值了一下,而h->af的值被设置成了0x0100。通过前面的基础知识,大家知道,accessFlags中每一位都表示该方法的一个特性,那么0x0100是什么呢?通过前面的表格可以看到,这一位是表示这个方法是不是Native的。所以,代码其实是将这个函数修改成了Native的

2)jniArgInfo

通过前面的介绍,大家知道了这个域其实是方便JNI的调用,提高调用速度的。将其修改成0x80000000就表明不使用这个域中记录的信息,而是在JNI调用的时候重新计算。这个方法原本可能并不是Native的,现在被你偷偷改成了Native的,所以肯定不能使用这个域进行优化。

3)insSizeregistersSizeoutsSize

大家知道,所谓的寄存器只存在于Dalvik虚拟机中,在Native的代码中并不存在这种虚拟寄存器的概念。因此,表示函数中用作调用别的函数传递参数的寄存器个数(outsSize)肯定是0。并且对于Native函数来说,其输入参数寄存器个数(insSize)和所有使用的寄存器个数(registersSize是相等的(Native函数内部肯定没用到虚拟寄存器)。而insSizeregistersSize值要被设置成Native函数对应的Java代码定义中参数传递所需要的寄存器个数

4)insnsnativeFunc

咦,奇怪了,代码中并没有修改这两个域呀? 这两个域是通过调用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函数来实现:

[cpp] view plain copy

#很简单,把原来备份下来的原始值再重新写回去,这样修改完成之后hook的代码就不起作用了。而 dalvik_postcall函数 的作用刚好相反,再修改Method结构体中的变量进行hook

[cpp] view plain copy

好了,ddi框架的所有代码全部解释清楚了,逻辑其实非常简单。

最后,看一下代码中包含的一个例子,看看这个框架到底怎么用。所有代码在smsdispatch.c源文件中:

[cpp] view plain copy

前面也说过了,ddi是要和adbi结合起来使用的,所有hook的代码最终会被编译成一个.so动态库。代码的第一行指定这个动态库被加载进进程后执行的第一个函数是my_init。而在函数my_init,调用adbi框架的hook函数,完成对进程中libc.so动态库epoll_wait函数的hook,将对其的调用自己编写的my_epoll_wait函数的调用:

[cpp] view plain copy

先是调用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函数中去:

[cpp] view plain copy

这个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 作者大牛的博客写很细致,说的很明白,值得学习。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章