
Exploit de preuve de concept pour CVE-2026-29923, une escalade de privilèges BYOVD dans pstrip64.sys. Démontre la lecture/écriture de la mémoire physique via IOCTL pour voler le token SYSTEM et générer un shell élevé.
Avertissement : Ce code est fourni à des fins éducatives et de recherche défensive uniquement. Il a été écrit pour approfondir la compréhension de l'exploitation du noyau et aider les défenseurs à se protéger contre des vulnérabilités similaires. Toute utilisation non autorisée, illégale ou malveillante de ce projet est strictement interdite.
Hash : ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
Nom du pilote : pstrip64.sys
CVE : CVE-2026-29923
Une attaque "Bring Your Own Vulnerable Driver" (BYOVD) est une méthode ancienne mais toujours très efficace pour contourner les protections de sécurité modernes de Windows en utilisant un pilote hérité que le système d'exploitation considère encore officiellement comme fiable. Une fois le pilote chargé, l'attaquant exploite ses failles pour combler le fossé entre un processus standard non privilégié et un contrôle total au niveau système.
Plus tôt cette semaine, une nouvelle vulnérabilité a été divulguée dans le pilote pstrip64.sys, suivie sous la référence CVE-2026-29923. Cet article de blog décompose l'ensemble du cycle de vie de l'exploit : de mes recherches initiales sur la vulnérabilité et du développement de la preuve de concept (PoC) aux stratégies d'atténuation concrètes pour les défenseurs sécurisant leurs environnements.
Le pilote pstrip64.sys est un composant en mode noyau hérité lié à EnTech Taiwan PowerStrip (jusqu'à la version 3.90.736). Bien que son objectif légitime soit de permettre des réglages avancés des cartes graphiques, ses privilèges système profonds en font une cible très attractive pour les attaquants.
Lorsque la vulnérabilité a été divulguée pour la première fois, j'ai commencé par analyser sa fonction DriverEntry. Celle-ci sert de routine d'initialisation principale pour le pilote du noyau, créant l'objet de périphérique \Device\PSTRIP64 et l'exposant aux applications en mode utilisateur via le lien symbolique \DosDevices\PSTRIP64. Plus important encore, elle configure la table de dispatch du pilote. L'entrée qui a immédiatement attiré mon attention était l'index 14 (IRP_MJ_DEVICE_CONTROL), qui achemine toutes les requêtes IOCTL fournies par l'utilisateur directement dans la fonction de gestion sub_11340, notre zone d'intérêt principale.
La fonction sub_11340 sert de répartiteur principal des IOCTL, interprétant les requêtes provenant du mode utilisateur.
Parmi tous les IOCTL exposés, 0x80002008 est sans aucun doute le plus intéressant. Alors que les cas par défaut gèrent des interactions mineures avec les ports d'E/S, 0x80002008 agit comme une passerelle vers sub_11000 en passant directement le SystemBuffer à cette fonction.
Cette routine sub_11000 est la preuve irréfutable. Tout d'abord, elle utilise HalTranslateBusAddress pour prendre notre adresse fournie par l'utilisateur et la traduire en une adresse physique système valide. Ensuite, elle ouvre \Device\PhysicalMemory et le mappe en utilisant ZwMapViewOfSection. En codant en dur le handle du processus cible à (HANDLE)0xFFFFFFFFFFFFFFFFLL (qui représente ZwCurrentProcess()), le pilote mappe cette mémoire physique directement dans l'espace d'adressage virtuel de notre processus appelant. Il écrit ensuite cette adresse virtuelle nouvellement mappée dans le SystemBuffer pour la retourner à l'utilisateur, remettant officiellement à notre application un pointeur direct pour lire et écrire dans la mémoire physique.
Avec la vulnérabilité parfaitement comprise et une primitive de lecture/écriture physique établie, j'ai toutes les pièces du puzzle nécessaires. Il est maintenant temps de commencer à écrire la preuve de concept.
Remarque : Cette PoC a été spécifiquement développée et testée sur un environnement Windows 10 22H2. Étant donné que l'exploit repose sur la manipulation brute de la mémoire physique, les décalages de structure du noyau et les limites de la mémoire physique sont actuellement codés en dur pour ma configuration. Pour tester cela sur votre propre machine, vous devez mettre à jour les décalages du noyau Windows et ajuster les plages de numérisation des adresses physiques en fonction de votre version spécifique du système d'exploitation et de votre configuration de RAM.
La première étape de mon exploit consiste à établir une communication avec le pilote. Pour cela, j'ai appelé CreateFileA sur le lien symbolique du pilote (\\.\PSTRIP64). Une fois que j'ai eu un handle valide, j'avais besoin d'une manière propre d'abuser de l'IOCTL 0x80002008 que j'avais analysé précédemment. J'ai créé une fonction wrapper appelée MapPhysicalMemory(). Cette fonction remplit ma structure personnalisée PSTRIP_MAP_REQUEST avec l'adresse physique cible et la longueur du bloc mémoire que je souhaite lire.
J'envoie ensuite cette structure directement au pilote via DeviceIoControl. En cas de succès, le pilote mappe cette mémoire physique directement dans mon application en mode utilisateur et retourne l'adresse de base virtuelle dans le champ OutputResult. Je peux maintenant convertir cette adresse retournée en un pointeur C++ standard, me donnant un accès brut et non privilégié à la RAM physique du système.
Avec ma primitive de lecture/écriture physique pleinement opérationnelle, mon objectif était de trouver les structures de données du noyau qui contiennent les privilèges des processus. Dans Windows, chaque processus en cours d'exécution est représenté par une structure EPROCESS.
Windows alloue les structures EPROCESS dans le pool du noyau en utilisant un identifiant spécifique de 4 octets appelé une étiquette de pool (Pool Tag). Pour les processus, cette étiquette est la chaîne Proc (qui se traduit par 0x636F7250 en hexadécimal). En scannant la RAM physique du système, je pouvais rechercher cette chaîne exacte.
Mon exploit parcourt l'espace mémoire physique de 0x10000000 à 0x140000000, en mappant la mémoire par blocs de 2 Mo (STEP_SIZE = 0x200000). Je convertis chaque bloc mappé en un tableau d'octets bruts et le scanne par morceaux de 16 octets (sizeof(_POOL_HEADER)).
Cependant, trouver simplement l'étiquette Proc dans la mémoire physique ne suffit pas. La mémoire est désordonnée ; cette étiquette pourrait être un artefact résiduel d'un processus terminé, ou simplement des données aléatoires qui correspondent à la valeur hexadécimale. Si je supposais aveuglément que chaque étiquette Proc était une structure EPROCESS valide et que je commençais à modifier la mémoire, je provoquerais immédiatement un écran bleu (BSOD).
Pour garantir la stabilité, j'ai dû valider la structure à l'aide d'heuristiques. Tout d'abord, je calcule le début de la structure EPROCESS (qui se trouve légèrement décalé par rapport à l'étiquette du pool). À partir de là, je vérifie quelques constantes connues pour un processus en cours d'exécution :
0x2 (Priorité normale).0x0.Si toutes ces heuristiques passent, je peux être très confiant que je regarde un processus valide et actif. Je lis ensuite son identifiant de processus unique (PID). Si le PID correspond à mon propre processus d'exploit, je sauvegarde l'adresse physique de son pointeur de jeton. Si le PID est 4 (le processus System de Windows), j'extrais et sauvegarde la valeur réelle de son jeton hautement privilégié.
Enfin, j'aligne l'adresse physique sauvegardée du pointeur de jeton de mon processus sur la limite de 4 Ko la plus proche et j'utilise MapPhysicalMemory() une dernière fois pour mapper uniquement cette page spécifique.
Ensuite, je me rends au décalage exact et je remplace mon jeton par la valeur du jeton système. Instantanément, le noyau Windows traite mon processus d'exploit comme NT AUTHORITY\SYSTEM.
Après avoir dé-mappé la page pour assurer la stabilité du système, j'appelle simplement CreateProcessA pour générer cmd.exe. Étant donné que mon processus actuel est élevé, la nouvelle invite de commande hérite de ces privilèges de premier ordre, complétant ainsi l'attaque avec succès !
Remarque : Un détail critique que j'ai découvert lors de ma phase de débogage précoce est la manière dont le pilote gère le pointeur mappé. En exécutant SystemBuffer->LowPart = (unsigned int)BaseAddress;, le pilote convertit l'adresse de base virtuelle 64 bits en une valeur 32 bits avant de la retourner. Cette troncature perd les bits de poids fort de l'adresse, ce qui a entraîné des violations d'accès immédiates lorsque j'essayais de déréférencer le pointeur dans mon exploit 64 bits. Pour contourner proprement ce problème, j'ai simplement compilé ma PoC en mode utilisateur en tant qu'application 32 bits, garantissant ainsi que le pointeur retourné restait parfaitement valide.
Remarque : Lors de mes premiers tests, j'ai rencontré un cas limite fascinant : ma PoC a réussi à localiser mon processus d'exploit en mémoire, mais elle n'a pas réussi à trouver le processus System (PID 4).
Pour comprendre pourquoi, j'avais besoin d'inspecter directement la mémoire physique. J'ai attaché un débogueur du noyau (WinDbg) et utilisé des commandes pour récupérer l'adresse virtuelle et la base du répertoire du processus System. J'ai ensuite utilisé !vtop pour traduire cette adresse virtuelle en son adresse physique exacte dans la RAM.
Je suis revenu à mon débogueur en mode utilisateur attaché à ma PoC. J'ai défini un point d'arrêt conditionnel sur ma boucle de numérisation mémoire, en lui demandant de suspendre l'exécution au moment où ma fonction MapPhysicalMemory() récupérait le bloc de 2 Mo contenant l'adresse physique du processus System.
Une fois le point d'arrêt atteint, j'ai commencé à inspecter manuellement les octets bruts de la mémoire mappée. C'est là que j'ai découvert un détail crucial concernant les allocations du pool du noyau Windows.
Lorsque Windows alloue de la mémoire pour un processus, il commence par un _POOL_HEADER (contenant notre étiquette Proc), suivi d'un _OBJECT_HEADER, et enfin de la structure EPROCESS elle-même. Pour les applications utilisateur standard, ces en-têtes contiennent des données de suivi supplémentaires, ce qui signifie que la structure EPROCESS réelle commence 0x80 octets après l'étiquette du pool.
Cependant, l'inspection de la mémoire du processus System a révélé une disposition différente. Le processus System manque de certains de ces en-têtes de suivi standard. Le décalage entre l'étiquette Proc et le début de la structure EPROCESS n'était que de 0x40 octets !
La correction était simple. J'ai mis à jour ma PoC pour gérer les deux tailles d'en-tête de pool en parcourant un tableau de décalages possibles (0x40 et 0x80) lorsqu'elle rencontre une étiquette Proc.
La cybersécurité est un jeu sans fin du chat et de la souris entre attaquants et défenseurs. Alors que les attaquants chassent constamment les pilotes vulnérables, les produits de sécurité modernes et les équipes blue disposent de plusieurs moyens robustes pour détecter et bloquer cette opération exacte.
Le moyen le plus efficace d'arrêter une attaque BYOVD (Bring Your Own Vulnerable Driver) est d'empêcher le pilote de se charger en premier lieu.
Si le pilote est déjà chargé, les produits de sécurité peuvent toujours détecter l'exploit pendant la phase de manipulation des jetons.
NT AUTHORITY\SYSTEM sans une chaîne d'authentification légitime est un énorme signal d'alarme.cmd.exe), surtout lorsque le processus parent n'a aucune raison de s'exécuter en tant que SYSTEM.