ELF PLT Hook 原理简述

4,632 阅读12分钟

【无线平台】ELF PLT Hook 原理简述

简述

Android 是基于Linux的操作系统,因此在Android开发平台上,ELF是原生支持的可执行文件格式;ELF文件格式除了作为可执行文件,还可以作为共享库格式,也就是我们常见的so文件, 以及 object文件 (.o)、core dumps文件等。

GOT/PLT HOOK 是ELF 文件函数hook的一种实现机制,GOT/PLT Hook 主要用于实现替换某个SO的外部调用,它的优点是非常稳定,因此在生产环境通常使用这种实现方案。

GOT/PLT Hook的方案命名 主要是因为该方案主要是通过修改 ELF 文件结构中的 GOT (The Global Offset Table) 和 PLT(The Procedure Linkage Table) 段的地址来实现的

场景

在Android 开发中,掌握ELF hook技术在诸多场景中可以起到妙用。 比如 apm 监控中 hook file io api实现 文件读写监控;
hook 加载dex的函数,实现简单加壳工具的脱壳效果等。

ELF File 结构及作用简述

常用的工具

  • readelf 用来查看ELF文件的头部、及各个section的内容
  • objdump 对elf指定的section内容进行反汇编
  • gdb 用于代码调试

在mac环境下 readelf objdump 可以用greadelf gobjump代替

elf文件格式

image.png 每个ELF文件由一个ELF头和文件数据组成。这些数据包括

  • 程序头表, 描述零或多个内存段
  • section 头表,描述零或多个section块
  • 有程序头表或section头 表引用的数据

ELF 文件参与程序的连接和程序的执行,因此可以分别 可链接文件角度和可执行文件角度来具体看待elf文件的格式

  • 如果用于编译和链接,则编译器和链接器把elf文件看作是节头表描述的节集合,程序头表可选
  • 如果用于加载执行,则加载器将elf 文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头表可选
  • 如果是共享文件,则两者都包含

image.png 关于详细的elf 文件的详细结构 可参见 wiki 及llvm 中对elf结构的定义 elf.h

动态链接

为了解决静态链接空间浪费以及程序更新困难的问题,通常会将程序模块互相分割,形成独立的文件,而不是将它们静态链接在一起。 动态库可以理解为是一些 object file的集合,在运行时,通过链接动态库,会将动态库函数在内存中的位置插入到可执行文件中。并且动态库只会被加载到内存中一次,一旦一个动态库被加载到内存中,它的代码就可以被任何需要它的程序使用。

动态链接的具体过程

关于动态链接的具体过程,可查看 Dynamic Linking

GOT 、PLT

.got (Global Offset Table)

got 位于数据段, 作用是用来记录代码中引用到的外部符号的地址映射。这里的符号包括,变量、函数等。

这里存在一个问题,如果一个引用的函数是在共享库中,而共享库在加载的时候是没有固定地址的,所以在got表中无法直接保存该符号的地址,此时就需要引入 plt表

.plt (Procedure Linkage Table)

PLT 位于代码段,动态库中的每一个外部函数都会在PLT 中有一条对应的记录,每一条PLT记录都是一小段可执行的代码。可以说,PLT是由代码片段组成的表,每个代码片段由会跳转到GOT表中的一个具体的函数调用

-fPIC说明

使用 PIC 参数作用于编译阶段,是告诉编译生成与 位置无关(Position-Independent Code)的代码, 对于共享库来说,如果不加 -fPIC,则.so文件的代码段在被加载时,代码段引用的数据对象需要重新定位,重定位会修改代码段的内容,这就总爱城每个使用这个.so文件代码段的进程在内核中都需要生成这个.so文件代码段的副本,每个副本都不一样,具体取决于这个.so文件代码和数据段内存映射的位置。 当添加 PIC参数后,则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。 因此对于动态库来说,一般产生位置无关的代码

got plt工作示例

我们首先创建一个共享库及使用该共享库的可执行程序来分析got plt表的具体工作过程

  • 创建一个共享库 testa.so,包含 函数print_hello
  • 创建一个elf格式的可执行文件,链接 testa库,病执行print_hello函数

创建testa 头文件 testa.h

#include <stdio.h>
void say_hello();

testa.c

#include "testa.h"
#include <stdio.h>

void say_hello(){
	printf("Hello,World! \n");
}

执行以下命令,编译生成 testa.so动态库

gcc ./testa.c -fPIC -shared -o libtesta.so

创建main.c文件

#include "testa.h"
#include <stdio.h>

void say_hello(){
	printf("Hello,World! \n");
}

执行以下命令,编译生成 main 可执行文件

gcc main.c -L. -ltesta -o main

 Linux动态链接库的默认搜索路径是/lib和/usr/lib,因此动态库被创建后,我们需要将 testa.so复制到这两个目录下面,或者是 执行 export LD_LIBRARY_PATH= 添加 testa.so的目录路径 输入 ./main 后,控制台成功打印了 Hello,World

执行 objdump命令查看 main 文件的汇编指令

objdump -d -M intel -S main
...
Disassembly of section .plt:

00000000000005d0 <.plt>:
 5d0:	ff 35 ea 09 20 00    	push   QWORD PTR [rip+0x2009ea]        # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
 5d6:	ff 25 ec 09 20 00    	jmp    QWORD PTR [rip+0x2009ec]        # 200fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
 5dc:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]

00000000000005e0 <say_hello@plt>:
 5e0:	ff 25 ea 09 20 00    	jmp    QWORD PTR [rip+0x2009ea]        # 200fd0 <say_hello>
 5e6:	68 00 00 00 00       	push   0x0
 5eb:	e9 e0 ff ff ff       	jmp    5d0 <.plt>

...
000000000000070a <main>:
 70a:	55                   	push   rbp
 70b:	48 89 e5             	mov    rbp,rsp
 70e:	48 83 ec 10          	sub    rsp,0x10
 712:	89 7d fc             	mov    DWORD PTR [rbp-0x4],edi
 715:	48 89 75 f0          	mov    QWORD PTR [rbp-0x10],rsi
 719:	b8 00 00 00 00       	mov    eax,0x0
 71e:	e8 bd fe ff ff       	call   5e0 <say_hello@plt>
 723:	b8 00 00 00 00       	mov    eax,0x0
 728:	c9                   	leave  
 729:	c3                   	ret    
 72a:	66 0f 1f 44 00 00    	nop    WORD PTR [rax+rax*1+0x0]

在上面输出的

函数汇编指令中,可以看到这样一段

call 5e0 <say_hello@plt>

可以看到,我们在源程序中调用的 say_hello()函数被替换成 汇编指令 call 5e0, 而 5e0的地址是PLT表的一个代码段, 我们转到 5e0地址看 这个代码段对应的函数

Disassembly of section .plt:

00000000000005d0 <.plt>:
 5d0:	ff 35 ea 09 20 00    	push   QWORD PTR [rip+0x2009ea]        # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
 5d6:	ff 25 ec 09 20 00    	jmp    QWORD PTR [rip+0x2009ec]        # 200fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
 5dc:	0f 1f 40 00          	nop    DWORD PTR [rax+0x0]

00000000000005e0 <say_hello@plt>:
 5e0:	ff 25 ea 09 20 00    	jmp    QWORD PTR [rip+0x2009ea]        # 200fd0 <say_hello>
 5e6:	68 00 00 00 00       	push   0x0
 5eb:	e9 e0 ff ff ff       	jmp    5d0 <.plt>

可以看到 jmp QWORD PTR [rip+x] 汇编指令生成的注释,该地址为GOT表中的地址,因此这里又执行了一次跳转,这个原因在前面也解释过了,对于外部共享库,虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里各加载一份. 因此,所有需要引用共享库外部地址的指令,都会查询GOT表,来找到该函数在当前运行程序的虚拟内存的对应位置.

前面提到过,GOT表位于数据段,这是有原因的,当该外部函数第一次被调用时,GOT表中保存的并不是该函数实际被加载的内存地址, 由于linux使用了延迟绑定技术,因此 在首次滴调用时,该地址需要由 动态链接库 dl_runtime_resolve 链接,解析后才能得到. 该流程的实现是 GOT表中初始对应的指令又会跳回到源函数对应PLT表位置中的第二条指令

push   0x0
jmp    5d0 <.plt>

这里的5d0对应是PLT表中的第一项,PLT[0]是一条特殊的记录,其内容为跳转到GOT表中保存了 dl_runtime_resolve 地址的位置. 当执行dl_runtime_resolve 解析出动态库函数的地址后,会将真实的地址写回到GOT表中,这样第二次调用该函数时就需要经过动态库解析了.

以下是网上的一个共享库函数调用在 plt+got机制下工作的流程图样例 image.png

PLT Hook的实现机制

以下简单介绍基于PLT Hook的实现机制 (基于该开源库方案) 1.假设存在 两个共享库 libfoo.so ,libbar.so 及依赖这两个共享库的程序

image.png foo_func 和 bar_func 分别是位于 libfoo.so 和 libar.so 的函数, 对于program 来说,这两个函数来自于外部库,因此在实际执行时,需要从PLT表中获取运行时的函数地址.

假设我们需要hook foo_func,那么最简答的实现机制就是替换 PLT表中 foo_func对应的地址为经过我们修改过的函数地址 (假设该函数名为hook_foo_func).

如果修改的函数不位于program内部(比如新建了一个so), 那么只需要将 PLT表中foo_func的地址替换成 libbar.so中hook_foo_func被加载到内存时的地址.

image.png 如果hook_foo_func位于program内部, 不得在 hoo_foo_func中调用源函数foo_func(), 因为此时PLT 中foo_func的地被替换成 hook_foo_func的地址, 如果hook_foo_func中存在调用 foo_func函数 则造成死循环. 对于该问题的解决方案是,在hook_foo_func中如果需要调用源函数,需要转为 先获取源函数的地址并设置为指针变量 再调用. 在unix环境中,可以通过 dysym 获取动态库符号(包括函数)的地址.

一些具体实现细节

1.如何根据动态库的函数名称知道对应的地址 首先通过 dlopen加载对应的动态库,获得该动态库的句柄

void *hndl = dlopen(filename, RTLD_LAZY | RTLD_NOLOAD);

通过dlsym获取各个函数符号在共享库中的相对地址

char *addr = dlsym(hndl, symbols[i])

函数实际被加载到内存的地址应为 该共享库的基地址+该函数的相对地址,因此,还需要获取对应共享库的基地址。 可以通过dl_iterate_phdr 遍历程序当前加载的动态库,从而获取基地址, dl_iterate_phdr 传入的第一个参数为回调函数,该函数的具体定义可以通过 man dl_iterate_phdr来查看

struct dl_iterate_data data = {0,};
data.addr = address;
//遍历共享库,获取address实际的内存地址
dl_iterate_phdr(dl_iterate_cb, &data);

dl_iterate_cb的函数实现为

static int dl_iterate_cb(struct dl_phdr_info *info, size_t size, void *cb_data)
{
    struct dl_iterate_data *data = (struct dl_iterate_data*)cb_data;
    Elf_Half idx = 0;

    for (idx = 0; idx < info->dlpi_phnum; ++idx) {
        const Elf_Phdr *phdr = &info->dlpi_phdr[idx];
        char* base = (char*)info->dlpi_addr + phdr->p_vaddr;
        if (base <= data->addr && data->addr < base + phdr->p_memsz) {
            break;
        }
    }
    if (idx == info->dlpi_phnum) {
        return 0;
    }
    for (idx = 0; idx < info->dlpi_phnum; ++idx) {
        const Elf_Phdr *phdr = &info->dlpi_phdr[idx];
        if (phdr->p_type == PT_DYNAMIC) { //保存动态链接库信息
            data->lmap.l_addr = info->dlpi_addr;
            data->lmap.l_ld = (Elf_Dyn*)(info->dlpi_addr + phdr->p_vaddr);
            return 1;
        }
    }
    return 0;
}

2.替换函数地址 首先在遍历动态库时,已经将动态库的基地址保存,此时通过获取到源函数在plt表中的位置,并将该地址值替换成目标地址值即可


static int check_rel(const plthook_t *plthook, const Elf_Plt_Rel *plt, Elf_Xword r_type, const char **name_out, void ***addr_out)
{
    if (ELF_R_TYPE(plt->r_info) == r_type) {
        size_t idx = ELF_R_SYM(plt->r_info);
        idx = plthook->dynsym[idx].st_name;
        if (idx + 1 > plthook->dynstr_size) {
            set_errmsg("too big section header string table index: %" SIZE_T_FMT, idx);
            return PLTHOOK_INVALID_FILE_FORMAT;
        }
        *name_out = plthook->dynstr + idx;
        //返回源函数plt的地址
        *addr_out = (void**)(plthook->plt_addr_base + plt->r_offset);
        return 0;
    }
    return -1;
}

// 遍历动态库plt中的函数,返回函数名称及地址
int plthook_enum(plthook_t *plthook, unsigned int *pos, const char **name_out, void ***addr_out)
{
    while (*pos < plthook->rela_plt_cnt) {
        const Elf_Plt_Rel *plt = plthook->rela_plt + *pos;
        int rv = check_rel(plthook, plt, R_JUMP_SLOT, name_out, addr_out);
        (*pos)++;
        if (rv >= 0) {
            return rv;
        }
    }
#ifdef R_GLOBAL_DATA
    while (*pos < plthook->rela_plt_cnt + plthook->rela_dyn_cnt) {
        const Elf_Plt_Rel *plt = plthook->rela_dyn + (*pos - plthook->rela_plt_cnt);
        int rv = check_rel(plthook, plt, R_GLOBAL_DATA, name_out, addr_out);
        (*pos)++;
        if (rv >= 0) {
            return rv;
        }
    }
#endif
    *name_out = NULL;
    *addr_out = NULL;
    return EOF;
}

int plthook_replace(plthook_t *plthook, const char *funcname, void *funcaddr, void **oldfunc)
{
    size_t funcnamelen = strlen(funcname);
    unsigned int pos = 0;
    const char *name;
    void **addr;
    int rv;

    if (plthook == NULL) {
        set_errmsg("invalid argument: The first argument is null.");
        return PLTHOOK_INVALID_ARGUMENT;
    }
    while ((rv = plthook_enum(plthook, &pos, &name, &addr)) == 0) {
        if (strncmp(name, funcname, funcnamelen) == 0) { //如果发现同名函数
            if (name[funcnamelen] == '\0' || name[funcnamelen] == '@') {
                int prot = get_memory_permission(addr);
                if (prot == 0) {
                    return PLTHOOK_INTERNAL_ERROR;
                }
                if (!(prot & PROT_WRITE)) {
                    if (mprotect(ALIGN_ADDR(addr), page_size, PROT_READ | PROT_WRITE) != 0) {
                        set_errmsg("Could not change the process memory permission at %p: %s",
                                   ALIGN_ADDR(addr), strerror(errno));
                        return PLTHOOK_INTERNAL_ERROR;
                    }
                }
                if (oldfunc) {
                    *oldfunc = *addr;
                }
                //替换plt中的地址值
                *addr = funcaddr;
                if (!(prot & PROT_WRITE)) {
                    mprotect(ALIGN_ADDR(addr), page_size, prot);
                }
                return 0;
            }
        }
    }
    if (rv == EOF) {
        set_errmsg("no such function: %s", funcname);
        rv = PLTHOOK_FUNCTION_NOT_FOUND;
    }
    return rv;
}

其他实现机制

除了基于PLT 的实现机制, native hook的实现方案 还包括 InlineHook,TrapHook的实现, 其中TrapHook使用系统的SIGTRAP信号中断的机制,性能并不好,在生产环境中使用较少,InlineHook 实现的是指令级别的替换机制 实现难度大。因此在生产环境中使用较多的还是 GOT/PLT 的实现方案。

在Android环境中,使用较多的开源库为iQIyi的xHook库,另外 facebook 开源库 profilo中也存在 plt hook的实现方案,可以单独扒出来使用

另外在Android环境中,plt的加载机制为非懒加载模式,即当共享库被首次加载时,就会完成所有的重定位操作,因此在具体实现时也会有差异。