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_t
pipe_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 int
pipe_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再次進行拷貝,此次拷貝的內容超過數組範圍,越界了。