最后介紹的這種hook方式原理比較簡單,只需要將GOT表中的目標函數地址替換為我們自己的函數地址即可,但它的缺點是只能對導入函數進行hook,還需要對elf文件的結構有所了解。
一、獲取到GOT表在內存中的地址
要得到GOT表在內存中的地址首先要解析elf文件,獲取其在文件中的偏移地址,內存地址就等於基地址加上文件偏移。
在elf的section header table中名為.got的節頭記錄着GOT表在文件中的偏移,所以第一件事就是獲取到.got節頭中的信息。
1、獲取到section header table的入口地址
1 Elf32_Ehdr elf_header; 2 memset(&elf_header, 0, sizeof(elf_header)); 3 fseek(fp, 0, SEEK_SET); 4 fread(&elf_header, sizeof(elf_header), 1, fp);
文件指針fp對應打開的elf文件。結構體Elf32_Ehdr對應elf文件頭,Elf32_Ehdr.e_shoff記錄着節區頭部表(section header table)在文件中的偏移。
2、獲取字符串表
獲取到section header table的起始地址后要通過名字來判斷出哪一項為.got節頭,這時就要用到字符串表。
1 char* parse_string_table(FILE *fp) 2 { 3 Elf32_Ehdr elf_header; 4 Elf32_Shdr elf_setion_header; 5 memset(&elf_header, 0, sizeof(elf_header)); 6 memset(&elf_setion_header, 0, sizeof(elf_setion_header)); 7 //讀取elf頭 8 fseek(fp, 0, SEEK_SET); 9 fread(&elf_header, sizeof(elf_header), 1, fp); 10 //字符串表頭在節區頭部表的第Elf32_Ehdr.e_shstrndx項 11 //通過節區頭部表偏移和每個節區頭的大小可以算出字符串表節頭的地址 12 fseek(fp, elf_header.e_shoff + elf_header.e_shstrndx * elf_header.e_shentsize, SEEK_SET); 13 fread(&elf_setion_header, sizeof(elf_setion_header), 1, fp); 14 int size_string_table = elf_setion_header.sh_size; 15 char *buffer = malloc(size_string_table); 16 //通過字符串表節區頭中記錄的偏移地址讀取字符串表 17 fseek(fp, elf_setion_header.sh_offset, SEEK_SET); 18 fread(buffer, size_string_table, 1, fp); 19 return buffer; 20 }
3、遍歷節區頭部表
遍歷整個節區頭部表,獲取.got節區頭,獲取GOT表在文件中的偏移地址。
1 void parse_got_table(FILE *fp, long *addr_got_table, long *size_got_table) 2 { 3 Elf32_Ehdr elf_header; 4 Elf32_Shdr elf_secion_header; 5 memset(&elf_header, 0, sizeof(elf_header)); 6 memset(&elf_secion_header, 0, sizeof(elf_secion_header)); 7 //讀取elf頭 8 fseek(fp, 0, SEEK_SET); 9 fread(&elf_header, sizeof(elf_header), 1, fp); 10 //獲取字符串表 11 char *string_table = parse_string_table(fp); 12 fseek(fp, elf_header.e_shoff, SEEK_SET); 13 //遍歷節區頭部表 14 for (int i = 0; i < elf_header.e_shnum; ++i) { 15 fread(&elf_secion_header, elf_header.e_shentsize, 1, fp); 16 if (elf_secion_header.sh_type == SHT_PROGBITS 17 && 0 == strcmp(".got", string_table + elf_secion_header.sh_name)) { 18 //返回GOT表偏移及大小 19 *addr_got_table = elf_secion_header.sh_addr; 20 *size_got_table = elf_secion_header.sh_size; 21 } 22 } 23 free(string_table); 24 }
4、獲取elf文件在內存中的基址
在Linux系統中可以通過讀取/proc/pid/maps來獲取各個elf文件在內存中的加載基址。在之前的文章中已經反復用到,這里就不再重復了。
最后可以得到:GOT表內存地址 = elf文件內存基址 + GOT表文件偏移
二、修改GOT表中存放的導入函數的地址
知道GOT表在內存中的地址后我們就可以着手對其修改了,里邊存放的全是外部符號地址(前三項有特殊作用,這里不做討論),很顯然每個表項占4個字節。接下來一個問題是我們怎么知道需要替換哪一個表項呢?如果我們知道需要hook的函數地址,就可以跟表里的地址進行逐一比對。如果是系統庫中的函數,我們可以直接獲取到函數地址,但如果是第三方庫中的函數呢?我們可以借助dlsym函數。
void hook_got_make(const char *elf, const char *symbol, const char *library, void *func, void **old_func) { FILE *file = fopen(elf, "rb"); long addr_got_table; long size_got_table; parse_got_table(file, &addr_got_table, &size_got_table); fclose(file); void *handle = dlopen(library, RTLD_LAZY); void *target = dlsym(handle, symbol); dlclose(handle); long addr_base = get_module_addr(-1, elf); for (int i = 0; i < size_got_table; i += 4) { if (*(uint32_t *)(addr_base + addr_got_table + i) == (uint32_t)target) { *old_func = target; write_code(addr_base + addr_got_table + i, (uint32_t)func); } } }
首先通過dlopen加載symbol(目標函數名)所在的可執行文件,當然這個文件肯定之前就已經加載到內存中了。然后通過dlsym獲取symbol對應的函數地址。
獲取到地址后首先保存到old_func中,然后用我們的新地址覆蓋GOT表中的原地址。注意在更改GOT表的內容時首先要將所在內存地址的屬性設為可寫。
