全局符号表(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 #include <unistd.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <android/log.h>
5 #include <EGL/egl.h>
6 #include <GLES/gl.h>
7 #include <elf.h>
8 #include <fcntl.h>
9 #include <sys/mman.h>
10
11 #define LOG_TAG "INJECT"
12 #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)
13
14 //FILE *fopen(const char *filename, const char *modes)
15 FILE* (*old_fopen)(const char *filename, const char *modes);
16 FILE* new_fopen(const char *filename, const char *modes){
17 LOGD("[+] New call fopen.\n");
18 if(old_fopen == -1){
19 LOGD("error.\n");
20 }
21 return old_fopen(filename, modes);
22 }
23
24 void* get_module_base(pid_t pid, const char* module_name){
25 FILE* fp;
26 long addr = 0;
27 char* pch;
28 char filename[32];
29 char line[1024];
30
31 // 格式化字符串得到 "/proc/pid/maps"
32 if(pid < 0){
33 snprintf(filename, sizeof(filename), "/proc/self/maps");
34 }else{
35 snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
36 }
37
38 // 打开文件/proc/pid/maps,获取指定pid进程加载的内存模块信息
39 fp = fopen(filename, "r");
40 if(fp != NULL){
41 // 每次一行,读取文件 /proc/pid/maps中内容
42 while(fgets(line, sizeof(line), fp)){
43 // 查找指定的so模块
44 if(strstr(line, module_name)){
45 // 分割字符串
46 pch = strtok(line, "-");
47 // 字符串转长整形
48 addr = strtoul(pch, NULL, 16);
49
50 // 特殊内存地址的处理
51 if(addr == 0x8000){
52 addr = 0;
53 }
54 break;
55 }
56 }
57 }
58 fclose(fp);
59 return (void*)addr;
60 }
61
62 #define LIB_PATH "/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"
63 int hook_fopen(){
64
65 // 获取目标pid进程中"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"模块的加载地址
66 void* base_addr = get_module_base(getpid(), LIB_PATH);
67 LOGD("[+] libvivosgmain.so address = %p \n", base_addr);
68
69 // 保存被Hook的目标函数的原始调用地址
70 old_fopen = fopen;
71 LOGD("[+] Orig fopen = %p\n", old_fopen);
72
73 int fd;
74 // 打开内存模块文件"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"
75 fd = open(LIB_PATH, O_RDONLY);
76 if(-1 == fd){
77 LOGD("error.\n");
78 return -1;
79 }
80
81 // elf32文件的文件头结构体Elf32_Ehdr
82 Elf32_Ehdr ehdr;
83 // 读取elf32格式的文件"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"的文件头信息
84 read(fd, &ehdr, sizeof(Elf32_Ehdr));
85
86 // elf32文件中节区表信息结构的文件偏移
87 unsigned long shdr_addr = ehdr.e_shoff;
88 // elf32文件中节区表信息结构的数量
89 int shnum = ehdr.e_shnum;
90 // elf32文件中每个节区表信息结构中的单个信息结构的大小(描述每个节区的信息的结构体的大小)
91 int shent_size = ehdr.e_shentsize;
92
93 // elf32文件节区表中每个节区的名称存放的节区名称字符串表,在节区表中的序号index
94 unsigned long stridx = ehdr.e_shstrndx;
95
96 // elf32文件中节区表的每个单元信息结构体(描述每个节区的信息的结构体)
97 Elf32_Shdr shdr;
98 // elf32文件中定位到存放每个节区名称的字符串表的信息结构体位置.shstrtab
99 lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET);
100 // 读取elf32文件中的描述每个节区的信息的结构体(这里是保存elf32文件的每个节区的名称字符串的)
101 read(fd, &shdr, shent_size);
102 LOGD("[+] String table offset is %lu, size is %lu", shdr.sh_offset, shdr.sh_size); //41159, size is 254
103
104 // 为保存elf32文件的所有的节区的名称字符串申请内存空间
105 char * string_table = (char *)malloc(shdr.sh_size);
106 // 定位到具体存放elf32文件的所有的节区的名称字符串的文件偏移处
107 lseek(fd, shdr.sh_offset, SEEK_SET);
108 // 从elf32内存文件中读取所有的节区的名称字符串到申请的内存空间中
109 read(fd, string_table, shdr.sh_size);
110
111 // 重新设置elf32文件的文件偏移为节区信息结构的起始文件偏移处
112 lseek(fd, shdr_addr, SEEK_SET);
113
114 int i;
115 uint32_t out_addr = 0;
116 uint32_t out_size = 0;
117 uint32_t got_item = 0;
118 int32_t got_found = 0;
119
120 // 循环遍历elf32文件的节区表(描述每个节区的信息的结构体)
121 for(i = 0; i<shnum; i++){
122 // 依次读取节区表中每个描述节区的信息的结构体
123 read(fd, &shdr, shent_size);
124 // 判断当前节区描述结构体描述的节区是否是SHT_PROGBITS类型
125 //类型为SHT_PROGBITS的.got节区包含全局偏移表
126 if(shdr.sh_type == SHT_PROGBITS){
127 // 获取节区的名称字符串在保存所有节区的名称字符串段.shstrtab中的序号
128 int name_idx = shdr.sh_name;
129
130 // 判断节区的名称是否为".got.plt"或者".got"
131 if(strcmp(&(string_table[name_idx]), ".got.plt") == 0
132 || strcmp(&(string_table[name_idx]), ".got") == 0){
133 // 获取节区".got"或者".got.plt"在内存中实际数据存放地址
134 out_addr = base_addr + shdr.sh_addr;
135 // 获取节区".got"或者".got.plt"的大小
136 out_size = shdr.sh_size;
137 LOGD("[+] out_addr = %lx, out_size = %lx\n", out_addr, out_size);
138 int j = 0;
139 // 遍历节区".got"或者".got.plt"获取保存的全局的函数调用地址
140 for(j = 0; j<out_size; j += 4){
141 // 获取节区".got"或者".got.plt"中的单个函数的调用地址
142 got_item = *(uint32_t*)(out_addr + j);
143 // 判断节区".got"或者".got.plt"中函数调用地址是否是将要被Hook的目标函数地址
144 if(got_item == old_fopen){
145 LOGD("[+] Found fopen in got.\n");
146 got_found = 1;
147 // 获取当前内存分页的大小
148 uint32_t page_size = getpagesize();
149 // 获取内存分页的起始地址(需要内存对齐)
150 uint32_t entry_page_start = (out_addr + j) & (~(page_size - 1));
151 LOGD("[+] entry_page_start = %lx, page size = %lx\n", entry_page_start, page_size);
152 // 修改内存属性为可读可写可执行
153 if(mprotect((uint32_t*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1){
154 LOGD("mprotect false.\n");
155 return -1;
156 }
157 LOGD("[+] %s, old_fopen = %lx, new_fopen = %lx\n", "before hook function", got_item, new_fopen);
158
159 // Hook函数为我们自己定义的函数
160 got_item = new_fopen;
161 LOGD("[+] %s, old_fopen = %lx, new_fopen = %lx\n", "after hook function", got_item, new_fopen);
162 // 恢复内存属性为可读可执行
163 if(mprotect((uint32_t*)entry_page_start, page_size, PROT_READ | PROT_EXEC) == -1){
164 LOGD("mprotect false.\n");
165 return -1;
166 }
167 break;
168 // 此时,目标函数的调用地址已经被Hook了
169 }else if(got_item == new_fopen){
170 LOGD("[+] Already hooked.\n");
171 break;
172 }
173 }
174 // Hook目标函数成功,跳出循环
175 if(got_found)
176 break;
177 }
178 }
179 }
180 free(string_table);
181 close(fd);
182 }
183
184 int hook_entry(char* a){
185 LOGD("[+] Start hooking.\n");
186 hook_fopen();
187 return 0;
188 }
运行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 #include <unistd.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <android/log.h>
5 #include <EGL/egl.h>
6 #include <GLES/gl.h>
7 #include <elf.h>
8 #include <fcntl.h>
9 #include <sys/mman.h>
10
11 #define LOG_TAG "INJECT"
12 #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)
13
14
15 //FILE *fopen(const char *filename, const char *modes)
16 FILE* (*old_fopen)(const char *filename, const char *modes);
17 FILE* new_fopen(const char *filename, const char *modes){
18 LOGD("[+] New call fopen.\n");
19 if(old_fopen == -1){
20 LOGD("error.\n");
21 }
22 return old_fopen(filename, modes);
23 }
24
25 void* get_module_base(pid_t pid, const char* module_name){
26 FILE* fp;
27 long addr = 0;
28 char* pch;
29 char filename[32];
30 char line[1024];
31
32 // 格式化字符串得到 "/proc/pid/maps"
33 if(pid < 0){
34 snprintf(filename, sizeof(filename), "/proc/self/maps");
35 }else{
36 snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
37 }
38
39 // 打开文件/proc/pid/maps,获取指定pid进程加载的内存模块信息
40 fp = fopen(filename, "r");
41 if(fp != NULL){
42 // 每次一行,读取文件 /proc/pid/maps中内容
43 while(fgets(line, sizeof(line), fp)){
44 // 查找指定的so模块
45 if(strstr(line, module_name)){
46 // 分割字符串
47 pch = strtok(line, "-");
48 // 字符串转长整形
49 addr = strtoul(pch, NULL, 16);
50
51 // 特殊内存地址的处理
52 if(addr == 0x8000){
53 addr = 0;
54 }
55 break;
56 }
57 }
58 }
59 fclose(fp);
60 return (void*)addr;
61 }
62
63 #define LIB_PATH "/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"
64 int hook_fopen(){
65
66 // 获取目标pid进程中"/data/app-lib/com.bbk.appstore-2/libvivosgmain.so"模块的加载地址
67 void* base_addr = get_module_base(getpid(), LIB_PATH);
68 LOGD("[+] libvivosgmain.so address = %p \n", base_addr);
69
70 // 保存被Hook的目标函数的原始调用地址
71 old_fopen = fopen;
72 LOGD("[+] Orig fopen = %p\n", old_fopen);
73
74 //计算program header table实际地址
75 Elf32_Ehdr *header = (Elf32_Ehdr*)(base_addr);
76 if (memcmp(header->e_ident, "\177ELF", 4) != 0) {
77 return 0;
78 }
79
80 Elf32_Phdr* phdr_table = (Elf32_Phdr*)(base_addr + header->e_phoff);
81 if (phdr_table == 0)
82 {
83 LOGD("[+] phdr_table address : 0");
84 return 0;
85 }
86 size_t phdr_count = header->e_phnum;
87 LOGD("[+] phdr_count : %d", phdr_count);
88
89
90 //遍历program header table,ptype等于PT_DYNAMIC即为dynameic,获取到p_offset
91 unsigned long dynamicAddr = 0;
92 unsigned int dynamicSize = 0;
93 int j = 0;
94 for (j = 0; j < phdr_count; j++)
95 {
96 if (phdr_table[j].p_type == PT_DYNAMIC)
97 {
98 dynamicAddr = phdr_table[j].p_vaddr + base_addr;
99 dynamicSize = phdr_table[j].p_memsz;
100 break;
101 }
102 }
103 LOGD("[+] Dynamic Addr : %x",dynamicAddr);
104 LOGD("[+] Dynamic Size : %x",dynamicSize);
105
106 /*
107 typedef struct dynamic {
108 Elf32_Sword d_tag;
109 union {
110 Elf32_Sword d_val;
111 Elf32_Addr d_ptr;
112 } d_un;
113 } Elf32_Dyn;
114 */
115 Elf32_Dyn* dynamic_table = (Elf32_Dyn*)(dynamicAddr);
116 unsigned long jmpRelOff = 0;
117 unsigned long strTabOff = 0;
118 unsigned long pltRelSz = 0;
119 unsigned long symTabOff = 0;
120 int i;
121 for(i=0;i < dynamicSize / 8;i ++)
122 {
123 int val = dynamic_table[i].d_un.d_val;
124 if (dynamic_table[i].d_tag == DT_JMPREL)
125 {
126 jmpRelOff = val;
127 }
128 if (dynamic_table[i].d_tag == DT_STRTAB)
129 {
130 strTabOff = val;
131 }
132 if (dynamic_table[i].d_tag == DT_PLTRELSZ)
133 {
134 pltRelSz = val;
135 }
136 if (dynamic_table[i].d_tag == DT_SYMTAB)
137 {
138 symTabOff = val;
139 }
140 }
141
142 Elf32_Rel* rel_table = (Elf32_Rel*)(jmpRelOff + base_addr);
143 LOGD("[+] jmpRelOff : %x",jmpRelOff);
144 LOGD("[+] strTabOff : %x",strTabOff);
145 LOGD("[+] symTabOff : %x",symTabOff);
146 //遍历查找要hook的导入函数,这里以fopen做示例
147 for(i=0;i < pltRelSz / 8;i++)
148 {
149 int number = (rel_table[i].r_info >> 8) & 0xffffff;
150 Elf32_Sym* symTableIndex = (Elf32_Sym*)(number*16 + symTabOff + base_addr);
151 char* funcName = (char*)(symTableIndex->st_name + strTabOff + base_addr);
152 //LOGD("[+] Func Name : %s",funcName);
153 if(memcmp(funcName, "fopen", 5) == 0)
154 {
155 // 获取当前内存分页的大小
156 uint32_t page_size = getpagesize();
157 // 获取内存分页的起始地址(需要内存对齐)
158 uint32_t mem_page_start = (uint32_t)(((Elf32_Addr)rel_table[i].r_offset + base_addr)) & (~(page_size - 1));
159 LOGD("[+] mem_page_start = %lx, page size = %lx\n", mem_page_start, page_size);
160 //void* pstart = (void*)MEM_PAGE_START(((Elf32_Addr)rel_table[i].r_offset + base_addr));
161 mprotect((uint32_t)mem_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);
162 LOGD("[+] r_off : %x",rel_table[i].r_offset + base_addr);
163 LOGD("[+] new_fopen : %x",new_fopen);
164 *(unsigned int*)(rel_table[i].r_offset + base_addr) = new_fopen;
165 }
166 }
167
168 return 0;
169 }
170
171 int hook_entry(char* a){
172 LOGD("[+] Start hooking.\n");
173 hook_fopen();
174 return 0;
175 }
运行后的结果为:
图3
参考文章:
http://gslab.qq.com/portal.php?mod=view&aid=169