转自:http://www.cnblogs.com/LittleHann/p/4127096.html
目录
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