随着 Android 开发的技术宽度不断向 native 层扩展,Native hook 已经被用于越来越多的业务场景中,之前作者一直游离于Java层面的逆向,后来工作使然,接触到了Native 层的Hook,熟悉了ELF的文件结构&GOT/PLT&In Line Hook的相关知识和实际操作,Android Native Hook 的实现方式有很多种,我们接下来要讲的是 GOT/PLT Hook (篇幅略略略长,阅读时长约 20 min )
官方是如何解释的呢,下面请看大屏幕:
ELF(Executable and Linking Format),即“可执行可连接格式”,最初由 UNIX 系统实验室(UNIX System Laboratories – USL)做为应用程序二进制接口(Application Binary Interface - ABI)的一部分而制定和发布。 ELF 作为一种可移植的格式,被 TIS 应用于基于 Intel 架构 32 位计算机的各种操作系统上。 ELF 的最大特点在于它有比较广泛的适用性,通用的二进制接口定义使之可以平滑地移植到多种不同的操作环境上。这样,不需要为每一种操作系统都定义一套不同的接口,因此减少了软件的重复编码与编译,加强了软件的可移植性。 ELF 文件格式规范由 TIS(Tool Interface Standards – 工具接口标准)委员会制定, TIS 委员会是一个微型计算机工业的联合组织,它致力于为 32 位操作系统下的开发工具提供标准化的软件接口。这种接口包括目标标志格式、可执行文件格式,以及调试信息的格式。
嘟嘟嘟嘟…一大堆,建议略过 ,著名哲学家嘟嘟斯基曾说过:“太长不看”
首先,综上所述,明确一个概念,ELF 是一个文件格式,诸如我们所见的.so动态库,均属于ELF文件格式
没图说个**?
下面是作者简单画了一个图,从两个不同的角度来进行分析
如果不太了解没关系,大概先看一下,我们接着往下说~~~
连接(链接)视图:可以简单理解为目标文件的储存视图,也就是文件的静态解析视图;
运行(执行)视图:可以简单理解为目标文件的内存视图,也就是文件的动态运行视图;
其实就是程序run没run起来的区别~
文件头部定义了Magic,以及指向节头表SHT(section_header_table ) 和 程序头表PHT(program_header_table) 的偏移
我们来拿curl.so文件来看一哈~
其中Magic表示了这是一个ELF文件: ELF 文件是以 7F 45 4C 46 开头 , 其中 7F 是一个二进制标志 , 45 4C 46 是 ELF 字符对应的 ASCII 码 ;
而节头表SHT(section_header_table ) 和 程序头表PHT(program_header_table) 的偏移地址也显示出来了~
运行命令: arm-linux-androideabi-readelf -h xxx.so
ELF文件在链接视图中是 以节(section)为单位来组织和管理各种信息
看图图~
其中比较重要的是圈起来的几个点,以下是说明:
记不住?没关系,我也是,先了解大概意思,慢慢往下走~
运行命令: arm-linux-androideabi-readelf -S xxx.so
ELF文件在执行视图中是 以段(Segment)为单位来组织和管理各种信息
所有类型为 PT_LOAD 的段(segment)都会被动态链接器(linker)映射(mmap)到内存中
运行命令: arm-linux-androideabi-readelf -l xxx.so
基于ELF的结构我们暂时先了解到这里,如果展开讲的话太鸡儿多了哈~~ 不是我懒~
作者是Android出身,所以仅从Android角度来分析如何加载so的,我们在使用一个动态库(.so)内的函数时,都要先对其进行加载,在android中,我们一般是使用System.loadLibrary的方式进行加载,它的内部实现其实也是调用系统内部linker中的dlopen、dlsym、dlclose函数完成对目标动态库的装载~
动态链接的原理是让程序按照既定模块拆分成各个相对独立的部分,在程序运行的时候才将它们链接在一起,从而形成一个完整的程序,相当于Android的热拔插、组件化等,而不是整体打包成一个dex使用~ 当so动态库被装载的时候,动态链接器linker会将动态库装载到进程的地址空间,并且将程序中所有没确定的符号绑定到相应的动态链接库中,并进行重定位的工作~
共享库进行重定位的主要原因是在于导入符号原因,在动态链接下,可执行文件如果依赖于其他共享对象,也就是说有导入的符号时(比如easy_curl_getopt函数),那么它的代码或数据中就会有对于导入符号的引用,在编译时这些导入符号的地址未知,在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位
动态链接的文件中,有专门的重定位表分别叫做.rel.dyn和.rel.plt:(刚才看表的时候有圈起来哦)
R_AARCH64_GLOB_DA和R_AARCH64_JUMP_SL是ARM64下的重定位方式,这两个类型的重定位入口表示,被修正的位置只需要直接填入符号的地址即可。比如我们看setopt函数这个重定位入口,它的类型为R_AARCH64_JUMP_SL,它的偏移为0x000000066400,它实际上位于.got中
运行命令: arm-linux-androideabi-readelf -r xxx.so
呼,喝杯水~ 在前面的装载->动态链接->重定位完成之后,我们目标动态库的基址已经确定了,在当我们调用某个函数(比如Curl的curl_easy_setopt函数时),调用函数其实并没有直接调用原始该函数地址,他会先经过PLT程序链接表(Procedure Link Table),跳转至GOT全局偏移表(Global Offset Table)获取目标函数curl_easy_setopt函数的全局偏移,这样就可以通过基址+偏移的方式定位真实curl_easy_setopt函数的地址,当然,目前android平台大部分CPU架构是没有提供延迟绑定(Lazy Binding)机制的(只有MIPS架构支持延迟绑定),所以所有外部过程引用都在映像执行之前解析~
PLT:程序链接表(Procedure Link Table),外部调用的跳板,在ELF文件中以独立的段存放,段名通常叫做”.plt”
GOT:全局偏移表(Global Offset Table),用于记录外部调用的入口地址,段名通常叫做”.got”
到这里我们就开始实际操作了,前面的内容仅作为基础知识了解,在边做边学是最快的学习方式,接下来我们会以curl的curl_easy_perform(请求)进行Hook,得到请求的时机,come on~
.dynsym:在之前的描述中,这个节里只保存了与动态链接相关的符号导入导出
我们先来找到自定义的目标函数curl_a_website:
运行命令: arm-linux-androideabi-readelf -s xxx.so
我们可以看到目标的perform函数在0x15fc的地方,我们再看下对应的反汇编代码是什么样子的~
注意检查你的abi,反正我的so使用arm不行哈哈哈, 可以使用如下:
运行命令: aarch64-linux-android-objdump.exe -D xx.so
这里会看到我们自己的curl_a_website 函数通过BLX(相对寻址)指令走到curl_a_website @plt里面~ 那么,由此可以得出当执行我们的代码段.text中的 curl_a_website 函数的时候,内部会通过BLX相对寻址的方式进入.plt节,计算程序计数器 PC 的当前值跳转进入.got节~ 中间经过经过PLT和GOT的跳转,到达我们最终的真实的导入函数的地址~
前面也提到过动态链接重定位表中的.rel.plt是对函数引用的修正,它所修正的位置位于.got。我们最终都是要通过.got确定目标函数的偏移,因此这里我们可以用readelf直接看到fwrite函数的偏移
通过如下可以查看ELF中需要重定位的函数,我们看下curl_a_website()函数。
运行命令: arm-linux-androideabi-readelf -r xxx.so
其中,我们可以看到 curl_a_website 的偏移是 0x3070 ,那么得到了偏移值,基址怎么确定呢? 来,跑起来~
使用命令获取:
cat /proc/对应进程的pid/maps
上图已经列举出了我们的应用加载的一些so库,左边标记红色的地址就是各个so库的基址
addr = base_addr + 0x3070
通过我们前面的一顿操作~,已经拿到目标函数curl_a_websute()的指针了 所以我们兴冲冲的开始写入我们目标函数的地址就可以啦!
#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 success");
//可以根据你的想法随便hook哪个函数
//curl_a_wbsite();
LOG_D("hook end");
//return curl_a_wbsite...;
}
void Java_com_test_hook_hookWebSite(JNIEnv *env, jobject obj, jstring jSoName) {
const char *soName = (*env)->GetStringUTFChars(env, jSoName, 0);
LOG_D("name=%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("value_addr=0x%08X", base_addr);
if (0 == base_addr) return;
//2. 基址+偏移=真实的地址
addr = base_addr + 0x2FE0;
LOG_D("value_addr=0x%08X", addr);
//保存旧的函数地址
你的函数= *(void **) addr;
//替换新的目标地址
*(void **) addr = hook_fwrite;
}
当然,以上的代码仅提供了示例,稍微微微修改一下即可~
tips:
在理论上朝这个地址写入我们目标函数的地址感觉就可以了,但是因为函数是我自己写的, 所以在hook其他函数时有两点需要注意:
1、目标函数的地址很可能没有写权限,因此需要提前调整目标函数地址的权限
2、由于ARM有缓存指令集,hook之后可能会不成功,读取的是缓存中的指令,因此这里需要清除一下指令缓存
综上所述,一套流程下来感觉Native Hook的流程并不太复杂,但是相关的基础例如ELF文件结构和组成、链接装载重定位等基础逻辑的认知还是比较重要~
当然这仅仅是有符号表的函数Hook,那如果是没有函数符号表的Hook呢?(什么?InIineHook?)
最后,由于作者能力有限,在部分细节的描述可能不全面或者会有偏差,欢迎大佬们指正!
https://github.com/bytedance/bhook https://www.cnblogs.com/goodhacker/p/9306997.html https://cstriker1407.info/blog/android-plt-got-hook-introduce/