
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.
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.
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.

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.

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

Version post-correctif : afd.sys version 10.0.22621.1105

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.

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.

Cette adresse se trouve juste avant AfdIrpCallDispatch.

Pour déclencher le bug, j'appellerai DeviceIoControl avec IOCTL_AFD_NOTIFY_SOCK et AfdNotifySock sera appelée.
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
);
Pour chaque pilote, un objet DRIVER_OBJECT est créé dans le noyau, défini comme suit :
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].
#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" :

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

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

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.

À 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.

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 !

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.