简单分析CVE-2015-1805

来源:互联网 发布:面板数据到底是怎样的 编辑:程序博客网 时间:2024/06/05 18:40

CVE-2015-1805这个安全漏洞的年限已久,几年前就在upstream Linux内核中被发现,并于2014年4月被修复。但不幸的是,并没有修补完全,直到今年二月C0RE小组告知谷歌该漏洞可以被用于攻击Android操作系统。

漏洞介绍:在linux 内核3.16版本之前的fs/pipe.c当中,由于pipe_read和pipe_write没有考虑到拷贝过程中数据没有同步的一些临界情况,造成了堆数组拷贝越界的问题,因此有可能导致系统crash以及系统权限提升,这种漏洞又称之为” I/O vector array overrun”。

漏洞函数为pipe_read()


<span style="font-size:14px;">static ssize_tpipe_read(struct kiocb *iocb, const struct iovec *_iov,   unsigned long nr_segs, loff_t pos){struct file *filp = iocb->ki_filp;struct inode *inode = filp->f_path.dentry->d_inode;struct pipe_inode_info *pipe;int do_wakeup;ssize_t ret;struct iovec *iov = (struct iovec *)_iov;size_t total_len;total_len = iov_length(iov, nr_segs);//这是拷贝数据的总长度,正是由于这个值没有能够即时更新所导致的漏洞/* Null read succeeds. */if (unlikely(total_len == 0))return 0;do_wakeup = 0;ret = 0;mutex_lock(&inode->i_mutex);pipe = inode->i_pipe;for (;;) {int bufs = pipe->nrbufs;if (bufs) {int curbuf = pipe->curbuf;struct pipe_buffer *buf = pipe->bufs + curbuf;const struct pipe_buf_operations *ops = buf->ops;void *addr;size_t chars = buf->len;int error, atomic;if (chars > total_len)chars = total_len;error = ops->confirm(pipe, buf);if (error) {if (!ret)ret = error;break;}atomic = !iov_fault_in_pages_write(iov, chars);//在此处判断iov->len是否大于0,且iov->base指向的地址是否可写                                                                                               //<span style="font-family: Arial, Helvetica, sans-serif;">且处于用户</span><span style="font-family: Arial, Helvetica, sans-serif;">态,之后返回atomic</span>redo:addr = ops->map(pipe, buf, atomic);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为1,且拷贝中途失败时,进入该分支atomic = 0;goto redo;}if (!ret)ret = error;break;}ret += chars;buf->offset += chars;buf->len -= chars;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 -= chars;//这里更新total_len的值if (!total_len)break;/* common path: read succeeded */}if (bufs)/* More to do? */continue;if (!pipe->writers)break;if (!pipe->waiting_writers) {/* syscall merging: Usually we must not sleep * if O_NONBLOCK is set, or if we got some data. * But if a writer sleeps in kernel space, then * we can wait for that data without violating POSIX. */if (ret)break;if (filp->f_flags & O_NONBLOCK) {ret = -EAGAIN;break;}}if (signal_pending(current)) {if (!ret)ret = -ERESTARTSYS;break;}if (do_wakeup) {wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM); kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);}pipe_wait(pipe);}mutex_unlock(&inode->i_mutex);/* Signal writers asynchronously that there is more room. */if (do_wakeup) {wake_up_interruptible_sync_poll(&pipe->wait, POLLOUT | POLLWRNORM);kill_fasync(&pipe->fasync_writers, SIGIO, POLL_OUT);}if (ret > 0)file_accessed(filp);return ret;}</span>

可以看到在
<span style="font-size:14px;">if (bufs){...}</span>

分支内进行拷贝,而且在分支内当出错后可以以goto的方式跳转。而total_len的更新在分支之外。这就有可能导致一种情况:拷贝中途发生错误,goto跳转,但是由于total_len值并未更新,所以在重新开始拷贝的时候,虽然已经拷贝了大小为n的内容(指针已经向前前进了n),还是会从当前位置向后拷贝total_len大小的数据,导致数组越界,多拷贝了大小为n的内容。

static intpipe_iov_copy_to_user(struct iovec *iov, const void *from, unsigned long len,      int atomic){unsigned long copy;while (len > 0) {while (!iov->iov_len)iov++;copy = min_t(unsigned long, len, iov->iov_len);if (atomic) {if (__copy_to_user_inatomic(iov->iov_base, from, copy))return -EFAULT;} else {if (copy_to_user(iov->iov_base, from, copy))return -EFAULT;}from += copy;len -= copy;iov->iov_base += copy;//指针向前前进已拷贝的大小iov->iov_len -= copy;//将长度减去已拷贝的大小}return 0;}
落实到具体代码,如果atomic=1,则pipe_iov_copy_to_user -> __copy_to_user_inatomic;如果atomic=0,则pipe_iov_copy_to_user -> copy_to_user 。成功拷贝,就会将当前iov数组内元素的ivo_base和ivo_len进行相应的变化。这个变化在函数内,也就是前文所说的分支内进行,与total_len的更新不同步。

下面来构造一个例子

首先构造一个struct iovec iov[5]的数组。如下赋值

ivo[0]->ivo_base = 0x00004000

ivo[0]->ivo_len = 0x100

ivo[1]->ivo_base = 0x00004200

ivo[1]->ivo_len = 0x100

ivo[2]->ivo_base = 0x00004400

ivo[2]->ivo_len = 0x100

ivo[3]->ivo_base = 0x00004600

ivo[3]->ivo_len = 0x100

ivo[4]->ivo_base = 0x00004800

ivo[4]->ivo_len = 0x100

total_len = 0x500

chars假设为0x200

先将ivo[1]的ivo_base设置为不可访问。

以atomic=1状态进入拷贝

首先ivo[0]拷贝成功,

ivo[0]->ivo_len = 0x0

然后ivo[1]拷贝失败进入分支

<span style="font-size:14px;">if (atomic) {atomic = 0;goto redo;}</span>
再将ivo[1]->ivo_base的地址设为可访问,每次成功拷贝0x200,两次分别将ivo[1]、ivo[2]和ivo[3]、ivo[4]的数据拷走

<span style="font-size:14px;">    total_len -= chars;//这里更新total_len的值 total_len两次0x200,最后会多出0x100,就是因为第一次的错误导致ivo[0]的长度没有减去<span style="white-space:pre"></span>if (!total_len)<span style="white-space:pre"></span>break;/* common path: read succeeded */</span>
此时由于第一次的错误导致此处的total_len不为0再次进行拷贝,此次拷贝的内容超过数组范围,越界了。







0 0
原创粉丝点击