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

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.

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

post-patch afd.sys version 10.0.22621.1105

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.

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

This address is located just before AfdIrpCallDispatch.

To trigger the bug, I will call DeviceIoControl with IOCTL_AFD_NOTIFY_SOCK, and AfdNotifySock will be called.
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
);
For each driver, a DRIVER_OBJECT object is created in the kernel, defined as follows:
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].
#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":

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

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

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.

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

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.
