
Proof-of-Concept-Exploit für CVE-2023-21768, eine Schwachstelle im Windows Ancillary Function Driver (AFD.sys), die einen beliebigen Kernel-Schreibvorgang ermöglicht und eine lokale Privilegienausweitung über den I/O-Ring erlaubt.
Laut der detaillierten Beschreibung von CVE-2023-21768, veröffentlicht vom Microsoft Security Response Center (MSRC), existiert die Schwachstelle im Ancillary Function Driver (AFD), dessen Dateiname im System afd.sys ist. Das AFD-Modul ist der Kernel-Einstiegspunkt der WinSock API. In dieser Analyse werde ich es verwenden, um eine Privilegieneskalation unter Windows 11 auszunutzen.
Laden Sie zwei Versionen von afd.sys von Winbindex herunter, eine Version kurz vor dem Patch und eine Version nach dem Patch. Verwenden Sie dann Bindiff, um diese beiden Versionen zu vergleichen.

Ein Vergleich der beiden Versionen zeigt, dass nur eine Funktion einen Unterschied aufweist: AfdNotifyRemoveIoCompletion. Sehen Sie sich die Unterschiede dieser Funktion zwischen den beiden Versionen genauer an.

Es gibt nicht allzu viele Unterschiede zwischen den beiden Versionen. In der Post-Patch-Version wurden zusätzliche Assembly-Anweisungen hinzugefügt, um die Parameter zu setzen und die Funktion ProbeForWrite aufzurufen. Laut Dokumentation von Microsoft wird diese Funktion verwendet, um zu überprüfen, ob eine Adresse tatsächlich zum User-Mode gehört, Schreibzugriff hat und korrekt ausgerichtet ist. Detailliertere Analyse dieses Codes:
afd.sys version 10.0.22621.608

-post-patch afd.sys version 10.0.22621.1105

Beide überprüfen den Wert von r15_1: Wenn er 0 ist, wird der Wert von var_304 in den Zeiger geschrieben, der in einem Feld von struct_1 angegeben ist. Wenn er nicht 0 ist, wird ProbeForWrite aufgerufen, um sicherzustellen, dass der Zeiger auf eine gültige Adresse zeigt. In der pre-patch-Version wird der Wert von var_304 danach in den Zeiger geschrieben – diese Überprüfung fehlt. Anhand dieses Patches können wir vermuten, dass wir diesen Codeabschnitt mit einem kontrollierten Wert von arg3_1->field_18 aufrufen können. Wenn wir eine Kernel-Adresse in field_18 setzen können, können wir var_304 in diesen Kernel-Speicherbereich schreiben.
=> Fehlertyp: beliebiges Kernel-Write-Where
Jetzt müssen wir einen Weg finden, den Bug auszulösen. Die Funktion AfdNotifyRemoveIoCompletion wird direkt in der Funktion AfdNotifySock aufgerufen.

Ebenso sehen wir bei der Suche nach Cross-Referenzen von AfdNotifySock, dass es nicht direkt von einer anderen Funktion aufgerufen wird, aber die Funktionsadresse wird an einer Adresse in .rdata gespeichert.

Diese Adresse liegt direkt vor AfdIrpCallDispatch.

Um den Bug auszulösen, werde ich DeviceIoControl mit IOCTL_AFD_NOTIFY_SOCK aufrufen, und dann wird AfdNotifySock aufgerufen.
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
);
Für jeden Treiber wird im Kernel ein DRIVER_OBJECT-Objekt erstellt, das wie folgt definiert ist:
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;
Das letzte Element MajorFunction ist ein Array, das die Dispatch-Funktionen des Treibers zur Verarbeitung der Kommunikation zwischen Kernel und Usermode enthält. Die Dispatch-Funktion, die dem Aufruf von DeviceIoControl entspricht, wird in MajorFunction[IRP_MJ_DEVICE_CONTROL] gespeichert.
#define IRP_MJ_DEVICE_CONTROL 0x0e
Aus der DriverEntry-Funktion von afd.sys können wir sehen, dass der Treiber das Device-Objekt "\Device\Afd" erstellt hat:

Setzt MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, sodass beim Aufruf von DeviceIoControl zur Kommunikation mit dem Kernel diese Funktion aufgerufen wird.

In AFD gibt es zwei Dispatch-Tabellen: AfdIrpCallDispatch und AfdImmediateCallDispatch.

Es ist leicht zu sehen, dass AfdDispatchDeviceIoControl den Index über den IoControlCode berechnet und den entsprechenden Wert aus der AfdIoctlTable holt, um ihn mit dem IoControlCode zu verifizieren.

Aus dem Abstand zwischen der Startadresse von AfdImmediateCallDispatch und der Adresse, an der AfdNotifySock gespeichert ist, berechnen wir den Index 73, der den Control-Code 0x12127 hat.

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;
}
Es funktioniert!
