内容纲要

Android native hook——got hook

image-20220723204132256

原理

使用动态链接的方法来提高程序运行的效率

在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用,那么它就不会在程序生命中被加载进来。这样的设计就能提高程序运行的流畅度,也减少了内存空间。

不允许修改代码段,只能修改数据段,所以通过指向plt表和got表的方式来修改跳转地址。

img

  • The Global Offset Table (GOT)。简单来说就是在数据段的地址表,假定我们有一些代码段的指令引用一些地址变量,编译器会引用 GOT 表来替代直接引用绝对地址,因为绝对地址在编译期是无法知道的,只有重定位后才会得到 ,GOT 自己本身将会包含函数引用的绝对地址。
  • The Procedure Linkage Table (PLT)。PLT 不同于 GOT,它位于代码段,动态库的每一个外部函数都会在 PLT 中有一条记录,每一条 PLT 记录都是一小段可执行代码。 一般来说,外部代码都是在调用 PLT 表里的记录,然后 PLT 的相应记录会负责调用实际的函数。我们一般把这种设定叫作“蹦床”(Trampoline)。

plt跳转got表的案例

跳转到plt

image-20220726192837632

跳转到got表的地址(0x804a00c)

image-20220726192905839

got表此次为第一次调用所以指回了plt(0x8048336),plt此时又指向了plt[0](0x8048320),plt[0]调用_dl_runtime_resolve(0x804a008)

image-20220726193028320

got第二次调用时,0x804a00c变了

image-20220726193327938

实现

SHT入手

头部找到section header table的偏移地址,在sht中找出静态.got表的位置,找到内存基地址,找到got表所在的位置,读取got的目标函数地址,进行匹配,找到则替换函数地址。

img

获取got表

void elf_analyze(){
    int fd;
    fd = open(LIB_PATH, O_RDONLY);
    if(-1 == fd){
        printf("Lib load error.\n");
        return -1;
    }

    printf("[+] ----------------------------------------------Ehdr INFO----------------------------------------------\n");
    Elf32_Ehdr ehdr;
    read(fd, &ehdr, sizeof(Elf32_Ehdr));
    unsigned long sh_off = ehdr.e_shoff;
    printf("sh offset is : %lu\n", sh_off);
    int sh_num = ehdr.e_shnum;
    printf("sh num is: %lu\n", sh_num);
    int shent_size = ehdr.e_shentsize;
    printf("sh entry size is: %lu\n", shent_size);
    unsigned long str_index = ehdr.e_shstrndx;
    printf("str index is: %lu\n", str_index);    //存放每个节区名的index

    printf("[+] ----------------------------------------------Shdr INFO----------------------------------------------\n");
    Elf32_Shdr shdr;
    lseek(fd, sh_off + str_index * shent_size, SEEK_SET);
    read(fd, &shdr, shent_size);                //获取到保存每个节区名称的字符串表
    unsigned long str_off = shdr.sh_offset;
    printf("str table offset is: %lu\n", str_off);
    unsigned long str_size = shdr.sh_size;
    printf("str table size is: %lu\n", str_size);

    char * string_table = (char *)malloc(str_size);     //开辟字符串表的空间
    lseek(fd, str_off, SEEK_SET);
    read(fd, string_table, str_size);
    lseek(fd, sh_off, SEEK_SET);    //重定位到sh的地址处

    int i;
    printf("[+] ----------------------------------------------Section INFO----------------------------------------------\n");
    for(i=0;i<sh_num;i++){
        read(fd, &shdr, shent_size);        //开始读取每个节区表
        auto sh_type = shdr.sh_type;
        printf("sh_type is: %u\n", sh_type);
        int name_idx = shdr.sh_name;
        char *s_name = &string_table[name_idx];
        printf("Section name is: %s\n", s_name);
    }

    lseek(fd, sh_off, SEEK_SET);    //重定位到sh的地址处
    printf("[+] ----------------------------------------------Find .got----------------------------------------------\n");
    Elf32_Shdr s_got;
    for(i=0;i<sh_num;i++){
        read(fd, &shdr, shent_size);        //开始读取每个节区表
        auto sh_type = shdr.sh_type;
        int name_idx = shdr.sh_name;
        char *s_name = &string_table[name_idx];
        if(strcmp(s_name, ".got.plt") == 0 || strcmp(s_name, ".got") == 0){
            s_got = shdr;
            unsigned long s_va = s_got.sh_addr;
            printf("got section virtual address is: %lu\n", s_va);
            int s_size = s_got.sh_size;
            printf("got section size is: %u\n", s_size);

        }
    }
}

输出状况:

 yoco@ubuntu  ~/yoco_source  ./got_hook/got_hook_test                                                                                     ✔  2167  16:52:59
[+] ----------------------------------------------Ehdr INFO----------------------------------------------
sh offset is : 262836
sh num is: 24
sh entry size is: 40
str index is: 23
[+] ----------------------------------------------Shdr INFO----------------------------------------------
str table offset is: 262643
str table size is: 192
[+] ----------------------------------------------Section INFO----------------------------------------------
sh_type is: 0
Section name is: 
sh_type is: 5
Section name is: .hash
sh_type is: 11
Section name is: .dynsym
sh_type is: 3
Section name is: .dynstr
sh_type is: 9
Section name is: .rel.dyn
sh_type is: 9
Section name is: .rel.plt
sh_type is: 1
Section name is: .init
sh_type is: 1
Section name is: .plt
sh_type is: 1
Section name is: .text
sh_type is: 1
Section name is: .fini
sh_type is: 1
Section name is: .rodata
sh_type is: 1879048193
Section name is: .ARM.exidx
sh_type is: 1
Section name is: .eh_frame
sh_type is: 14
Section name is: .init_array
sh_type is: 15
Section name is: .fini_array
sh_type is: 1
Section name is: .jcr
sh_type is: 6
Section name is: .dynamic
sh_type is: 1
Section name is: .got
sh_type is: 1
Section name is: .data
sh_type is: 8
Section name is: .bss
sh_type is: 1
Section name is: .comment
sh_type is: 1879048195
Section name is: .ARM.attributes
sh_type is: 1
Section name is: .gnu_debuglink
sh_type is: 3
Section name is: .shstrtab
[+] ----------------------------------------------Find .got----------------------------------------------
got section virtual address is: 315392
got section size is: 2312

替换got表中的函数

遍历got表(每4字节),寻找原函数对应地址的位置,将该内存页改为可读可写可执行状态,并将该值替换为自己写的函数地址,最后恢复内存页的状态(可读可执行)。

                int j = 0;
                // 遍历节区".got"或者".got.plt"获取保存的全局的函数调用地址
                for(j = 0; j<out_size; j += 4){
                    // 获取节区".got"或者".got.plt"中的单个函数的调用地址
                    got_item = *(uint32_t*)(out_addr + j);
                    // 判断节区".got"或者".got.plt"中函数调用地址是否是将要被Hook的目标函数地址
                    if(got_item == old_fopen){
                        LOGD("[+] Found fopen in got.\n");
                        got_found = 1;
                        // 获取当前内存分页的大小
                        uint32_t page_size = getpagesize();
                        // 获取内存分页的起始地址(需要内存对齐)
                        uint32_t entry_page_start = (out_addr + j) & (~(page_size - 1));
                        LOGD("[+] entry_page_start = %lx, page size = %lx\n", entry_page_start, page_size);
                        // 修改内存属性为可读可写可执行
                        if(mprotect((uint32_t*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1){
                            LOGD("mprotect false.\n");
                            return -1;
                        }
                        LOGD("[+] %s, old_fopen = %lx, new_fopen = %lx\n", "before hook function", got_item, new_fopen);

                        // Hook函数为我们自己定义的函数
                        got_item = new_fopen;
                        LOGD("[+] %s, old_fopen = %lx, new_fopen = %lx\n", "after hook function", got_item, new_fopen);
                        // 恢复内存属性为可读可执行
                        if(mprotect((uint32_t*)entry_page_start, page_size, PROT_READ | PROT_EXEC) == -1){
                            LOGD("mprotect false.\n");
                            return -1;
                        }
                        break;
                    // 此时,目标函数的调用地址已经被Hook了
                    }else if(got_item == new_fopen){
                        LOGD("[+] Already hooked.\n");
                        break;
                    }

总结

在elf header中找到section header偏移地址(sh_offset),sectionheader表项个数(sh_num),表项大小(sh_size)

sh_offset+sh_num*sh_size得到的地址为sectionHeader中的存放每个节区名称的字符串表(大小也为sh_size)

在字符串表(Shdr类)中获取字符串内容的偏移地址(sh_offset:相对于文件的偏移地址)盒字符串大小(sh_size),读取字符串内容

字符串读取完后,文件重定位到section header处,开始读取每一表项

注意:sh_name并不是字符串,而是在字符串中的索引号,这也是为什么要获取字符串表的原因

通过获取每个表项的name,在字符串中索引得到该表项的名称,与".got"/".got.plt"比较

如果相等,则获取该表项指向section的偏移(sh_addr)

遍历got表,查找地址是否为我们要hook的地址,如果是,则修改该内存分页属性(wrx),将该地址写为我们要执行的地址,最后恢复内存分页属性

PHT入手

ph表可能需要执行才会有,且对内存的访问是个问题,容易出问题。

void elf_analyze_ph(){
    void* base_addr = get_module_base(92849, "/lib/x86_64-linux-gnu/libm-2.27.so");
    printf("%p", base_addr);
    Elf32_Ehdr *header = (Elf32_Ehdr*)(base_addr);
    unsigned char* h_ident = header->e_ident;
    // printf("header ident is: %s\n", h_ident[0]);
    unsigned long ph_off = header->e_phoff;
    printf("program header offset is: %lu\n", ph_off);
    Elf32_Phdr* phdr_table = (Elf32_Phdr*)(base_addr+ph_off);
    int ph_count = header->e_phnum;
    printf("ph count is: %u\n", ph_count);
    int i = 0;
    //获取dynamic段
    unsigned long dynamic_addr = 0;
    unsigned long dynamic_size = 0;
    for(i = 0;i<ph_count;i++){
        int ph_type = phdr_table[i].p_type;
        printf("ph type is: %d", ph_type);      //Dynamic linking information
        if(ph_type == PT_DYNAMIC){
            unsigned long dy_off = phdr_table[i].p_paddr;
            dynamic_addr = base_addr + dy_off;
            unsigned long dy_size = phdr_table[i].p_memsz;
            dynamic_size = dy_size;
            break;
        }
    }

// typedef struct
// {
//   Elf64_Sxword   d_tag;          /* Dynamic entry type */
//   union
//     {
//       Elf64_Xword d_val;     /* Integer value */
//       Elf64_Addr d_ptr;          /* Address value */
//     } d_un;
// } Elf64_Dyn;

    Elf32_Dyn* dynamic_table = (Elf32_Dyn*)(dynamic_addr);
    int j;
    unsigned long jmpRelOff = 0;
    unsigned long strTabOff = 0;
    unsigned long pltRelSz = 0;
    unsigned long symTabOff = 0;
    for(j=0;j<dynamic_size/8;j++){
        int val = dynamic_table[j].d_un.d_val;
        if (dynamic_table[i].d_tag == DT_JMPREL)
        {
            jmpRelOff = val;
        }
        if (dynamic_table[i].d_tag == DT_STRTAB)
        {
            strTabOff = val;
        }
        if (dynamic_table[i].d_tag == DT_PLTRELSZ)
        {
            pltRelSz = val;
        }
        if (dynamic_table[i].d_tag == DT_SYMTAB)
        {
            symTabOff = val;
        }
    }
    //重定位表
    Elf32_Rel* rel_table = (Elf32_Rel*)(jmpRelOff + base_addr);
    //遍历plt表
    for(i=0;i < pltRelSz / 8;i++){
// typedef struct
// {
//   Elf32_Addr r_offset;       /* Address */
//   Elf32_Word r_info;         /* Relocation type and symbol index */
// } Elf32_Rel;
        int number = (rel_table[i].r_info >> 8) & 0xffffff;//symbol index
        Elf32_Sym* symTableIndex = (Elf32_Sym*)(number*16 + symTabOff + base_addr);
        char* funcName = (char*)(symTableIndex->st_name + strTabOff + base_addr);
        //LOGD("[+] Func Name : %s",funcName);
        if(memcmp(funcName, "fopen", 5) == 0){
            //...
        }

}

参考

got hook详解