徐 敏 ,熊盛武
(武汉理工大学 湖北 武汉 430070)
可加载内核模块[1](Loadable Kernle Module(LKM))作为对Linux宏内核结构的一种改进,它可以在不需要重新编译内核下被动态加载,作为内核级代码存在于内核空间,能够访问内核的重要数据结构。通常情况下模块通过合理修改系统的控制转移地址或添加门来和系统联系成一个整体,如果是恶意模块,它就可以进行多种非法操作,留下系统级后门。
在Linux中劫持系统调用的思想是修改程序执行过程中跳转的目的地址。在Linux 2.4.18内核以前,是直接对sys_call_table[2]表进行修改,sys_call_table表是保存所有系统调用函数地址的指针数组,直接用自定义的函数地址替换原来的地址,即可实现对系统调用的劫持。在Linux2.4.18内核以后,为了解决这个安全问题,sys_call_table表不能直接导出,利用中断描述符表IDT及字符设备文件/dev/kmem依然可以查找到sys_call_table表的地址,可以通过sIDT指令获取到中断向量表的地址,然后通过读取/dev/kmem来定位到该地址,然后通过调用read函数或者mmap函数的方式来读取出0×80中断描述符的内容,然后从其中可以提取出sys_call_table表的地址。2.6内核采用被称为“快速系统调用”的sysenter指令来完成从用户态到内核态的快速切换方法,这是一种不检查特权级的系统调用,在实际情况下,发生系统调用时的特权检查工作会浪费许多时间且没有必要,由于通过INT$0×80指令进入系统调用时,其门描述符的描述符特权级DPL为3,而系统调用在正常情况下也是从用户态进入内核态,故进入之前的权限当前特权级CPL也是3,权限提高前后级别不变,则CPU检查门描述符的DPL和调用者的CPL就没有必要。根据sysenter指令的实现原理,MSR_IA32_SYSENTER_EIP寄存器存放着指令到内核态的跳转地址,只需要通过改变MSR_IA32_SYSENTER_EIP寄存器来完成劫持。首先利用rdmsr指令把系统定义的sysenter指令的目的地址读到某一变量old_sysenter,然后利用wrmsr指令将自定义的函数地址写到MSR_IA32_SYSENTER_EIP寄存器,撤消截获的方法是将old_sysenter通过wrmsr指令写回到MSR_IA32_SYSENTER_EIP寄存器。
在Linux 2.6内核最新版本中,为了防止恶意模块通过IDT表和/dev/kmem设备找出sys_call_table表的地址,/dev/kmem设备也不再导出。但对于恶意的LKM,只要它躲过安全检查进入到内核态下,它仍然可以利用中断描述符表中的0x80中断以及MSR_IA32_SYSENTER_EIP寄存器中的跳转地址来搜索内存,寻找到sys_call_table表的地址来完成系统调用劫持,这是需要改进的地方,在后面会讨论这一点。
LKM注射攻击[3]通过感染系统正常的LKM,在不影响原有功能的情况下将攻击模块链接到该LKM中。LKM的目标文件格式是ELF格式,其中有两个重要节.symtab节和.strtab节,.strtab节是一个表示函数或变量名的非空字符串列表,.symtab节是个结构体数组,结构体元素是struct Elf32_Sym类型的,其中字段st_name是.strtab节的索引,字段st_value表示.strtab节中函数或变量的地址值。通过检查.strtab节里的字符串(如init_module),可以定位模块中原先init函数的地址,如果修改这个字符串,则可以在模块被加载时只执行修改后的字符串所对应的恶意函数。如果在这个恶意函数中再调用原来的函数,那么就可以使这种攻击变得更加隐秘。由于Linux 2.6内核模块子系统被完全重写,修改.strtab的方法已经失效,即使修改了.strtab节中init_module,运行的始终是原来的init_module。因为Linux 2.6内核符号的重定位不再需要.strtab节的参与,攻击的目标从.strtab节变成了.rel.gnu.linkonce.this_module重定位节中的Elf32_Rel项,被注射攻击的模块会通过遍历.rel.gnu.linkonce.this_module节中Elf32_Rel项, 如 orig_init_idx== ELF32_R_SYM (rel->r_info), 就 将rel_r_info的symbol部分替换成被被注射模块函数的索引号。
1.3 LKM隐藏技术
由于在加载新的LKM后会在/proc/modules文件中留下记录,只需利用/sbin/lsmod命令就可以查看模块信息。如果能够使该攻击模块在/proc/modules中不被显示,就能使系统管理员无从察觉该攻击模块的痕迹,从而实现了隐藏。为了完成该攻击模块的隐藏,有大致3类方法。
1)将该攻击模块从module_list链表中摘下而不真正地将它从内存中卸载。由于该模块加载时通过宏EXPORT_SYMBOL输出的变量和函数已经被其他模块和内核所引用,故从链表中摘除该攻击模块并不影响其功能。
2)截获并替换查询模块信息的系统调用sys_query_module()函数,在替换后的调用中首先执行原来的sys_query_module()函数,把所有模块信息读到内存某个地方,然后对这些信息进行查询并过滤掉需要隐藏的模块信息。
3)不是把自己从module_list链表中删除,而是把某个正常的模块从该链表删除,然后将该攻击模块改名成刚才被删除的正常模块。但这样会导致系统在使用这个被删除的正常模块时出现问题,不利于自己的隐藏。
在Linux 2.6内核中,绝大部分内核数据结构都改用双向链表组织,模块也不例外。因为双向链表从链表中间删除数据项比单向链表简单,而且还可以从后往前遍历链表。
LSM[4]框架为了实现一个通用的访问控制框架,为安全模块的开发提供一致性接口。它是在内核对象的数据结构中添加一个透明安全字段,并在内核源代码的适当位置放置钩子函数,LSM框架只提供这些字段以及这些钩子函数的调用接口,字段的分配、释放、管理等是由安全模块处理的,钩子函数的实现是在安全模块完成的,其用来对内核对象的访问权限进行判断,LSM框架钩子函数结构如图1所示。LSM框架指定的内核对象有task_struct(进程)、super_block(文件系统)、inode(文件、管道、套接字)、sk_buff(套接字缓冲区)等等。LSM框架提供了两种钩子函数,一种用来分配、释放、管理内核对象的安全字段,如alloc_security、free_security;另一种用来对内核对象进行访问控制。对这两种钩子函数的调用方式是security_ops->钩子函数名,其中security_ops是指向struct security_opertaions结构体的函数指针,这些钩子函数被放置在内核源代码的某些关键点,以完成相应安全检查。
还需要向LSM框架注册安全模块,安全模块的注册和注销功能是由 register_security()和 unregister_security()函数分别完成的,注册函数将设置全局表指针security_ops,使其指向这个安全模块的钩子函数表。一旦这个安全模块被加载,就不会被后面的register_security()函数覆盖,直至这个安全模块使用unregister_security()向LSM框架注销。LSM框架只允许注册一个主安全模块,为了完善主安全模块的不足,它还提供了辅助安全模块的模块堆栈机制:辅助安全模块向主安全模块注册,使用mod_reg_security()和mod_unreg_security函数分别向主安全模块注册和注销。
图1 LSM框架钩子函数结构图Fig.1 Structure of LSM frame’s hook
在Linux2.6内核中,LSM框架对LKM安全的控制可以从模块加载过程中得知,它主要是通过基于安全钩子函数的调用来为系统的各种访问控制提供可扩展的安全模块框架。模块是通过insmod程序进行加载的,insmod中的执行过程是先调用grabfile函数在用户态下获取文件的内存映象,然后调用sys_init_module函数进入内核态进行模块链接。在grabfile函数中,需要调用security_inode_permission函数来对inode索引节点的访问权限进行检查以及调用security_dentry_open函数来对文件打开权限进行检查。在sys_init_module函数中,需要调用security_capable函数进行基于CAP_SYS_MODULE能力的加载权限的检查。所以专门针对module的安全钩子的调用只有 security_inode_permission、security_dentry_open、security_capable这3个函数。在攻击方式的分析中,如果不导出sys_call_table表,LKM恶意程序需要访问/dev/kmem文件,在LSM框架下,访问文件的时候内核会调用security_inode_permission函数以及security_inode_permission函数来对访问文件的权限进行检查。在一些Linux 2.6最新内核中,/dev/kmem文件不再导出。但恶意模块仍然可以通过中断描述符和MSR的方法搜索出sys_call_table表的起始地址,但这一方法的前提是这个恶意模块已经被加载到内核中,这样恶意模块在加载的时候就需要这3个函数的检查。这3个安全钩子函数是基于进程是否对文件具有访问权以及对模块加载权,其具体的判断准则是以LSM模块的具体实现为标准。
由于在LSM框架下内核对象的数据结构中的安全域指针只有一个,这种设计决定了LSM框架只能同时支持一个安全模块,多个独立设计且分别具有自己相应安全域的安全模块,不能够在LSM框架共存,即使采用模块叠加技术,本质上还是只支持一个独立安全模块,因为主安全策略决定了从模块的安全策略。为了真正解决这个问题,实现多个安全模块策略综合的安全模块管理器(SMM)[5]就出现了,它支持多个独立的安全模块共存,可对多安全模块进行有效管理,且与LSM框架兼容。SMM也以LKM形式存在。装载到内核的安全模块并没有直接向LSM框架注册,而是先在SMM注册,即在安全模块链表中添加一个节点。LSM框架首先调用SMM的钩子函数,再由SMM钩子函数调用各个安全模块中的安全钩子函数。当用户进程进行系统调用时,系统调用内要调用SMM中的钩子函数进行安全检测,SMM把这个安全检测交给各个独立安全模块分别进行检测,只要有一个安全模块的检测没有通过,系统调用就会被SMM拒绝而退出整个检测过程。最后SMM将各个安全模块的检测结果综合并返回给系统调用,然后系统调用根据结果决定是否继续执行。当然在SMM上加载多个安全模块会增加系统开销,且略大于各个单个安全模块开销的代数和,这主要是因为LSM和各个安全模块之间多了SMM模块。为此可以对多模块的调用顺序进行算法优化,而且可以引入Cache机制,来缓存一些安全检测结果,从而来提高SMM的时间效率。另外为了适应不同环境下特定的安全应用,还可以通过设计策略开关灵活地进行安全策略的动态选择。SMM工作原理图如图2所示。
LSM框架自身并不提供任何安全策略,它只是为安全模型提供了一致性接口。它使得各种不同的安全模型以内核模块的方式得到实现,而且不需改动内核源代码以及重新编译内核。目前基于LSM框架实现的最主要的安全模块主要有SELinux[6](安全增强型 Linux)、LIDS(Linux 入侵检测系统)、POSIX 1e Capabilities以及DTE(域和类安全增强)。
图2 SMM工作原理图Fig.2 Working principle of SMM
SELinux模型的安全体系结构被称为Flask体系,其体系结构如图3所示,它是基于动态策略配置的MAC(强制访问控制)子系统,能够支持较细粒度的权限管理,开始是以内核补丁的方式实现,在Linux 2.6内核中是以模块形式出现。SELinux由3部分组成:安全服务器,访问向量缓存,对象管理器。安全服务器为获得安全策略的决策判定提供通用接口,使其余部分保持安全策略的独立性。访问向量缓存AVC提供了从安全服务器获得的访问策略决策结果的缓冲区,以减少性能开销。对象管理器集成在内核的各个子系统,如进程管理子系统,文件子系统,它从安全服务器或者访问向量缓存AVC中获得安全策略判定结果,然后应用这些判定结果给进程和对象的安全标记赋值,最后根据这些安全标记控制系统上的各种操作。
图3 FLASK体系结构图Fig.3 Architecture of FLASK
在传统的安全机制下,Linux安全是基于自主存取控制(DAC)机制的,只要符合规定的权限,如规定的所有者和文件属性(读、写、执行)等,就可存取资源。一些通过setuid/setgid的程序就能形成严重的安全隐患,甚至一些错误的配置就可引发巨大漏洞,使系统被轻易攻击。而SELinux则基于强制存取控制(MAC),透过强制性的安全策略,应用程序或用户必须同时符合DAC及对应SELinux的MAC才能进行正常操作,否则都将遭到拒绝或失败,而这些问题将不会影响其他正常运作的程序和应用,并保持它们的安全系统结构。
LKM 注入攻击一般是对/lib/modules/(uname-r)/kernel/下的模块文件进行修改,SELinux下的权限设置能限制对模块文件的修改,为模块文件的完整性提供保证,从而防止针对模块文件的LKM注入攻击。同时SELinux下的权限设置限制了恶意程序对/dev/目录下文件访问和修改。由于进行了基于CAP_SYS_MODULE权能的加载权限的检查,阻止了许多获取root权限后的恶意加载。
虽然SELinux对LKM的安全有很好的保证,但是一旦由于访问策略的设置失误以及直接在原模块代码中植入恶意函数片段,这样的LKM恶意模块依然能链接进内核,由于LSM框架并没有相应的安全钩子函数来保障诸如IDT表、sys_call_table表等内核重要数据结构的安全,恶意程序就可以利用这些信息进行恶意攻击。为此可以扩展LSM框架,在内核源代码相应关键点插入钩子函数。由于LKM攻击的根本特性是init_module函数的执行,init_module函数能修改内核数据中跳转地址值,所以init_module的正确运行是模块安全的关键。如果能在init_module的前后进行重要数据结构内容的检测和恢复,则可保障内核重要数据结构的安全,因此我们可以在init_module函数的前后位置各放置一个安全钩子函数,分别用于记录重要数据结构在init_module函数执行之前的值以及在init_module函数执行之后校验这些数据结构的值,从而防止已加载进内核的恶意模块利用这些信息进行攻击。
针对LKM的攻击手段有很多,以介绍Linux 2.4内核下的LKM攻击技术为切入点,重点介绍Linux 2.6内核下LKM攻击技术的原理和方法,同时着重分析了针对这些攻击的防御体系LSM框架,针对目前的LSM防御体系只支持一个独立安全模块的不足,提出了安全管理器SMM的解决方案。通过分析已经在Linux下实现的基于LSM框架的SELinux模型针对LKM安全性的作用以及不足之处,提出了使用LSM扩展技术在init_module函数的前后位置各放置安全钩子函数的解决方案。
[1]博韦,西斯特.深入理解LINUX内核[M].3版.陈莉君,张琼声,张宏伟,译.北京:中国电力出版社,2007.
[2]毛德操,胡希明.Linux内核源代码情景分析(上、下册)[M].杭州:浙江大学出版社,2001.
[3]耿显虎.Linux下LKM安全的分析与改进研究[D].成都:电子科技大学,2008.
[4]马桂媛.基于通用访问控制框架LSM的Linux安全内核的研究[D].四川:西南交通大学,2004.
[5]李婷妤.基于LSM框架的安全模块管理器的设计与实现[D].湖南:中南大学,2008.
[6]龚友.Linux下内核级Rootkit检测防护机制的研究 [D].成都:电子科技大学,2009.