简单来说,hook也就是我们常说的钩子,以替换的方式把改变程序中原有的函数功能,而注入,则更偏向于插入自定义函数/代码,代码注入一般是一次性的,而Hook劫持是比较稳定持久的
正常情况下, Linux 动态加载器ld-linux
(见 man 手册 ld-linux(8)) 会搜寻并装载程序所需的共享链接库文件, 而LD_PRELOAD
是一个可选的环境变量, 包含一个或多个指向共享链接库文件的路径. 加载器会先于 C 语言运行库之前载入LD_PRELOAD
指定的共享链接库,也就是所谓的预装载 preload
做个简单的演示
#include <stdio.h> #include <string.h> int main(int argc, char const *argv[]) { puts("welcome!"); sleep(1); char *ptr = malloc(0x100); puts("what's your name:"); read(0,ptr,0x20); printf("nice to meet you,%s\n", ptr); return 0; }
这个是我们的目标程序target,编译gcc ./target.c -o target
#include <stdio.h> int sleep(int t) { puts("your sleep is hook by me!"); }
这个是要用于制作so文件的hook1.c
编译生成so:gcc -fPIC --shared hook1.c -o hook1.so
然后进行hook
LD_PRELOAD=./hook1.so ./target
可以看到sleep函数已经被替换成功了,这就是简单的hook演示,但这种东西似乎并没有什么卵用,就跟给程序打个patch一样
因此这里演示一个稍微有点卵用的东西,如果我们想统计某个函数在整个程序运行过程中运行了几次,每次运行的相关数据情况等等,那么hook就能派上一点用场
修改一下我们的target程序
#include <stdio.h> #include <string.h> void function() { for (int i = 0; i < 10; ++i) { sleep(1); } puts("good bye~"); } int main(int argc, char const *argv[]) { puts("welcome!"); sleep(1); char *ptr = malloc(0x100); puts("what's your name:"); read(0,ptr,0x20); printf("nice to meet you,%s\n", ptr); function(); return 0; }
然后hook2.c如下
#include <stdio.h> #include <string.h> #include <dlfcn.h> typedef int(*SLEEP)(unsigned int t); static int sleep_times=0; int sleep(unsigned int t) { static void *handle = NULL; static SLEEP true_sleep = NULL; sleep_times++; if( !handle ) { handle = dlopen("libc.so.6", RTLD_LAZY); true_sleep = (SLEEP)dlsym(handle, "sleep"); } printf("sleep has been called for %d times!\n", sleep_times); return true_sleep(t); }
这次的hook的作用是自定义sleep函数,每次调用sleep就计数一次,然后马上执行glibc中真正的sleep函数
编译的命令是gcc -fPIC -shared -o hook2.so hook2.c -ldl
最后一个参数-ldl
是为了加载<dlfcn.h>
所在的共享库dl
void *dlopen(const char **filename*, int flag**);**
而dlsym函数用于取函数的地址,存放在一个函数指针中
void *dlsym(void **handle*, const char **symbol*);
上面的hook2.c中也就是用这两个函数实现先调用自定义sleep记录次数,然后再调用glibc中的sleep,从而既达到了我们的目的,又不影响程序的执行逻辑
运行效果如下,可以看到sleep被调用了11次
为了方便hook,可以定义以下宏
#include <sys/types.h> #include <dlfcn.h> #if defined(RTLD_NEXT) # define REAL_LIBC RTLD_NEXT #else # define REAL_LIBC ((void *) -1L) #endif #define FN(ptr,type,name,args) ptr = (type (*)args)dlsym (REAL_LIBC, name)
当调用dlsym的时传入RTLD_NEXT参数,gcc的共享库加载器会按照装载顺序获取下一个共享库中的符号地址
因此通过上面的宏定义,REAL_LIBC代表当前调用链中紧接着下一个共享库,从调用方链接映射列表中的下一个关联目标文件获取符号
在使用的时候只需要在自定义hook函数中加入FN即可方便进行替换,如替换execve函数
int execve(const char *filename, char *const argv[], char *const envp[]) { static int (*func)(const char *, char **, char **); FN(func,int,"execve",(const char *, char **const, char **const)); printf("execve has been called!"); return (*func) (filename, (char**) argv, (char **) envp); }
利用LD_PRELOAD方法进行hook,很多时候是限制比较多的,它要求在程序在执行前就把hook.so加入环境变量中,对于已经运行了的程序,则没有办法采用这种方法进行hook
这里就介绍另外一种hook的方法,利用ptrace进行hook
众所周知,ptrace是Linux提供的一种专门用于调试的系统调用,具体的用法可见man文档
这里直接介绍利用ptrace进行hook的原理和步骤
这5步当中最麻烦的就是第二步,接下来通过代码逐步分析五个步骤的实现方式,最终的完整代码可见附件
这里主要是涉及ptrace的基本运用,首先定义一系列有关ptrace的操作函数
void ptrace_attach(pid_t pid) { if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) { error_msg("ptrace_attach error\n"); } waitpid(pid, NULL, WUNTRACED); ptrace_getregs(pid, &oldregs); } //oldregs为全局变量
在attach上目标程序后马上保存他的所有的原始寄存器的值,对应在最后detach的时候还原
为了调用dlopen函数加载hook.so到目标函数的内存空间,就必须知道dlopen函数的地址,但是一般情况下我们的程序不会#include <dlfcn.h>
,因此我们这里选择找到__libc_dlopen_mode
的地址,利用他来打开so,该函数的参数用法和dlopen完全一样
如何查找指定函数名的真实地址呢?
通过link_map链表的指针链,在各个so文件中寻找函数对应的地址
这里定义了两个函数
map = get_linkmap(pid);
sym_addr = find_symbol(pid, map, oldfunname);
首先从get_linkmap开始讲解
首先从程序头部IMAGE_ADDR(64为的一般为0x400000)开始读取信息找到头部表的地址
根据头部表再找.dynamic节
再遍历.dynamic节,找到.got.plt节,而这个就是我们平常说的got表了
GOT表中每一项都是64bit的Elf64_Addr
地址
但其中GOT表前三项用于保存特殊的数据结构地址:
GOT[0]为段”.dynamic”的加载地址
GOT[1]为ELF所依赖的动态链接库链表头struct link_map结构体描述符地址
GOT[2]为_dl_runtime_resolve
函数地址
于是这样就找到了link_map
struct link_map* get_linkmap(int pid) { int i; Elf_Ehdr *ehdr = (Elf_Ehdr *) malloc(sizeof(Elf_Ehdr)); Elf_Phdr *phdr = (Elf_Phdr *) malloc(sizeof(Elf_Phdr)); Elf_Dyn *dyn = (Elf_Dyn *) malloc(sizeof(Elf_Dyn)); Elf_Addr *gotplt; // 读取文件头 ptrace_getdata(pid, IMAGE_ADDR, ehdr, sizeof(Elf_Ehdr)); // 获取program headers table的地址 phdr_addr = IMAGE_ADDR + ehdr->e_phoff; // 遍历program headers table,找到.dynamic for (i = 0; i < ehdr->e_phnum; i++) { ptrace_getdata(pid, phdr_addr + i * sizeof(Elf_Phdr), phdr, sizeof(Elf_Phdr)); if (phdr->p_type == PT_DYNAMIC) { dyn_addr = phdr->p_vaddr; break; } } if (0 == dyn_addr) { error_msg("cannot find the address of .dynamin\n"); } else { printf("[+]the address of .dynamic is %p\n", (void *)dyn_addr); } // 遍历.dynamic,找到.got.plt for (i = 0; i * sizeof(Elf_Dyn) <= phdr->p_memsz; i++ ) { ptrace_getdata(pid, dyn_addr + i * sizeof(Elf_Dyn), dyn, sizeof(Elf_Dyn)); if (dyn->d_tag == DT_PLTGOT) { gotplt = (Elf_Addr *)(dyn->d_un.d_ptr); break; } } if (NULL == gotplt) { error_msg("cannot find the address of .got.plt\n"); }else { printf("[+]the address of .got.plt is %p\n", gotplt); } // 获取link_map地址 ptrace_getdata(pid, (Elf_Addr)(gotplt + 1), &lmap_addr, sizeof(Elf_Addr)); printf("[+]the address of link_map is %p\n", (void *)lmap_addr); free(ehdr); free(phdr); free(dyn); return (struct link_map *)lmap_addr; }
找到后返回一个结构指针,link_map的结构体如下
typedef struct link_map { caddr_t l_addr; /* Base Address of library */ #ifdef __mips__ caddr_t l_offs; /* Load Offset of library */ #endif const char *l_name; /* Absolute Path to Library */ const void *l_ld; /* Pointer to .dynamic in memory */ struct link_map *l_next, *l_prev; /* linked list of of mapped libs */ } Link_map;
接下来讲解find_symbol函数
上面说到GOT[2]为_dl_runtime_resolve
函数地址
该函数的作用是遍历GOT[1]指向的动态链接库链表直至找到某个符号的地址,然后将该符号地址保存至相应的GOT表项中,而find_symbol函数的作用正是模拟_dl_runtime_resolve
函数,在动态链接库中找到我们想要的函数地址
lf_Addr find_symbol(int pid, Elf_Addr lm_addr, char *sym_name) { char buf[STRLEN] = {0}; struct link_map lmap; unsigned int nlen = 0; while (lm_addr) { // 读取link_map结构内容 ptrace_getdata(pid, lm_addr, &lmap, sizeof(struct link_map)); lm_addr = (Elf_Addr)(lmap.l_next);//获取下一个link_map // 判断l_name是否有效 if (0 == lmap.l_name) { printf("[-]invalid address of l_name\n"); continue; } nlen = ptrace_getstr(pid, (Elf_Addr)lmap.l_name, buf, 128); //读取so名称 if (0 == nlen || 0 == strlen(buf)) { printf("[-]invalud name of link_map at %p\n", (void *)lmap.l_name); continue; } printf(">> start search symbol in %s:\n", buf); Elf_Addr sym_addr = find_symbol_in_linkmap(pid, &lmap, sym_name); if (sym_addr) { return sym_addr; } } return 0; }
最后执行了Elf_Addr sym_addr = find_symbol_in_linkmap(pid, &lmap, sym_name);
继续来看find_symbol_in_linkmap函数,这个函数的主要作用是根据handle_one_lmap返回的lmap_result结构体中的信息来判断 我们需要找的函数是否在这个so中
Elf_Addr find_symbol_in_linkmap(int pid, struct link_map *lm, char *sym_name) { int i = 0; char buf[STRLEN] = {0}; unsigned int nlen = 0; Elf_Addr ret; Elf_Sym *sym = (Elf_Sym *)malloc(sizeof(Elf_Sym)); struct lmap_result *lmret = handle_one_lmap(pid, lm); //lmap_result结构体,包含了SYMTAB、STRTAB、RELPLT、REPLDYN等信息 /* struct lmap_result { Elf_Addr symtab; Elf_Addr strtab; Elf_Addr jmprel; Elf_Addr reldyn; uint64_t link_addr; uint64_t nsymbols; uint64_t nrelplts; uint64_t nreldyns; }; */ for(i = 0; i >= 0; i++) { // 读取link_map的符号表 ptrace_getdata(pid, lmret->symtab + i * sizeof(Elf_Sym) ,sym ,sizeof(Elf_Sym)); // 如果全为0,是符号表的第一项 if (!sym->st_name && !sym->st_size && !sym->st_value) { continue; } nlen = ptrace_getstr(pid, lmret->strtab + sym->st_name, buf, 128); if (buf[0] && (32 > buf[0] || 127 == buf[0]) ) { printf(">> nothing found in this so...\n\n"); return 0; } if (strcmp(buf, sym_name) == 0) { printf("[+]has find the symbol name: %s\n",buf); if(sym->st_value == 0) {//如果sym->st_value值为0,代表这个符号本身就是重定向的内容 continue; } else {// 否则说明找到了符号 return (lmret->link_addr + sym->st_value); } } } free(sym); return 0; }
再来康康handle_one_lmap是如何把当前link_map指向的so中的SYMTAB、STRTAB、RELPLT、REPLDYN信息提取出来的:
struct lmap_result *handle_one_lmap(int pid, struct link_map *lm) { Elf_Addr dyn_addr; Elf_Dyn *dyn = (Elf_Dyn *)calloc(1, sizeof(Elf_Dyn)); struct lmap_result *lmret = NULL; // 符号表 Elf_Addr symtab; Dyn_Val syment; Dyn_Val symsz; // 字符串表 Elf_Addr strtab; // rel.plt Elf_Addr jmprel; Dyn_Val relpltsz; // rel.dyn Elf_Addr reldyn; Dyn_Val reldynsz; // size of one REL relocs or RELA relocs Dyn_Val relent; // 每个lmap对应的库的映射基地址 Elf_Addr link_addr; link_addr = lm->l_addr; dyn_addr = lm->l_ld; ptrace_getdata(pid, dyn_addr, dyn, sizeof(Elf_Dyn)); while(dyn->d_tag != DT_NULL) { switch(dyn->d_tag) { // 符号表 case DT_SYMTAB: symtab = dyn->d_un.d_ptr; break; case DT_SYMENT: syment = dyn->d_un.d_val; break; case DT_SYMINSZ: symsz = dyn->d_un.d_val; break; // 字符串表 case DT_STRTAB: strtab = dyn->d_un.d_ptr; break; // rel.plt, Address of PLT relocs case DT_JMPREL: jmprel = dyn->d_un.d_ptr; break; // rel.plt, Size in bytes of PLT relocs case DT_PLTRELSZ: relpltsz = dyn->d_un.d_val; break; // rel.dyn, Address of Rel relocs case DT_REL: case DT_RELA: reldyn = dyn->d_un.d_ptr; break; // rel.dyn, Size of one Rel reloc case DT_RELENT: case DT_RELAENT: relent = dyn->d_un.d_val; break; //rel.dyn Total size of Rel relocs case DT_RELSZ: case DT_RELASZ: reldynsz = dyn->d_un.d_val; break; } ptrace_getdata(pid, dyn_addr += (sizeof(Elf_Dyn)/sizeof(Elf_Addr)), dyn, sizeof(Elf_Dyn)); } if (0 == syment || 0 == relent) { printf("[-]Invalid ent, syment=%u, relent=%u\n", (unsigned)syment, (unsigned)relent); return lmret; } lmret = (struct lmap_result *)calloc(1, sizeof(struct lmap_result)); lmret->symtab = symtab; lmret->strtab = strtab; lmret->jmprel = jmprel; lmret->reldyn = reldyn; lmret->link_addr = link_addr; lmret->nsymbols = symsz / syment; lmret->nrelplts = relpltsz / relent; lmret->nreldyns = reldynsz / relent; free(dyn); return lmret; }
可以看到 这里利用了link_map->l_ld
读取到 Elf_Dyn *dyn
,从而拿到有关当前so的 .dynamic
的内容
再用switch语句区分各种dyn->d_tag
下的不同类别的信息
循环处理完毕后将存储的有用信息的(struct lmap_result *)lmret
返回
至此,我们构造了一个find_symbol函数用于查找目标程序内存空间里已加载so的函数
通过第二步的find_symbol函数,可以得到__libc_dlopen_mode
的地址,接下来就是对目标程序的寄存器进行操作
/* 查找要被替换的函数 */ old_sym_addr = find_symbol(pid, map, oldfunname); /* 查找hook.so中hook的函数 */ new_sym_addr = find_symbol(pid, map, newfunname); /* 查找__libc_dlopen_mode,并调用它加载hook.so动态链接库 */ dlopen_addr = find_symbol(pid, map, "__libc_dlopen_mode"); /*把hook.so动态链接库加载进target程序 */ inject_code(pid, dlopen_addr, libpath);
这里的重点在于inject_code(pid, dlopen_addr, libpath);
int inject_code(pid_t pid, unsigned long dlopen_addr, char *libc_path) { char sbuf1[STRLEN], sbuf2[STRLEN]; struct user_regs_struct regs, saved_regs; int status; puts(">> start inject_code to call the dlopen"); ptrace_getregs(pid, ®s);//获取所有寄存器值 ptrace_getdata(pid, regs.rsp + STRLEN, sbuf1, sizeof(sbuf1)); ptrace_getdata(pid, regs.rsp, sbuf2, sizeof(sbuf2));//获取栈上数据并保存在sbuf1、2 /*用于引发SIGSEGV信号的ret内容*/ unsigned long ret_addr = 0x666; ptrace_setdata(pid, regs.rsp, (char *)&ret_addr, sizeof(ret_addr)); ptrace_setdata(pid, regs.rsp + STRLEN, libc_path, strlen(libc_path) + 1); memcpy(&saved_regs, ®s, sizeof(regs)); printf("before inject:rsp=%zx rdi=%zx rsi=%zx rip=%zx\n", regs.rsp,regs.rdi, regs.rsi, regs.rip); regs.rdi = regs.rsp + STRLEN; regs.rsi = RTLD_NOW|RTLD_GLOBAL|RTLD_NODELETE; regs.rip = dlopen_addr+2; printf("after inject:rsp=%zx rdi=%zx rsi=%zx rip=%zx\n", regs.rsp,regs.rdi, regs.rsi, regs.rip); if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) < 0) {//设置寄存器 error_msg("inject_code:PTRACE_SETREGS 1 failed!"); } if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {//设置完寄存器后让目标进程继续运行 error_msg("inject_code:PTRACE_CONT failed!"); } waitpid(pid, &status, 0);//按照最后的ret指令会使得rip=0x666,从而引发SIGSEGV ptrace_getregs(pid, ®s); printf("after waitpid inject:rsp=%zx rdi=%zx rsi=%zx rip=%zx\n", regs.rsp,regs.rdi, regs.rsi, regs.rip); //恢复现场,恢复所有寄存器和栈上数据 if (ptrace(PTRACE_SETREGS, pid, 0, &saved_regs) < 0) { error_msg("inject_code:PTRACE_SETREGS 2 failed!");; } ptrace_setdata(pid, saved_regs.rsp + STRLEN, sbuf1, sizeof(sbuf1)); ptrace_setdata(pid, saved_regs.rsp, sbuf2, sizeof(sbuf2)); puts("-----inject_code done------"); return 0; }
通过以上代码,就能使得hook.so被加载进target程序,从而实现注入so
这里就简单很多了,有了函数地址,再找到got表地址就能通过修改got表从而实现hook函数
这里首先实现一个find_sym_in_rel函数,用于找到指定函数的的got表地址
Elf_Addr find_sym_in_rel(int pid, char *sym_name) { Elf_Rel *rel = (Elf_Rel *) malloc(sizeof(Elf_Rel)); Elf_Sym *sym = (Elf_Sym *) malloc(sizeof(Elf_Sym)); int i; char str[STRLEN] = {0}; unsigned long ret; struct lmap_result *lmret = get_dyn_info(pid); for (i = 0; i<lmret->nrelplts; i++) { ptrace_getdata(pid, lmret->jmprel + i*sizeof(Elf_Rela), rel, sizeof(Elf_Rela)); ptrace_getdata(pid, lmret->symtab + ELF64_R_SYM(rel->r_info) * sizeof(Elf_Sym), sym, sizeof(Elf_Sym)); int n = ptrace_getstr(pid, lmret->strtab + sym->st_name, str, STRLEN); printf("self->st_name: %s, self->r_offset = %p\n",str, rel->r_offset); if (strcmp(str, sym_name) == 0) { break; } } if (i == lmret->nrelplts) ret = 0; else ret = rel->r_offset; free(rel); return ret; }
找好了got表地址后最后进行的就是修改got表了
/* 找到旧函数在重定向表的地址 */ old_rel_addr = find_sym_in_rel(pid, oldfunname); ptrace_getdata(pid, old_rel_addr, &target_addr, sizeof(Elf_Addr)); ptrace_setdata(pid, old_rel_addr, &new_sym_addr, sizeof(Elf_Addr)); //修改oldfun的got表内容为newfun To_detach(pid);//退出并还原ptrace attach前的寄存器内容
至此利用ptrace进行hook的操作就这样完成了,其实可以发现,这种hook手段离不开注入技术
在这里,我们的target程序如下
#include <stdio.h> #include <unistd.h> int main() { int num=10; printf("my pid is %d\n", getpid()); puts("start hook?"); while(--num) { puts("hello?"); sleep(1); } return 0; } //gcc target.c -o target
hook_so源码如下
#include <stdio.h> int newputs(const char *str) { write(1,"hook puts! ",11); puts(str); return 0; } //gcc hook_so.c -o hook_so.so -fPIC --shared
hook3源码见附件,太长了不贴了
编译gcc hook3.c -o hook3 -ldl && gcc target.c -o target && gcc hook_so.c -o hook_so.so -fPIC --shared
运行:
$ sudo ./hook3 ./hook_so.so puts newputs 26600 --------------------------------- target pid = 26600 target oldfunname: puts patch libpath: ./hook_so.so patch newfunname: newputs --------------------------------- [+]the address of .dynamic is 0x600e28 [+]the address of .got.plt is 0x601000 [+]the address of link_map is 0x7fc95aa38168 [-]invalud name of link_map at 0x7fc95aa386f8 [-]invalud name of link_map at 0x7fc95aa38b90 >> start search symbol in /lib/x86_64-linux-gnu/libc.so.6: [+]has find the symbol name: puts found puts at addr 0x7fc95a4b6690 [-]invalud name of link_map at 0x7fc95aa386f8 [-]invalud name of link_map at 0x7fc95aa38b90 >> start search symbol in /lib/x86_64-linux-gnu/libc.so.6: [+]has find the symbol name: __libc_dlopen_mode found __libc_dlopen_mode at addr 0x7fc95a58a610 >> start inject_code to call the dlopen before inject:rsp=7fff4b267ac8 rdi=7fff4b267ad0 rsi=7fff4b267ad0 rip=7fc95a5132f0 after inject:rsp=7fff4b267ac8 rdi=7fff4b267ec8 rsi=1102 rip=7fc95a58a612 after waitpid inject:rsp=7fff4b267ad0 rdi=7fc95aa37948 rsi=7fff4b267a98 rip=666 -----inject_code done------ [-]invalud name of link_map at 0x7fc95aa386f8 [-]invalud name of link_map at 0x7fc95aa38b90 >> start search symbol in /lib/x86_64-linux-gnu/libc.so.6: >> nothing found in this so... >> start search symbol in /lib64/ld-linux-x86-64.so.2: >> nothing found in this so... >> start search symbol in ./hook_so.so: [+]has find the symbol name: newputs ===> found newputs at addr 0x7fc95a2456e0 self->st_name: puts, self->r_offset = 0x601018 oldfunname: puts rel addr:0x601018 oldfunction addr:0x7fc95a4b6690 newfunction addr:0x7fc95a2456e0 hook has done! ***detach***
可以看到puts函数被hook成功
ps:我的环境是Ubuntu16.04,以上所有的源码编译操作都是在以64位进行的,32位的没有实现
如果我们希望进行的操作不仅仅只是hook一个函数,我还想让程序运行一系列的代码,该如何操作?
这里主要想介绍第二种,这种方法执行注入代码非常方便,基本上不需要考虑目标程序的运行环境
把hook_so.c进行修改
#include <stdio.h> //gcc hook_so.c -o hook_so.so -fPIC --shared int newputs(const char *str) { write(1,"hook puts! ",11); puts(str); return 0; } __attribute__((constructor)) void loadMsg() { puts("hook.so has been injected!"); puts("now let's do somesthing..."); printf("->pid:%d\n\n", getpid()); } __attribute__((destructor)) void eixtMsg() { puts("bye bye~"); }
这里使用了 __attribute__
关键词,专门用它设计两个函数分别在最开始的时候 执行和结束的时候执行
再次进行之前的hook操作:sudo ./hook3 ./hook_so.so puts newputs 26868
可以看到不仅成功hook,还多执行了两个函数,这里可以发挥想象,如果在hook3对target进行ptrace时得到的信息写入一个文本文件中,然后在hook.so中再读取这个文件,就能获取到本程序的大部分信息,如一些函数的地址,got表的地址等等,有了这些信息简直就是为所欲为之为所欲为
再骚一点的话,还可以新开一个子进程or线程执行execve,从而执行各种其他程序
https://www.cnblogs.com/LittleHann/p/3854977.html
https://jmpews.github.io/2016/12/27/pwn/linux%E8%BF%9B%E7%A8%8B%E5%8A%A8%E6%80%81so%E6%B3%A8%E5%85%A5/