Ormai abbiamo coperto avvio del kernel, debug e meccanismi di sicurezza: tutto ciò che serve per iniziare a lavorare con i kernel exploit. Da qui in avanti vedremo come scrivere davvero l’exploit e come eseguirlo su qemu.

Esecuzione su qemu

Scrivere, compilare ed eseguire l’exploit direttamente dentro qemu è scomodo, perché ogni crash del kernel costringe a ripartire da capo. Per questo conviene compilare il programma in locale e poi trasferirlo nella VM.
Dato che farlo a mano tutte le volte è noioso, è utile preparare uno script template. Per esempio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
gcc exploit.c -o exploit
mv exploit root
cd root; find . -print0 | cpio -o --null --format=newc > ../debugfs.cpio
cd ../

qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
-no-reboot \
-cpu qemu64 \
-gdb tcp::12345 \
-smp 1 \
-monitor /dev/null \
-initrd debugfs.cpio \
-net nic,model=virtio \
-net user

Lo script compila exploit.c, copia il binario nell’albero del root filesystem, rigenera un cpio e poi avvia qemu. Per non toccare il rootfs.cpio originale, usa un’immagine separata chiamata debugfs.cpio, ma puoi chiamarla come preferisci.
Ricorda inoltre che, quando si crea il cpio, lavorare senza privilegi root può alterare owner e permessi dei file. Se usi uno script simile, eseguilo con attenzione.

Ora prova a mettere in exploit.c un programma banale:

1
2
3
4
5
6
#include <stdio.h>

int main() {
puts("Hello, World!");
return 0;
}

Se esegui transfer.sh, comparirà un errore. Come mai?

Un exploit compilato con GCC non parte

L’immagine distribuita usa uClibc, non la libc del tuo host. Se compili con GCC nel tuo ambiente, il binario risultante verrà linked dinamicamente contro una libc diversa e non partirà nella VM.
Quindi, quando vuoi eseguire l’exploit su qemu, ricordati di compilarlo staticamente:

1
gcc exploit.c -o exploit -static

Con questa modifica il programma dovrebbe funzionare.

Con il linking statico l'exploit parte

Esecuzione su una macchina remota: usare musl-gcc

A questo punto sappiamo già eseguire l’exploit su qemu. Nel nostro ambiente di esercizio la rete è disponibile, quindi volendo si potrebbe trasferire l’exploit con wget o strumenti simili.
In alcuni CTF, però, l’ambiente è così minimale da non avere nemmeno accesso alla rete. In questi casi bisogna trasferire il binario usando solo i comandi disponibili in busybox. Di solito si usa base64, ma un binario statico compilato con GCC può pesare da centinaia di KB a decine di MB, quindi il trasferimento diventa molto lento. La causa principale è che il linking statico porta dentro una grossa fetta della libc.

Per ridurre la dimensione con GCC bisognerebbe evitare del tutto la libc e implementare tutto tramite system call e inline assembly, il che è decisamente scomodo.
Per questo molti CTFer usano musl-gcc per i kernel exploit. Puoi scaricarlo, compilarlo e installarlo dal sito ufficiale:

https://www.musl-libc.org/

Dopo l’installazione, modifica la riga di compilazione in transfer.sh in qualcosa del genere. Ovviamente il percorso dipende da dove hai installato musl-gcc:

1
/usr/local/musl/bin/musl-gcc exploit.c -o exploit -static

Nell’ambiente dell’autore, il classico programma “Hello, World!” pesava 851 KB compilato con gcc, ma solo 18 KB con musl-gcc. Se vuoi ridurre ancora le dimensioni, puoi anche togliere i simboli di debug con strip.

Lupetto

Alcuni header, soprattutto quelli collegati al kernel Linux, non sono disponibili in musl-gcc. In quei casi puoi impostare opportunamente gli include path oppure compilare prima in assembly con gcc e poi linkare con musl-gcc. Così sfrutti il frontend di gcc ma mantieni il binario piccolo.
$ gcc -S sample.c -o sample.S
$ musl-gcc sample.S -o sample.elf

Quando hai finito questa parte, conviene prepararsi anche uno script che trasferisca il binario remoto via base64 (per esempio attraverso nc). Nei CTF torna utilissimo, quindi meglio averne una propria versione pronta.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from ptrlib import *
import time
import base64
import os

def run(cmd):
sock.sendlineafter("$ ", cmd)
sock.recvline()

with open("./root/exploit", "rb") as f:
payload = bytes2str(base64.b64encode(f.read()))

#sock = Socket("HOST", PORT) # remote
sock = Process("./run.sh")

run('cd /tmp')

logger.info("Uploading...")
for i in range(0, len(payload), 512):
print(f"Uploading... {i:x} / {len(payload):x}")
run('echo "{}" >> b64exp'.format(payload[i:i+512]))
run('base64 -d b64exp > exploit')
run('rm b64exp')
run('chmod +x exploit')

sock.interactive()

Dopo un po’ l’upload dovrebbe completarsi, come nello screenshot seguente.

Risultato dell'esecuzione di upload.py

In questo sito non serve davvero fare upload, perché l’obiettivo è sperimentare in locale. Ma nei CTF conviene ricordarsi questa tecnica.