Retme的未来道具研究所

世界線の収束には、逆らえない

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



以上


评论已关闭