Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2023-21768 — Proof-of-concept exploit for CVE-2023-21768, a Windows Ancillary Function Driver (AFD.sys) arbitrary kernel write vulnerability enabling local privilege escalation via I/O ring. | Kitploit
Tools/GitHubGitHub/h1bana/cve-2023-21768
Privilege EscalationVulnerability AnalysisExploitationBinary Exploitation
GitHubh1bana/cve-2023-21768

CVE-2023-21768

Proof-of-concept exploit for CVE-2023-21768, a Windows Ancillary Function Driver (AFD.sys) arbitrary kernel write vulnerability enabling local privilege escalation via I/O ring.

View Repository
33 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2023-21768

Windows Ancillary Function Driver for WinSock

According to the detailed description of CVE-2023-21768 published by the Microsoft Security Response Center (MSRC), the vulnerability exists in the Ancillary Function Driver (AFD), whose system file is afd.sys. The AFD module is the kernel entry point for the WinSock API. In this analysis, I will use it to exploit privilege escalation on Windows 11.

Patch Diff and Root Cause Analysis

Download two versions of afd.sys from Winbindex: one version just before the patch, and one version after the patch. Then use Bindiff to compare these two versions. bindiff

Comparing the overall two versions, we see that only one function differs: AfdNotifyRemoveIoCompletion. Look at the details of the differences in this function between the two versions. bindiff

There are not many differences between the two versions. In the post-patch version, additional assembly instructions have been added to set parameters and call the ProbeForWrite function. According to Microsoft documentation, this function checks whether an address truly belongs to user-mode, has write permission, and is correctly aligned. Let's analyze this code in more detail:

  • pre-patch afd.sys version 10.0.22621.608 code1

  • post-patch afd.sys version 10.0.22621.1105 code2

Both check the value of r15_1; if it is 0, write the value of var_304 to the pointer specified in a field of struct_1. If it is non-zero, ProbeForWrite is called to ensure the pointer points to a valid address. In the pre-patch version, the value at var_304 is then written to the pointer, but this check is missing. From this patch, we can guess that we can call this code with a controlled value of arg3_1->field_18. If we can set a kernel address at field_18, we can write var_304 to a kernel memory address.

=> bug type: arbitrary kernel Write-Where

Now we need to find a way to trigger the bug. The function AfdNotifyRemoveIoCompletion is called directly in the function AfdNotifySock. crossRef

Similarly, searching for cross references of AfdNotifySock shows that it is not called directly from any other function, but the function address is stored at an address in .rdata cross2

This address is located just before AfdIrpCallDispatch. cross3

To trigger the bug, I will call DeviceIoControl with IOCTL_AFD_NOTIFY_SOCK, and AfdNotifySock will be called.

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 and debug

For each driver, a DRIVER_OBJECT object is created in the kernel, defined as follows:

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;

The last member MajorFunction is an array of the driver's dispatch functions to handle communication between kernel and usermode. The dispatch function corresponding to calling DeviceIoControl is stored at MajorFunction[IRP_MJ_DEVICE_CONTROL].

root@kitploit:~
#define IRP_MJ_DEVICE_CONTROL           0x0e

From the DriverEntry function of afd.sys, we can see that the driver created the device object "\Device\Afd": code3

It sets MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, so when calling DeviceIoControl to communicate with the kernel, this function is called. code4

In AFD, there are two dispatch tables: AfdIrpCallDispatch and AfdImmediateCallDispatch. dispatchtable1 dispatchtable2

It is easy to see that AfdDispatchDeviceIoControl computes the subscript via the IoControlCode and retrieves the corresponding value from AfdIoctlTable to verify against the IoControlCode. 1

From the distance between the start address of AfdImmediateCallDispatch and the address storing AfdNotifySock, we calculate the index as 73, with control code 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;
}

it works!

bp1

As mentioned earlier, the vulnerability occurs when we can pass an unvalidated pointer through a struct. This struct is passed directly from usermode via lpInBuffer of DeviceIoControl. It is then passed to AfdNotifySock as the 4th parameter and then to AfdNotifyRemoveIoCompletion as the 3rd parameter.

para1 para2 para3

Since I don't know what the struct contains, I let IDA automatically create the struct. Now I need to find a way to pass data into this struct and bypass the necessary checks to reach the vulnerable code. Start from the AfdNotifySock function:

check1

First, the size of the struct must be 0x30 bytes.

check2

The following values must be non-zero:

check3

Another thing: during debugging, I saw it jumps to fail at the UserBuffer check earlier, so when calling DeviceIoControl, set this value to NULL. After setting the above values, I passed check2.

debug1 debug2

Next check to bypass:

check4

ObReferenceObjectByHandle must return STATUS_SUCCESS to pass this check. That means I need to pass a valid handle. I searched and found no documentation on how to create an IoCompletionObjectType. So I followed the analysis at https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/. Use the NtCreateIoCompletion function to create an IoCompletionObjectType and pass its handle to ObReferenceObjectByHandle. After bypassing this check, the program flow enters a loop. In this loop, there is no place leading to a fail path, so I simply set the value at dword20 to 0x1 to exit the loop.

check5

After exiting the loop, the program calls AfdNotifyRemoveIoCompletion. Continue analyzing the AfdNotifyRemoveIoCompletion function:

check6

First, it checks another field of the struct; it must be non-zero. Then it is multiplied by 0x20, and used as a parameter to call ProbeForWrite along with another field of the struct. Here, we just need to use a user-mode address with write permission and dwLen = 1. The final check before triggering the vulnerability is that the return value of calling IoRemoveCompletion must be STATUS_SUCCESS. After searching, I found that the NtRemoveIoCompletion function, when called, invokes IoRemoveCompletion. According to this document, the NtRemoveIoCompletion function acts as a "waiting call" and completes when at least one completion record exists in a specified Io Completion Object. A record is added when I/O completes.

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

Additionally, there is an optional parameter Timeout; when the timeout value is reached, the function returns. However, just setting timeout = 0 is not enough for the function to return successfully; it returns a timeout error code. We can use the NtSetIoCompletion function to increment the count of pending IO operations in the IoCompletionObjectType by 1 and cause the NtRemoveIoCompletion to finish before timeout. After multiple attempts, I found that the value written is always 0x1.

exploit - LPE with IORING

With the ability to write the value 0x1 to a kernel-mode address, we can use this vulnerability to gain full arbitrary read/write ability by leveraging I/O ring (a new I/O mechanism introduced by Microsoft). Yarden Shafir wrote a very detailed analysis of this method; you can read it here. One of the operations an application can perform is to allocate all buffers for its future I/O operations, then register them with the I/O ring. Pre-registered buffers are referenced through an I/O object:

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;

If a security vulnerability, like the one discussed in this article, allows you to update/modify the RegBuffersCount and RegBuffers fields, then standard I/O ring API can be used to read and write kernel memory. However, using the NtQuerySystemInformation function requires Medium IL privilege. To LPE from Low IL, another method to leak a kernel address is needed.

After IoRing->RegBuffers points to a user-controlled fakeBuffer, we can use standard I/O ring operations to create read and write to any address we want by specifying an index into the fake buffer:

  • Read operation + kernel address: the kernel will "read" from a file we choose into the specified kernel address, leading to arbitrary write.
  • Write operation + kernel address: the kernel will "write" data from the specified address to a file we choose, leading to arbitrary read.

For a better understanding, you can read Yarden Shafir's analysis at the link above.

issue

After trying to create an IO Ring object and write using the above POC code, Windows crashed after calling DeviceIOControl /_ . So I used a method that directly calls Nt functions (˘・_・˘)

Affect range

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

The patch

  • The patch added code to call ProbeForWrite
  • Patched versions:
    • 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

Download Tool