Linux下Hook方式汇总
-
#### 导语
-
Linux下的Hook方式,从ring3到ring0,姿势很多,但其实是互通的,这里总结自己实现的几个。
/****** Ring3 ******/ LD_PRELOAD劫持.so ptrace API调试技术Hook PLT劫持 /******* Ring 0 *******/ 针对系统调用的hook --首先获得sys_call_table 利用sys函数的嵌套实现hook调用的子函数 修改系统调用的前几个字节为jmp之类的指令(内联
-
网上很多教程是针对Linux2.6左右的,很多方法需要自己重新摸索,记录踩坑。
- 注: 以下所有代码在Linux 5.0.3. x86_64内核调试通过。
-
-
#### LD_PRELOAD劫持.so
-
LD_PRELOAD是一个Linux下的动态链接的程序的环境变量,几乎我们用到的函数实现来自于glibc,.so文件是glibc编译得到的库,类似于Win下的DLL。而LD_PRELOAD变量优先于相关配置指定链接到哪个.so文件。
-
一旦我们可以控制该变量,也就可以决定程序调用函数时会做什么。
-
实例展示
-
目标文件target.c
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { if( strcmp(argv[1], "password") ) { printf("Incorrect password\n"); } else { printf("Correct password\n"); } return 0; } //gcc target.c -o target
-
伪造的.so文件preload.c
#include <stdio.h> #include <string.h> #include <dlfcn.h> typedef int(*Strcmp)(const char*, const char*); int strcmp(const char* s1, const char* s2) { static void* handle = NULL; static Strcmp org_strcmp = NULL; if(!handle) { //解析得到真实的strcmp函数 handle = dlopen("libc.so.6", RTLD_LAZY); org_strcmp = (Strcmp)dlsym(handle, "strcmp"); } //做我们想做的 printf("Hacked by way of ld_preload\n\n\n"); //完成真实地功能 return org_strcmp(s1, s2); } //gcc -fPIC preload.c -shared -o preload.so -ldl
-
-
这种方式比较简单,前提是程序不是静态链接的。(不会再解析.so,程序已经包含了库函数的实现)。
-
防护方案
- 关闭LD_PRELOAD
-
-
#### ptrace API调试技术Hook
-
ptrace是很多Linux平台下调试器实现的基础,包括syscall跟踪程序strace。
-
ptrace可以实现调试程序、跟踪;但是一个进程只能被一个进程跟踪。所以无法在gdb或者其他程序调试的时候去ptrace一个程序,同样也无法在ptrace一个进程的时候,再去gdb调试。后者经常作为一个简单的反调试手段。
-
而且Linux下的攻防中,ptrace也由于自己的特殊性,常常是必争之地。
-
总体思路
ptrace attach目标进程 保存rip 控制跳转到mmap分配一段rwx内存 将一段机器码copy进去 控制跳转到机器码(可以以bin文件的形式) 恢复执行。
-
简单示例代码
- 首先需要知道一些函数在目标进程的地址,下面是已知pid获取libc基地址(读取/proc/pid/maps),和函数地址(dlsym)
size_t getLibcbase(int pid) { size_t libcAddr; char* buf; char* end; char* mapfile[0x18]; sprintf(mapfile, Mapfile, pid); FILE* fd = fopen(mapfile, "r"); if(!fd) { printf("open maps error!"); exit(1); } //search the libc-..... buf = (char*) malloc(0x100); do{ fgets(buf, 0x100, fd); } while(!strstr(buf, "libc-")); end = strchr(buf, '-'); libcAddr = strtol(buf, &end, 16); printf("The process %d's libcbase is: 0x%lx\n", pid, libcAddr); fclose(fd); return libcAddr; } size_t getFuncAddr(int pid, char* funcName) { size_t funcAddr; char* buf; char* end; char* mapfile[0x18]; sprintf(mapfile, Mapfile, pid); //get function offset from self process, the shared libc.so funcAddr = (size_t)dlsym(0, funcName); funcAddr -= getLibcbase(getpid()); funcAddr += libc_addr; printf("function %s address is: 0x%lx\n", funcName, funcAddr); return funcAddr; }
-
main代码
-
为了得到存放shellcode的地址,我们需要先执行mmap,而执行mmap也需要一段可执行地址。这里其实我们可以直接使用libc_base。在libc_base处写入下面的opcode,其中int 0x3是为了发出信号,让我们知道该opcode执行完成。
call rax int 0x3 ;"\xff\xd0\xcd\x03"
-
实现mmap调用
-
备份数据,写入opcode,设置mmap参数
//save a bak of regs ptrace(PTRACE_GETREGS, traced, 0, ®s_bak); memcpy(®s, ®s_bak, sizeof(struct user_regs_struct)); //use libc_base to write our short hook code buf.val = ptrace(PTRACE_PEEKTEXT, traced, libc_addr, 0); hook_bak.val = buf.val; memcpy(buf.chars, Call, 4); ptrace(PTRACE_POKETEXT, traced, libc_addr, buf.val); fd = open(argv[2], O_RDONLY); fstat(fd, &sb); if(fd < 0) { printf("open shellcode error!\n"); exit(1); } shellcode = malloc(sb.st_size + 1); read(fd, shellcode, sb.st_size); regs.rax = mmap_addr; //prepare for the mmap args regs.rdi = 0; regs.rsi = sb.st_size; regs.rdx = 0x7; regs.rcx = MAP_PRIVATE | MAP_ANONYMOUS; regs.r8 = -1; regs.r9 = 0; regs.rip = libc_addr; //jmp to call rax ptrace(PTRACE_SETREGS, traced, 0, ®s); ptrace(PTRACE_CONT, traced, 0, 0); //wait mmap is executed waitpid(traced, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP){ printf("SIGTRAP receivedn\n"); }
-
-
这里有几个坑
- mmap的fd参数,必须是open打开的返回值,而不能是fopen这种。
- x64传参,顺序是rdi、rsi、rdx、rcx、r8、r9;有些地方可能会说第四个参数是r10,发现有误导。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 由于我们open是在注入进程得到的fd,而mmap是在被hook进程执行的,所以这个fd是不可以被当作参数的。我们必须使用flag MAP_PRIVATE | MAP_ANONYMOUS
-
往mmap返回的地址里写入shellcode,并执行。
ptrace(PTRACE_GETREGS, traced, 0, ®s); tmp_addr = regs.rax;/ printf("We get address from mmap: 0x%lx\n", tmp_addr); copy_code(tmp_addr, shellcode, sb.st_size); tmp_val = ptrace(PTRACE_PEEKTEXT, traced, tmp_addr, 0); printf("the mapped val: 0x%lx\n", tmp_val); //jmp to shellcode from file regs.rip = tmp_addr; ptrace(PTRACE_SETREGS, traced, 0, ®s); ptrace(PTRACE_CONT, traced, 0, 0); //wait shellcode is executed waitpid(traced, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP){ printf("SIGTRAP receivedn\n"); } ptrace(PTRACE_SETREGS, traced, 0, ®s_bak); ptrace(PTRACE_CONT, traced, 0, 0); close(fd);
-
-
其中copy_code的实现通过POKETEXT,每次可以向目标进程任意地址写入long大小的值
void copy_code(size_t addr, char* shellcode, int len) { int i=0; long word; for(i=0; i<len; i+=sizeof(long)){ memcpy(&word, shellcode+i, sizeof(long)); if(ptrace(PTRACE_POKETEXT, traced, addr+i, word) == -1){ printerror(); exit(1); } } }
-
我的shellcode,就是write(1, 'cba', 8),目标进程就是当前终端/bin/bash看下效果。
-
-
#### PLT重定向劫持Hook
-
这个主要是利用ELF文件的,GOT和PLT的方式解决地址无关的链接.so文件的机制。
-
在第一次调用前,Got里是PLT的地址;一般在调用之后Got里会写入库函数的真实地址。
-
PLT在text段,一般不可写;(所以迷,为啥有这一技术)
- 是因为ptrace可以无视'rwx',这也是为什么gdb可以修改text,下断点。
- 所以这个来说,算是plt和ptrace结合的一种方式。
-
一个简单的在PLT处下断,执行完操作后恢复的样例。这里主要解决的是代码重定位的问题,我们从/proc/pid/maps下读出Codebase即可。
#include <sys/reg.h> #include <sys/ptrace.h> #include <sys/user.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #define Mapfile "/proc/%d/maps" //用于获取plt内容 union pltval{ size_t val; unsigned char chars[sizeof(size_t)]; }; void usage(char** argv){ printf("USAGE: \n --- %s pid plt_offset \n", argv[0]); } void printerror(){ printf("Status: %s\n", strerror(errno)); } void mod_handle(pid_t tracee, void* addr1, void* addr2) { union pltval buf; buf.val = ptrace(PTRACE_PEEKDATA, tracee, addr1, 0); printf("mod_handle: "); printerror(); memcpy(buf.chars, "hooked", 6); buf.chars[6] = 0; ptrace(PTRACE_POKEDATA, tracee, addr1, buf.val); printf("hook: "); printerror(); buf.val = ptrace(PTRACE_PEEKDATA, tracee, addr2, 0); printf("mod_handle: "); printerror(); memcpy(buf.chars, "/hooked", 7); buf.chars[7] = 0; ptrace(PTRACE_POKEDATA, tracee, addr2, buf.val); printf("/hooked: "); printerror(); } size_t getCodebase(pid_t pid) { size_t addr; char buf[2 * sizeof(size_t)]; char* end; char* mapfile[0x18]; sprintf(mapfile, Mapfile, pid); int fd = open(mapfile, O_RDONLY); if(fd == -1) { printf("open maps error!"); exit(1); } read(fd, buf, 2 * sizeof(size_t)); end = strchr(buf, '-'); addr = strtol(buf, &end, 16); printf("The codebase is: 0x%lx\n", addr); close(fd); return addr; } int main(int argc, char* argv[]){ pid_t tracee; union pltval plt; struct user_regs_struct regs; siginfo_t si; int status; size_t plt_offset, plt_addr, bak; if(argc < 2){ usage(argv); exit(1); } tracee = atoi(argv[1]); plt_offset = atoi(argv[2]); //获取codebase plt_addr = plt_offset + getCodebase(tracee); printf("plt_addr ==> %lx\n", plt_addr); //attach the process ptrace(PTRACE_ATTACH, tracee, 0, 0); printf("Attach: "); printerror(); wait(&status); //获取目标的plt值,保存,修改,写入, 继续运行 plt.val = ptrace(PTRACE_PEEKDATA, tracee, plt_addr, 0); bak = plt.val; plt.chars[0] = 0xcc; //breakpoint ptrace(PTRACE_POKEDATA, tracee, plt_addr, plt.val); ptrace(PTRACE_CONT, tracee, 0, 0); //监视有没有触发断点 while(1){ printf("Wait....\n"); wait(&status); printf("Done!\n"); if(WIFEXITED(status)) break; //获取regs和sig信息,判断是否到达plt ptrace(PTRACE_GETSIGINFO, tracee, 0, &si); ptrace(PTRACE_GETREGS, tracee, 0, ®s); if((si.si_signo != SIGTRAP) || (regs.rip != (size_t)plt_addr + 1)){ ptrace(PTRACE_GETREGS, tracee, 0, ®s); ptrace(PTRACE_CONT, tracee, 0, 0); continue; } //hook & modify mod_handle(tracee, (void*)argv[0], (void*)argv[1]); //修改回原值 plt.val = bak; ptrace(PTRACE_POKEDATA, tracee, plt_addr, plt.val); //返回0xcc前 regs.rip -= 1; ptrace(PTRACE_SETREGS, tracee, 0, ®s); ptrace(PTRACE_SINGLESTEP, tracee, 0, 0); wait(0); ptrace(PTRACE_GETREGS, tracee, 0, ®s); plt.chars[0] = 0xcc; ptrace(PTRACE_POKEDATA, tracee, plt_addr, plt.val); ptrace(PTRACE_CONT, tracee, 0, 0); } return 0; }
-
-
#### Ring0级别的Hook
-
##### 前置知识
-
linux内核的编译
- 最好选择一个和自己虚拟机内核版本一致的源码,网上也很多教程。
- 再编译一个busybox的文件系统,为了方便添加文件。
-
模块编译
-
makefile的基本格式(用于本机加载的模块)
obj-m += inter.o CURRENT_PATH := $(shell pwd) LINUX_KERNEL := $(shell uname -r) LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL) all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
-
交叉编译(本机和编译的内核不同)
obj-m += mod1.o CURRENT_PATH := $(shell pwd) LINUX_KERNEL := $(shell uname -r) LINUX_KERNEL_PATH := /home/tree/kernel/linux-5.0.3 all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
-
-
-
sys_call_table
-
都知道linux通过int 0x80或者syscall都可以进入kernel完成系统调用,而我们需要把对应的系统调用号传给rax。两者最终还是用了system_call。
-
int 0x80工作原理
1、系统维护一个叫做“向量中断表的”,每个int xx都会去对应的向量表的xx处 2、0x80对应系统调用的服务例程,记录了syscall的地址。 3、而eax的值,对应具体的系统调用号。
-
syscall的具体实现,可以看到这里有sys_call_table符号。
.globl system_call, buserr, trap, resume .globl sys_call_table ................ ................ ENTRY(system_call) SAVE_ALL_SYS //保存 GET_CURRENT(%d1) movel %d1,%a1 | save top of frame movel %sp,%curptr@(TASK_THREAD+THREAD_ESP0) | syscall trace? //有没有被ptrace跟踪 tstb %a1@(TINFO_FLAGS+2) jmi do_trace_entry cmpl #NR_syscalls,%d0 jcc badsys syscall: //真实的执行系统调用 jbsr @(sys_call_table,%d0:l:4)@(0) movel %d0,%sp@(PT_OFF_D0) | save the return value ret_from_syscall: |oriw #0x0700,%sr movel %curptr@(TASK_STACK),%a1 movew %a1@(TINFO_FLAGS+2),%d0 jne syscall_exit_work 1: RESTORE_ALL
-
-
获得sys_call_table地址的方式
-
由于syscall实现处有sys_call_table的符号,我们可以从这里拿到地址。
1. 获取中断描述符表(IDT)的地址(使用C ASM汇编) 2. 从中查找0x80中断(系统调用中断)的服务例程(8*0x80偏移) 3. 搜索该例程的内存空间 4. 从其中获取sys_call_table(保存所有系统调用例程的入口地址)的地址
-
使用kallsyms_lookup_name读取。该函数本身也是一个符号,如果没有导出就不能使用。
sys_call_table_addr = kallsyms_lookup_name("sys_call_table")
-
读取/proc/kallsyms文件。我的理解就是和/proc/pid/maps差不多特殊的一个文件,由内核动态生成,需要root权限,普通用户读到的全是0(但是加载模块也是需要root权限的,所以不是问题)
sudo cat /proc/kallsyms | grep sys_call_table
-
修改内核,添加EXPORT_SYMBOL(sys_call_table)或EXPORT_SYMBOL_GPL(sys_call_table)。
这种方法适用于可以修改内核的情形。在可以修改内核的情况下,这是最简单的方式。
-
-
实战——hook系统调用 mkdir,我这里使用kallsyms_lookup_name
//This kernel module locates the sys_call_table by kallsyms_lookup_name #include<linux/init.h> #include<linux/module.h> #include<linux/moduleparam.h> #include<linux/unistd.h> #include<linux/sched.h> #include<linux/syscalls.h> #include<linux/string.h> #include<linux/fs.h> #include<linux/fdtable.h> #include<linux/uaccess.h> #include <linux/kallsyms.h> #include<linux/rtc.h> #include<linux/vmalloc.h> #include <linux/slab.h> //module macros MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("hook sys_mkdir"); //module constructor/destructor typedef unsigned long (*sys_call_ptr_t)(void); sys_call_ptr_t *_sys_call_table = NULL; typedef asmlinkage long (*old_mkdir_t)(const char __user *pathname, umode_t mode); old_mkdir_t old_mkdir = NULL; // hooked mkdir function asmlinkage long hooked_mkdir(const char __user *pathname, umode_t mode) { printk("hooked sys_mkdir(), mkdir name: "); printk(pathname); old_mkdir(pathname, mode); } // memory protection shinanigans unsigned int level; pte_t *pte; //obtain sys_call_table static int get_sys_call_table(void){ unsigned long tmp_sys_call_table = 0; int ans = 0; tmp_sys_call_table = kallsyms_lookup_name("sys_call_table"); if(tmp_sys_call_table != 0) { ans = 1; _sys_call_table = tmp_sys_call_table; printk("[+] find sys_call_table: 0x%lx\n", tmp_sys_call_table); } return ans; } // initialize the module static int hooked_init(void) { printk("+ Loading hook_mkdir module\n"); if(!get_sys_call_table()){ return 0; } // now we can hook syscalls ...such as uname // first, save the old gate (fptr) old_mkdir = (old_mkdir_t) _sys_call_table[__NR_mkdir]; // unprotect sys_call_table memory page pte = lookup_address((unsigned long) _sys_call_table, &level); // change PTE to allow writing set_pte_atomic(pte, pte_mkwrite(*pte)); printk("+ unprotected kernel memory page containing sys_call_table\n"); // now overwrite the __NR_uname entry with address to our uname _sys_call_table[__NR_mkdir] = (sys_call_ptr_t) hooked_mkdir; printk("+ sys_mkdir hooked!\n"); return 0; } static void hooked_exit(void) { if(old_mkdir != NULL) { // restore sys_call_table to original state _sys_call_table[__NR_mkdir] = (sys_call_ptr_t) old_mkdir; // reprotect page set_pte_atomic(pte, pte_clear_flags(*pte, _PAGE_RW)); } printk("+ Unloading hook_mkdir module\n"); } /*entry/exit macros*/ module_init(hooked_init); module_exit(hooked_exit);
- 效果(不知道为啥pathname参数不能输出)
- 效果(不知道为啥pathname参数不能输出)
-
system_call函数内存内搜索sys_call_table,实现execve的hook
-
这里注意这种方式在x86和x64上的区别。
1、x86或者x64的兼容模式:使用int 0x80,MSR寄存器地址为0xc0000083,宏MSR_CSTAR来代表. 使用sidt获取system_call地址 2、x64的long模式:使用syscall,MSR寄存器地址为0xc0000082,宏MSR_LSTAR来代表. 使用rdmsrl指令获取system_call地址 3、x86 sys_call_table的特征码 \xff\x14\x85 4、x86_64下 sys_call_table的特征码 \xff\x14\xc5
-
网上有一大堆x86的获取sys_call_table的方法,我本来打算用下面的思路实现一个x64的。
1、在x64下,通过rdmsrl(MSR_LSTAR, xxxx)可以拿到entry_SYSCALL_64的地址。entry_SYSCALL_64的实现里会有sys_call_table的机器码。 2、所以有两种hook的思路 a、修改entry_SYSCALL_64起始的几个字节(内联hook) b、找到sys_call_table,修改对应的系统调用。
-
但是在linux内核5.x里,entry_SYSCALL_64的实现改了,不再试图用过call sys_call_table[index]的方式进行系统调用,而是引入了一个do_syscall_64的符号。具体的看下面截取的源码
ENTRY(entry_SYSCALL_64) UNWIND_HINT_EMPTY ....... ....... /* IRQs are off. */ movq %rax, %rdi movq %rsp, %rsi call do_syscall_64 /* returns with IRQs disabled */
-
于是我找到了do_syscall_64的实现,幸运的是在这里找到了对sys_call_table的直接引用。
#ifdef CONFIG_X86_64 __visible void do_syscall_64(unsigned long nr, struct pt_regs *regs) { struct thread_info *ti; enter_from_user_mode(); local_irq_enable(); ti = current_thread_info(); if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) nr = syscall_trace_enter(regs); /* * NB: Native and x32 syscalls are dispatched from the same * table. The only functional difference is the x32 bit in * regs->orig_ax, which changes the behavior of some syscalls. */ nr &= __SYSCALL_MASK; if (likely(nr < NR_syscalls)) { nr = array_index_nospec(nr, NR_syscalls); regs->ax = sys_call_table[nr](regs); //这里sys_call_table的直接引用 } syscall_return_slowpath(regs); } #endif
-
所以,显然我们就需要多一次的搜索,特征码可以在gdb里查或者看它的汇编指令。
1、在entry_SYSCALL_64里搜索do_syscall_64; 特征码'\x48\x89\xe6\xe8' <= mov rsi, rsp; call .... 2、在do_syscall_64里搜索sys_call_table; 特征码 '\x48\x8b\x04\xfd' <= mov rax []
-
通过entry_SYSCALL_64的call do_syscall_64找到call do_syscall_64地址
-
通过do_syscall_64对sys_call_table的引用找到sys_call_table
-
代码实现
static void* get_lstar_dosys_addr(void){ unsigned long lstar; // temp variables for scan unsigned int i; unsigned char *off; rdmsrl(MSR_LSTAR, lstar); // print out int 0x80 handler printk("[+] entry_SYSCALL_64 is at 0x%lx\n", lstar); // scan for known pattern(0xff14c5xx) // pattern is just before sys_call_table address for(i = 0; i <= PAGE_SIZE; i++) { off = (char*)lstar + i; if(*(off) == 0x48 && *(off+1) == 0x89 && *(off+2) == 0xe6) { return (off + 3); //call do_syscall_64 } } return NULL; } static void* get_lstar_dosys(void) { unsigned long* lstar_dosys_addr = get_lstar_dosys_addr(); if(lstar_dosys_addr != NULL) { printk("[+] call_do_syscall_64 at: 0x%lx\n", lstar_dosys_addr); unsigned int offset = *(unsigned int*)((char*)lstar_dosys_addr + 1); printk("[+] offset is: 0x%08x\n", offset); unsigned long base = 0xffffffff00000000; return (void*)(base | ((unsigned long)lstar_dosys_addr + 5 + offset)); } return NULL; } static void* get_sys_sct_addr(unsigned long* do_syscall_64_addr) { unsigned char* off; int i; for(i = 0; i <= PAGE_SIZE; i++) { off = (char*)do_syscall_64_addr + i; if(*(off) == 0x48 && *(off+1) == 0x8b && *(off+2) == 0x04 && *(off+3) == 0xfd) { return (off+4); } } return NULL; } static void* get_sys_sct(unsigned long* do_syscall_64_addr) { unsigned long* sct_addr = get_sys_sct_addr(do_syscall_64_addr); if(!sct_addr){ return NULL; } unsigned int offset = *(unsigned int*)(sct_addr); unsigned long base = 0xffffffff00000000; return (void*)(base | offset); } //hooked execve static int hook_execve_init(void){ printk("[+] Finding sys_call_table\n"); unsigned long* do_syscall_64_addr = 0; do_syscall_64_addr = get_lstar_dosys(); if(!do_syscall_64_addr){ printk("[x] Failed to find do_syscall_64_addr\n"); return 0; } printk("[+] Found do_syscall_64_addr at: 0x%lx\n", do_syscall_64_addr); _sys_call_table = get_sys_sct(do_syscall_64_addr); if(!_sys_call_table) { printk("[x] Failed to find sys_call_table\n"); return 0; } printk("[+] Found sys_call_table at: 0x%lx\n", _sys_call_table); return 0; }
- 注意处理一些细节问题(比如unsigned long 还是 unsinged int),最终可以达到想要的效果。
-
-
-
参考链接