CVE-2015-3636,由Keen Team发现的Linux内核UAF漏洞(Keen Team的paper:Own your Android! Yet Another Universal Root),可以用来提权,但是在x86_64上只能导致内核panic。影响范围为4.0.3以下的Linux内核,可用于Android 6.0以下的系统Root。

漏洞分析

具体的漏洞成因paper中已经讲得很清楚了,原因就是net/ipv4/ping.c中的ping_unhash函数对于hlist node的错误处理,没有将pprev置NULL导致sokcet中的sk可被free进而导致UAF。这里主要再记录一些poc触发流程的分析。

在socket相关的代码中,有几个结构体对于理解比较关键,首先是struct socket中的ops指针指向的struct proto_ops和struct sock中的sk_prot指向的struct proto,前者代表当前socke所暴露出的对socket的操作接口,后者是针对某一协议的具体操作实现,ops一般都是简单包装后调用对应的sk_prot中的函数,两者通过inetsw_array数组来完成组合,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1016 {
1017 .type = SOCK_DGRAM,
1018 .protocol = IPPROTO_UDP,
1019 .prot = &udp_prot,
1020 .ops = &inet_dgram_ops,
1021 .no_check = UDP_CSUM_DEFAULT,
1022 .flags = INET_PROTOSW_PERMANENT,
1023 },
1024
1025 {
1026 .type = SOCK_DGRAM,
1027 .protocol = IPPROTO_ICMP,
1028 .prot = &ping_prot,
1029 .ops = &inet_dgram_ops,
1030 .no_check = UDP_CSUM_DEFAULT,
1031 .flags = INET_PROTOSW_REUSE,
1032 },

对于ping协议来说,涉及的就是ping_prot与inet_dgram_ops。还有就是struct net_proto_family,这个结构体只有三个成员,其中就包含了对应协议族的socket所用的create函数指针,在其中会完成ops和sk_prot指针的初始化。

漏洞的主要目的在于将sk对象的refcnt减为零从而触发free,同时还必须要先把这个sk hash一次,这样才能进入if(sk_hashed(sk))后面的逻辑中去。而这个hash过程,跟进去具体看一下可以知道,主要就是把目标socket和端口号建立个联系(所以之后对next的pprev指针的操作是不用担心稳定性问题的,一开始还有些疑惑orz)。

关于refcnt的操作,在一开始创建socket的时候,会从sys_socketcall一路调用到inet_create来创建AF_INET对应的socket,并在inet_create中调用sock_init_data,其中使用:

1
2130     atomic_set(&sk->sk_refcnt, 1);

初始化refcnt为1。

关于hash的操作,对于ping协议来说,不是在相对应的ping_hash中做的,ping_hash中什么也没有,而是在ops中的inet_dgram_connect中完成的。具体地是由调用inet_autobind再调用sk_prot中的get_port,在其中完成refcnt++以及hash操作。

所以这样看下来,最快的触发UAF的方式就是先create socket,此时refcnt=1,然后正常connect,此时refcnt=2并且完成hash,然后就可以两次触发disconnect使refcnt=0触发free了。

Exploit

具体的漏洞利用思路Keen Team也已经在paper中很好地秀给我们了,在这里还是记录一些实践中的坑点或是收获。

  • 关于结构体偏移,可以使用gdb来看:
1
(gdb) p &(((strcut sock *)0))->sk_stamp)
  • 关于mc_list,在利用中除了直接覆写sk_prot中的函数指针外,还有一个检查点要过,在inet_release中调用的ip_mc_drop_socket,如果mc_list成员为NULL可以直接从这里出去:
1
2
3
4
5
6
7
8
9
10
2286 void ip_mc_drop_socket(struct sock *sk)
2287 {
2288 struct inet_sock *inet = inet_sk(sk);
2289 struct ip_mc_socklist *iml;
2290 struct net *net = sock_net(sk);
2291
2292 if (inet->mc_list == NULL)
2293 return;
2294
... [...]
  • 关于JOP链,一开始找的时候执着于找到paper中的core gadget,想着能够复用gadget可以省事,结果还不如直接找更快些,真的是被恶心到了…
  • 关于确认task_struct位置,可以利用task_struct中的三个cpu_timers的next和prev指针相同以及real_cred与cred相同的特性来避免硬编码offset,提高exp的通用性。
  • 可实际上即便拿到了u:r:init:s0的context,却依然是受到限制的,这里还需要继续研究
  • 除了使用JOP去改写addr_limit以外,还可以劫持PC到内核中调用了set_fs(KERNEL_DS)的代码后面去,这样就可以让内核为我们修改addr_limit,当然要保证跳过去之后的控制流依然在我们的掌控中,比如像kernel_setsockopt这样的函数就可以:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3331 int kernel_setsockopt(struct socket *sock, int level, int optname,
3332 char *optval, unsigned int optlen)
3333 {
3334 mm_segment_t oldfs = get_fs();
3335 char __user *uoptval;
3336 int err;
3337
3338 uoptval = (char __user __force *) optval;
3339
3340 set_fs(KERNEL_DS);
3341 if (level == SOL_SOCKET)
3342 err = sock_setsockopt(sock, level, optname, uoptval, optlen);
3343 else
3344 err = sock->ops->setsockopt(sock, level, optname, uoptval,
3345 optlen);
3346 set_fs(oldfs);
3347 return err;
3348 }

最后分享一句在看Android的sepolicy文件时看到的注释,看得我害怕地躲在角落瑟瑟发抖:

1
2
65 # - You are running an exploit which switched to the init task credentials
66 # and is then trying to exec a shell or other program. You lose!