linux内核模块的安全
linux可以动态的加载内核模块,在很多场合可能需要确保加载内核的安全性。如果被攻击者加载恶意内核模块,将会使得内核变得极其危险。
当然,稳妥的做法就是给内核模块进行签名,内核只加载能正确验证的签名。这是最首先想到的方法,当然,这个方法并不是很简单,你需要选用一套公钥加密方法,一般就是rsa算法了。难点是要在内核中进行验证模块的签名,这需要修改内核中的一些对应的地方。明显在load_moduler处需要添加签名验证逻辑。为此还需要在内核里实现rsa算法,rsa算法实现的难点就是大数的支持,可能需要一直类似于mpi这样的库。另外还需要在内核中解析公钥文件,以及与应用层的公钥文件交互问题。这些实现起来,工作量确实不小,但令人欣慰的是,linux在3.7版本的内核里都已经实现了这些。其实这些工作以前是以补丁的方式存在的,参见http://lwn.net/Articles/470435/,称为linux-modsign,终于在3.7的内核里修成正果了.呵呵
其实,看到3.7内核里实现了这些,并不能引起某些人的欣慰.因为自己使用的或者公司使用的内核版本没有这么高,估计都是在2.6的样子.你可能想着将3.7的机制移植到2.6上去.我想这工作量也足够大的让人心出胆怯.内核移植,搞不好造成不稳定,容易引起很大的麻烦.从http://lwn.net/Articles/470435/ 上的给出的git地址 http://git.kernel.org/cgit/linux/kernel/git/dhowells/linux-modsign.git/ 的历史中,可以看出工作量还是挺大的 . 况且内核为了提供通用性和扩展性,使用的都是一些小框架,还实现了体系结构相关以及对加密硬件模块的支持,这些都会让源代码难以阅读和理解。如果是我的话,我更宁愿冒着升级内核的风险,而不是去尝试移植代码。当然熟悉内核的人,完全可以跟踪linux-modsign去给自己的系统打补丁,或者根据原理实现一个简化的版本[参见论文 http://www.ee.ryerson.ca/~courses/coe518/LinuxJournal/elj2004-117-signedmodules.pdf].(毕竟内核主线实现的版本还是太复杂了,个人短时间内无法搞定)
鉴于内核改动的风险比较高,可能很多人更倾向于把问题尝试在用户层来解决.这个思想是好的,因为避开了风险高的途径.关键问题是,能否在用户空间来解决这个问题呢.结果是否定的.完全的依赖用户层来解决内核模块的安全问题是不现实的,或者只能解决一部分的问题而已.要想达到上述的在内核中给内核模块进行签名验证的同级别安全问题,是不可能的.在root权限下,用户空间的安全壁垒会被完全攻破.这估计也是linux内核主线使用签名验证机制的一个主要原因.确实这是一个真理:没有内核的支持,操作系统无法在用户层保障安全的问题。
但用户层空间的路走死了嘛?其实不然,我们还可以另辟蹊径。
在内核模块的支持下,可以在用户层进行内核模块的签名验证工作。
首先需要实现一个lsm模块(在高本版内核里,例如2.6.32,内核不再导出register_security类的符号,可能动态 的加载lsm模块需要重新编译内核,让内核导出相关的符号,关于lsm的使用,大家可以查阅相关资料,这里不再介绍),该模块的作用主要是保护insmod不被篡改,保护模块自身不被篡改以及相关的启动脚本不被篡改。lsm做到这些并不复杂,可以将他们放在一个特定目录下,让lsm阻止任何对该目录的写操作。
内核模块使用工具insmod或者modprobe(属于modutils提供系统程序)来加载内核模块,modprobe最后还是会调用insmod来加载。insmod最终会调用init_module系统调用来加载模块。
如果修改insmod,在调用init_module系统调用之前进行签名验证逻辑的处理,那么这个前提是insmod的安全性要得到保证,insmod不能被攻击者篡改。
在lsm模块被加载前,可能运行的是被污染的insmod程序,当然它可以加载恶意的内核模块。因此,应该在系统启动后,首先加载lsm模块,然后加载其他的模块。这只是理论上的,实际上,这些过程都很快,攻击者是无法在这些模块加载之间实施攻击的(因为网络服务什么的都还没开启)。
因此,问题被转移到保障lsm安全模块的顺利加载。当然该模块可能是使用insmod加载的。(如果是被编译到内核,随内核启动而启动的,这个问题就得到保障了)。所以,insmod是不能被替换的,以及lsm模块本身也不能被替换。
有三个关键点:
1. insmod不会被替换;
2. lsm模块必须被正确加载。
3. lsm模块不能被卸载。(卸载意味着安全机制不再起作用了)
问题1和2解决后,能达到在root权限没有被攻击者获取额前提下,实施对内核模块的保护。Root权限会卸载你的lsm模块,然后修改insmod,然后加载恶意模块....不过攻击者不能持续保留这种攻击行为,下次启动后,以前加载的恶意模块便不会被加载。
从上面的论述看来,问题1和2的解决是可以的:
假设一个前提:系统第一次启动是绝对安全的。
在这个前提下,在lsm模块里阻止任何对insmod和lsm模块本身的修改操作(利用lsm的hook点实现这些很轻松,实际情况里还需要保护一些加载lsm模块之类的敏感的脚本等),即使root权限也不行。这样就解决了这两个问题。第一次启动是安全的,也就意味着lsm模块被正确加载,这样保护机制就建立起来了。因为假设的前提是可以保证的,因此原理上是可行的。这个问题看上去像是insmod保护内核模块,而lsm模块又反过来保护insmod和lsm模块字节,相互帮助实现安全。
对于问题3,确实很棘手。如果不解决,那么系统的安全级别还是不够高。其实最容易想到的方法就是把lsm模块直接编译进内核,这样就一起解决了问题2和问题3.不过,又要改内核了,不过这里的改动相对在内核里改load_module等相关联代码而言,要简单的多。
在2.6的内核里,添加了CONFIG_MODULE_UNLOAD标记,用于禁止卸载内核模块的,参见网址http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/。
其实还可以通过编程的技巧来决绝这个问题,不注册模块的module_exit函数,将会导致模块无法被卸载。
看一下一个hello内核模块代码:
1 #include <linux/module.h> 2 #include <linux/kernel.h> 3 #include <linux/init.h> 4 5 static int __init lkp_init(void) 6 { 7 printk("<1>hello world! form the kernel sapce ...\n"); 8 return 0; 9 } 10 11 static void __exit lkp_cleanup(void) 12 { 13 printk("<a>goodbye ,world,leaving kernel sapce ...\n"); 14 } 15 16 module_init(lkp_init); 17 //module_exit(lkp_cleanup); 18 MODULE_LICENSE("GPL");
这里我故意注释掉了module_exit(lkp_cheanup),模块被编译后正常加载到内核中。
使用lsmod可以看到信息:
[root@localhost hello]# lsmod Module Size Used by hello 501 0 [permanent] 8021q 20355 0 garp 5703 1 8021q stp 1626 1 garp llc 4258 2 garp,stp …
可以看到,hello模块被标记为permanent,意味着模块被永久加载,也就是不能卸载,下面使用rmmod测试一下:
root@localhost hello]# rmmod hello.ko ERROR: Removing 'hello': Device or resource busy
居然是不能卸载该模块,呵呵。可见这方法也能达到效果,而且还很简单。
上述代码测试环境:
Cenos 6.4 [root@localhost hello]# uname -a Linux localhost.localdomain 2.6.32-358.el6.i686 #1 SMP Thu Feb 21 21:50:49 UTC 2013 i686 i686 i386 GNU/Linux
由上可见,在内核模块的基础上,配合应用程序也可以实现内核模块的安全加载。
本文简要介绍了linux内核模块安全的问题,随着内核3.7对于内核模块签名验证的支持,这个问题后面越来越变得简单啦。不过不过对于很多老的版本的内核也可以使用一些其他的方法来达到同样的功能。如果读者有什么更好的方法或者建议,渴望被指导!