最近这今年,寻找一个android系统上可通用使用的root提权漏洞变得越来越困难,一方面是因为android系统的碎片化十分严重,另一方面是因为android系统上的漏洞缓冲机制在不断的引入。在这篇文章中我将为大家简单的讲述一下CVE-2015-3636这个漏洞的poc和exploit。其实要去稳定和有效的去利用这样一个漏洞并不是一件容易的事情,我们一步一步的来看。
0.漏洞概述:
漏洞属于linux内核级别的use-after-free漏洞,存在于Linux内核的Ping.c文件中。当用户尝试通过一个socket(AF_INET,SOCK_DGRAM,IPPROTO_ICMP)所返回的socket
file descriptor来调用两次一个sa_family==AF_UNSPEC的connect()时就会因为访问0x200200这个地址引起系统crash。除此,如果攻击者巧妙的填充或者覆盖PING socket对象,就能达到获取root权限的目的。并且,正是因为漏洞存在于android系统的基础部分Linux内核当中,才让这个漏洞有能通用利用的可能性。
1.漏洞分析&poc
这个漏洞是被keen team团队的Wen Xu和Wu shi发现并尝试利用的,目前已经被修补,CVE编号为:CVE-2015-3636。
我们从漏洞的补丁来分析漏洞:
我们从补丁源码可以看出,里面添加了sk_nulls_node_init(sk->nulls_node);这样一句代码。那么好,以这句话为切入点,结合前面概述中提到的poc思路分析漏洞原理。
通过浏览内核代码中关于sk_nulls_node_init(hlist_nulls_node *node)函数的具体实现后发现,其实这个函数的作用就是将参数所传入的的node的next成员和pprev成员赋为NULL。
概述中我们讲到的,利用socket(AF_INET,SOCK_DGRAM,IPPROTO_ICMP)所返回的socket file descriptor来调用两次一个sa_family==AF_UNSPEC的connect()时就会因为访问0x200200这个地址引起系统crash。接下来我们跟踪一下代码执行流程。
首先使用sa_family==AF_UNSPEC调用connect时会进入到inet_dgram_connect()函数中,
我们从补丁源码可以看出,里面添加了sk_nulls_node_init(sk->nulls_node);这样一句代码。那么好,以这句话为切入点,结合前面概述中提到的poc思路分析漏洞原理。
通过浏览内核代码中关于sk_nulls_node_init(hlist_nulls_node *n)函数的具体实现后发现,其实这个函数的作用就是将参数所传入的的hlist_null_node的next成员和pprev成员赋为NULL。
概述中我们讲到的,利用socket(AF_INET,SOCK_DGRAM,IPPROTO_ICMP)所返回的socket file descriptor来调用两次一个sa_family==AF_UNSPEC的connect()时就会因为访问0x200200这个地址引起系统crash。接下来我们跟踪一下代码执行流程。
首先使用sa_family==AF_UNSPEC调用connect时会进入到inet_dgram_connect()函数中,在函数中我们可以看到因为sa_family==AF_UNSPEC导致程序会执行红框中
的逻辑,而这条语句中最终调用了sk->sk_prot->disconnect(sk,flags)函数,sk_prot是sk对象的的一个成员,指向一个包含了确定数量函数指针的指针表,而具体的这些函数执行哪里取决于它的协议类型,这些协议包含TCP、UDP等。因此sk->sk_prot_disconnect(sk,flag)这条语句最终是调用的udp_disconnect(struct sock *sk,int flag)这个函数:
>
并且在socket对象不绑定端口的情况下,会执行sk->sk_prot->unhash(sk)这条语句,同样的我们找到对应的函数:ping_unhash(struct sock* sk)
此时我们看到,程序逻辑进入到了漏洞所在位置,经过分析我们关注点重点放在了41和42行这两句代码中,41句这句其实是将sk对象在其对应的内核hlist中删除。具体实现我们可以看一下:
其实就是将hlist_nulls_node类型的节点对应链表中删除,并且将n节点的前向二级指针pprev赋值为LIST_POISON2这个值,进一步我们在内核源码中搜索这个宏发现,其对应的值就是0x200200。
第一次调用sin_family== AF_UNSPEC connect时程序产生不会任何异常,而仅仅只是为了使这个sock对象sk的对应节点成员的pprev值被赋值为0x200200。
而第二次调用sin_family== AF_UNSPEC connect时才是希望真正的触发漏洞使系统crash。因为当第二次调用的时候程序逻辑按照之前的流程走到42行代码时会再次删除对应的节点,并且当执行到*pprev=next 这句时会导致系统crash。这是因为*pprev=next这条语句其实是对第一次节点的删除操作后LIST_POISON2这个指针解引用,让其为next,而在内核中,这个LIST_POISON2地址是不能被访问的,所以会引起系统crash。
其实在这里出了一点小问题,澄清一下,第二次尝试调用后并没有像我们想象的crash,这是因为要进入if(sk_hashed())逻辑中必须在两次调用sin_family== AF_UNSPEC connect()之前先使用sin_family==AF_INET 来调用一次connect(),这样才可以sk对象在内核中hashed(其实就是被加入到内核hlist中)。
综上所述,完整的poc(或者这个其实应该叫漏洞检测代码)如下:
#include <unistd.h> #include <sys/socket.h> #include <errno.h> #include <linux/netlink.h> #include <linux/if.h> #include <linux/filter.h> #include <linux/if_pppox.h> #include <linux/sock_diag.h> #include <linux/inet_diag.h> #include <linux/unix_diag.h> #include <string.h> #include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <jni.h> #define MMAP_BASE 0x200000 #define LIST_POISON 0x200200 #define MMAP_SIZE 0x200000 int checkIsVulnerable() { void * magic = mmap((void *) MMAP_BASE, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED | MAP_ANONYMOUS, -1, 0);//向0x20000到0x40000这个虚拟内存地址映射,并且将这个地址段中所有值设为0 memset(magic, 0, MMAP_SIZE); *((long *)(LIST_POISON)) = 0xfefefefe;//给0x200200这个虚拟内存地址中赋值为0xfefefefe; int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; connect(sock, (const struct sockaddr *) &sa, sizeof(sa)); // 第一次用AF_INET sin_family来connect是为了让sk(sock 对象)在内核中hashed sa.sin_family = AF_UNSPEC; connect(sock, (const struct sockaddr *) &sa, sizeof(sa)); /*每次用AF_UNSPEC调用connect会触发inet_dgram_connect()中的sk->sk_prot->disconnect()逻辑 其中disconnect的具体实现是根据协议类型而定的,PING(ICMP)socket的具体实现disconnect()是 udp_disconnect()未绑定端口的情况下会触发sk->sk_prot->unhash(sk)逻辑*/ connect(sock, (const struct sockaddr *) &sa, sizeof(sa));//如果漏洞存在,第二次调用就会触发这个漏洞 if (*((long *)(LIST_POISON)) != 0xfefefefe){ printf("Device is vulnerable\n"); return 1; }else{ printf("Device is not vulnerable\n"); return 0; } }
2.漏洞利用:
前面我们说到这是一个use-after-free漏洞,然而在poc中我们并没有看到哪里进行use-after-free了。我们仔细再来看看漏洞存在位置的代码:
我们来看看42行sock_put(sk);这句代码到底执行了怎样的操作:
其实就是将的sock对象sk的引用计数进行减一操作,并且判断其值是否为0,如果为0的话就释放掉sk这个对象的内存。也就说当我们第二次调用sin_family== AF_UNSPEC connect时会进入到if逻辑中的sock_put(struct sock*sk)函数当中去然后释放掉sk对象的内存。然而这个sk对象是我们在用户空间创建的,其文件描述符还在用户空间中也就是在我们的掌控之中,并且使用free来告知内核释放掉这个对象所对应的内存,但是并不会清除内存中的数据,这就是一个典型的use-after-free漏洞。
想要获取通过此漏洞过得root权限我们要完成的目标有:
1.覆盖use-after-free的Ping sock 对象的内容;
2.在用户空间中去使用已经free掉的Ping sock对象,尝试通过某种方式获得在内核中执行代码的能力;
3.进行拥有在用户态去调用内核代码的能力执行后,就可以修改对应的进程权限,提升为root权限。
先来看第一步,用我们可控的数据来覆盖use-after-free的Ping sock对象,也就是re-filling操作,也是最困难和最关键,关系到exploit能否稳定有效的运行成功,
我们知道,目前Linux内核采用的heap 管理机制是在分配内核对象的使用使用SLUB/SLAB分配器,不同大小的内核对象对应不同的SLABs,在这种情况下我们想要做的是类似
使用类型为A的内核对象去填充类型为B的内核对象,这在多进程的linux内核中来说几乎是不可能完成的事情,因为影响的因素太多,内存布局很容易被别的进程影响。
最后,一种比较好的选择是使用physmap(内核物理直接映射区),physmap是一块内核空间中一块为了提高系统性能的而设置的区域,这块区域允许直接将用户空间地址映射到内核空间中去。并且在内核空间中physmap区域和PING sock SLABs一个在比较高的地址,一个在比较低的地址,通过lifting可能产生重叠。如图:
我们看到,在进行了一定次数的lifting操作之后,和我们用户空间映射的区域发生重叠的也就是我们能够使用的Ping sock对象,位于内核空间中相对比较高的地址处。我们在映射的这块用户空间区域中填充相同的数据,以便之后我们判断是否已经有我们可控的Ping sock对象,并决定是否停止lifting,我们将这些数据成为Magic value。
当判断已经有我们可控的Ping sock对象之后,我们就停止进行lifting,并释放掉在lifting过程中创建的Ping sock对象,仅保留已经由我们控制的对象句柄。
首先来看看我们的Magic value的值到底是什么:
可以看到,其实是我们通过mmap映射的这块空间中填充的是以8字节为单位的值,其中前四字节填充当前的地址,而后四字节填充一个我们定义的一个32位值。
这是因为我们在判断是否已经产生重叠时,是通过ioctl(sockfd,SIOCGSTAMPNS,(struct timespec*))这个系统调用来判断的:
这个函数的执行结果是什么呢,通过源码我们可以分析得出,其实这个函数是将sockfd对应的socket对象的中sk_stamp成员的值返回用户空间中,而这个成员的值是对象创建时的时间戳。
试想想,当lifting一定次数后,现在我们手中的能控制的Ping sock对象中其实全是已经被我们填充了之前提到的8字节Magic value的,调用此函数后就会返回一个8字节的时间戳值,而这个值对应的前四个字节就是sk_stamp在Ping sock对象中的地址,后四个字节用来判断返回结果的正确性。
一旦我们判断已经有可控的PING sock对象在mmap映射的区域了,我们就停止lifting,并释放无用的PING sock对象。
此时我们就完成了覆盖的工作,也就是走完了第一步。
在得到一个用户空间可控的PING sock对象之后,很容易我们就可以通过分析Ping sock对象的内容来控制处于内核上下文的PC寄存器值。
首先来看一下Ping sock对象的结构:
从中我们可以看出,一个Ping sock对象对应一个叫sk_prot的结构体指针,这个结构体中存放了一系列的函数指针,而整个对象中的数据都是可控的,并且存放这些函数指针的
地址我们根据其在Ping sock对象中的偏移是可以计算出来的,于是现在内核上下文中的一个PC寄存器就是我们可控的了。比如我们可以通过简单的close(sockfd)就可以控制程序流向至我们想要的函数地址。但是此时只能是从内核空间到用户空间函数的跳转。(因为虽然从内核可以向用户空间和内核空间任意跳转,但是我们没办法知道内核中某个函数的地址,其实我们最想知道的是commit_creds,prepare_kernel_cred的地址,因为传统的shellcode就是直接通过内核中这两个符号地址来进行的。)
那么我们该怎么做才能达到提升为root权限的目的呢?
这里用到的一种方法就是采用泄露内核栈顶指针sp的方法,来改写addr_limit的值解除限制,并且根据sp来推算thread_info的地址,然后根据thread_info推算task_struct的地址,因为task_struct结构存放着当前进程的所有信息,包括权限,于是就根据task_struct的结构来解析其中对应的权限相关成员,并修改其值为root权限进程应该有的值:
至此,我们利用CVE-2015-3636进行root提权的目的已经完成。
结果如下: