CVE-2015-3636

Posted koozxcv

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CVE-2015-3636相关的知识,希望对你有一定的参考价值。

最近这今年,寻找一个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提权的目的已经完成。

结果如下:

技术分享























以上是关于CVE-2015-3636的主要内容,如果未能解决你的问题,请参考以下文章

快排CMS-文件上传漏洞

如何修复漏洞

操作系统漏洞有哪几种分类

漏洞复现Weblogic 任意文件上传漏洞

漏洞复现Weblogic 任意文件上传漏洞

漏洞复现Weblogic 任意文件上传漏洞