目錄
1. sys_call_table:系統調用表 2. 內核符號導出表:Kernel-Symbol-Table 3. Linux 32bit、64bit環境下系統調用入口的異同 4. Linux 32bit、64bit環境下sys_call_table replace hook
1. sys_call_table:系統調用表
0x1: sys_call_table簡介
sys_call_table在Linux內核中是在Linux內核中的一段連續內存的數組,數組中的每個元素保存着對應的系統調用處理函數的內存地址
1. 32bit: cat /boot/System.map-3.13.0-32-generic | grep sys_call_table c1663140 R sys_call_table 2. 64bit cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep sys_call_table ffffffff81600460 R sys_call_table 3. 64bit 兼容 32bit cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep ia32sys_call_table ffffffff8160a1f8 r ia32_sys_call_table
sys_call_table由Linux內核在初始化的時候填充,從內核源代碼中可以得到它的聲明和定義
1. 32bit: /source/arch/x86/kernel/syscall_32.c __visible const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* * Smells like a compiler bug -- it doesn't work * when the & below is removed. */ [0 ... __NR_syscall_max] = &sys_ni_syscall, #include <asm/syscalls_32.h> }; 2. 64bit /source/arch/x86/kernel/syscall_64.c asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* * Smells like a compiler bug -- it doesn't work * when the & below is removed. */ [0 ... __NR_syscall_max] = &sys_ni_syscall, #include <asm/syscalls_64.h> }; 3. 64bit 兼容 32bit /source/arch/x86/ia32/syscall_ia32.c const sys_call_ptr_t ia32_sys_call_table[__NR_ia32_syscall_max+1] = { /* * Smells like a compiler bug -- it doesn't work * when the & below is removed. */ [0 ... __NR_ia32_syscall_max] = &compat_ni_syscall, #include <asm/syscalls_32.h> };
Relevant Link:
深入linux內核架構(中文版).pdf http://www.cnblogs.com/LittleHann/p/3850653.html http://www.cnblogs.com/LittleHann/p/3854977.html
0x2: Linux下獲取sys_call_table內核地址的方式
http://www.cnblogs.com/LittleHann/p/3854977.html //搜索:0x3: 獲取sys_call_table的常用方法
2. 內核符號導出表:Kernel-Symbol-Table
驅動LKM也是存在於內核空間的,函數中的函數、變量都會有對應的符號,這部分符號也可以稱作內核符號,這些內核符號有兩種狀態
1. 導出(EXPORT_SYMBOL) 在內核代碼中明確聲明EXPORT_SYMBOL(xxxx_FUNCTION)之后,這個函數就可以作為內核對外的接口,供外部使用了。對於這部分導出的內核符號表我們稱之為"內核導出符號表" 2. 不導出 對於不導出的內核函數,只能在內核中使用
insmod的時候並不是所有的函數都得到內核符號表去尋找對應的符號,每一個驅動在自已的分配的空間里也會存在一份符號表,里面有關於這個驅動里使用到的變量以及函數的一些符號,首先驅動會在這里面找,如果發現找不到就會去公共內核符號表中搜索,搜索到了則該模塊加載成功,搜索不到則該模塊加載失敗
可以通過nm -l xx.ko來查看一個模塊里的符號情況
nm -l find_sys_call_table.ko /* 00000000 T cleanup_module 00000030 T find_sys_call_table 000000b6 T init_module U loops_per_jiffy U mcount 00000ef8 r __module_depends U printk find_sys_call_table.c:0 000000b6 t syscall_init 000001c1 t syscall_release 00000000 B syscall_table U sys_close 00007980 D __this_module 00000ec9 r __UNIQUE_ID_license0 00000ed5 r __UNIQUE_ID_srcversion1 00000f01 r __UNIQUE_ID_vermagic0 00003c20 r ____versions */
在Linux內核中,大部分的函數、變量都是導出的,我們可以通過對內核符號表的遍歷搜索得到我們想要獲得的內核函數、內核變量的地址
3. Linux 32bit、64bit下系統調用入口的異同
以sys_execve、sys_socketcall、sys_init_module這三個系統調用作為研究對象,為了更好的說明問題,我們打印一下Linux 64bit的sys_call_table的函數指針地址
find_sys_call_table.c
#include <linux/module.h>
#include <linux/init.h> #include <linux/types.h> #include <asm/uaccess.h> #include <asm/cacheflush.h> #include <linux/syscalls.h> #include <linux/delay.h> // loops_per_jiffy /* Just so we do not taint the kernel */ MODULE_LICENSE("GPL"); void **syscall_table; unsigned long **find_sys_call_table(void); unsigned long **find_sys_call_table() { unsigned long ptr; unsigned long *p; for (ptr = (unsigned long)sys_close; ptr < (unsigned long)&loops_per_jiffy; ptr += sizeof(void *)) { p = (unsigned long *)ptr; if (p[__NR_close] == (unsigned long)sys_close) { printk(KERN_DEBUG "Found the sys_call_table!!!\n"); return (unsigned long **)p; } } return NULL; } static int __init syscall_init(void) { int ret; unsigned long addr; unsigned long cr0; int num = 0; syscall_table = (void **)find_sys_call_table(); if (!syscall_table) { printk(KERN_DEBUG "Cannot find the system call address\n"); return -1; } do { printk("%d: the address is: %16x\n", num, syscall_table[num]); num++; } while (num < 400); return 0; } static void __exit syscall_release(void) { } module_init(syscall_init); module_exit(syscall_release);
Makefile
obj-m := find_sys_call_table.o
PWD := $(shell pwd) all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: rm -rf *.o *~ core .*.cmd *.mod.c ./tmp_version *.ko modules.order Module.symvers clean_omit: rm -rf *.o *~ core .*.cmd *.mod.c ./tmp_version modules.order Module.symvers
0x1: Linux 32bit
1. sys_execve
對於Linux 32bit操作系統來說,sys_execve的系統調用號、以及在它在sys_call_table中的索引位置
\linux-3.15.5\arch\sh\include\uapi\asm\unistd_32.h #define __NR_execve 11 //系統調用處理函數在內核內存中的地址可以通過以下方式得到 cat /boot/System.map-3.13.0-32-generic | grep sys_execve c117fc00 T sys_execve //和sys_call_table中的函數地址進行逐行對比 cat info | grep c117fc00 [16595.247404] 11: the address is: c117fc00
在正常情況下(當前linux沒有被rootkit、sys_call_table沒有被hooked),sys_call_table(系統調用表)中的函數地址和內核導出符號表中的函數地址應該是相同的,即
sys_call_table[__NR_sys_execve] = cat /boot/System.map-3.13.0-32-generic | grep sys_execve
系統調用函數的入口點跟蹤如下
linux-3.15.5\fs\exec.c
SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp) { return do_execve(getname(filename), argv, envp); }
這是個宏定義,等價於對sys_execve的聲明
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) { struct user_arg_ptr argv = { .ptr.native = __argv }; struct user_arg_ptr envp = { .ptr.native = __envp }; return do_execve_common(filename, argv, envp); }
2. sys_socketcall
\linux-3.15.5\arch\sh\include\uapi\asm\unistd_32.h #define __NR_socketcall 102 //系統調用處理函數在內核內存中的地址可以通過以下方式得到 cat /boot/System.map-3.13.0-32-generic | grep sys_socketcall c1560100 T sys_socketcall //和sys_call_table中的函數地址進行逐行對比 cat info | grep c1560100 [16595.247515] 102: the address is: c1560100
\linux-3.15.5\net\socket.c
/* 進行socket調用派發 */ SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args) { unsigned long a[AUDITSC_ARGS]; unsigned long a0, a1; int err; unsigned int len; if (call < 1 || call > SYS_SENDMMSG) return -EINVAL; len = nargs[call]; if (len > sizeof(a)) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, len)) return -EFAULT; err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); if (err) return err; a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: err = sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = sys_listen(a0, a1); break; case SYS_ACCEPT: err = sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: err = sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_GETPEERNAME: err = sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_SOCKETPAIR: err = sys_socketpair(a0, a1, a[2], (int __user *)a[3]); break; case SYS_SEND: err = sys_send(a0, (void __user *)a1, a[2], a[3]); break; case SYS_SENDTO: err = sys_sendto(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], a[5]); break; case SYS_RECV: err = sys_recv(a0, (void __user *)a1, a[2], a[3]); break; case SYS_RECVFROM: err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], (int __user *)a[5]); break; case SYS_SHUTDOWN: err = sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); break; case SYS_GETSOCKOPT: err = sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); break; case SYS_SENDMSG: err = sys_sendmsg(a0, (struct msghdr __user *)a1, a[2]); break; case SYS_SENDMMSG: err = sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3]); break; case SYS_RECVMSG: err = sys_recvmsg(a0, (struct msghdr __user *)a1, a[2]); break; case SYS_RECVMMSG: err = sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], (struct timespec __user *)a[4]); break; case SYS_ACCEPT4: err = sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], a[3]); break; default: err = -EINVAL; break; } return err; }
3. sys_init_module
\linux-3.15.5\arch\sh\include\uapi\asm\unistd_32.h #define __NR_init_module 128 //系統調用處理函數在內核內存中的地址可以通過以下方式得到 cat /boot/System.map-3.13.0-32-generic | grep sys_init_module c10c4820 T sys_init_module //和sys_call_table中的函數地址進行逐行對比 cat info | grep c10c4820 [16595.247540] 128: the address is: c10c4820
\linux-3.15.5\kernel\module.c
SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long, len, const char __user *, uargs) { int err; struct load_info info = { }; err = may_init_module(); if (err) return err; pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n", umod, len, uargs); err = copy_module_from_user(umod, len, &info); if (err) return err; return load_module(&info, uargs, 0); }
0x2: Linux 64bit
在Linux 64bit下,系統調用的入口點和32bit下有一點區別
1. sys_execve
/source/arch/x86/syscalls/syscall_64.tbl # # 64-bit system call numbers and entry vectors # # The format is: # <number> <abi> <name> <entry point> # # The abi is "common", "64" or "x32" for this file. 59 64 execve stub_execve //在內核符號導出表中得到的內核內存地址 cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep stub_execve ffffffff8100b4e0 T stub_execve cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep sys_execve ffffffff810095b0 T sys_execve //在sys_call_table中搜索系統調用函數地址 cat info | grep 8100b4e0: [10298.905575] 59: the address is: 8100b4e0 cat info | grep 810095b0: no result //對於64bit的Linux系統來說,在系統調用外層使用了stub(wrapper functions) \linux-3.15.5\arch\x86\kernel\entry_64.S ENTRY(stub_execve) CFI_STARTPROC addq $8, %rsp PARTIAL_FRAME 0 SAVE_REST FIXUP_TOP_OF_STACK %r11 call sys_execve movq %rax,RAX(%rsp) RESTORE_REST jmp int_ret_from_sys_call CFI_ENDPROC END(stub_execve)
在Linux 64bit下,stub_execve就是sys_execve的wrapper函數
/source/arch/x86/um/sys_call_table_64.c
#define stub_execve sys_execve
這也意味着在Linux 64bit下,sys_execeve在sys_call_table里不存在了,而是用stub_execve取代了,我們的hook對象也就是stub_execve
2. sys_socketcall
sys_socketcall 只適用於x86-32平台下適用,在非x86-32平台下,sys_socketcall是不存在的,Linux 64bit將sys_socketcall的"系統調用派發機制"拆分成了分別獨立的系統調用,例如sys_socket、sys_bind、 sys_connect
//找到Linux 64bit對應的unistd_64.h文件 find /usr/src/kernels/`uname -r` -name unistd_64.h vim /usr/src/kernels/2.6.32-220.23.2.ali878.el6.x86_64/arch/x86/include/asm/unistd_64.h 1. sys_socket cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep sys_socket ffffffff8140a210 T sys_socket cat info | grep 8140a210 [924227.139549] 41: the address is: 8140a210 cat /usr/src/kernels/2.6.32-220.23.2.ali878.el6.x86_64/arch/x86/include/asm/unistd_64.h | grep __NR_socket #define __NR_socket 41 __SYSCALL(__NR_socket, sys_socket) 2. sys_connect cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep sys_connect ffffffff8140bfb0 T sys_connect [924227.139550] 42: the address is: 8140bfb0 cat /usr/src/kernels/2.6.32-220.23.2.ali878.el6.x86_64/arch/x86/include/asm/unistd_64.h | grep __NR_connect #define __NR_connect 42 __SYSCALL(__NR_connect, sys_connect) 3. sys_bind cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep sys_bind ffffffff8140c0a0 T sys_bind [924227.139558] 49: the address is: 8140c0a0 cat /usr/src/kernels/2.6.32-220.23.2.ali878.el6.x86_64/arch/x86/include/asm/unistd_64.h | grep __NR_bind #define __NR_bind 49 __SYSCALL(__NR_bind, sys_bind)
在Linux 64bit環境下,可以直接針對sys_connect(即TCP_CONNECT動作進行監控)
3. sys_init_module
//找到Linux 64bit對應的unistd_64.h文件 find /usr/src/kernels/`uname -r` -name unistd_64.h vim /usr/src/kernels/2.6.32-220.23.2.ali878.el6.x86_64/arch/x86/include/asm/unistd_64.h cat /boot/System.map-2.6.32-220.23.2.ali878.el6.x86_64 | grep sys_init_module ffffffff810afe50 T sys_init_module cat info | grep 810afe50 [924227.139712] 175: the address is: 810afe50 cat /usr/src/kernels/2.6.32-220.23.2.ali878.el6.x86_64/arch/x86/include/asm/unistd_64.h | grep __NR_init_module #define __NR_init_module 175 __SYSCALL(__NR_init_module, sys_init_module)
Relevant Link:
http://stackoverflow.com/questions/9940391/looking-for-a-detailed-document-on-linux-system-calls
4. Linux 32bit、64bit環境下sys_call_table replace hook:建立通用兼容性的sys_call_table Replace Hook Engine
0x1: Linux 32/64bit Hook Point
1. 32bit 1) sys_execve 2) sys_socketcall 3) sys_init_module 2. 64bit 1) stub_execve 2) sys_connect 3) sys_init_module
0x2: Linux 32/64bit __NR_XX定義的異同
在進行sys_call_table replace hook的時候,我們往往需要借助系統頭文件<asm/unistd.h>的__NR_XX宏定義來定位尋址到我們要hook的目標系統調用處理函數,需要注意的是,這個宏定義在32/64bit的Linux上存在着一些差異,我們在編寫sys_call_table hook engine的時候需要特別注意這塊的兼容性
print_NR.c
#include <asm/unistd.h> #include <linux/module.h> // included for all kernel modules #include <linux/kernel.h> // included for KERN_INFO #include <linux/init.h> // included for __init and __exit macros static int __init hello_init(void) { //在32bit Linux下編譯(未做跨平台兼容) /* printk("__NR_execve: %d\n", __NR_execve); printk("__NR_socketcall: %d\n", __NR_socketcall); printk("__NR_init_module: %d\n", __NR_init_module); */ //在64bit Linux下編譯(未做跨平台兼容) printk("__NR_execve: %d\n", __NR_execve); printk("__NR_connect: %d\n", __NR_connect); printk("__NR_init_module: %d\n", __NR_init_module); printk(KERN_INFO "Hello world!\n"); return 0; // Non-zero return means that the module couldn't be loaded. } static void __exit hello_cleanup(void) { printk(KERN_INFO "Cleaning up module.\n"); } module_init(hello_init); module_exit(hello_cleanup);
在Linux 32bit、Linux 64bit環境下分別作編譯,結果如下
1. 32 bit [22726.762562] __NR_execve: 11: sys_execve [22726.762570] __NR_socketcall: 102: sys_socketcall [22726.762573] __NR_init_module: 128: sys_init_module 2. 64 bit [ 2295.036784] __NR_execve: 59: stub_execve [ 2295.036785] __NR_connect: 42: sys_connect [ 2295.036786] __NR_init_module: 175: sys_init_module
從結果上來看,_NR_XX宏定義的打印結果和我們的調研結果是一致的,所不同的是在32、64環境下系統調用對應的__NR_XX宏定義名字不同了
0x3: Linux 64bit 環境下 sys_execve hook特殊處理
為了充分利用Linux 64bit下的寄存器資源、以及提高Linux 64bit針對進程執行系統調用的執行效率,Linux 64bit內核針對sys_execve進行了特殊處理,使用了"wrapper function":stub_execve,對sys_execve進行了包裝
\linux-3.15.5\arch\x86\kernel\entry_64.S ENTRY(stub_execve) /* 匯編代碼對rsp,堆棧參數尋址寄存器進行了修正 說明內核在進入stub_execve之前,rsp本身就是"不准確"的,需要進行修正,而rsp不准確也意味着棧上參數尋址是不准確的,這直接帶來一個問題就是我們進行replace hook的fake_xxx_function不能簡單的直接使用函數聲明中傳遞進來的參數 */ CFI_STARTPROC addq $8, %rsp PARTIAL_FRAME 0 SAVE_REST FIXUP_TOP_OF_STACK %r11 //修復棧、寄存器狀態之后,進入真正的sys_execve系統調用 call sys_execve movq %rax,RAX(%rsp) RESTORE_REST jmp int_ret_from_sys_call CFI_ENDPROC END(stub_execve)
在Linux 64bit環境下,我們對sys_call_table[__NR_execve]進行replace hook之后,我們在fake_sys_execve中得到的參數不是准確的,因為這個時候rsp是不准確的,解決這個問題的思路有兩個
1. 在fake_sys_execve中使用"inline asm 內聯匯編"的方式,模仿stub_execve在進入sys_execve之前所做的事情 /* 這種方式需要在內核代碼中插入大量的匯編 並且承當更大的兼容性風險 */ 2. 直接對sys_execve進行"inline hook" 1) 通過kprobe監控sys_execve的系統調用,使用爭奪自旋鎖的方式強制當前所有CPU等待"inline hook"的地址替換動作完成 2) 通過kprobe獲取到sys_execve在內核中的函數地址 3) 直接拷貝sys_execve入口點開始的9字節的字節碼,將這9字節字節碼替換為:jmp fake_sys_execve(總共9字節) 4) 在fake_sys_execve中,將竊取的原始的sys_execve入口點的匯編字節碼重新執行一次 5) 由於fake_sys_execve是inline hook到sys_execve中的,所以這個時候fake_sys_execve可以直接使用sys_execve的棧上的參數,這個時候,我們就可以正常執行fake_sys_execve的主體代碼 5) 當fake_sys_execve執行完畢之后,直接跳回到之前sys_execve開頭那段被替換的地址之后的第一條指令,繼續執行即可 /* 使用inline hook需要重點關注的問題就是: 1) "inline hook"的關鍵動作是一段內核內存地址的替換過程 2) 大多數情況下這個地址替換過程需要消耗超過1條的CPU指令去完成 3) 在多線程/多CPU情況下,CPU在執行多條指令的期間可能會被打斷,從而破壞"inline hook"這個過程的一致性,可能導致地址替換失敗 4) 需要使用自旋鎖的機制強制保證這個地址替換(hook)的過程的"原子性" */
0x4: 模塊卸載時舊的函數調用的返回導致的"bad memory address access"問題
模塊在卸載的時候有可能有之前舊的用戶態習系統調用請求hung在當前我們的hook模塊中,不能直接粗暴的rmmod,否則會引起原始函數返回后ret到一塊"not valid memory"中,解決這個問題的方案是采用內聯匯編,構造直接返回的棧狀態,下圖解釋
因為Linux GCC不支持nick裸函數,所以我們不能直接push origin_func、ret的方式構造特殊的棧狀態,跳轉到原始系統調用函數中
if(sizeof(void*)==4) { asm volatile ("movl %1,%%eax \n movl %%eax,%0":"=r"(old_func):"r"(old_func)); asm volatile (".intel_syntax");//這里換用intel語法,gas語法簡直不是給人用的 asm volatile ("mov %esp,%ebp"); asm volatile ("pop %ebp");//現在ebp的原始值已經被恢復了 asm volatile (".att_syntax");//換回gas的att語法 asm volatile ("pushl %eax"); asm volatile ("ret"); }else { asm volatile ("movq %1,%%rax \n movq %%rax,%0":"=r"(old_func):"r"(old_func)); asm volatile (".intel_syntax");//這里換用intel語法,gas語法簡直不是給人用的 asm volatile ("mov %rsp,%rbp"); asm volatile ("pop %rbp"); asm volatile (".att_syntax");//換回gas的att語法 asm volatile ("pushq %rax"); asm volatile ("ret"); }
0x5: 動態獲取系統調用函數在sys_call_table中的索引號
1. 獲取"kallsyms_lookup_name"的函數地址 1) 如果linux內核直接導出了這個函數,則可以直接使用 2) 或者使用kprobe機制去獲取這個函數的地址 2.1) kprobe注冊"kallsyms_lookup_name" 2.2) 獲取kprobe注冊后的函數地址 2.3) kprobe解除注冊 2. 調用kallsyms_lookup_name()獲取我們要HOOK的函數在內核符號導出表的地址: hook_func_address 3. 使用kallsyms_lookup_name()獲取sys_call_table的內核地址 3. 將hook_func_address在sys_call_table中逐行遍歷,得到對應的偏移索引號 4. 使用動態獲取的索引號進行sys_call_table replace hook
0x6: 總結
/* sys_call_table replace hook: 要解決的問題是讓原始系統調用直接返回用戶態系統調用的入口點之后 inline hook: 要解決的問題是保證地址替換的過程的原子性 */ 1. 32 bit sys_execve: sys_call_table replace hook sys_socketcall: sys_call_table replace hook sys_init_module: sys_call_table replace hook 2. 64 bit stub_execve: inline hook sys_connect: sys_call_table replace hook sys_init_module: sys_call_table replace hook
Copyright (c) 2014 LittleHann All rights reserved