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 |
|
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 |
|
Se esegui transfer.sh, comparirà un errore. Come mai?
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.
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:
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.

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 | from ptrlib import * |
Dopo un po’ l’upload dovrebbe completarsi, come nello screenshot seguente.
In questo sito non serve davvero fare upload, perché l’obiettivo è sperimentare in locale. Ma nei CTF conviene ricordarsi questa tecnica.