Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2023-21768 — 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. | Kitploit
Tools/GitHubGitHub/h1bana/cve-2023-21768
Privilege EscalationSchwachstellenanalyseExploitationBinary-Exploitation
GitHubh1bana/cve-2023-21768

CVE-2023-21768

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.

Repository anzeigen
3vor 3 JahrenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2023-21768

Windows Ancillary Function Driver für WinSock

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.

Patch-Diff und Ursachenanalyse

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

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

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:

  • pre-patch afd.sys version 10.0.22621.608 code1

-post-patch afd.sys version 10.0.22621.1105 code2

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

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

Diese Adresse liegt direkt vor AfdIrpCallDispatch. cross3

Um den Bug auszulösen, werde ich DeviceIoControl mit IOCTL_AFD_NOTIFY_SOCK aufrufen, und dann wird AfdNotifySock aufgerufen.

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 und Debug

Für jeden Treiber wird im Kernel ein DRIVER_OBJECT-Objekt erstellt, das wie folgt definiert ist:

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;

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.

root@kitploit:~
#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: code3

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

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

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

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

Es funktioniert!

bp1

Wie bereits zu Beginn erwähnt, tritt die Schwachstelle auf, wenn wir einen unvalidierten Pointer über eine Struktur übergeben können. Diese Struktur wird direkt aus dem Usermode über lpInBuffer von DeviceIoControl übergeben. Anschließend wird sie an AfdNotifySock als vierten Parameter und an AfdNotifyRemoveIoCompletion als dritten Parameter übergeben.

para1 para2 para3

Da die Struktur unbekannt ist, lasse ich IDA die Struktur automatisch erstellen. Jetzt müssen wir einen Weg finden, Daten in diese Struktur zu übergeben und die erforderlichen Prüfungen zu umgehen, um zum fehlerhaften Codeabschnitt zu gelangen. Beginnen wir mit der Funktion AfdNotifySock:

check1

Zunächst muss die Größe der Struktur 0x30 Bytes betragen.

check2

Die folgenden Werte müssen ungleich 0 sein:

check3

Ein weiterer Punkt: Beim Debuggen habe ich gesehen, dass es bei der vorherigen Prüfung von UserBuffer zum Fehler springt. Daher setze ich diesen Wert beim Aufruf von DeviceIoControl auf NULL. Nachdem ich die obigen Werte gesetzt hatte, konnte ich die Prüfung in check2 passieren.

debug1 debug2

Nächste zu umgehende Prüfung:

check4

ObReferenceObjectByHandle muss STATUS_SUCCESS zurückgeben, um diese Prüfung zu bestehen. Das bedeutet, ich muss ein gültiges Handle übergeben. Bei der Suche habe ich keine Stelle gefunden, die erklärt, wie man einen IoCompletionObjectType erstellt. Also bin ich der Analyse https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/ gefolgt. Verwenden Sie die Funktion NtCreateIoCompletion, um einen IoCompletionObjectType zu erstellen und dessen Handle an ObReferenceObjectByHandle zu übergeben.

Nachdem diese Prüfung umgangen wurde, springt der Programmablauf in eine Schleife. In dieser Schleife gibt es keine Stelle, die zu einem Fehler führt, also setze ich einfach den Wert bei dword20 auf 0x1, um die Schleife zu verlassen.

check5

Nachdem die Schleife verlassen wurde, ruft das Programm AfdNotifyRemoveIoCompletion auf. Fahren wir mit der Analyse der Funktion AfdNotifyRemoveIoCompletion fort:

check6

Zunächst prüft das Programm ein weiteres Feld der Struktur, das ungleich 0 sein muss. Es wird dann mit 0x20 multipliziert und als Parameter für den Aufruf von ProbeForWrite zusammen mit einem anderen Feld der Struktur verwendet. Hier reicht es, eine Adresse im User-Mode-Speicher mit Schreibzugriff und dwLen = 1 zu verwenden. Die letzte Prüfung vor dem Auslösen des Bugs ist, dass der Rückgabewert des Aufrufs von IoRemoveCompletion STATUS_SUCCESS sein muss. Nach einiger Recherche habe ich herausgefunden, dass die Funktion NtRemoveIoCompletion nach dem Aufruf die Funktion IoRemoveCompletion aufruft. Laut dieser Dokumentation handelt es sich bei NtRemoveIoCompletion um einen "waiting call", der beendet wird, wenn mindestens ein Datensatz in einem bestimmten Io Completion Object abgeschlossen ist. Ein Datensatz wird hinzugefügt, wenn der E/A-Vorgang abgeschlossen ist.

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

Außerdem gibt es einen weiteren optionalen Parameter Timeout. Wenn der Timeout-Wert erreicht ist, wird die Funktion beendet. Allerdings reicht es nicht, Timeout = 0 zu setzen, damit die Funktion zurückkehrt; stattdessen wird ein Timeout-Fehlercode zurückgegeben. Wir können die Funktion NtSetIoCompletion verwenden, um den Zähler der ausstehenden E/A-Vorgänge im IoCompletionObjectType um 1 zu erhöhen und die Funktion NtRemoveIoCompletion vor dem Timeout zu beenden. Nach mehrmaligem Testen stellte ich fest, dass der geschriebene Wert immer 0x1 ist.

Exploit - LPE mit IORING

Da wir den Wert 0x1 an eine Kernel-Mode-Adresse schreiben können, können wir diesen Fehler nutzen, um vollständige Lese-/Schreibfähigkeiten auf beliebige Adressen zu erlangen, indem wir den I/O-Ring (einen neuen I/O-Mechanismus von Microsoft) ausnutzen. Yarden Shafir hat eine sehr detaillierte Analyse dieser Methode geschrieben, die Sie hier lesen können. Eine der Operationen, die eine Anwendung ausführen kann, ist die Zuweisung aller Puffer für ihre zukünftigen I/O-Operationen und deren Registrierung beim I/O-Ring. Die vorregistrierten Puffer werden über das I/O-Objekt referenziert:

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;

Wenn eine Sicherheitslücke, wie die in diesem Beitrag beschriebene, es Ihnen erlaubt, die Felder RegBuffersCount und RegBuffers zu aktualisieren/zu ändern, dann können Sie die standardmäßige I/O-Ring-API verwenden, um Kernel-Speicher zu lesen und zu schreiben. Allerdings erfordert die Verwendung der Funktion NtQuerySystemInformation die Medium IL-Berechtigung. Für eine LPE von Low IL aus ist ein anderer Weg erforderlich, um die Kernel-Adresse zu leaken.

Nachdem IoRing->RegBuffers auf den vom Benutzer kontrollierten fakeBuffer zeigt, können wir normale I/O-Ring-Operationen verwenden, um an jede beliebige Adresse zu lesen und zu schreiben, indem wir einen Index in den fakeBuffer als Puffer angeben:

  • Leseoperation + Kernel-Adresse: Der Kernel "liest" aus einer von uns gewählten Datei in die angegebene Kernel-Adresse, was zu einem beliebigen Schreibvorgang führt.
  • Schreiboperation + Kernel-Adresse: Der Kernel "schreibt" Daten von der angegebenen Adresse in eine von uns gewählte Datei, was zu einem beliebigen Lesevorgang führt.

Für ein besseres Verständnis können Sie die Analyse von Yarden Shafir unter dem obigen Link lesen.

Problem

Nachdem ich versucht habe, ein IO-Ring-Objekt zu erstellen und mit dem obigen POC-Code zu schreiben, stürzte Windows nach dem Aufruf von DeviceIOControl ab /_ , also habe ich die Methode verwendet, die Nt-Funktionen direkt aufzurufen (˘・_・˘)

Betroffene Versionen

  • Windows 11 21H1/22H2 vor OS-Build 22000.1455/22621.1105
  • Windows Server 2022 vor OS-Build 20348.1487

Der Patch

  • Der Patch hat Code hinzugefügt, der ProbeForWrite aufruft.
  • Patch-Versionen:
    • 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

Tool herunterladen