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-21768 — Preuve de concept d'exploit pour CVE-2023-21768, une vulnérabilité d'écriture arbitraire dans le noyau du pilote de fonction auxiliaire Windows (AFD.sys) permettant une élévation de privilèges locale via I/O ring. | Kitploit
Outils/GitHubGitHub/h1bana/cve-2023-21768
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationExploitation de Binaires
GitHubh1bana/cve-2023-21768

CVE-2023-21768

Preuve de concept d'exploit pour CVE-2023-21768, une vulnérabilité d'écriture arbitraire dans le noyau du pilote de fonction auxiliaire Windows (AFD.sys) permettant une élévation de privilèges locale via I/O ring.

Voir le dépôt
3il y a 3 ansPas encore vérifié

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

CVE-2023-21768

Windows Ancillary Function Driver for WinSock

Selon la description détaillée de CVE-2023-21768 publiée par le Microsoft Security Response Center (MSRC), la vulnérabilité existe dans le Ancillary Function Driver (AFD), dont le nom de fichier système est afd.sys. Le module AFD est le point d'entrée noyau de l'API WinSock. Dans cette analyse, je l'utiliserai pour effectuer une escalade de privilèges sous Windows 11.

Analyse du correctif et cause racine

Téléchargez deux versions de afd.sys depuis Winbindex, une version proche avant le correctif et une version après le correctif. Utilisez ensuite Bindiff pour comparer ces deux versions. bindiff

En comparant globalement les deux versions, on voit qu'une seule fonction est différente : AfdNotifyRemoveIoCompletion. Examinez plus en détail les différences de cette fonction entre les deux versions. bindiff

Il n'y a pas trop de différences entre les deux versions. Dans la version post-correctif, des instructions assembleur supplémentaires sont ajoutées pour définir les paramètres et appeler la fonction ProbeForWrite. Selon la documentation de Microsoft, cette fonction sert à vérifier qu'une adresse est bien en mode utilisateur, accessible en écriture et correctement alignée. Analysons ce code plus en détail :

  • Version pré-correctif : afd.sys version 10.0.22621.608 code1

  • Version post-correctif : afd.sys version 10.0.22621.1105 code2

Les deux vérifient la valeur de r15_1 ; si elle est égale à 0, la valeur de var_304 est écrite dans le pointeur spécifié à un champ de struct_1. Si différente de 0, ProbeForWrite est appelée pour s'assurer que le pointeur pointe vers une adresse valide. Dans la version pré-correctif, la valeur de var_304 est ensuite écrite dans le pointeur, mais cette vérification manque. Grâce à ce correctif, on peut supposer que nous pouvons appeler ce code avec la valeur de arg3_1->field_18 contrôlée. Si l'on peut définir une adresse noyau dans field_18, on peut écrire var_304 à une adresse mémoire noyau.

=> type de bug : écriture arbitraire dans le noyau (Write-Where)

Maintenant, il faut trouver comment déclencher le bug. La fonction AfdNotifyRemoveIoCompletion est appelée directement dans AfdNotifySock. crossRef

De même, en recherchant les références croisées de AfdNotifySock, on voit qu'elle n'est appelée directement par aucune autre fonction, mais son adresse est stockée à une adresse dans .rdata. cross2

Cette adresse se trouve juste avant AfdIrpCallDispatch. cross3

Pour déclencher le bug, j'appellerai DeviceIoControl avec IOCTL_AFD_NOTIFY_SOCK et AfdNotifySock sera appelée.

root@kitploit:~
BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,
  [in]                DWORD        dwIoControlCode,
  [in, optional]      LPVOID       lpInBuffer,
  [in]                DWORD        nInBufferSize,
  [out, optional]     LPVOID       lpOutBuffer,
  [in]                DWORD        nOutBufferSize,
  [out, optional]     LPDWORD      lpBytesReturned,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

Reverse et debug

Pour chaque pilote, un objet DRIVER_OBJECT est créé dans le noyau, défini comme suit :

root@kitploit:~
typedef struct _DRIVER_OBJECT {
  CSHORT             Type;
  CSHORT             Size;
  PDEVICE_OBJECT     DeviceObject;
  ULONG              Flags;
  PVOID              DriverStart;
  ULONG              DriverSize;
  PVOID              DriverSection;
  PDRIVER_EXTENSION  DriverExtension;
  UNICODE_STRING     DriverName;
  PUNICODE_STRING    HardwareDatabase;
  PFAST_IO_DISPATCH  FastIoDispatch;
  PDRIVER_INITIALIZE DriverInit;
  PDRIVER_STARTIO    DriverStartIo;
  PDRIVER_UNLOAD     DriverUnload;
  PDRIVER_DISPATCH   MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;

Le dernier élément MajorFunction est un tableau contenant les fonctions de dispatch du pilote pour gérer les communications entre le noyau et le mode utilisateur. La fonction de dispatch correspondant à l'appel DeviceIoControl est stockée dans MajorFunction[IRP_MJ_DEVICE_CONTROL].

root@kitploit:~
#define IRP_MJ_DEVICE_CONTROL           0x0e

À partir de la fonction DriverEntry de afd.sys, nous pouvons voir que le pilote a créé l'objet de périphérique "\Device\Afd" : code3

Affecte MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, donc lorsqu'on appelle DeviceIoControl pour communiquer avec le noyau, cette fonction est invoquée. code4

Dans AFD, il y a deux tables de dispatch : AfdIrpCallDispatch et AfdImmediateCallDispatch. dispatchtable1 dispatchtable2

On voit facilement que AfdDispatchDeviceIoControl calcule un indice via IoControlCode et récupère la valeur correspondante à partir de AfdIoctlTable pour la vérifier avec IoControlCode. 1

À partir de la distance entre l'adresse de début de AfdImmediateCallDispatch et l'adresse où est stockée AfdNotifySock, on calcule un indice de 73, avec le code de contrôle 0x12127. ioctl

root@kitploit:~
int main() {
    WSADATA WSAData;
    SOCKET s;
    SOCKADDR_IN sa;
    int ierr;

    WSAStartup(0x2, &WSAData);
    s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    memset(&sa, 0, sizeof(sa));
    sa.sin_port = htons(135);
    sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    sa.sin_family = AF_INET;
    ierr = connect(s, (const struct sockaddr*)&sa, sizeof(sa));

    char outBuf[100];
    DWORD bytesRet;
    DWORD inbuf1[100];

    memset(inbuf1, 0, sizeof(inbuf1));

    DeviceIoControl((HANDLE)s, 0x12127, (LPVOID)inbuf1, 0x30, outBuf, 0, &bytesRet, NULL);
    return 0;
}

Ça fonctionne !

bp1

Comme dit au début, la vulnérabilité survient lorsque nous pouvons passer un pointeur non validé via une structure. Cette structure est transmise directement depuis le mode utilisateur via lpInBuffer de DeviceIoControl. Elle est ensuite passée à AfdNotifySock en tant que 4ᵉ paramètre, puis à AfdNotifyRemoveIoCompletion en tant que 3ᵉ paramètre.

para1 para2 para3

Comme on ne connaît pas encore la composition de la structure, je laisse IDA la créer automatiquement. Il faut maintenant trouver comment passer des données dans cette structure et contourner les vérifications nécessaires pour atteindre le code vulnérable. Commençons par la fonction AfdNotifySock :

check1

D'abord, la taille de la structure doit être de 0x30 octets.

check2

Les valeurs doivent être non nulles :

check3

En outre, lors du débogage, j'ai vu qu'il revenait en échec à la vérification précédente de UserBuffer, donc lors de l'appel à DeviceIoControl, cette valeur doit être définie à NULL. Après avoir défini les valeurs ci-dessus, j'ai réussi à passer la vérification 2.

debug1 debug2

Vérification suivante à contourner :

check4

ObReferenceObjectByHandle doit retourner STATUS_SUCCESS pour passer cette vérification. Il faut donc fournir un handle valide. En cherchant, je n'ai trouvé nulle part comment créer un IoCompletionObjectType. J'ai donc suivi l'analyse https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/. J'utilise la fonction NtCreateIoCompletion pour créer un IoCompletionObjectType et passer son handle à ObReferenceObjectByHandle. Après avoir contourné cette vérification, le flux du programme entre dans une boucle. Dans cette boucle, il n'y a pas de chemin menant à un échec, donc j'ai simplement défini la valeur à dword20 à 0x1 pour sortir de la boucle.

check5

Après être sorti de la boucle, le programme appelle AfdNotifyRemoveIoCompletion. Continuons l'analyse de la fonction AfdNotifyRemoveIoCompletion :

check6

D'abord, le programme vérifie un autre champ de la structure, il doit être non nul. Ensuite, il est multiplié par 0x20, puis utilisé comme paramètre pour appeler ProbeForWrite avec un autre champ de la structure. Ici, il suffit d'utiliser une adresse en mémoire utilisateur accessible en écriture avec dwLen = 1. La dernière vérification avant de pouvoir déclencher le bug est que la valeur retournée par l'appel à IoRemoveCompletion doit être STATUS_SUCCESS. Après quelques recherches, j'ai appris que la fonction NtRemoveIoCompletion, une fois appelée, invoque IoRemoveCompletion. Selon cette documentation, la fonction NtRemoveIoCompletion est un "appel bloquant" qui se termine lorsqu'au moins un enregistrement est terminé dans un Io Completion Object spécifié. L'enregistrement est ajouté lorsque l'opération d'E/S est terminée.

root@kitploit:~
NtRemoveIoCompletion(
  IN HANDLE               IoCompletionHandle,
  OUT PULONG              CompletionKey,
  OUT PULONG              CompletionValue,
  OUT PIO_STATUS_BLOCK    IoStatusBlock,
  IN PLARGE_INTEGER       Timeout OPTIONAL );

De plus, il y a un paramètre optionnel Timeout ; lorsque le délai d'attente est atteint, la fonction se termine. Cependant, définir simplement timeout = 0 ne suffit pas pour que la fonction retourne, elle retournera un code d'erreur de timeout. Nous pouvons utiliser la fonction NtSetIoCompletion pour incrémenter le compteur des E/S en attente dans IoCompletionObjectType de 1 et terminer la fonction NtRemoveIoCompletion avant le timeout. Après plusieurs essais, j'ai constaté que la valeur écrite est toujours 0x1.

Exploitation - LPE avec IORING

Avec la possibilité d'écrire la valeur 0x1 à une adresse mémoire noyau, nous pouvons utiliser ce bug pour obtenir une capacité complète de lecture/écriture arbitraire en exploitant l'anneau d'E/S (un nouveau mécanisme d'E/S introduit par Microsoft). Yarden Shafir a écrit une analyse très détaillée de cette méthode, que vous pouvez lire ici. L'une des opérations qu'une application peut effectuer est d'allouer tous les tampons pour ses futures opérations d'E/S, puis de les enregistrer avec l'anneau d'E/S. Les tampons pré-enregistrés sont référencés via l'objet I/O :

root@kitploit:~
typedef struct _IORING_OBJECT
{
    USHORT Type;
    USHORT Size;
    NT_IORING_INFO UserInfo;
    PVOID Section;
    PNT_IORING_SUBMISSION_QUEUE SubmissionQueue;
    PMDL CompletionQueueMdl;
    PNT_IORING_COMPLETION_QUEUE CompletionQueue;
    ULONG64 ViewSize;
    ULONG InSubmit;
    ULONG64 CompletionLock;
    ULONG64 SubmitCount;
    ULONG64 CompletionCount;
    ULONG64 CompletionWaitUntil;
    KEVENT CompletionEvent;
    UCHAR SignalCompletionEvent;
    PKEVENT CompletionUserEvent;
    ULONG RegBuffersCount;
    PVOID RegBuffers;
    ULONG RegFilesCount;
    PVOID* RegFiles;
} IORING_OBJECT, *PIORING_OBJECT;

Si une vulnérabilité de sécurité, comme celle mentionnée dans cet article, permet de mettre à jour/modifier les champs RegBuffersCount et RegBuffers, il est possible d'utiliser l'API d'anneau d'E/S standard pour lire et écrire dans le noyau. Cependant, l'utilisation de la fonction NtQuerySystemInformation nécessite le privilège Medium IL. Pour une escalade de privilèges depuis Low IL, il faut un moyen de fuiter une adresse noyau.

Une fois que IoRing->RegBuffers pointe vers un faux tampon contrôlé par l'utilisateur, nous pouvons utiliser les opérations d'anneau d'E/S normales pour créer des lectures et écritures à n'importe quelle adresse en spécifiant un index dans le faux tampon comme tampon :

  • Opération de lecture + adresse noyau : le noyau "lira" d'un fichier choisi vers l'adresse noyau spécifiée, entraînant une écriture arbitraire.
  • Opération d'écriture + adresse noyau : le noyau "écrira" les données de l'adresse spécifiée dans un fichier choisi, entraînant une lecture arbitraire.

Pour mieux comprendre, vous pouvez lire l'analyse de Yarden Shafir au lien ci-dessus.

Problème

Après avoir essayé de créer un objet IO Ring et d'écrire avec le code POC ci-dessus, Windows a planté après avoir appelé DeviceIOControl /_ \ j'ai donc utilisé une méthode d'appel direct aux fonctions Ntfunctions (˘・_・˘) (voir [https://www.x86matthew.com/view_post?id=ntsockets]).

Plage affectée

  • Windows 11 21H1/22H2 avant les builds OS 22000.1455/22621.1105
  • Windows Server 2022 avant le build OS 20348.1487

Le correctif

  • Le correctif a ajouté un appel à ProbeForWrite
  • Versions corrigées :
    • Windows 11 21H1 : KB5022287 (OS Build 22000.1455)
    • Windows 11 22H2 : KB5022303 (OS Build 22621.1105)
    • Windows Server 2022 : KB5022291 (OS Build 20348.1487)

POC

Télécharger l’outil