Android 免 Root 权限通过 Hook 系统函数修改程序运行时内存指令逻辑
一知识回顾
在之前一篇文章中, 已经介绍了 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 原始函数地址, 2hook 的新函数地址, 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/] 目录下而这些结构体信息也是相互包含的, 下面就来整理一下:
来源: http://blog.csdn.net/jiangwei0910410003/article/details/78068654