PLT Hook 是对底层动态库中函数进行 hook 的一种技术。
在 linux 系统中,可执行文件,动态库,可重定位文件和核心转储文件都是 elf 格式。
对于静态链接来说,所有文件会打包成一个整体,模块对外部符号的引用位置会被记录在重定位表(.rel.xxx)中。在链接时,链接器会根据重定位表在其他模块中找到需要解析的符号引用的实际地址并将其填写到引用位置。
不同于静态链接,动态链接时动态库是独立的,在运行过程中加载时它位置是不固定的,无法确定它的绝对地址。所以对外符号的引用会改为对 got(全局偏移表)中对应项的引用,之后由链接器将符号的实际地址填写到 got 中,从而完成对外部符号的引用。
PLT 表则用来为外部函数提供懒加载的能力。函数只有在首次调用时,才会对函数地址进行解析,之后将函数地址填入 got 表中,之后再次调用时会使用上次解析后的地址。
可参考:GOT表和PLT表
这里我们可以知道,外部动态库中的函数地址都保存在 GOT 表中,我们只要将 GOT 表中相应函数的地址替换为我们自己的函数地址就可以实现 hook。
接下来这里以 xHook 库,来看一下实现 plt hook 的大体流程。
代码实现
下面主要根据如下步骤进行分析:
- 获取目标动态库在内存中的地址和权限信息。
- 计算目标动态库基地址。
- 解析 .dynamic 段中符号表,字符表和重定位表。
- 根据重定位表,找到目标符号在 got 表中位置。
- 替换 got 表中对应位置的地址为 hook 函数地址。
xHook 的使用
参考文档中示例:
#include <stdlib.h>
#include <stdio.h>
#include <test.h>
#include <xhook.h>
void *my_malloc(size_t size)
{
printf("%zu bytes memory are allocated by libtest.so\n", size);
return malloc(size);
}
int main()
{
xhook_register(".*/libtest\\.so$", "malloc", my_malloc, NULL);
xhook_refresh(0);
say_hello();
return 0;
}
可以看到使用起来非常简单。xhook_register 指定要 hook 的动态库,目标函数,以及 hook 函数。之后调用 xhook_refresh 开始进行 hook。
实现分析
xhook_register 主要是保存参数信息。这里主要看 xh_core_refresh 实现。
async 参数用来控制是同步还是异步进行。
int xh_core_refresh(int async)
{
...
if(async)
{
...
}
else
{
//refresh sync
pthread_mutex_lock(&xh_core_refresh_mutex);
xh_core_refresh_impl();
pthread_mutex_unlock(&xh_core_refresh_mutex);
}
return 0;
}
获取动态库的加载信息
/proc/{pid}/maps 会记录程序加载时的内存映射信息。如:依赖的 so,堆和栈等。
所以进行 hook 的第一步就是找到目标动态库在内存中的起始地址。
查找过程如下:
- 解析
/proc/self/maps文件,读取每行中记录的起始地址,权限信息,偏移和名称。
如:
address 内存区域的起始和结束地址。
perms 表示内存区域的访问权限。 offset 对于文件映射它代表的是这段内存映射在文件中的偏移位置。
pathname 对于文件映射它代表的是文件名,其他可能表示为堆、栈或为空。
address perms offset dev inode pathname
b6ec6000-b6ec9000 r-xp 00000000 b3:19 753708 /data/local/tmp/libtest.so
b6ec9000-b6eca000 r--p 00002000 b3:19 753708 /data/local/tmp/libtest.so
b6eca000-b6ecb000 rw-p 00003000 b3:19 753708 /data/local/tmp/libtest.so
- 过滤掉权限和名字不符合规则的记录。不处理共享内存类型,文件映射的名称一般是正常的路径名。
- elf 文件头部一般在只读(r--p)段或可执行段(r-xp)。
- 只读段中 offset 为 0,并且 r-xp 段中 offset 不为 0,头部在只读段。如果 r-xp 中 offset 为 0,头部在可执行段。
static void xh_core_refresh_impl()
{
...
xh_core_map_info_tree_t map_info_refreshed = RB_INITIALIZER(&map_info_refreshed);
if(NULL == (fp = fopen("/proc/self/maps", "r"))) // 打开 /proc/self/maps 文件
{
XH_LOG_ERROR("fopen /proc/self/maps failed");
return;
}
while(fgets(line, sizeof(line), fp))
{
//
if(sscanf(line, "%"PRIxPTR"-%*lx %4s %lx %*x:%*x %*d%n", &base_addr, perm, &offset, &pathname_pos) != 3) continue;
// do not touch the shared memory
if (perm[3] != 'p') continue;
// Ignore permission PROT_NONE maps
if (perm[0] == '-' && perm[1] == '-' && perm[2] == '-')
continue;
//get pathname
while(isspace(line[pathname_pos]) && pathname_pos < (int)(sizeof(line) - 1))
pathname_pos += 1;
if(pathname_pos >= (int)(sizeof(line) - 1)) continue;
pathname = line + pathname_pos;
pathname_len = strlen(pathname);
if(0 == pathname_len) continue;
if(pathname[pathname_len - 1] == '\n')
{
pathname[pathname_len - 1] = '\0';
pathname_len -= 1;
}
if(0 == pathname_len) continue;
if('[' == pathname[0]) continue;
// Find non-executable map, we need record it. Because so maps can begin with
// an non-executable map.
if (perm[2] != 'x') { // 头部在 r--- 区域中的情况
prev_offset = offset;
prev_base_addr = base_addr;
memcpy(prev_perm, perm, sizeof(prev_perm));
strcpy(prev_pathname, pathname);
continue;
}
// Find executable map if offset == 0, it OK,
// or we need check previous map for base address.
if (offset != 0) { // 在 r-xp 中 offset 不为零说明在 r--- 中。
// 进一步判断
if (strcmp(prev_pathname, pathname) || prev_offset != 0 || prev_perm[0] != 'r') {
continue;
}
// The previous map is real begin map
base_addr = prev_base_addr;
}
// 后续对 base_addr 进行验证是否能加载 elf 文件头部。
...
}
基地址和解析 dynamic 段
elf 文件格式中记录的虚拟地址(p_vaddr)是一个相对地址,不是真实际的虚拟地址,因此需要用前面获取到的 elf 文件头地址减去 elf 文件中第一个被加载段的 p_vaddr 地址计算出一个基地址。后续涉及获取虚拟地址的地方都会用这个基地址加上相应的 p_vaddr 得到。
dynamic 段中保存着程序动态链接相关的信息,它有自己的符号表,字符串表和重定位表。
下面格式解析的部分,参考 elf 文件格式和 elf.h 头文件中定义的结构。
以 64 位为例简单介绍一下.dynmaic 段的项结构和重定位结构 dynamic 段中的内容可以看成是 Elf64_Dyn 的数组。根据 d_tag 表示不同的类型,如符号表,字符串表等。
typedef struct {
Elf64_Xword d_tag; // 动态段的类型
union {
Elf64_Xword d_val; // 数值
Elf64_Addr d_ptr; // 地址
} d_un;
} Elf64_Dyn;
d_tag 类型对应重定向表:
DT_JMPREL:对应 .rel(a).plt 主要是与 plt 关联的重定向表,可以理解为记录的都是函数引用。
DT_REL(A):对应 .rel(a).dyn 可以理解为记录的都是变量。
DT_ANDROID_REL(A): 对应 .rel(a).android
重定位项结构: 重定位表项类型:RELA 和 REL。前者会有一个显示的加数,用来计算保存在重定位中字段值。后者是隐式的。
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend; // 显示加数。
} Elf64_Rela;
r_offset 代表修改位置的偏移或地址。对于可重定位文件来说表示偏移。对于可执行文件或共享库文件来说表示地址。
r_info 由索引和重定位类型组成。索引表示符号在符号表中的索引。重定向类型决定了,如何计算 r_offset 处符号引用的值。
int xh_elf_init(xh_elf_t *self, uintptr_t base_addr, const char *pathname)
{
if(0 == base_addr || NULL == pathname) return XH_ERRNO_INVAL;
//always reset
memset(self, 0, sizeof(xh_elf_t));
self->pathname = pathname;
// ElfW 是一个宏用来用为生成 Elf64_ 或 Elf32_ 前缀
// 如:ElfW(Ehdr) -> Elf64_Ehdr 或 Elf32_Ehdr
self->base_addr = (ElfW(Addr))base_addr;
self->ehdr = (ElfW(Ehdr) *)base_addr;
self->phdr = (ElfW(Phdr) *)(base_addr + self->ehdr->e_phoff); //segmentation fault sometimes
// PT_LOAD 类型的段会被加载到内存,偏移为 0 表示第一个被加载的段。
//find the first load-segment with offset 0
ElfW(Phdr) *phdr0 = xh_elf_get_first_segment_by_type_offset(self, PT_LOAD, 0);
...
//save load bias addr
if(self->base_addr < phdr0->p_vaddr) return XH_ERRNO_FORMAT;
self->bias_addr = self->base_addr - phdr0->p_vaddr; // 计算出基地址。
//find dynamic-segment
ElfW(Phdr) *dhdr = xh_elf_get_first_segment_by_type(self, PT_DYNAMIC);
if(NULL == dhdr)
{
XH_LOG_ERROR("Can NOT found dynamic segment. %s", pathname);
return XH_ERRNO_FORMAT;
}
// 动态段的起始地址。
//parse dynamic-segment
self->dyn = (ElfW(Dyn) *)(self->bias_addr + dhdr->p_vaddr);
self->dyn_sz = dhdr->p_memsz; // 总大小
ElfW(Dyn) *dyn = self->dyn;
// 动态段是用一个 Elf64_Dyn 或 Elf32_Dyn 结构体描述,计算出最后一个结构体的位置
ElfW(Dyn) *dyn_end = self->dyn + (self->dyn_sz / sizeof(ElfW(Dyn)));
uint32_t *raw;
for(; dyn < dyn_end; dyn++)
{
switch(dyn->d_tag) //segmentation fault sometimes
{
case DT_NULL: // 表示动态段结束结束
//the end of the dynamic-section
dyn = dyn_end;
break;
case DT_STRTAB: // 字符串表
{
self->strtab = (const char *)(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))(self->strtab) < self->base_addr) return XH_ERRNO_FORMAT;
break;
}
case DT_SYMTAB: // 符号表
{
self->symtab = (ElfW(Sym) *)(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))(self->symtab) < self->base_addr) return XH_ERRNO_FORMAT;
break;
}
case DT_PLTREL: // 重定位项的类型
//use rel or rela?
self->is_use_rela = (dyn->d_un.d_val == DT_RELA ? 1 : 0);
break;
case DT_JMPREL:
{
self->relplt = (ElfW(Addr))(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))(self->relplt) < self->base_addr) return XH_ERRNO_FORMAT;
break;
}
case DT_PLTRELSZ: // 与过程链接表相关的重定位项的总大小
self->relplt_sz = dyn->d_un.d_val;
break;
case DT_REL:
case DT_RELA: // 可重定位表
{
self->reldyn = (ElfW(Addr))(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))(self->reldyn) < self->base_addr) return XH_ERRNO_FORMAT;
break;
}
case DT_RELSZ:
case DT_RELASZ: // 可重定位表大小
self->reldyn_sz = dyn->d_un.d_val;
break;
case DT_ANDROID_REL:
case DT_ANDROID_RELA:
{
self->relandroid = (ElfW(Addr))(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))(self->relandroid) < self->base_addr) return XH_ERRNO_FORMAT;
break;
}
case DT_ANDROID_RELSZ:
case DT_ANDROID_RELASZ:
self->relandroid_sz = dyn->d_un.d_val;
break;
case DT_HASH: // 符号 hash 表
{
//ignore DT_HASH when ELF contains DT_GNU_HASH hash table
if(1 == self->is_use_gnu_hash) continue;
raw = (uint32_t *)(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))raw < self->base_addr) return XH_ERRNO_FORMAT;
self->bucket_cnt = raw[0];
self->chain_cnt = raw[1];
self->bucket = &raw[2];
self->chain = &(self->bucket[self->bucket_cnt]);
break;
}
case DT_GNU_HASH: // 符号 hash 表
{
raw = (uint32_t *)(self->bias_addr + dyn->d_un.d_ptr);
if((ElfW(Addr))raw < self->base_addr) return XH_ERRNO_FORMAT;
self->bucket_cnt = raw[0];
self->symoffset = raw[1];
self->bloom_sz = raw[2];
self->bloom_shift = raw[3];
self->bloom = (ElfW(Addr) *)(&raw[4]);
self->bucket = (uint32_t *)(&(self->bloom[self->bloom_sz]));
self->chain = (uint32_t *)(&(self->bucket[self->bucket_cnt]));
self->is_use_gnu_hash = 1;
break;
}
default:
break;
}
}
//check android rel/rela
if(0 != self->relandroid)
{
const char *rel = (const char *)self->relandroid;
if(self->relandroid_sz < 4 ||
rel[0] != 'A' ||
rel[1] != 'P' ||
rel[2] != 'S' ||
rel[3] != '2')
{
XH_LOG_ERROR("android rel/rela format error\n");
return XH_ERRNO_FORMAT;
}
self->relandroid += 4;
self->relandroid_sz -= 4;
}
...
return 0;
}
根据重定位表查找
- 检查重定向表中获取的索引是否与符号在符号表中的索引一致。
- 目标符号可能存在于 .rel(a).plt、.rel(a).dyn 或 .rel(a).android 中所以分别在这些表中查找目标符号所在的表项。
- 获取原函数所在地址,并替换为 hook 函数所在地址。
这里不分析 .rel(a).android 查找过程。
int xh_elf_hook(xh_elf_t *self, const char *symbol, void *new_func, void **old_func)
{
uint32_t symidx;
void *rel_common;
xh_elf_plain_reloc_iterator_t plain_iter;
xh_elf_packed_reloc_iterator_t packed_iter;
int found;
int r;
...
//find symbol index by symbol name
if(0 != (r = xh_elf_find_symidx_by_name(self, symbol, &symidx))) return 0;
//replace for .rel(a).plt
if(0 != self->relplt)
{
xh_elf_plain_reloc_iterator_init(&plain_iter, self->relplt, self->relplt_sz, self->is_use_rela);
while(NULL != (rel_common = xh_elf_plain_reloc_iterator_next(&plain_iter)))
{
if(0 != (r = xh_elf_find_and_replace_func(self,
(self->is_use_rela ? ".rela.plt" : ".rel.plt"), 1,
symbol, new_func, old_func,
symidx, rel_common, &found))) return r;
if(found) break;
}
}
//replace for .rel(a).dyn
if(0 != self->reldyn)
{
xh_elf_plain_reloc_iterator_init(&plain_iter, self->reldyn, self->reldyn_sz, self->is_use_rela);
// 遍历重定位表
while(NULL != (rel_common = xh_elf_plain_reloc_iterator_next(&plain_iter)))
{
if(0 != (r = xh_elf_find_and_replace_func(self,
(self->is_use_rela ? ".rela.dyn" : ".rel.dyn"), 0,
symbol, new_func, old_func,
symidx, rel_common, NULL))) return r;
}
}
//replace for .rel(a).android
if(0 != self->relandroid)
{
xh_elf_packed_reloc_iterator_init(&packed_iter, self->relandroid, self->relandroid_sz, self->is_use_rela);
while(NULL != (rel_common = xh_elf_packed_reloc_iterator_next(&packed_iter)))
{
if(0 != (r = xh_elf_find_and_replace_func(self,
(self->is_use_rela ? ".rela.android" : ".rel.android"), 0,
symbol, new_func, old_func,
symidx, rel_common, NULL))) return r;
}
}
return 0;
}
查找符号
elf 中包含 hash 和 gun hash 表。优先通过 gun hash 表来查找符号。
static int xh_elf_find_symidx_by_name(xh_elf_t *self, const char *symbol, uint32_t *symidx)
{
if(self->is_use_gnu_hash)
return xh_elf_gnu_hash_lookup(self, symbol, symidx);
else
return xh_elf_hash_lookup(self, symbol, symidx);
}
gun hash 表
bucket_cnt // bucket 数组大小
symoffset //
bloom_sz // chain 链表大小
bloom_shift //
bloom[0]
...
bloom[bloom_sz-1]
bucket[0]
...
bucket[bucket_cnt-1]
chain[0]
...
static int xh_elf_gnu_hash_lookup_def(xh_elf_t *self, const char *symbol, uint32_t *symidx)
{
uint32_t hash = xh_elf_gnu_hash((uint8_t *)symbol);
static uint32_t elfclass_bits = sizeof(ElfW(Addr)) * 8;
size_t word = self->bloom[(hash / elfclass_bits) % self->bloom_sz];
size_t mask = 0
| (size_t)1 << (hash % elfclass_bits)
| (size_t)1 << ((hash >> self->bloom_shift) % elfclass_bits);
//if at least one bit is not set, this symbol is surely missing
if((word & mask) != mask) return XH_ERRNO_NOTFND;
//ignore STN_UNDEF
uint32_t i = self->bucket[hash % self->bucket_cnt];
if(i < self->symoffset) return XH_ERRNO_NOTFND;
//loop through the chain
while(1)
{
const char *symname = self->strtab + self->symtab[i].st_name;
const uint32_t symhash = self->chain[i - self->symoffset];
if((hash | (uint32_t)1) == (symhash | (uint32_t)1) && 0 == strcmp(symbol, symname))
{
*symidx = i;
XH_LOG_INFO("found %s at symidx: %u (GNU_HASH DEF)\n", symbol, *symidx);
return 0;
}
//chain ends with an element with the lowest bit set to 1
if(symhash & (uint32_t)1) break;
i++;
}
return XH_ERRNO_NOTFND;
}
static uint32_t xh_elf_gnu_hash(const uint8_t *name)
{
uint32_t h = 5381;
while(*name != 0)
{
h += (h << 5) + *name++;
}
return h;
}
static int xh_elf_gnu_hash_lookup_undef(xh_elf_t *self, const char *symbol, uint32_t *symidx)
{
uint32_t i;
for(i = 0; i < self->symoffset; i++)
{
const char *symname = self->strtab + self->symtab[i].st_name;
if(0 == strcmp(symname, symbol))
{
*symidx = i;
XH_LOG_INFO("found %s at symidx: %u (GNU_HASH UNDEF)\n", symbol, *symidx);
return 0;
}
}
return XH_ERRNO_NOTFND;
}
hash 表
.hash 用来快速找到符号在符号表中的索引。
结构如下:
bucket_cnt // bucket 数组大小
chain_cnt // chain 链表大小
bucket[0]
...
bucket[bucket_cnt-1]
chain[0]
...
chain[chain_cnt-1]
bucket 为 hash 值分配一个位置,并返回该符号在符号表中的索引。
chain 返回的结果不仅作为在符号表中的索引,为作为 chain 下一项的索引。因为不同符号可能配置到同一个位置,所以 chain 用来记录这些符号在符号表中的位置。
查找过程如下:
- 计算符号的 hash。
- 检查
bucket[hash 与 bucket_cnt]返回的索引,在符号表中对应的符号是否与要查的符号相同。 - 如果不同,继续在 chain 中查找。
static int xh_elf_hash_lookup(xh_elf_t *self, const char *symbol, uint32_t *symidx)
{
uint32_t hash = xh_elf_hash((uint8_t *)symbol); // 计算符号的 hash
const char *symbol_cur;
uint32_t i;
for(i = self->bucket[hash % self->bucket_cnt]; 0 != i; i = self->chain[i])
{
symbol_cur = self->strtab + self->symtab[i].st_name;
if(0 == strcmp(symbol, symbol_cur))
{
*symidx = i;
XH_LOG_INFO("found %s at symidx: %u (ELF_HASH)\n", symbol, *symidx);
return 0;
}
}
return XH_ERRNO_NOTFND;
}
//ELF hash func
static uint32_t xh_elf_hash(const uint8_t *name)
{
uint32_t h = 0, g;
while (*name) {
h = (h << 4) + *name++;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
}
return h;
}
替换 got 中的函数地址
根据前面重定位的结构的介绍,可以知道,r_offset 代表要修改的位置即重定位的位置。由于这里是动态链接所以 r_offset 其实指向的是 got 表中符号引用的位置。所以这里通过 r_offset 就可以知道被 hook 函数的地址了。
static int xh_elf_find_and_replace_func(xh_elf_t *self, const char *section,
int is_plt, const char *symbol,
void *new_func, void **old_func,
uint32_t symidx, void *rel_common,
int *found)
{
ElfW(Rela) *rela;
ElfW(Rel) *rel;
ElfW(Addr) r_offset;
size_t r_info;
size_t r_sym;
size_t r_type;
ElfW(Addr) addr;
int r;
if(NULL != found) *found = 0;
if(self->is_use_rela)
{
rela = (ElfW(Rela) *)rel_common;
r_info = rela->r_info;
r_offset = rela->r_offset;
}
else
{
rel = (ElfW(Rel) *)rel_common;
r_info = rel->r_info;
r_offset = rel->r_offset;
}
//check sym
r_sym = XH_ELF_R_SYM(r_info); // 解析出符号引用
if(r_sym != symidx) return 0;
//check type
r_type = XH_ELF_R_TYPE(r_info); // 解析出重定位类型
// 以 arm64 为例: .rel(a).plt 一般对应 R_AARCH64_JUMP_SLOT 类型
if(is_plt && r_type != XH_ELF_R_GENERIC_JUMP_SLOT) return 0;
// .rel(a).dyn 一般对应 R_AARCH64_GLOB_DAT 和 R_AARCH64_ABS64 类型
if(!is_plt && (r_type != XH_ELF_R_GENERIC_GLOB_DAT && r_type != XH_ELF_R_GENERIC_ABS)) return 0;
//we found it
XH_LOG_INFO("found %s at %s offset: %p\n", symbol, section, (void *)r_offset);
if(NULL != found) *found = 1;
//do replace
addr = self->bias_addr + r_offset;
if(addr < self->base_addr) return XH_ERRNO_FORMAT;
if(0 != (r = xh_elf_replace_function(self, symbol, addr, new_func, old_func)))
{
XH_LOG_ERROR("replace function failed: %s at %s\n", symbol, section);
return r;
}
return 0;
}
- 通过 /proc/self/maps 获取要修改地址的权限信息。
- 如果不是可读写,则使用 mprotect 进行修改。
- 保存原符号实际地址,并替换为 hook 函数。
- 还原地址的权限。
- 清空处理器缓存。使处理器重新从内存中读取这部分指令。
static int xh_elf_replace_function(xh_elf_t *self, const char *symbol, ElfW(Addr) addr, void *new_func, void **old_func)
{
void *old_addr;
unsigned int old_prot = 0;
unsigned int need_prot = PROT_READ | PROT_WRITE;
int r;
//already replaced?
//here we assume that we always have read permission, is this a problem?
if(*(void **)addr == new_func) return 0;
// 利用 /proc/self/maps 获取要修改地址处的权限
//get old prot
if(0 != (r = xh_util_get_addr_protect(addr, self->pathname, &old_prot)))
{
XH_LOG_ERROR("get addr prot failed. ret: %d", r);
return r;
}
// 原地址权限不是可读写,则调用 mprotect 进行修改。
if(old_prot != need_prot)
{
//set new prot
if(0 != (r = xh_util_set_addr_protect(addr, need_prot)))
{
XH_LOG_ERROR("set addr prot failed. ret: %d", r);
return r;
}
}
// 保存原来符号的实际地址。
//save old func
old_addr = *(void **)addr;
if(NULL != old_func) *old_func = old_addr;
// 替换为 hook 函数
//replace func
*(void **)addr = new_func; //segmentation fault sometimes
// 替换后,还原此处权限
if(old_prot != need_prot)
{
//restore the old prot
if(0 != (r = xh_util_set_addr_protect(addr, old_prot)))
{
XH_LOG_WARN("restore addr prot failed. ret: %d", r);
}
}
// 调用 __builtin___clear_cache
//clear cache
xh_util_flush_instruction_cache(addr);
XH_LOG_INFO("XH_HK_OK %p: %p -> %p %s %s\n", (void *)addr, old_addr, new_func, symbol, self->pathname);
return 0;
}