HeroCTF 2024 - buafflet
Description
You have one bullet, use it wisely…
Files index
buafllet.koconfigImageinitramfs.cpio.gzrun.sh
Overview Kernel Module
The kernel module, seen from the perspective of the IDA decompiler.
__int64 __fastcall buafllet_ioctl(file *fp, __int64 cmd, unsigned __int64 arg)
{
__int64 v3; // x1
char *v5; // x19
unsigned __int64 v6; // x0
unsigned __int64 v7; // x1
size_t v8; // x0
unsigned __int64 StatusReg; // x0
unsigned __int64 v10; // x0
__int64 v11; // x3
if ( (_DWORD)cmd == 18 )
{
if ( bullet )
{
StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0));
if ( (*(_DWORD *)(StatusReg + 44) & 0x200000) != 0
|| (v11 = *(_QWORD *)StatusReg, v10 = arg, (v11 & 0x4000000) != 0) )
{
v10 = ((__int64)(arg << 8) >> 8) & arg;
}
if ( v10 > 0xFFFFFFFFFC00LL )
return -14LL;
if ( _arch_copy_to_user(arg & 0xFF7FFFFFFFFFFFFFLL) )
return -14LL;
}
return 0LL;
}
if ( (unsigned int)cmd > 0x12 )
{
if ( (_DWORD)cmd == 19 )
{
v5 = bullet;
if ( bullet )
{
v6 = _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0));
v7 = arg;
if ( ((_DWORD *)(v6 + 44) & 0x200000) != 0 || ((_QWORD *)v6 & 0x4000000) != 0 )
arg &= (__int64)(arg << 8) >> 8;
if ( arg > 0xFFFFFFFFFC00LL )
{
v8 = 1024LL;
LABEL_13:
memset(v5, 0, v8);
return -14LL;
}
v8 = _arch_copy_from_user(v5, v7 & 0xFF7FFFFFFFFFFFFFLL, 1024LL);
if ( v8 )
{
v5 = &v5[-v8 + 1024];
goto LABEL_13;
}
}
}
return 0LL;
}
if ( (_DWORD)cmd == 16 )
return ioctl_get_bullet(arg);
if ( (_DWORD)cmd != 17 )
return 0LL;
mutex_lock(&g_mutex, cmd, arg);
if ( bullet_used )
{
kfree(bullet);
mutex_unlock(&g_mutex, v3);
printk("Now what you gonna do?");
return 0LL;
}
else
{
mutex_unlock(&g_mutex, &bullet_used);
return -22LL;
}
The module implements 4 different ioctls.
CMD_ALLOC (0x10)
Creates a single allocation via kzalloc and then sets the global variable bullet_used to 1. The size can vary between 0x490 and 0x3000.
CMD_FREE (0x11)
Frees the previously allocated chunk.
This ioctl contains a bug: we can free the chunk multiple times, since the pointer is not cleared. Easy Use After Free :)
CMD_READ (0x12)
Reads the content of our allocation. This gets us a UAF read primitive.
CMD_WRITE (0x13)
Edits the content of the chunk we have created. This gets us a UAF write primitive. That means we have both r/w UAF primitives :D.
This challenge has quite a simple vulnerability with really powerful primitives. However the kernel is compiled with some annoying mitigations enabled.
CONFIG_SLUB=y
CONFIG_SLAB_MERGE_DEFAULT=n
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_RANDOM_KMALLOC_CACHES=y
CONFIG_USERFAULTFD=n
CONFIG_FUSE_FS=n
In particular, CONFIG_RANDOM_KMALLOC_CACHES=y creates multiple kmalloc caches for the same size!
When making an allocation with kmalloc, an appropriate cache (for our size) is selected from the available ones,
using a secret random key (generated at boot) and the address of the call site from which kmalloc was invoked,
thus making it basically impossible to guess which cache will be selected for our allocation.
Even two identical objects may not be allocated in the same cache if they are allocated in different functions!
Exploitation Overview
There are two ways to exploit this challenge:
- Control Flow hijacking
- Data Only
We opted for the second solution, because it seemed easier to solve it that way.
The technique we used some sort of cross caching attack.
We create an allocation greater than 8k bytes, so that kmalloc will allocate chunk of memory just for
our object, partially bypassing the random caches mitigation.
if (size > KMALLOC_MAX_CACHE_SIZE) /* KMALLOC_MAX_CACHE_SIZE is 0x2000 (or 8k bytes) */
return kmalloc_large_node_noprof(size, flags, node);
The exploitation flow goes as follows:
- Create the allocation larger than 8k bytes via the
CMD_ALLOCioctl.

- Free the allocation via the
CMD_FREEioctl, so that the kernel will return the pages used by our chunk back to the page allocator.

- Now we use the
capsetsyscall in combination withio_uring’s personalities, to spray a f*ck ton of cred structs (NOTE: The cred struct is the data structure that is used to manage access permissions). Anio_uringpersonality is a set of permission we can use when making operations, such as opening a file, withio_uring. To spam creds, what we did is:- Allocate a cred struct using
capset. - Register it as a personality for use with
io_uring. - Optionally, store the ID of the personality somewhere, we’ll need it later (optional since the ID can be guessed).
- Allocate a cred struct using

-
Keep checking if we managed to get a cache collision by using the
CMD_READioctl. -
When we get a collision, we overwrite the cred structs to get root privileges (by setting all the
*uidand*gidfields to0). -
We try to open
flag.txtby using the IDs of theio_uringpersonalities we registered earlier, until we find one that open the file!

This was the first time we used this method.
Most of the io_uring code we literally copied from here.
You can find the commented exploit here.
Final considerations
I really enjoyed this challenge. The vulnerability was pretty easy, but the exploitation teqnique was kinda hard for me, since I’m new to kernel pwning. Also shout out to my friend markx86 that managed to solve it on time :)
Feel free to contact us if anything is unclear or if I missed something I’m open to any tips!