2008年8月26日星期二

针对Linux Kernel vmsplice Exploit的分析

参考文档:
http://lwn.net/Articles/268783/
http://www.avertlabs.com/researc … l-vmsplice-exploit/
http://lwn.net/Articles/271688/
溢出程序在http://www.milw0rm.com/exploits/5092,存在该问题的内核版本是2.6.17 – 2.6.24.1。
首先分析一下造成溢出的原因:
Vmsplice的作用是将一个文件描述符(必须是一个pipe)和一段内存连接起来。这个功能的实现是通过fs/splice.c的do_vmsplice()function来实现,在该function种,定义了两个数组:
struct page *pages[PIPE_BUFFERS];
struct partial_page partial[PIPE_BUFFERS];
PIPE_BUFFERS的值在存在溢出问题的版本中的是定义为16。这两个函数都传递到了 get_iovec_page_array()这个function中。
以2.6.22.14版本的源代码为例,看看在fs/splice.c的1565行开始的get_iovec_page_array函数。
在该函数中我们看到:
error = get_user(len, &iov->iov_len);
if (unlikely(!len))
break;
在这里仅仅判断len是正数就ok,而len是可以通过用户控制的。
npages = (off + len + PAGE_SIZE - 1) >> PAGE_SHIFT;
if (npages > PIPE_BUFFERS - buffers)
npages = PIPE_BUFFERS – buffers;
error = get_user_pages(current, current->mm,
(unsigned long) base, npages, 0, 0,&pages[buffers], NULL);
npages的值是通过len计算得出,那么我们将len值设为UINT32_MAX的话,那么计算off+len+PAGE_SIZE的结果就会导致整 型数包裹(integer wrap),那么npages的将会是0,这是unexpected的。我们现在来分析get_user_pages在得到了unexpected的 npages值后,会有什么样的结果。get_user_pages是用来将用户空间中页(pages)映射(pin)入内存,并且得到他们页结构 (struct page)的指针。然而在get_user_pages()函数内部,处理页面时使用的do{}while()的结尾处是:
len–;
} while (len && start <>vm_end);
如果len的值是0的话(正如我们期望的那样),那么这个循环将会至少循环一次,将len值减为-1后,继续在页面中错误地执行,直到执行到没有有效的 mapping的地址后,指针将会停止并返回。但是在这时,他或许已经在储了比他当时所分配的内存空间更多的内容,到其page数组中。也就是说在这种情 况下get_user_pages()将会溢出pages数组,写了不仅仅是PIPE_BUFFERS(16)个指针到数组中。然而真正被溢出程序所利用 数组是partial数组。
在do_vmsplice()中定义的partial数组同样被传递给了get_iovec_page_array()。在partial数组中描述 了需要写入到管道中的页面的其他的部分。 在get_user_pages()返回后,紧跟着一个循环语句:
for (i = 0; i < error; i++) {
const int plen = min_t(size_t, len, PAGE_SIZE - off);

partial[buffers].offset = off;
partial[buffers].len = plen;

off = 0;
len -= plen;
buffers++;
}
在这种情况下,因为所有页面都被写入,被计算的偏移量(offset)将会是zero,并且长度(length)值是PAGE_SIZE(4096).而 从get_user_pages()的返回值error,将会是在被溢出的情况下 被mapped的pages页的数量:46。那么实际上partial数组的同样是被定义为16个元素,因此上边的这个循环同样会导致溢出的发生。
这两个数组都是在vmsplice_to_page()中声明的。在内存分配中partial数组将会放在pages的下边,因此一点partial数组 被overflow,那么这个循环将会同样溢出放在上边的pages数组。因此pages数组的内容将会被改写为0,而不是先前的指向pages结构的指 针。
当这些完成后,控制权返回到vmsplice_to_page()-溢出并不足以覆盖返回地址。针对splice_to_pipe()的调用目前看来要结束了,但是一些有趣的事情发生了。在这个function的开始,有一个test:
if (!pipe->readers) {
send_sig(SIGPIPE, current, 0);
if (!ret)
ret = -EPIPE;
break;
}
如果我们看攻击代码的话,我们会看到
if (pipe(pi) < 0) die(”pipe”, errno);
close(pi[0]);
在调用vmsplice()之前,已经将pipe的读取端关闭了。因此splice_to_pipe将会立即退出,然而在退出时,将会执行如下操作:
while (page_nr < spd_pages)
page_cache_release(spd->pages[page_nr++]);
我们知道get_user_pages()函数的调用将会lock内存中的相关页,以便允许内核对其进行访问;上边这两行是一段清理代码用来返回并 unlock先前锁住而目前不再使用的pages。但是在我们这个例子中,pages数组的内容已经被改写为0。那么接下来发生的事情,将会是内核欺骗 (kernel oops),因为pages数组中填充的内容并不是合法的地址。溢出代码通过一些小方法,比如使用一些特定的mmap()调用,将会在内存地址的底部构造 任意的内容。
当运行在内核模式,直接去取指向用户空间的指针的值尽管可能会造成很多问题,但是确实可以被忍受的。如果地址是有效的并且相关也驻留内存当中,那么直接的 取值也是能够成功的。因此当kernel开始工作在他以为是指向struct page空间的指针的内存时,并没有得到任何的错误提示;而是得到了通过exploit程序所构造的数据内容。
kernle 一般情况下将每页page看为个体。但是在有些时候,或有多个page组成的集合,被称为”compound pages”.这种情况发生在一段被kernel所需要的连续的空间的大小大于一个page的大小时;当这种调用发生时,一组compound pages被传递给调用者。比较特别的地方是,他们在被释放时,是会被拆分开,因此就会有拆分的动作发生。因此compound pages会有一个普通pages所没有的属性:当pages被释放时,会调用destructor。
我们来看一下攻击程序中是如何设置low-memory page structures的:
pages[0]->flags = 1 << PG_compound;
pages[0]->private = (unsigned long) pages[0];
pages[0]->count = 1;
pages[1]->lru.next = (long) kernel_code;
当内核在用户空间的0位置开始寻找page structure时,将会发现该page structure是组compound page. destructor(存放在第2个page 结构中的lru.next)所指向的是一段先前在exploit代码中定义的kernel_code()。因为count被设置为1,因此执行 page_cache_release()(会将count值减1)将会得出没有剩余的指针了,而这段page看起来像一段compound page,destructor将会被调用。这时,存放在kernel_code位置的任意代码就可以在内核状态运行。

没有评论: