android基于plt/got的hook原理
目录
概述
简单示例
ELF文件格式初探
装载、动态链接与重定位
PLT与GOT
如何定位基址?
如何修改呢?
解析基址和偏移
思考和小结
概述
我们日常开发中编写的C/C++代码经过NDK进行编译和链接之后,生成的动态链接库或可执行文件都是ELF格式的,它也是Linux的主要可执行文件格式。我们今天就要借助一个示例来理解一下android平台下native层hook的操作和原理,不过在这之前,我们还是要先了解一下ELF相关的内容。
简单示例
这里给了一段示例代码:写入一段文本到文件中去。
为了简单起见,后面的都是以armeabi-v7a为例
void writeText(const char *path, const char *text) {FILE *fp = NULL;if ((fp = fopen(path, "w")) == NULL) {LOG_E("file cannot open");}//写入数据fwrite(text, strlen(text), 1, fp);if (fclose(fp) != 0) {LOG_E("file cannot be closed");}
}
输出目标共享库:libnative-write.so,这个共享库的作用是写入一段文本,我们今天的目标就是对这个目标共享库的fwrite函数进行hook操作。
ELF文件格式初探
ELF文件有两种视图形式:链接视图和执行视图
链接视图:可以理解为目标文件的内容视图
执行视图:可以理解为目标文件的内存视图
文件头(elf_header)
文件头部定义了魔数,以及指向节头表SHT(section_header_table)和程序头表PHT(program_header_table)的偏移。
节头表SHT(section_header_table)
ELF文件在链接视图中是 以节(section)为单位来组织和管理各种信息。
.dynsym:为了完成动态链接,最关键的还是所依赖的符号和相关文件的信息。为了表示动态链接这些模块之间的符号导入导出关系,ELF有一个叫做动态符号表(Dynamic Symbol Table)的段用来保存这些信息。
.rel.dyn:实际上是对数据引用的修正,它所修正的位置位于.got以及数据段。
.rel.plt:是对函数引用的修正,它所修正的位置位于.got。
.plt:程序链接表(Procedure Link Table),外部调用的跳板。
.text:为代码段,也是反汇编处理的部分,以机器码的形式存储。
.dynamic:描述了模块动态链接相关的信息。
.got:全局偏移表(Global Offset Table),用于记录外部调用的入口地址。
.data: 数据段,保存的那些已经初始化了的全局静态变量和局部静态变量。
程序头表PHT(program_header_table)
ELF文件在执行视图中是 以段(Segment)为单位来组织和管理各种信息。
所有类型为 PT_LOAD 的段(segment)都会被动态链接器(linker)映射(mmap)到内存中。
装载、动态链接与重定位
1、装载
这个很好理解,我们在使用一个动态库内的函数时,都要先对其进行加载,在android中,我们通常是使用System.loadLibrary的方式加载我们的目标共享库,它的内部实现其实也是调用系统内部linker中的dlopen、dlsym、dlclose函数完成对目标共享库的装载。
2、动态链接
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个个单独的可执行文件。
当共享库被装载的时候,动态链接器linker会将共享库装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。
3、重定位
共享库需要重定位的主要原因是导入符号的存在。动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,也就是说有导入的符号时(比如fwrite函数),那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号的地址未知,在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位。
动态链接的文件中,有专门的重定位表分别叫做.rel.dyn和.rel.plt:
# arm-linux-androideabi-readelf -r libnative-write.so
R_ARM_GLOB_DAT和R_ARM_JUMP_SLOT是ARM下的重定位方式,这两个类型的重定位入口表示,被修正的位置只需要直接填入符号的地址即可。比如我们看fwrite函数这个重定位入口,它的类型为R_ARM_JUMP_SLOT,它的偏移为0x0002FE0,它实际上位于.got中。
PLT与GOT
前面的过程装载->动态链接->重定位完成之后,目标共享库的基址已经确定了,当我们调用某个函数时(比如fwrite函数),调用函数并不是直接调用原始fwrite函数的函数地址,它会先经过PLT程序链接表(Procedure Link Table),跳转至GOT全局偏移表(Global Offset Table)获取目标函数fwrite函数的全局偏移,这时候就可以通过基址+偏移的方式定位真实的fwrite函数地址了,目前android平台大部分CPU架构是没有提供延迟绑定(Lazy Binding)机制的(只有MIPS架构支持延迟绑定),所有外部过程引用都在映像执行之前解析。
PLT:程序链接表(Procedure Link Table),外部调用的跳板,在ELF文件中以独立的段存放,段名通常叫做".plt"
GOT:全局偏移表(Global Offset Table),用于记录外部调用的入口地址,段名通常叫做".got"
前面的内容都是一些概念性的内容,比较枯燥,接下来会以writeText函数为入口,一步一步查看我们最终的目标函数fwrite的地址。
从.dynsym开始
.dynsym:上面也说到了,这个节里只保存了与动态链接相关的符号导入导出关系。
# arm-linux-androideabi-readelf -s libnative-write.so
我们可以看到目标的writeText函数在0x705的地方,我们再看下对应的反汇编代码:
# arm-linux-androideabi-objdump -D libnative-write.so
这里会看到我们自己的writeText函数通过BLX(相对寻址)指令走到fwrite@plt里面,简化上面的图:
从上面的简图中,我们可以看到,当执行我们的代码段.text中的writeText函数的时候,内部会通过BLX相对寻址的方式进入.plt节,计算程序计数器 PC 的当前值跳转进入.got节。
00000668 <fwrite@plt>:668: e28fc600 add ip, pc, #0, 12 //由于ARM三级流水,PC = 0x668 + 0x8;66c: e28cca02 add ip, ip, #8192 ; 0x2000 // ip = ip + 0x2000670: e5bcf970 ldr pc, [ip, #2416]! ; 0x970 // pc = ip + 0x970
以上三条指令执行完,从0x668 + 0x8 + 0x2000 + 0x970 = 0x2FE0位置取值给PC,通过LDR完成间接寻址的跳转。因此在.got(全局符号表)中偏移为0x2FE0的位置就是目标函数fwrite的偏移了。
可以看到,当我们通过libnative-write.so共享库中的writeText函数调用libc中的导入函数fwrite的时候,还是经历了一些曲折的过程,这里的过程,指的就是经过PLT和GOT的跳转,到达我们最终的真实的导入函数的地址。
更快速的找到目标函数的偏移
前面也提到过动态链接重定位表中的.rel.plt是对函数引用的修正,它所修正的位置位于.got。我们最终都是要通过.got确定目标函数的偏移,因此这里我们可以用readelf直接看到fwrite函数的偏移
通过如下可以查看ELF中需要重定位的函数,我们看下fwrite()函数。
# arm-linux-androideabi-readelf -r libnative-write.so
可以看到我们从libc库中的导入函数fwrite,这个偏移和我们刚才计算的偏移是一致的都是:0x2FE0
如何定位基址?
我们首先来看基址的获取,这里要用到linux系统的一些特性
# 进程的虚拟地址空间
# 进程的虚拟地址空间
# cat /proc/<pid>/maps
上图已经列举出了我们的应用加载的一些so库,左边标记红色的地址就是各个so库的基址
#在进程ID为32396的进程中加载的几个库中
libhook-simple.so库的基址为:0xD40D8000
libnative-hook.so库的基址为:0xD411B000
libnative-write.so库的基址为:0xD414F000
因此我们实际需要hook的函数fwrite的地址为:
addr = base_addr + 0x2FE0
如何修改呢?
通过前面的分析,我们已经拿到目标函数fwrite()的地址指针了,理论上只要朝这个地址写入我们目标函数的地址就可以了?
并不是!!!
注意:
1、目标函数的地址很可能没有写权限,因此需要提前调整目标函数地址的权限
2、由于ARM有缓存指令集,hook之后可能会不成功,读取的是缓存中的指令,因此这里需要清除一下指令缓存
这时候我们就需要用到linux中的函数:
//调整目标内存区域的权限
int mprotect(void* __addr, size_t __size, int __prot);
//清除缓存指令
__builtin___clear_cache(void * __page_start,void * __page_end)
操作如下:
//调整写权限
mprotect((void *) PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
//朝目标函数的地址写新的地址
*(void **) addr = hook_fwrite;
//清除指令缓存
__builtin___clear_cache((void *) PAGE_START(addr), (void *) PAGE_END(addr));
完整的hook操作:
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <inttypes.h>
#include <sys/mman.h>
#include "hook_simple.h"
#include "logger.h"#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)size_t hook_fwrite(const void *buf, size_t size, size_t count, FILE *fp) {LOG_D("hook fwrite success");//这里插入一段文本const char *text = "hello ";fwrite(text, strlen(text), 1, fp);return fwrite(buf, size, count, fp);
}/*** 直接硬编码的方式进行* hook演示操作* @param env* @param obj* @param jSoName*/
void Java_com_feature_hook_NativeHook_hookSimple(JNIEnv *env, jobject obj, jstring jSoName) {const char *soName = (*env)->GetStringUTFChars(env, jSoName, 0);LOG_D("soName=%s", soName);char line[1024] = "\n";FILE *fp = NULL;uintptr_t base_addr = 0;uintptr_t addr = 0;// 1. 查找自身对应的基址if (NULL == (fp = fopen("/proc/self/maps", "r"))) return;while (fgets(line, sizeof(line), fp)) {if (NULL != strstr(line, soName) &&sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)break;}fclose(fp);LOG_D("base_addr=0x%08X", base_addr);if (0 == base_addr) return;//2. 基址+偏移=真实的地址addr = base_addr + 0x2FE0;LOG_D("addr=0x%08X", addr);//注意:调整写权限mprotect((void *) PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);//替换目标地址*(void **) addr = hook_fwrite;//注意:清除指令缓存__builtin___clear_cache((void *) PAGE_START(addr), (void *) PAGE_END(addr));
}
可以看到这里已经成功完成了hook操作
看了上面的例子,大家觉得native-hook复杂吗?看上去不复杂?那如果让你来设计一个类似于xHook的库,你能直接在框架里硬编码0x2FE0吗?,当然不行,因此需要一个通用的逻辑来定位具体的偏移和基址才行,接下来我们重点来看下偏移和基址如何通过通用的代码来动态确定
解析基址和偏移
我们接下来要做的重要的工作是在运行期间,动态定位目标共享库中的基址和偏移。
这里主要如下几个步骤:
1、获取目标so库的基址
基址很好确定:
void *get_module_base(pid_t pid, const char *module_name) {FILE *fp;long addr = 0;char filename[32] = "\n";char line[1024] = "\n";LOG_D("pid=%d ", pid);if (pid < 0) {snprintf(filename, sizeof(filename), "/proc/self/maps");} else {snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);}// 获取指定pid进程加载的内存模块信息fp = fopen(filename, "r");while (fgets(line, sizeof(line), fp)) {if (NULL != strstr(line, module_name) &&sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &addr) == 1)break;}fclose(fp);return (void *) addr;
}
我们只需要读取自身进程的/proc/self/maps就可以获取当前进程装载的模块信息,这个不算复杂。
2、保存原始的调用地址
当我们自己的共享库完成对目标共享库的hook操作之后,要保证功能正常运行,需要先保存原始的函数调用地址。
3、解析ELF文件头部
这里先根据魔数来确定是否为ELF文件格式,而且文件头部里实际已经指明了SHT和PHT的偏移信息了
4、根据(基址 + e_phoff)确定程序头表PHT(Program Header Table)的地址
上图中的这个e_phoff的值是指向程序头表PHT的偏移,0x34 = 52
5、遍历程序头表PHT(Program Header Table)
看上面的图示,程序头表PHT内的元素是个数组,但是我们目前只关心类型为PT_DYNAMIC(指定动态链接信息)的项,获取对应的p_vaddr
6、根据(基址+p_vaddr)确定.dynamic段的地址,遍历dynamic link table
接着遍历出d_tag=DT_JMPREL类型的项的d_val值,这个值是指向重定位表的偏移,不要疑惑下图中的偏移是0x2E7C,为什么下面Start却是0x1E7C,刚才也说了ELF文件有两种视图,一个链接视图,一个执行视图,下面的图是链接视图,但我们最终要以执行视图里的结果为准。
7、根据(基址+d_val)确定重定位表的地址,接下来我们遍历函数名称对比即可找到目标函数的偏移
参考下面这张图吧
也就是说上面的那么多步骤,实际目的就是确定运行期间的目标共享库中的重定位表的地址。
实际应用
笔者只是借助一个示例来理解基于PLT/GOT进行hook操作的原理,实际项目中,我们完全可以借助这种方案对目标共享库中的malloc,free进行hook操作,在没有源码的情况下,以此来分析第三方共享库中可能存在的内存泄露问题。
具体可以看看:LoliProfiler的实现。
思考
Q:比如我要hook我当前应用中的malloc函数,是否只对某个共享库进行hook即可?
A:并不是,每一个共享库都有它自己的PLT/GOT表,因此需要对每个共享库都要进行hook操作才行。
Q:我在共享库中通过dlopen、dlsym的方式调用系统导入函数,这中方式可以被hook住吗?
A:不可以,上面的整个内容其实都是基于PLT/GOT表定位目标函数进行hook操作,而dlopen、dlsym是目标共享库在运行期间,动态定位导入函数,这种方式并不生效。
小结
其实hook操作本身的技术原理并不复杂,但是要针对android平台下的共享库进行hook操作,仅仅只了解hook操作是不够的,可以看到上面大部分的内容其实是在跟ELF文件周旋,要结合它的加载、动态链接、重定位过程,才能更好的理解基于PLT/GOT的hook原理,由于笔者能力有限,在部分细节的描述可能不全面或者会有偏差,欢迎指正!
项目地址
native-hook
参考
《程序员的自我修养:链接、装载与库》
https://github.com/iqiyi/xHook/
https://www.cnblogs.com/goodhacker/p/9306997.html
android基于plt/got的hook原理相关推荐
- ELF PLT Hook 原理简述
[无线平台]ELF PLT Hook 原理简述 简述 Android 是基于Linux的操作系统,因此在Android开发平台上,ELF是原生支持的可执行文件格式:ELF文件格式除了作为可执行文件,还 ...
- Android免Root环境下Hook框架Legend原理分析
0x1 应用场景 现如今,免Root环境下的逆向分析已经成为一种潮流! 在2015年之前的iOS软件逆向工程领域,要想对iOS平台上的软件进行逆向工程分析,越狱iOS设备与安装Cydia是必须的!几乎 ...
- android hook 第三方app_基于 VirtualApp 结合 whale hook框架实现hook第三方应用
要点 1. whale hook framework 使用示例: 2. 参考项目:VirtualHook: 3. 按照 VirtualHook 修改 VirtualApp: 4. 编写 hook pl ...
- xposed hook java_[原创]Android Hook 系列教程(一) Xposed Hook 原理分析
章节内容 一. Android Hook 系列教程(一) Xposed Hook 原理分析 二. Android Hook 系列教程(二) 自己写APK实现Hook Java层函数 三. Androi ...
- Android so注入( inject)和Hook(挂钩)的实现思路讨论
本文博客:http://blog.csdn.net/qq1084283172/article/details/54095995 前面的博客中分析一些Android的so注入和Hook目标函数的代码,它 ...
- Xposed hook原理
先来个总结 java源码经过编译后,得到很多个class文件, 考虑到手机的内存较小,google改进了字节码的组织形式,将一个app中的所有class文件合到了一起构成dex文件,当然并不是简单的拼 ...
- 简单概括Xposed hook原理
转载自:https://www.jianshu.com/p/b29a21a162ad 这块知识本身是挺多的,网上有对应的源码分析,本文尽量从不分析代码的角度来把原理阐述清楚. Xposed是一个在an ...
- Android插件化开发指南——Hook技术(一)【长文】
文章目录 1. 前言 2. 将外部dex加载到宿主app的dexElements中 3. 插件中四大组件的调用思路 4. Hook 2.1 对startActivity进行Hook 2.1.1 AMS ...
- Android基于XMPP Smack openfire 开发的聊天室
公司刚好让做即时通讯模块,服务器使用openfire,偶然看到有位仁兄的帖子,拷贝过来细细研究,感谢此仁兄的无私,期待此仁兄的下次更新 转自http://blog.csdn.net/lnb333666 ...
最新文章
- 十进制中正整数N中1的个数(2)
- python 类方法装饰器_python类装饰器即__call__方法
- 十周年“设计大佬”首谈行业变革:数据驱动超过 10% 的业绩增长
- java 热补丁_Android热补丁之AndFix原理解析
- CNN for Semantic Segmentation(语义分割,论文,代码,数据集,标注工具,blog)
- 开源软件 安全风险_3开源安全风险及其解决方法
- kali linux 升级命令_作为高级Java,你应该了解的Linux知识
- Python-体育竞技模拟
- 原来歌这样唱也很好听
- filter动态参数 maven_多环境下Maven项目的管理
- Jquery学习 -千锋学习
- 高通工具QXDM、QCAT和QPST关系及功能
- 计算机一级wpsoffice知识,全国计算机一级WPSOffice考试试题
- 如何查看APP ID
- Quartz定时任务框架(二):Trigger触发器详解
- echarts柱形图根据数据排序顺序要求更改颜色
- 苹果录屏没声音_苹果手机扬声器没声音是怎么回事?
- Linux unison 效率,linux利用unison实现双向或多向实时同步
- 2D骨骼动画工具DragonBones的使用教程
- 用四种不同的方法实现 tab栏切换