全局符号表(GOT表)hook实际是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数。
GOT表其实包含了导入表和导出表,导出表指将当前动态库的一些函数符号保留,供外部调用,导入表中的函数实际是在该动态库中调用外部的导出函数。
这里有几个关键点要说明一下:
(1) so文件的绝对路径和加载到内存中的基址是可以通过 /proc/[pid]/maps 获取到的。
(2) 修改导入表的函数地址的时候需要修改页的权限,增加写权限即可。
(3) 一般的导入表Hook是基于注入操作的,即把自己的代码注入到目标程序,本次实例重点讲述Hook的实现,注入代码采用上节所有代码inject.c。
导入表的hook有两种方法,以hook fopen函数为例。
方法一:
通过解析elf格式,分析Section header table找出静态的.got表的位置,并在内存中找到相应的.got表位置,这个时候内存中.got表保存着导入函数的地址,读取目标函数地址,与.got表每一项函数入口地址进行匹配,找到的话就直接替换新的函数地址,这样就完成了一次导入表的Hook操作了。
hook流程如下图所示:

图1 导入表Hook流程图
具体代码实现如下:
entry.c:
1 1 #include <unistd.h> 2 2 #include <stdio.h> 3 3 #include <stdlib.h> 4 4 #include <android/log.h> 5 5 #include <EGL/egl.h> 6 6 #include <GLES/gl.h> 7 7 #include <elf.h> 8 8 #include <fcntl.h> 9 9 #include <sys/mman.h> 10 10 11 11 #define LOG_TAG "INJECT" 12 12 #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args) 13 13 14 14 //FILE *fopen(const char *filename, const char *modes) 15 15 FILE* (*old_fopen)(const char *filename, const char *modes); 16 16 FILE* new_fopen(const char *filename, const char *modes){ 17 17 LOGD("[+] New call fopen.\n"); 18 18 if(old_fopen == -1){ 19 19 LOGD("error.\n"); 20 20 } 21 21 return old_fopen(filename, modes); 22 22 } 23 23 24 24 void* get_module_base(pid_t pid, const char* module_name){ 25 25 FILE* fp; 26 26 long addr = 0; 27 27 char* pch; 28 28 char filename[32]; 29 29 char line[1024]; 30 30 31 31 // 格式化字符串得到 "/proc/pid/maps" 32 32 if(pid < 0){ 33 33 snprintf(filename, sizeof(filename), "/proc/self/maps"); 34 34 }else{ 35 35 snprintf(filename, sizeof(filename), "/proc/%d/maps", pid); 36 36 } 37 37 38 38 // 打开文件/proc/pid/maps,获取指定pid进程加载的内存模块信息 39 39 fp = fopen(filename, "r"); 40 40 if(fp != NULL){ 41 41 // 每次一行,读取文件 /proc/pid/maps中内容 42 42 while(fgets(line, sizeof(line), fp)){ 43 43 // 查找指定的so模块 44 44 if(strstr(line, module_name)){ 45 45 // 分割字符串 46 46 pch = strtok(line, "-"); 47 47 // 字符串转长整形 48 48 addr = strtoul(pch, NULL, 16); 49 49 50 50 // 特殊内存地址的处理 51 51 if(addr == 0x8000){ 52 52 addr = 0; 53 53 } 54 54 break; 55 55 } 56 56 } 57 57 } 58 58 fclose(fp); 59 59 return (void*)addr; 60 60 } 61 61 62 62 #define LIB_PATH "/data/app-lib/com.bbk.appstore-2/libvivosgmain.so" 63 63 int hook_fopen(){ 64 64 65 65 // 获取目标pid进程中"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"模块的加载地址 66 66 void* base_addr = get_module_base(getpid(), LIB_PATH); 67 67 LOGD("[+] libvivosgmain.so address = %p \n", base_addr); 68 68 69 69 // 保存被Hook的目标函数的原始调用地址 70 70 old_fopen = fopen; 71 71 LOGD("[+] Orig fopen = %p\n", old_fopen); 72 72 73 73 int fd; 74 74 // 打开内存模块文件"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so" 75 75 fd = open(LIB_PATH, O_RDONLY); 76 76 if(-1 == fd){ 77 77 LOGD("error.\n"); 78 78 return -1; 79 79 } 80 80 81 81 // elf32文件的文件头结构体Elf32_Ehdr 82 82 Elf32_Ehdr ehdr; 83 83 // 读取elf32格式的文件"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"的文件头信息 84 84 read(fd, &ehdr, sizeof(Elf32_Ehdr)); 85 85 86 86 // elf32文件中节区表信息结构的文件偏移 87 87 unsigned long shdr_addr = ehdr.e_shoff; 88 88 // elf32文件中节区表信息结构的数量 89 89 int shnum = ehdr.e_shnum; 90 90 // elf32文件中每个节区表信息结构中的单个信息结构的大小(描述每个节区的信息的结构体的大小) 91 91 int shent_size = ehdr.e_shentsize; 92 92 93 93 // elf32文件节区表中每个节区的名称存放的节区名称字符串表,在节区表中的序号index 94 94 unsigned long stridx = ehdr.e_shstrndx; 95 95 96 96 // elf32文件中节区表的每个单元信息结构体(描述每个节区的信息的结构体) 97 97 Elf32_Shdr shdr; 98 98 // elf32文件中定位到存放每个节区名称的字符串表的信息结构体位置.shstrtab 99 99 lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET); 100100 // 读取elf32文件中的描述每个节区的信息的结构体(这里是保存elf32文件的每个节区的名称字符串的) 101101 read(fd, &shdr, shent_size); 102102 LOGD("[+] String table offset is %lu, size is %lu", shdr.sh_offset, shdr.sh_size); //41159, size is 254 103103 104104 // 为保存elf32文件的所有的节区的名称字符串申请内存空间 105105 char * string_table = (char *)malloc(shdr.sh_size); 106106 // 定位到具体存放elf32文件的所有的节区的名称字符串的文件偏移处 107107 lseek(fd, shdr.sh_offset, SEEK_SET); 108108 // 从elf32内存文件中读取所有的节区的名称字符串到申请的内存空间中 109109 read(fd, string_table, shdr.sh_size); 110110 111111 // 重新设置elf32文件的文件偏移为节区信息结构的起始文件偏移处 112112 lseek(fd, shdr_addr, SEEK_SET); 113113 114114 int i; 115115 uint32_t out_addr = 0; 116116 uint32_t out_size = 0; 117117 uint32_t got_item = 0; 118118 int32_t got_found = 0; 119119 120120 // 循环遍历elf32文件的节区表(描述每个节区的信息的结构体) 121121 for(i = 0; i<shnum; i++){ 122122 // 依次读取节区表中每个描述节区的信息的结构体 123123 read(fd, &shdr, shent_size); 124124 // 判断当前节区描述结构体描述的节区是否是SHT_PROGBITS类型 125125 //类型为SHT_PROGBITS的.got节区包含全局偏移表 126126 if(shdr.sh_type == SHT_PROGBITS){ 127127 // 获取节区的名称字符串在保存所有节区的名称字符串段.shstrtab中的序号 128128 int name_idx = shdr.sh_name; 129129 130130 // 判断节区的名称是否为".got.plt"或者".got" 131131 if(strcmp(&(string_table[name_idx]), ".got.plt") == 0 132132 || strcmp(&(string_table[name_idx]), ".got") == 0){ 133133 // 获取节区".got"或者".got.plt"在内存中实际数据存放地址 134134 out_addr = base_addr + shdr.sh_addr; 135135 // 获取节区".got"或者".got.plt"的大小 136136 out_size = shdr.sh_size; 137137 LOGD("[+] out_addr = %lx, out_size = %lx\n", out_addr, out_size); 138138 int j = 0; 139139 // 遍历节区".got"或者".got.plt"获取保存的全局的函数调用地址 140140 for(j = 0; j<out_size; j += 4){ 141141 // 获取节区".got"或者".got.plt"中的单个函数的调用地址 142142 got_item = *(uint32_t*)(out_addr + j); 143143 // 判断节区".got"或者".got.plt"中函数调用地址是否是将要被Hook的目标函数地址 144144 if(got_item == old_fopen){ 145145 LOGD("[+] Found fopen in got.\n"); 146146 got_found = 1; 147147 // 获取当前内存分页的大小 148148 uint32_t page_size = getpagesize(); 149149 // 获取内存分页的起始地址(需要内存对齐) 150150 uint32_t entry_page_start = (out_addr + j) & (~(page_size - 1)); 151151 LOGD("[+] entry_page_start = %lx, page size = %lx\n", entry_page_start, page_size); 152152 // 修改内存属性为可读可写可执行 153153 if(mprotect((uint32_t*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1){ 154154 LOGD("mprotect false.\n"); 155155 return -1; 156156 } 157157 LOGD("[+] %s, old_fopen = %lx, new_fopen = %lx\n", "before hook function", got_item, new_fopen); 158158 159159 // Hook函数为我们自己定义的函数 160160 got_item = new_fopen; 161161 LOGD("[+] %s, old_fopen = %lx, new_fopen = %lx\n", "after hook function", got_item, new_fopen); 162162 // 恢复内存属性为可读可执行 163163 if(mprotect((uint32_t*)entry_page_start, page_size, PROT_READ | PROT_EXEC) == -1){ 164164 LOGD("mprotect false.\n"); 165165 return -1; 166166 } 167167 break; 168168 // 此时,目标函数的调用地址已经被Hook了 169169 }else if(got_item == new_fopen){ 170170 LOGD("[+] Already hooked.\n"); 171171 break; 172172 } 173173 } 174174 // Hook目标函数成功,跳出循环 175175 if(got_found) 176176 break; 177177 } 178178 } 179179 } 180180 free(string_table); 181181 close(fd); 182182 } 183183 184184 int hook_entry(char* a){ 185185 LOGD("[+] Start hooking.\n"); 186186 hook_fopen(); 187187 return 0; 188188 }
运行ndk-build编译,得到libentry.so,push到/data/local/tmp目录下,运行上节所得到的inject程序,得到如下结果,表明hook成功:

图2.
方法二
通过分析program header table查找got表。导入表对应在动态链接段.got.plt(DT_PLTGOT)指向处,但是每项的信息是和GOT表中的表项对应的,因此,在解析动态链接段时,需要解析DT_JMPREL、DT_SYMTAB,前者指向了每一个导入表表项的偏移地址和相关信息,包括在GOT表中偏移,后者为GOT表。
具体代码如下:
1 1 #include <unistd.h> 2 2 #include <stdio.h> 3 3 #include <stdlib.h> 4 4 #include <android/log.h> 5 5 #include <EGL/egl.h> 6 6 #include <GLES/gl.h> 7 7 #include <elf.h> 8 8 #include <fcntl.h> 9 9 #include <sys/mman.h> 10 10 11 11 #define LOG_TAG "INJECT" 12 12 #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args) 13 13 14 14 15 15 //FILE *fopen(const char *filename, const char *modes) 16 16 FILE* (*old_fopen)(const char *filename, const char *modes); 17 17 FILE* new_fopen(const char *filename, const char *modes){ 18 18 LOGD("[+] New call fopen.\n"); 19 19 if(old_fopen == -1){ 20 20 LOGD("error.\n"); 21 21 } 22 22 return old_fopen(filename, modes); 23 23 } 24 24 25 25 void* get_module_base(pid_t pid, const char* module_name){ 26 26 FILE* fp; 27 27 long addr = 0; 28 28 char* pch; 29 29 char filename[32]; 30 30 char line[1024]; 31 31 32 32 // 格式化字符串得到 "/proc/pid/maps" 33 33 if(pid < 0){ 34 34 snprintf(filename, sizeof(filename), "/proc/self/maps"); 35 35 }else{ 36 36 snprintf(filename, sizeof(filename), "/proc/%d/maps", pid); 37 37 } 38 38 39 39 // 打开文件/proc/pid/maps,获取指定pid进程加载的内存模块信息 40 40 fp = fopen(filename, "r"); 41 41 if(fp != NULL){ 42 42 // 每次一行,读取文件 /proc/pid/maps中内容 43 43 while(fgets(line, sizeof(line), fp)){ 44 44 // 查找指定的so模块 45 45 if(strstr(line, module_name)){ 46 46 // 分割字符串 47 47 pch = strtok(line, "-"); 48 48 // 字符串转长整形 49 49 addr = strtoul(pch, NULL, 16); 50 50 51 51 // 特殊内存地址的处理 52 52 if(addr == 0x8000){ 53 53 addr = 0; 54 54 } 55 55 break; 56 56 } 57 57 } 58 58 } 59 59 fclose(fp); 60 60 return (void*)addr; 61 61 } 62 62 63 63 #define LIB_PATH "/data/app-lib/com.bbk.appstore-2/libvivosgmain.so" 64 64 int hook_fopen(){ 65 65 66 66 // 获取目标pid进程中"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"模块的加载地址 67 67 void* base_addr = get_module_base(getpid(), LIB_PATH); 68 68 LOGD("[+] libvivosgmain.so address = %p \n", base_addr); 69 69 70 70 // 保存被Hook的目标函数的原始调用地址 71 71 old_fopen = fopen; 72 72 LOGD("[+] Orig fopen = %p\n", old_fopen); 73 73 74 74 //计算program header table实际地址 75 75 Elf32_Ehdr *header = (Elf32_Ehdr*)(base_addr); 76 76 if (memcmp(header->e_ident, "\177ELF", 4) != 0) { 77 77 return 0; 78 78 } 79 79 80 80 Elf32_Phdr* phdr_table = (Elf32_Phdr*)(base_addr + header->e_phoff); 81 81 if (phdr_table == 0) 82 82 { 83 83 LOGD("[+] phdr_table address : 0"); 84 84 return 0; 85 85 } 86 86 size_t phdr_count = header->e_phnum; 87 87 LOGD("[+] phdr_count : %d", phdr_count); 88 88 89 89 90 90 //遍历program header table,ptype等于PT_DYNAMIC即为dynameic,获取到p_offset 91 91 unsigned long dynamicAddr = 0; 92 92 unsigned int dynamicSize = 0; 93 93 int j = 0; 94 94 for (j = 0; j < phdr_count; j++) 95 95 { 96 96 if (phdr_table[j].p_type == PT_DYNAMIC) 97 97 { 98 98 dynamicAddr = phdr_table[j].p_vaddr + base_addr; 99 99 dynamicSize = phdr_table[j].p_memsz; 100100 break; 101101 } 102102 } 103103 LOGD("[+] Dynamic Addr : %x",dynamicAddr); 104104 LOGD("[+] Dynamic Size : %x",dynamicSize); 105105 106106 /* 107107 typedef struct dynamic { 108108 Elf32_Sword d_tag; 109109 union { 110110 Elf32_Sword d_val; 111111 Elf32_Addr d_ptr; 112112 } d_un; 113113 } Elf32_Dyn; 114114 */ 115115 Elf32_Dyn* dynamic_table = (Elf32_Dyn*)(dynamicAddr); 116116 unsigned long jmpRelOff = 0; 117117 unsigned long strTabOff = 0; 118118 unsigned long pltRelSz = 0; 119119 unsigned long symTabOff = 0; 120120 int i; 121121 for(i=0;i < dynamicSize / 8;i ++) 122122 { 123123 int val = dynamic_table[i].d_un.d_val; 124124 if (dynamic_table[i].d_tag == DT_JMPREL) 125125 { 126126 jmpRelOff = val; 127127 } 128128 if (dynamic_table[i].d_tag == DT_STRTAB) 129129 { 130130 strTabOff = val; 131131 } 132132 if (dynamic_table[i].d_tag == DT_PLTRELSZ) 133133 { 134134 pltRelSz = val; 135135 } 136136 if (dynamic_table[i].d_tag == DT_SYMTAB) 137137 { 138138 symTabOff = val; 139139 } 140140 } 141141 142142 Elf32_Rel* rel_table = (Elf32_Rel*)(jmpRelOff + base_addr); 143143 LOGD("[+] jmpRelOff : %x",jmpRelOff); 144144 LOGD("[+] strTabOff : %x",strTabOff); 145145 LOGD("[+] symTabOff : %x",symTabOff); 146146 //遍历查找要hook的导入函数,这里以fopen做示例 147147 for(i=0;i < pltRelSz / 8;i++) 148148 { 149149 int number = (rel_table[i].r_info >> 8) & 0xffffff; 150150 Elf32_Sym* symTableIndex = (Elf32_Sym*)(number*16 + symTabOff + base_addr); 151151 char* funcName = (char*)(symTableIndex->st_name + strTabOff + base_addr); 152152 //LOGD("[+] Func Name : %s",funcName); 153153 if(memcmp(funcName, "fopen", 5) == 0) 154154 { 155155 // 获取当前内存分页的大小 156156 uint32_t page_size = getpagesize(); 157157 // 获取内存分页的起始地址(需要内存对齐) 158158 uint32_t mem_page_start = (uint32_t)(((Elf32_Addr)rel_table[i].r_offset + base_addr)) & (~(page_size - 1)); 159159 LOGD("[+] mem_page_start = %lx, page size = %lx\n", mem_page_start, page_size); 160160 //void* pstart = (void*)MEM_PAGE_START(((Elf32_Addr)rel_table[i].r_offset + base_addr)); 161161 mprotect((uint32_t)mem_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC); 162162 LOGD("[+] r_off : %x",rel_table[i].r_offset + base_addr); 163163 LOGD("[+] new_fopen : %x",new_fopen); 164164 *(unsigned int*)(rel_table[i].r_offset + base_addr) = new_fopen; 165165 } 166166 } 167167 168168 return 0; 169169 } 170170 171171 int hook_entry(char* a){ 172172 LOGD("[+] Start hooking.\n"); 173173 hook_fopen(); 174174 return 0; 175175 }
运行后的结果为:

图3
参考文章:
http://gslab.qq.com/portal.php?mod=view&aid=169