some points on CVE-2015-1805
作者:retme 发布时间:March 19, 2016 分类:AndroidSec No Comments
by retme
这是一篇去年的笔记,1805是一个非常强力的提权漏洞. Google已经发了这个漏洞的advisory , 所以我现在可以把这个贴出来
CVE details
http://www.cvedetails.com/cve-details.php?t=1&cve_id=cve-2015-1805
commit which bring bug in
http://permalink.gmane.org/gmane.linux.kernel.commits.head/78321
fix
http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=637b58c2887e5e57850865839cc75f59184b23d1
0x1.综述
pipe.c
pipe_iov_copy_to/from_user在处理readv/writev时,
对当前已经拷贝的buffer长度统计可能与pipe_read/pipe_write不同步,
导致"iovec overrun",构造内存后可造成任意内核地址写
0x2.atomic copy的逻辑
static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov,
unsigned long nr_segs, loff_t pos)
{
...snip...
for (;;) {//循环读取内存到iovec,直到读取缓冲区(iovecs)用尽
int bufs = pipe->nrbufs;
if (bufs) {
...snip...
if (chars > total_len)
chars = total_len;
error = ops->confirm(pipe, buf);
if (error) {
if (!ret)
ret = error;
break;
}
//检查iovecs中的每一个iov->base是否是一个可写的用户态内存页
//如果全部可写,那么atomic=1,接下来会直接使用__copy_to_user,不对目标地址再作检查
atomic = !iov_fault_in_pages_write(iov, chars);
redo:
addr = ops->map(pipe, buf, atomic);
//对iovec进行copy,如果atomic=1,直接调用__copy_to_user,否则使用copy_to_user进行地址检查(access_ok)
// !!!注意!!!
//pipe_iov_copy_to_user会在每次copy完一个iov的时候对iov->len的长度进行更新
//如果copy到len=X,出错返回,那么已经copy成功的iov->len会被减去;
//但是读取缓冲区的长度total_len,不会同步减少。
//进入redo逻辑后,pipe_iov_copy_to_user还会继续copy 长度为total_len的字节
//那么最终会向后越界copy len=X个长度的iov,aka "iovec overrun"
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
ops->unmap(pipe, buf, addr);
if (unlikely(error)) {
/*
* Just retry with the slow path if we failed.
*/
//如果本来是atomic=1,却copy失败了,那么使用atomic=0的方法重新copy
//假设total_len=0x1010,第一次copy已经让iovecs总长度用掉了len=0x20
//那么redo开始的时候total_len还是0x1010,但是iovces的长度已经被减少了0x20
//所以最终pipe_iov_copy_to_user会越界读取iov,并向其中copy内存
if (atomic) {
atomic = 0;
goto redo;
}
if (!ret)
ret = error;
break;
}
ret += chars;
buf->offset += chars;
buf->len -= chars;
/* Was it a packet buffer? Clean up and exit */
if (buf->flags & PIPE_BUF_FLAG_PACKET) {
total_len = chars;
buf->len = 0;
}
if (!buf->len) {
buf->ops = NULL;
ops->release(pipe, buf);
curbuf = (curbuf + 1) & (pipe->buffers - 1);
pipe->curbuf = curbuf;
pipe->nrbufs = --bufs;
do_wakeup = 1;
}
//走到这里才对total_len进行更新,也就是只有copy成功才会更新。redo时不会更新
total_len -= chars;
if (!total_len)
break; /* common path: read succeeded */
}
if (bufs) /* More to do? */
continue;
0x3. 利用难点,造成redo
redo的逻辑不是那么好进的,如果要进入redo,需要做一个race condition on page table:
static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov,
unsigned long nr_segs, loff_t pos)
{
...snip...
for (;;) {
int bufs = pipe->nrbufs;
if (bufs) {
...snip...
if (chars > total_len)
chars = total_len;
error = ops->confirm(pipe, buf);
if (error) {
if (!ret)
ret = error;
break;
}
//loop_time = 1时,iov_fault_in_pages_write 这里必须执行成功
//也就是所有的iov->base指向的内存页有效
atomic = !iov_fault_in_pages_write(iov, chars);
redo:
addr = ops->map(pipe, buf, atomic);
//loop_time = 1时,pipe_iov_copy_to_user 这里必须中途执行失败
//也就是其中某一个iov->base指向的内存页无效。
//那么进入redo
//redo,也就是loop_time = 2时,pipe_iov_copy_to_user必须成功,
//需要让无效的iov->base重新生效
// !!!注意!!!
//不能在loop_time = 2的时候触发overrun,
//否则overrun会使用copy_to_user而不是__copy_to_user,那么还是无法写内核地址
//解决办法是让total_len 稍稍大于 buf->len(0x1000)
//这样loop_time = 2的时候能保证把一个合法的buf->len读完。
//并且会因为buf->len被读完,tolen_len却还有剩余,而进入第三个loop
//然后在loop_time = 3的时候走atomic=1的路线,进行越界使用iov
error = pipe_iov_copy_to_user(iov, addr + buf->offset, chars, atomic);
ops->unmap(pipe, buf, addr);
if (unlikely(error)) {
/*
* Just retry with the slow path if we failed.
*/
if (atomic) {
atomic = 0;
goto redo;
}
if (!ret)
ret = error;
break;
}
ret += chars;
buf->offset += chars;
buf->len -= chars;
...snip...
total_len -= chars;
if (!total_len)
break;
}
if (bufs) /* More to do? */ //进入loop_time=3
continue;
假设readv使用iovec[512] = 8k,正好占据一个kmalloc-8192
iovec[0].len=0
iovec[1].len=0x20
iovec[2→511].len=8
race时间轴如下
thread1 thread2
map(iovec[0→511].base)
loop_time = 1 iov_fault_in_pages_write
return 1
unmap(iovec[2].base)
pipe_iov_copy_to_user
__copy_from_user
return -EFAULT
goto redo
map(iovec[2].base)
loop_time = 2(redo)
pipe_iov_copy_to_user
copy_from_user
return 0
loop_time = 3(More to do)
pipe_iov_copy_to_user
__copy_to_user
OVER_RUN
如果我加上每次loop iovec长度的标记,时间轴如下:
total_len = 0x1010 chars=0x1000
iovec[0].len=0
iovec[1].len=0x20
iovec[2→511].len=8
thread1 thread2
map(iovec[0→511].base)
loop_time = 1 iov_fault_in_pages_write
return 1
total_len = 0x1010 chars=0x1000
iovec[0].len=0
iovec[1].len=0x20
iovec[2→511].len=8
unmap(iovec[2].base)
pipe_iov_copy_to_user
__copy_from_user
return -EFAULT
goto redo
total_len = 0x1010 chars=0xff0
iovec[0].len=0
iovec[1].len=0
iovec[2→511].len=8
map(iovec[2].base)
loop_time = 2(redo)
pipe_iov_copy_to_user
copy_from_user
return 0
total_len = 0x1010 - 0xff0 =0x20 chars=0xff0 - 0xff0=0
iovec[0].len=0
iovec[1].len=0
iovec[2→511].len=0
loop_time = 3(More to do)
total_len =0x20 chars=min(buf->len,total_len)=0x20
iovec[513] aka overun_iov[0]
overun_iov[0].len=0
overun_iov[1].len=8
overun_iov[1].base=KERNEL_ADDR
overun_iov[2->511].len=4096
pipe_iov_copy_to_user
__copy_to_user
OVER_RUN
overun_iov[1].base就是要写的内核地址,overun_iov就是8k内存页的下一页。
iov是在kmallc-8192的slab里面的,可以用sendmmsg去喷overun_iov
以上