
Proof-of-concept exploit per CVE-2023-21768, una vulnerabilità di scrittura arbitraria del kernel nel driver Windows Ancillary Function Driver (AFD.sys) che consente l'escalation dei privilegi locali tramite I/O ring.
Secondo la descrizione dettagliata di CVE-2023-21768 pubblicata da Microsoft Security Response Center (MSRC), la vulnerabilità risiede in Ancillary Function Driver (AFD), il cui file di sistema è afd.sys. Il modulo AFD è il punto di ingresso kernel di WinSock API. In questa analisi lo useremo per sfruttare l'escalation dei privilegi su Windows 11.
Scarica due versioni di afd.sys da Winbindex: una versione recente prima della patch e una dopo la patch. Quindi usa Bindiff per confrontare le due versioni.

Confrontando le due versioni a livello generale, vediamo che solo una funzione presenta differenze: AfdNotifyRemoveIoCompletion. Osserva il dettaglio delle differenze di questa funzione tra le due versioni.

Non ci sono molte differenze tra le due versioni. Nella versione post-patch sono state aggiunte istruzioni assembly per impostare i parametri e chiamare la funzione ProbeForWrite. Secondo la documentazione di Microsoft, questa funzione viene utilizzata per verificare se un indirizzo appartiene effettivamente allo spazio utente, se ha permessi di scrittura e se è correttamente allineato. Analizziamo più nel dettaglio questo codice:
pre-patch afd.sys version 10.0.22621.608

post-patch afd.sys version 10.0.22621.1105

Entrambi controllano il valore di r15_1. Se è uguale a 0, scrivono il valore di var_304 nel puntatore specificato in un campo di struct_1. Se è diverso da 0, viene chiamato ProbeForWrite per assicurarsi che il puntatore punti a un indirizzo valido. Nella versione pre-patch, la scrittura del valore di var_304 nel puntatore avviene dopo, ma manca questo controllo. Da questa patch possiamo dedurre che possiamo chiamare questo codice con il valore di arg3_1->field_18 controllato. Se riusciamo a impostare un indirizzo kernel in field_18, possiamo scrivere var_304 in un indirizzo di memoria del kernel.
=> tipo di bug: scrittura arbitraria Write-Where nel kernel
Ora dobbiamo trovare un modo per attivare il bug. La funzione AfdNotifyRemoveIoCompletion viene chiamata direttamente nella funzione AfdNotifySock.

Analogamente, cercando il riferimento incrociato di AfdNotifySock, notiamo che non viene chiamata direttamente da altre funzioni, ma l'indirizzo della funzione è salvato in un indirizzo in .rdata.

Questo indirizzo si trova subito prima di AfdIrpCallDispatch.

Per attivare il bug, chiamerò DeviceIoControl con IOCTL_AFD_NOTIFY_SOCK e verrà chiamata AfdNotifySock.
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
);
Per ogni driver viene creato un oggetto DRIVER_OBJECT nel kernel, definito come segue:
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;
L'ultimo componente, MajorFunction, è un array contenente le funzioni di dispatch del driver per gestire le comunicazioni tra kernel e usermode. La funzione di dispatch corrispondente alla chiamata DeviceIoControl è salvata in MajorFunction[IRP_MJ_DEVICE_CONTROL].
#define IRP_MJ_DEVICE_CONTROL 0x0e
Dalla funzione DriverEntry di afd.sys possiamo vedere che il driver ha creato l'oggetto device "\Device\Afd":

Assegna MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, quindi quando si chiama DeviceIoControl per comunicare con il kernel, viene richiamata questa funzione.

In AFD esistono due tabelle di dispatch: AfdIrpCallDispatch e AfdImmediateCallDispatch.

È facile notare che AfdDispatchDeviceIoControl calcola un indice tramite IoControlCode e prende il valore corrispondente da AfdIoctlTable per verificarlo con IoControlCode.

Dalla distanza tra l'indirizzo di inizio di AfdImmediateCallDispatch e l'indirizzo in cui è salvato AfdNotifySock, calcoliamo l'indice 73, con codice di controllo 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;
}
it works!

Come detto all'inizio, la vulnerabilità si verifica quando possiamo passare un puntatore non convalidato tramite una struct. Questa struct viene passata direttamente da usermode tramite lpInBuffer di DeviceIoControl. Viene poi passata a AfdNotifySock come quarto parametro e a AfdNotifyRemoveIoCompletion come terzo parametro.