Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2023-2598 — Analyse technique et exploit de preuve de concept pour CVE-2023-2598, une vulnérabilité d'élévation de privilèges du noyau Linux dans l'enregistrement des tampons d'io_uring, avec une explication détaillée des internes de Compound Page et de folio. | Kitploit
Outils/GitHubGitHub/cainiao159357/cve-2023-2598
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationArticles et RechercheApprentissage et ÉducationExploitation de Binaires
GitHubcainiao159357/cve-2023-2598

CVE-2023-2598

Analyse technique et exploit de preuve de concept pour CVE-2023-2598, une vulnérabilité d'élévation de privilèges du noyau Linux dans l'enregistrement des tampons d'io_uring, avec une explication détaillée des internes de Compound Page et de folio.

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôt
il y a 1 anPas encore vérifié

Élévation de privilèges CVE-2023-2598

Comprendre les mécanismes de Compound Page et folio sous Linux à travers CVE-2023-2598, puis voir si nous pouvons par la suite exploiter le 1-day CVE-2023-6560.

Compound Page (huge page)

La mémoire ne cesse de croître, mais l'unité de base d'allocation de pages sous Linux reste à 4 Ko, ce qui devient limité. C'est pourquoi les pages composées (compound pages) ont été introduites pour résoudre ce problème. Une page composée regroupe plusieurs pages en un ensemble, en combinant deux ou plusieurs pages physiquement contiguës en une seule unité, considérée à bien des égards comme une seule page plus grande. Elles sont le plus souvent utilisées pour créer de grandes pages, employées dans hugetlbfs ou le sous-système de grandes pages transparentes (transparent huge pages), mais elles apparaissent également dans d'autres scénarios. Les pages composées peuvent être utilisées comme mémoire anonyme ou comme tampons dans le noyau ; cependant, elles ne peuvent pas apparaître dans le cache de pages (page cache), qui ne peut traiter que des pages individuelles.

L'allocation d'une page composée se fait en appelant alloc_pages() avec le drapeau __GFP_COMP et un nombre de cadres de page supérieur à 1, c'est-à-dire un ordre d'au moins 1. C'est ce qu'impose le mécanisme d'implémentation des pages composées.

Note : une page composée est nécessairement physiquement contiguë.

Le flag du premier page est marqué PG_head, indiquant qu'il s'agit de la tête de la page composée ;

Toutes les pages suivantes possèdent deux attributs : mapping et compound_head, et via compound_head on détermine s'il s'agit d'une page de queue ou de tête. Voir la fonction compound_head() pour plus de détails ;

La deuxième page contient davantage d'informations sur la page composée, ce qui explique pourquoi l'ordre minimal d'une page composée est 1 ;

root@kitploit:~
static inline unsigned long _compound_head(const struct page *page)
{
        unsigned long head = READ_ONCE(page->compound_head);
 
        if (unlikely(head & 1))
                return head - 1;
        return (unsigned long)page;
}

On voit donc que ce champ contient non seulement le drapeau, mais aussi un pointeur vers la page de tête.

Ainsi, lorsqu'on a une page, on peut facilement déterminer s'il s'agit d'une page composée et, le cas échéant, si c'est la page de tête ou une page de queue. Mais il manque une information cruciale : la taille de cette page composée. Si on ignore sa taille, lors de la libération de la page composée, on a besoin de connaître cette taille. Toutes ces informations sont stockées dans le champ lru de la première page de queue : la taille (ordre) de la page composée est d'abord convertie en type pointeur, puis stockée dans lru.prev, et le destructeur est stocké dans lru.next.

Tant que l'on connaît la page de tête et la taille de la page composée, on peut libérer correctement cette grande page, car les pages composées sont toutes physiquement contiguës.

La structure est illustrée dans le diagramme ci-dessous :

img

folio

Un folio peut être considéré comme une enveloppe autour d'une page, sans surcoût. Un folio peut être une seule page ou une page composée.

img

L'image ci-dessus est un schéma de la structure page, qui occupe 64 octets et gère les informations telles que flags, lru, mapping, index, private, {ref_, map_}count, memcg_data. Lorsque la page est une page composée, ces flags et autres informations se trouvent dans la page de tête, tandis que les pages de queue réutilisent les champs de gestion compound_{head, mapcount, order, nr, dtor}.

root@kitploit:~
struct folio {
        /* private: ne pas documenter l'union anonyme */
        union {
                struct {
        /* public: */
                        unsigned long flags;
                        struct list_head lru;
                        struct address_space *mapping;
                        pgoff_t index;
                        void *private;
                        atomic_t _mapcount;
                        atomic_t _refcount;
#ifdef CONFIG_MEMCG
                        unsigned long memcg_data;
#endif
        /* private: l'union avec struct page est transitoire */
                };
                struct page page;
        };
};

Dans la définition de la structure folio, les champs flags, lru, etc. sont identiques à ceux de page, ce qui permet une union. Ainsi, on peut utiliser directement folio->flags au lieu de folio->page->flags.

root@kitploit:~
#define page_folio(p)           (_Generic((p),                          \
        const struct page *:    (const struct folio *)_compound_head(p), \
        struct page *:          (struct folio *)_compound_head(p)))

#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n)    nth_page(&(folio)->page, n)

À première vue, page_folio peut sembler déroutant, mais en réalité cela équivaut à :

root@kitploit:~
switch (typeof(p)) {
  case const struct page *:
    return (const struct folio *)_compound_head(p);
  case struct page *:
    return (struct folio *)_compound_head(p)));
}

Grâce à la macro page_folio, on découvre qu'un folio est en fait la page de tête d'une page composée. Quand on convertit un folio en page, folio->page permet d'obtenir la page de tête, et folio_page(folio, n) permet d'obtenir une page de queue.

Mais à quoi sert le folio ? Essentiellement pour des raisons de développement et d'efficacité. Sans folio, une fonction ne peut pas savoir si la page courante est la page de tête, et doit donc appeler _compound_head. Si le chemin d'exécution est long et que chaque fonction sur ce chemin utilise _compound_head, cela affecte l'efficacité. En revanche, si une fonction n'accepte que des paramètres de type struct folio *, ce folio pointe déjà vers la page de tête, donc la fonction n'a plus besoin d'appeler _compound_head.

Ainsi, le folio remplit principalement trois rôles :

  1. Réduire les appels redondants à _compound_head.
  2. Donner une indication aux développeurs : voir un folio, c'est être sûr qu'il s'agit de la page de tête.
  3. Corriger potentiellement des bugs causés par l'utilisation de pages de queue.

Principe de la vulnérabilité

Dans io_uring, la fonction io_uring_register_buffer comporte la logique suivante :

image-20240830214419290

Lorsque l'utilisateur transmet plus d'une page, io_uring vérifie si le buffer passé est un folio. La vérification consiste à utiliser page_folio() pour obtenir la page de tête de page[i]. Si la page de tête de page[i] est égale à page[0], on considère qu'elles appartiennent à la même table de pages composées.

En général, ce traitement ne pose pas de problème, mais il existe un cas particulier : si l'utilisateur utilise mmap pour mapper la même table de pages physiques sur des adresses virtuelles contiguës, la condition est également satisfaite, et l'on entre dans cette branche :

image-20240830225137539

Dans ce cas, l'utilisateur n'a alloué qu'une seule page physique, mais la taille (size) correspond à celle des adresses virtuelles contiguës, ce qui fait que size peut être supérieure à la zone physique réellement allouée. Cela permet des lectures et écritures hors limites.

Exploitation

On pulvérise (spray) des structures cred, puis on utilise cette interface de lecture/écriture hors limites pour modifier l'UID.

Comparé à l'exploit trouvé en ligne, cet exploit modifie l'UID et n'a donc pas de dépendance d'adresse ; toute machine présentant cette vulnérabilité peut utiliser cet exploit.

root@kitploit:~
// (le code en C reste inchangé)

Réflexion

Notez ce passage de code :

image-20240830230232004

Si l'utilisateur enregistre effectivement une page composée, io_uring n'augmente pas le compteur de référence pour les pages suivantes. Si l'utilisateur annule le mappage au milieu de cette page composée, la zone mémoire correspondante, n'ayant qu'une seule référence, sera complètement libérée. Cependant, la taille (size) enregistrée dans io_uring n'est pas modifiée, ce qui permettrait des lectures/écritures hors limites via io_uring. Malheureusement, après mes tests, Linux ne permet pas d'annuler le mappage au milieu d'une page composée, ce qui est logique, car sinon la gestion des pages deviendrait très compliquée.

Télécharger l’outil