一、知识回顾
在之前一篇文章中,已经介绍了Android中如何修改内存指令改变方法执行逻辑,当时那篇文章的大致流程很简单,在程序运行起来,dex文件被加载到内存中之后,通过读取maps文件,获取dex文件的内存其实地址,然后通过文件头信息找到指定dex在内存中的数据结构,这里还需要详细了解Dex文件的格式,不了解的同学可以看这篇文章:Android中Dex文件格式解析,然后使用系统函数修改内存读写属性,在通过指定方法名找到该方法在内存的指令地址,然后替换即可。我们可以简单看一下dex文件被映射到内存之后的地址:
二、免root进行native层hook
上面的这种方案有一点不好就是,需要熟悉dex文件格式,然后通过方法名通过地址转化获取其内存对应的指令,操作有点繁琐,而本文将介绍一种简便的方式,修改起来非常简单。就是通过hook系统函数来做到这一点。而这种hook功能是免root的,所以只能hook应用内部逻辑。对其他应用程序没有任何效果,不过这个就已经满足本文操作的需求了,关于hook系统函数网上已经有现成的框架:https://github.com/ele7enxxh/Android-Inline-Hook,这个框架用法也非常简单。自己下载之后导入工程即可。下面来看看它的具体用法:
新建一个NDK工程,这个不多多说了,然后把这几个框架中的文件拷贝到jni目录下,hook代码主要在InlineHook.cpp中:
这里看到我们会用到两个函数进行hook:
第一个是注册函数:registerInlineHook
参数:1、原始函数地址,2、hook的新函数地址,3、原始函数的二级指针
第二个是hook函数:inlineHook
参数:1、原始函数地址
这里为了演示效果,我们先Hook系统函数puts,我们需要在hook之前定义新函数以及旧函数的函数指针类型,这里一定要注意,新函数定义类型要和原始函数保持一致。不然hook失败的:
接下来,我们需要出发这个hook操作,我们可以在java层定义一个native方法,然后加载出发即可:
这里定义一个native方法了,然后用javah命令生成指定的头文件即可:
在native方法中开始进行hook操作,运行程序看日志信息即可:
看到了,我们成功的hook了系统函数puts。接下来我们开始进入本文的正题了,如何hook系统函数来修改程序运行时态指令。
三、hook系统加载类函数
在上面的hook操作中可以看到,如果想hook一个函数,需要先找到这个函数的声明,所以我们第一步需要想好hook哪个系统函数,如何获取这个函数的声明?这个不难,因为我们想修改程序运行时态指令,那么肯定和dex加载解析过程分不开,这个就简单了,直接去 [Android源码/dalvik/libdex/] 下找到DexFile.h头文件,查看他的内部函数声明和一些数据结构定义信息:
我们发现了这个函数,为什么呢?因为我们知道一个方法执行之前肯定需要解析类信息加载到内存,而这个函数就是加载类必定运行的函数,在看看这个函数的声明:
返回值是DexClassDef结构体指针,看看DexClassDef定义结构体:
这个结构体就是描述了一个类的详细信息,每个字段在这里不多解释了,不了解的同学可以去看看之前介绍dex文件格式的那篇文章。这里我们关心的就是类代码数据的偏移地址,这个值在后面会用到,用它获取类代码结构体信息,后面会介绍。
两个参数是:
第一个参数:DexFile结构体指针
这个结构体包括了整个类的全局信息。后面再获取其他结构体信息都会用到这个值。
第二个参数:是加载类的名称
这个参数在这里也非常重要,因为我们想修改一个方法的指令,肯定需要通过类去查找的,这个类名就非常重要了。
分析完了这个函数声明之后,下面就开始操作了hook了,不过还需要做两件事:
第一件事:因为我们看到上面涉及到很多dex的结构体定义,所以我们需要手动的把这个系统头文件DexFile.h拷贝到我们的工程中,我们可以只保留一些有用的结构体定义和函数即可。
第二件事:因为hook的时候需要原始函数地址的,所以这里我们需要利用系统函数dlopen和dlsym来获取指定函数的地址,关于这两个函数用法网上介绍的知识非常多了,这里不在详细介绍了,他们大致的功能就是可以通过函数名获取so文件中的函数地址。
这里又要注意,为了获取正确的函数名称,我们需要导出设备中的libdvm.so文件,在设备的/system/lib/libdvm.so下,然后用IDA打开libdvm.so文件:
搜索dexFindClass函数名,然后查看他的代码位置,获取导出的函数名。
上面两件事完成之后,下面就可以开始hook操作了,操作过程和上面hook系统函数puts方式完全一样:
然后我们使用dlopen和dlsym函数获取正确的函数地址即可。
hook触发逻辑,依然是之前定义的native方法:
到这里,我们还需要做一个操作,就是手动利用DexClassLoader来加载一个我们自己编写的dex文件,来看看hook是否成功了。所以我们还需要在构建一个工程:
这个工程非常简单,有一个核心的工具类,类中有一个计算方法:
我们的目的就是把这个方法的乘法改成加法操作。运行这个工程,获取dex文件,这里为了加载简单,直接把这个dex文件放到SD卡目录下,然后在回到上面的hook工程,需要在Java成编写一个加载dex文件的方法:
我们利用DexClassLoader加载之前将CoreDex工程编译获取的dex文件,然后加载类利用反射执行计算方法,传入的参数是2和3,正常结果是乘法也就是6,我们就要把乘法变成加法,让结果输出的是5。加载逻辑我们用一个点击事件来触发:
在回到native层中的hook代码:
这里主要看hook的新函数功能,过程有点复杂,这里一步一步来详细分析。首先我们需要过滤处理的类,不能所有的类都做处理,然后通过原始函数,获取类的DexClassDef结构信息,然后利用系统函数dlsym调用函数dexReadAndVerifyClassData获取类对应的数据结构信息,这里依然需要用IDA打开libdvm.so文件查看这个函数的导出名称:
获取到类对应的数据结构DexClassData信息,之后就可以获取类中的方法个数和具体信息了,这里再来看一下DexClassData数据结构信息,这个结构体在DexClass.h中,我们依然把结构信息拷贝到我们的工程中即可:
有了这个结构体,下面就来获取方法的个数,这里的方法分为类方法和对象方法,在DexDataClassHeader结构体信息中,这里我们利用系统函数dexGetClassData获取类的代码数据结构:
接下来,继续看如何获取类中的方法信息:
因为我们知道那个calculateMoney方法是对象方法,所以这里直接获取对象方法结构体信息,然后依次遍历获取每个方法,通过系统函数dexGetMethodId获取DexMethodIds结构体信息:
这里需要注意的是每个方法都是在内存中依次挨着的,所以直接利用指针操作即可获取每个方法的结构体信息。然后在利用系统函数dexStringById获取方法名称,这个也是系统函数,一样方式拷贝到工程中来即可:
有了方法名就需要进行过滤了,只处理我们的那个calculateMoney方法,然后在获取方法对应的数据结构信息DexCode了,依然如此,我们需要把DexCode结构体信息从系统中拷贝到工程中:
然后利用系统函数dexGetCode通过DexMethod结构体获取DexCode结构体信息
有了DexCode结构体信息之后,我们可以打印方法的原始指令数据:
然后我们因为需要修改内存指令,所以还需要把内存修改为可读属性:
这里需要注意的是,修改的起始地址一定是系统内存页的整数倍,所以需要做一次转化。修改完内存属性之后。
四、修改指令逻辑
接下来就可以构造指令,然后替换内存指令即可。那么如何获取原始指令,怎么把乘法改成加法呢?这里就需要利用010Editor软件了,直接查看这个方法的指令数据:
这里看到,这个方法有三条指令,但是一条指令是两个字节,所以一共是6个字节,这里看到的是十进制的数据了,我们可以把这三个十进制数据转化成6个十六进制数据:
然后我们现在只需要把乘法指令码改成加法指令码即可,这个需要参考Bytecode of Dalvik了:
这里也看到加法指令就是十六进制的90,也就是十进制的144,所以咋们替换指令就简单了:
替换指令之后,在此打印指令数据即可,好了,到这里我们就完成了所有的操作了,下面就来运行看看日志信息了:
看到日志,我们成功的把指令146变成了144了,在往下看日志,就可以看到计算结果是加法了,也就是5:
就这样我们在内存中修改了这个方法的指令逻辑,把乘法逻辑变成了加法逻辑了。神奇吧。到这里我们也算介绍完了本文的大致内容了。不过有的同学可以看得没太明白,没关系,下面在来总结一下流程:
五、流程总结
首先明确我们的目的就是想能修改内存中指定方法的运行指令逻辑,那么不用之前介绍的那个读取内存中的dex数据然后靠地址来检索到指令地址,而是采用hook系统函数来实现:
第一步:就需要找到hook点,每个方法要想运行肯定是先将方法所属的类加载到内存中,那么就需要调用系统的函数:dexFindClass,而这个函数的返回值是一个DexClassDef结构体信息。
第二步:通过DexClassDef结构体信息获取类的数据结构体信息DexClassData,然后获取类的所有方法信息。
第三步:遍历方法结构体信息DexMethod,找到我们想要处理的方法信息,然后在获取其DexCode方法数据结构体信息。
第四步:有了方法数据结构体信息之后,就可以获取到方法的指令个数和具体指令数据,在修改之前必须修改内存属性为可读写的。
第五步:通过查阅虚拟机指令集,找到加法指令码替换原来的乘法指令码,然后覆盖内存中的原始指令即可。
所以在这个过程中发现,会涉及到很多数据结构体,不过好在这些结构体信息都定义在DexFile.h和DexClass.h这两个头文件中,他们存放在[Android源码/dalvik/libdex/]目录下。而这些结构体信息也是相互包含的,下面就来整理一下:
严重声明
本文的所有代码可以进入编码美丽小密圈自取,为了安全考虑,请大家不要利用本文代码做非法操作,秉着技术爱好学习本文尼内容!如果有人利用本文技术操作非法东西带来的法律后果由操作者自己负责,与本文作者没任何关系!
六、总结
当然本文介绍完了之后,有一个很大的用途就是hook虚拟机的一些函数来做一些事情,其实说到这里本文并不是本次研究的重点,重点是下一篇文章,只是为了需要hook系统的函数,本文就先做个铺垫而已。聪明的人应该知道我下一篇文章要介绍啥了,而下一篇文章才是重点。篇幅原因,不得不将其内容拆分了。看文本文之后,一定要记得一点,不仅在java层可以免root进行hook操作,在native也是可以的。这个知识点对未来安全防护,逆向分析,应用开发都非常重要。