漏洞分析一百篇-04-pingpongroot-WX利用

Author Avatar
leo00000 7月 04, 2018

pingpong(CVE-2015-3636)这个漏洞能用来提升权限,Linux kernel的ping套接字实现上存在释放后重利用漏洞,x86-64架构的本地用户利用此漏洞可造成系统崩溃,非x86-64架构的用户可提升其权限,漏洞利用了kernel UAF(use-after-free) bug。

漏洞分析

分析环境

我选择编译了aarch64最新的android-goldfish-3.10内核,然后revert patch,在Android-27的模拟器环境中进行分析和调试。在调试编写EXP的过程中,由于gdb版本和一些插件的原因,会导致emulator崩溃,这里我选择较低版本的NDK-r10e进行分析调试。关于去补丁可以参考我的这篇文章

在过程中有一些小坑,这里排一下,Android模拟器中支持的Android版本和内核版本有一定的对应关系,就我尝试过的avd-22对应goldfish3.4.67,avd-19和avd-27对应goldfish3.10.0+。
gdb插件gef和内核调试冲突,会直接导致emulator直接奔溃。

漏洞成因

构建一个简单的POC触发漏洞。

int sockfd= socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);  // refcount =1;       
structsockaddr addr = { .sa_family = AF_INET };
int ret =connect(sockfd, &addr, sizeof(addr));  // refcount ++;  创建hash
structsockaddr _addr = { .sa_family = AF_UNSPEC };
ret =connect(sockfd, &_addr, sizeof(_addr));  //删除hash;refcount --;
ret =connect(sockfd, &_addr, sizeof(_addr)); // bug导致继续删除hash;refcount --; refcount

当用户用ICMP socket和AF_UNSPEC作参数进行connect时,内核会进入inet_dgram_connect,继而sk->sk_prot->discnnect函数,实质上是udp_disconnect函数,当用户不指定端口号最终会调用ping_unhash,删除当前sock对象的hash,并且让refcount递减一次。关键函数代码:

int inet_dgram_connect(struct socket *sock, struct sockaddr *uaddr,
               int addr_len, int flags)
{
    ...
    if (uaddr->sa_family == AF_UNSPEC)
        return sk->sk_prot->disconnect(sk, flags);
    ...
}

int udp_disconnect(struct sock *sk, int flags)
{
    ...
    if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {
        sk->sk_prot->unhash(sk);
        inet->inet_sport = 0;
    }
    ...
}

void ping_unhash(struct sock *sk)
{
    ...
    if (sk_hashed(sk)) {
        write_lock_bh(&ping_table.lock);
        hlist_nulls_del(&sk->sk_nulls_node);
        ...
    }
}

在第一次调用hlist_nuhlist_nulls_del删除hash后会将hlist_node.pprv=0x200200,然后在使用相同的参数调用一次connect,因为hash已经删除了,此时语句本应该不执行,但是由于hlist_node.pprv=0x200200!=NULL判断逻辑继续执行,refcount被多减了一次。因此,攻击者只需要创建一个ICMP socket,连续调用3个connect(第一个connect用来生成hash),就可以把refcount置为0,从而释放sock对象导致UAF。
在最新的arm64中,已经定义了ILLEGAL_POINTER_VALUE为0xdead000000000000,而用户无法map到这个地址,为了还原漏洞环境,在编译的时候,我们做了如下修改:

打造EXP

这里主要参考了KeenTeam的这篇文章Own your Android! Yet Another Universal Root。大致思路如下:

  1. 大量分配ICMP socket,抬高SLAB空间,将存在漏洞的sock布局在其中。
  2. 在用户态大量mmap使得内核physmap和SLAB地址相撞,覆盖其中存在漏洞的sock。
  3. 判断sock指针位置,计算并覆盖close函数,控制内核EIP。
  4. 利用JOP泄露出内核栈指针sp,获取thread_info结构。
  5. 利用JOP修改thread_info.addr_limit的值为0xffffffff,绕过PAX。
  6. 修改thread_info.task.cred提权。
  7. 绕过SELinux,执行system(sh)。

论文中描述,在内核空间中,physmap和SLABs一般会处于不同的地方,physmap位于相对较高的地址,SLABs位于相对较低的地址,由于内核空间里physmap和SLABs靠得很近,可以通过先创建大量的socket对象抬高SLAB地址,exp中会先获取单个进程可创建的最大socket数max_fds,然后循环每个进程创建max_fds个正常的socket然后加上一个漏洞的vul_socket,最终生成了65000个正常的socket加上535个vul_socket。

然后我们将正常的socket都释放掉,在用户空间不断map内存把数据映射到physmap,通过sk->sk_stamp判断内核空间physmap是否和SLAB重叠,可以得到3个被覆盖的socket。过程中通过ioctl(sockfd, SIOCGSTAMPNS, (struct timespec* ))函数获取到sk->sk_stamp的值:

int sock_get_timestampns(struct sock *sk, struct timespec __user *userstamp)
{
    struct timespec ts;
    if (!sock_flag(sk, SOCK_TIMESTAMP))
        sock_enable_timestamp(sk, SOCK_TIMESTAMP);
    ts = ktime_to_timespec(sk->sk_stamp);
    if (ts.tv_sec == -1)
        return -ENOENT;
    if (ts.tv_sec == 0) {
        sk->sk_stamp = ktime_get_real();
        ts = ktime_to_timespec(sk->sk_stamp);
    }
    return copy_to_user(userstamp, &ts, sizeof(ts)) ? -EFAULT : 0;
}

最终效果如下图:

我们对用户态mmap出来的内存修改,实际也会映射到physmap空间,如果找到两个以上sock被覆盖,这时候我们就可以利用socket的sk->sk_prot->close函数,将其指针覆盖掉,然后调用close函数,这个时候我们就已经控制了内核eip,控制了代码的执行流程。中间还有很多小坑,比方说close执行后会执行到ip_mc_drop_socket函数,会对mc_list进行判断,需要我们将其置0,这里就不再一一赘述。针对不同的SePolicy,我们有时能直接setenforce 0,相对就简单多了。给出源码EXP
在虚拟机中我们root成功。

补丁

补丁很简单,在删除指针后将指针置为NULL。

总结

在physmap和SLAB共用内存时,有一些版本的physmap是可执行的,这种利用类型就是ret2dir,此时可以直接将shellcode布置到内核中执行,会让exp变得简单的多。
针对SElinux,在没有开启KASLR时并且有任意写权限时,有一种思路时直接patch掉selinux_enforcing和selinux_enable变量。