
CVE-2023-32233
Le code affecté provient du noyau Linux officiel depuis https://kernel.org/ et fait partie du composant Netfilter nf_tables (net/netfilter/nf_tables_api.c).
Netfilter nf_tables permet de mettre à jour sa configuration sous forme d'opération atomique. Lors de l'utilisation de cette fonctionnalité, les clients en mode utilisateur envoient des requêtes par lots contenant une liste d'opérations de base. Netfilter nf_tables traite alors toutes les opérations du lot comme une seule transaction. Lors du traitement du lot, Netfilter nf_tables vérifie les mises à jour de l'état de la configuration pour s'assurer que chaque opération de base successive est valide, et cela prend également en compte les mises à jour d'état de toutes les opérations précédentes du lot. Cependant, la vérification actuellement implémentée est insuffisante.
Dans notre scénario spécifique, nous partons d'une configuration Netfilter nf_tables
qui possède une nft_rule avec une expression lookup sur un nft_set anonyme,
et où le nft_set anonyme contient certains éléments. Ensuite, nous envoyons une requête par lots
contenant les deux opérations de base suivantes :
NFT_MSG_DELRULE pour supprimer la nft_rule.lookup et le
nft_set anonyme.NFT_MSG_DELSETELEM pour supprimer l'un des éléments du
nft_set anonyme supprimé.La version actuelle de Netfilter nf_tables accepte la requête par lots ci-dessus.
Elle appelle ensuite nf_tables_commit_release() qui ajoute les ressources libérées à
nf_tables_destroy_list. La nf_tables_destroy_list est ensuite traitée par
nf_tables_trans_destroy_work() qui commence par désallouer les ressources liées à
l'opération NFT_MSG_DELRULE en appelant :
nft_commit_release()
nf_tables_rule_destroy()
nf_tables_expr_destroy()
expr->ops->destroy() qui pointe vers nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() qui désalloue la mémoire utilisée par `nft_set`
avant de traiter l'opération NFT_MSG_DELSETELEM, où une référence au
nft_set désalloué est accédée via nft_trans_elem_set() lors des
appels suivants :
nft_commit_release()
nf_tables_set_elem_destroy()
nft_set_elem_ext()
Dans nft_set_elem_ext() ci-dessus, l'emplacement mémoire du nft_set
désalloué est accédé pour déterminer l'emplacement de nft_set_ext :
static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
void *elem)
{
return elem + set->ops->elemsize;
}
pour les opérations qui suivent. Ainsi, lorsque la valeur de set->ops->elemsize
est corrompue, un emplacement mémoire inattendu peut être interprété comme
une liste de nft_expr à détruire :
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
Exploiter la vulnérabilité ci-dessus nécessite de gagner une course avec nf_tables_trans_destroy_work() qui s'exécute à partir d'un thread de travail en arrière-plan du noyau Linux. Cela semble compliquer l'exploitation pratique, même avant de prendre en compte les mitigations existantes, telles que le renforcement de l'allocateur de slabs du noyau, l'Address Space Layout Randomization (KASLR) et surtout l'Intégrité du Flux de Contrôle. Cependant, le PoC joint prouve qu'il est toujours possible d'obtenir une exploitation raisonnablement fiable en pratique.
Afin d'exploiter la vulnérabilité, nous devons modifier le contenu de la mémoire
du nft_set après qu'il a été désalloué sous nf_tables_rule_destroy(), mais
avant qu'il ne soit utilisé sous nf_tables_set_elem_destroy(). Les deux
nf_tables_rule_destroy() et nf_tables_set_elem_destroy() sont appelées dans
une seule invocation de nf_tables_trans_destroy_work() qui s'exécute à partir
d'un thread de travail en arrière-plan du noyau Linux. De plus, le bloc mémoire
désalloué n'est généralement disponible pour réutilisation que sur le même cœur de CPU.
En faisant la course avec nf_tables_trans_destroy_work(), nous améliorons nos chances en
ajoutant un délai contrôlé pour le thread de travail en arrière-plan entre ses appels
à nf_tables_rule_destroy() et nf_tables_set_elem_destroy(). Pour cela, nous insérons
une opération supplémentaire pour détruire un autre nft_set contenant un grand
nombre d'éléments. De plus, nous maintenons tous les autres cœurs de CPU occupés,
de sorte que le thread de travail en arrière-plan soit susceptible d'être planifié sur un cœur
de CPU spécifique, afin de pouvoir tenter d'allouer une nouvelle structure depuis le même cœur
de CPU juste après avoir désalloué nft_set sous nf_tables_rule_destroy(). Notre objectif
est d'allouer un nouveau nft_set de type différent pour réutiliser l'emplacement mémoire du
nft_set désalloué sous nf_tables_rule_destroy().
Le nouveau type de nft_set est choisi pour utiliser une valeur différente pour
set->ops->elemsize. Ainsi, lorsque le thread de travail en arrière-plan appelle finalement
nf_tables_set_elem_destroy() pour traiter l'opération NFT_MSG_DELSETELEM, il
interprète son argument elem de manière incorrecte, de sorte que le
nft_set_ext *ext corrompu se trouve quelques octets après l'emplacement correct. Cela signifie que
certains champs de données contrôlés par l'utilisateur du nft_set_ext original sont désormais
interprétés comme des en-têtes, entraînant une confusion de type.
Une façon d'abuser de cette confusion de type est de fabriquer les en-têtes
nft_set_ext corrompus avec des valeurs de décalage telles que
nf_tables_set_elem_destroy() interprète le contenu de tout bloc mémoire adjacent
comme la liste des nft_expr à détruire via les appels suivants :
nft_set_elem_expr_destroy()
__nft_set_elem_expr_destroy()
nf_tables_expr_destroy()
expr->ops->destroy()
À ce stade de l'exploitation, nous n'avons pas encore de détails sur la disposition
de la mémoire du noyau. Il n'est donc pas possible de fabriquer des adresses de pointeur
absolues. Cependant, lors de la fabrication des en-têtes nft_set_ext corrompus, nous pouvons
toujours utiliser des décalages hors limites, de sorte que expr->ops->destroy() soit appelé
sur certains nft_expr valides dans les blocs mémoire adjacents.
Pour cela, nous pulvérisons des expressions nft_log, avec un NFTA_LOG_PREFIX contrôlé.
Ce nft_log->prefix est ensuite désalloué par nft_log_destroy() une fois
que expr->ops->destroy() est appelé :
static void nft_log_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_log *priv = nft_expr_priv(expr);
struct nf_loginfo *li = &priv->loginfo;
if (priv->prefix != nft_log_null_prefix)
kfree(priv->prefix);
Notez que nous pouvons toujours accéder et même désallouer à nouveau cette mémoire via
l'autre référence provenant de l'expression nft_log pulvérisée.
De plus, nous pouvons également contrôler la taille de nft_log->prefix, de sorte qu'elle
puisse être allouée depuis n'importe quel slab kmalloc-{8, ..., 192}. Enfin, la
mémoire référencée est interprétée comme une chaîne de caractères par le noyau, donc
pas besoin de se soucier des corruptions lorsque nous superposons différents objets dessus.
C'est essentiellement la fin du jeu.
Un inconvénient est que les caractères NULL terminent nft_log->prefix, donc
nous ne pouvons pas lire au-delà des octets NULL lors de la fuite du contenu mémoire. Cela
est résolu à l'étape suivante, où nous allouons nft_object->udata pour réutiliser
le bloc mémoire nft_log->prefix et détruisons l'expression nft_log. Cela
désalloue la mémoire nft_object->udata, mais nous pouvons toujours utiliser le
pointeur pendant nft_object->udata pour fuir le contenu mémoire sans
restrictions sur les octets NULL.
En cherchant des structures appropriées pour les étapes suivantes, nous avons opté pour
nft_expr allouée depuis nft_dynset_new(). Celles-ci vivent dans les mêmes slabs que
nft_log->prefix et nft_object->udata. Et aussi, nous avons un contrôle
raisonnable sur la taille d'allocation, de sorte que plus tard nous pourrions facilement passer
à des slabs de taille différente si nécessaire.
Pour utiliser ces structures, nous créons un filtre de paquets avec l'expression nft_dynset.
Et lorsque nous envoyons des paquets sur l'interface loopback,
l'expression nft_dynset appelle nft_dynset_new() pour créer de nouveaux éléments pour le
nft_set associé. Les éléments créés sont des expressions avec état des types
suivants :
nft_counter pour obtenir l'emplacement de nf_tables.ko dans la mémoire du noyau.
La structure inclut un pointeur vers nft_counter_ops dans le module
nf_tables.ko du noyau. Nous fuyons ce pointeur en lisant nft_object->udata.
nft_quota pour une lecture et écriture mémoire arbitraires.
Nous pouvons désallouer et réallouer à plusieurs reprises nft_object->udata pour modifier
le pointeur nft_quota->consumed. Ensuite, nous effectuons l'opération
NFT_MSG_GETSETELEM qui appelle nft_quota_do_dump() pour lire le contenu de la
mémoire référencée et transmet le résultat sous forme d'attribut NFTA_QUOTA_CONSUMED
dans le résultat. Quant aux écritures, nous envoyons simplement des paquets sur
l'interface loopback, où nft_quota_do_eval() appelle :
static inline bool nft_overquota(struct nft_quota *priv,
const struct sk_buff *skb)
{
return atomic64_add_return(skb->len, priv->consumed) >=
Nous utilisons la lecture mémoire arbitraire ci-dessus pour obtenir l'adresse de base du noyau. Et ensuite, nous procédons à la modification de la sous-chaîne "sbin" du nom de chemin "/sbin/modprobe", afin qu'elle soit remplacée par "/tmp". Le nom de chemin résultant "//tmp/modprobe" est alors utilisé par le noyau pour démarrer un processus avec les privilèges root, où nous contrôlons le contenu du fichier.
Notez que nous n'avons fait aucun effort intentionnel pour contourner l'Intégrité du Flux de Contrôle. Cependant, pour chacune des étapes d'exploitation, nous avons consciemment choisi les primitives les plus flexibles et les plus robustes. Il s'avère que notre sélection a évité toute primitive qui pourrait potentiellement être bloquée par l'Intégrité du Flux de Contrôle. Nous sommes maintenant curieux de confirmer par des tests que l'exploit résultant fonctionne vraiment contre les systèmes avec des mitigations d'Intégrité du Flux de Contrôle.
pour modifier nft_quota->consumed.