# Patch Tuesday -> Exploit Wednesday: Pwning Windows Ancillary Function Driver for WinSock (afd.sys) in 24 Hours
‘Patch Tuesday, Exploit Wednesday’ is an old hacker adage that refers to the weaponization of vulnerabilities the day after monthly security patches become publicly available. As security improves and exploit mitigations become more sophisticated, the amount of research and development required to craft a weaponized exploit has increased. This is especially relevant for memory corruption vulnerabilities.
![](https://images.seebug.org/1679471776549-w331s)
*Figure 1 — Exploitation timeline*
However, with the addition of new features (and memory-unsafe C code) in the Windows 11 kernel, ripe new attack surfaces can be introduced. By honing in on this newly introduced code, we demonstrate that vulnerabilities that can be trivially weaponized still occur frequently. In this blog post, we analyze and exploit a vulnerability in the Windows Ancillary Function Driver for Winsock, `afd.sys`, for Local Privilege Escalation (LPE) on Windows 11. Though neither of us had any previous experience with this kernel module, we were able to diagnose, reproduce, and weaponize the vulnerability in about a day. You can find the exploit code [here](https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768).
## Patch Diff and Root Cause Analysis
Based on the details of [CVE-2023-21768](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21768) published by the Microsoft Security Response Center (MSRC), the vulnerability exists within the Ancillary Function Driver (AFD), whose binary filename is `afd.sys`. The AFD module is the kernel entry point for the [Winsock API](https://en.wikipedia.org/wiki/Winsock). Using this information, we analyzed the driver version from December 2022 and compared it to the version newly released in January 2023. These samples can be obtained individually from [Winbindex](https://winbindex.m417z.com/) without the time-consuming process of extracting changes from Microsoft patches. The two versions analyzed are shown below.
- AFD.sys / Windows 11 22H2 / 10.0.22621.608 (December 2022)
- AFD.sys / Windows 11 22H2 / 10.0.22621.1105 (January 2023)
[Ghidra](https://ghidra-sre.org/) was used to create binary exports for both of these files so they could be compared in [BinDiff.](https://www.zynamics.com/bindiff.html) An overview of the matched functions is shown below.
![](https://images.seebug.org/1679471803898-w331s)
*Figure 2 — Binary comparison of AFD.sys*
Only one function appeared to have been changed, `afd!AfdNotifyRemoveIoCompletion`. This significantly sped up our analysis of the vulnerability. We then compared both of the functions. The screenshots below show the changed code pre- and post-patch when looking at the decompiled code in [Binary Ninja.](https://binary.ninja/)
Pre-patch, `afd.sys version 10.0.22621.608`.
![](https://images.seebug.org/1679471836952-w331s)
*Figure 3 — afd!AfdNotifyRemoveIoCompletion pre-patch*
Post-patch, `afd.sys version 10.0.22621.1105`.
![](https://images.seebug.org/1679471858108-w331s)
*Figure 4 — afd!AfdNotifyRemoveIoCompletion post-patch*
This change shown above is the only update to the identified function. Some quick analysis showed that a check is being performed based on `PreviousMode`. If `PreviousMode` is zero (indicating that the call originates from the kernel) a value is written to a pointer specified by a field in an unknown structure. If, on the other hand, `PreviousMode` is not zero then `ProbeForWrite` is called to ensure that the pointer set out in the field is a valid address that resides within user mode.
This check is missing in the pre-patch version of the driver. Since the function has a specific switch statement for `PreviousMode`, the assumption is that the developer intended to add this check but forgot (we all lack coffee sometimes).
From this update, we can infer that an attacker can reach this code path with a controlled value at `field_0x18` of the unknown structure. If an attacker is able to populate this field with a kernel address, then it’s possible to create an arbitrary kernel Write-Where primitive. At this point, it is not clear what value is being written, but any value could potentially be used for a Local Privilege Escalation primitive.
The function prototype itself contains both the `PreviousMode` value and a pointer to the unknown structure as the first and third arguments respectively.
![](https://images.seebug.org/1679471927680-w331s)
*Figure 5 — afd!AfdNotifyRemoveIoCompletion function prototype*
## Reverse Engineering
We now know the location of the vulnerability, but not how to trigger the execution of the vulnerable code path. We’ll do some reverse engineering before beginning to work on a Proof-of-Concept (PoC).
First, the vulnerable function was cross-referenced to understand where and how it was used.
![](https://images.seebug.org/1679471986947-w331s)
*Figure 6 — afd!AfdNotifyRemoveIoCompletion cross-references*
A single call to the vulnerable function is made in `afd!AfdNotifySock`.
We repeat the process, looking for cross-references to `AfdNotifySock`. We find no direct calls to the function, but its address appears above a table of function pointers named `AfdIrpCallDispatch`.
![](https://images.seebug.org/1679472013631-w331s)
*Figure 7 — afd!AfdIrpCallDispatch*
This table contains the dispatch routines for the AFD driver. Dispatch routines are used to handle requests from Win32 applications by calling `DeviceIoControl`. The control code for each function is found in `AfdIoctlTable`.
However, the pointer above is not within the `AfdIrpCallDispatch` table as we expected. From [Steven Vittitoe](https://twitter.com/bool101)’s [Recon talk](https://recon.cx/2015/slides/recon2015-20-steven-vittitoe-Reverse-Engineering-Windows-AFD-sys.pdf) slides, we discovered that there are actually two dispatch tables for AFD. The second being `AfdImmediateCallDispatch`. By calculating the distance between the start of this table and where the pointer to `AfdNotifySock` is stored, we can calculate the index into the `AfdIoctlTable` which shows the control code for the function is `0x12127`.
![](https://images.seebug.org/1679472084317-w331s)
*Figure 8 — afd!AfdIoctlTable*
It’s worth noting that it’s the last input/output control (IOCTL) code in the table, indicating that `AfdNotifySock` is likely a new dispatch function that has been recently added to the AFD driver.
At this point, we had a couple of options. We could reverse engineer the corresponding Winsock API in a user space to better understand how the underlying kernel function was called, or reverse engineer the kernel code and call into it directly. We didn’t actually know which Winsock function corresponded to `AfdNotifySock`, so we opted to do the latter.
We came across some [code](https://www.x86matthew.com/view_post?id=ntsockets) published by [x86matthew](https://twitter.com/x86matthew) that performs socket operations by calling into the AFD driver directly, forgoing the Winsock library. This is interesting from a stealth perspective, but for our purposes, it is a nice template to create a handle to a TCP socket to make IOCTL requests to the AFD driver. From there, we were able to reach the target function, as evidenced by reaching a breakpoint set in [WinDbg](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) while kernel debugging.
![](https://images.seebug.org/1679472139858-w331s)
*Figure 9 — afd!AfdNotifySock breakpoint*
Now, refer back to the function prototype for `DeviceIoControl`, through which we call into the AFD driver from user space. One of the parameters, `lpInBuffer`, is a user mode buffer. As mentioned in the previous section, the vulnerability occurs because the user is able to pass an unvalidated pointer to the driver within an unknown data structure. This structure is passed in directly from our user mode application via the lpInBuffer parameter. It’s passed into `AfdNotifySock` as the fourth parameter, and into `AfdNotifyRemoveIoCompletion` as the third parameter.
At this point, we don’t know how to populate the data in `lpInBuffer`, which we’ll call `AFD_NOTIFYSOCK_STRUCT`, in order to pass the checks required to reach the vulnerable code path in `AfdNotifyRemoveIoCompletion`. The remainder of our reverse engineering process consisted of following the execution flow and examining how to reach the vulnerable code.
Let’s go through each of the checks.
The first check we encounter is at the beginning of `AfdNotifySock`:
![](https://images.seebug.org/1679472165796-w331s)
*Figure 10 — afd!AfdNotifySock size check*
This check tells us that the size of the `AFD_NOTIFYSOCK_STRUCT` should be equal to `0x30` bytes, otherwise the function fails with `STATUS_INFO_LENGTH_MISMATCH`.
The next check validates values in various fields in our structure:
![](https://images.seebug.org/1679472250685-w331s)
*Figure 11 — afd!AfdNotifySock structure validation*
At the time we didn’t know what any of the fields correspond to, so we pass in a `0x30` byte array filled with `0x41` bytes (`AAAAAAAAA...`).
The next check we encounter is after a call to `ObReferenceObjectByHandle`. This function takes the first field of our input structure as its first argument.
![](https://images.seebug.org/1679472272586-w331s)
*Figure 12 — afd!AfdNotifySock call nt!ObReferenceObjectByHandle*
The call must return success in order to proceed to the correct code execution path, which means that we must pass in a valid handle to an `IoCompletionObject`. There is no officially documented way to create an object of that type via Win32 API. However, after some searching, we found an undocumented NT function `NtCreateIoCompletion` that did the job.
Afterward, we reach a loop whose counter was one of the values from our struct:
![](https://images.seebug.org/1679472295234-w331s)
*Figure 13 — afd!AfdNotifySock loop*
This loop checked a field from our structure to verify it contained a valid user mode pointer and copied data to it. The pointer is incremented after each iteration of the loop. We filled in the pointers with valid addresses and set the counter to 1. From here, we were able to finally reach the vulnerable function `AfdNotifyRemoveIoCompletion`.
![](https://images.seebug.org/1679472318532-w331s)
*Figure 14 — afd!AfdNotifyRemoveIoCompletion call*
Once inside `AfdNotifyRemoveIoCompletion`, the first check is on another field in our structure. It must be non-zero. It’s then multiplied by 0x20 and passed into `ProbeForWrite` along with another field in our struct as the pointer parameter. From here we can fill in the struct further with a valid user mode pointer (`pData2`) and field `dwLen = 1` (so that the total size passed to `ProbeForWrite` is equal 0x20), and the checks pass.
![](https://images.seebug.org/1679472337661-w331s)
*Figure 15 — afd! Afd!AfdNotifyRemoveIoCompletion field check*
Finally, the last check to pass before reaching the target code is a call to `IoRemoveCompletion` which must return 0 (`STATUS_SUCCESS`).
This function will block until either:
- A completion record becomes available for the `IoCompletionObject` parameter
- The timeout expires, which is passed in as a parameter of the function
We control the timeout value via our structure, but simply setting a timeout of 0 is not sufficient for the function to return success. In order for this function to return with no errors, there must be at least one completion record available. After some research, we found the undocumented function `NtSetIoCompletion`, which manually increments the I/O pending counter on an `IoCompletionObject`. Calling this function on the `IoCompletionObject` we created earlier ensures that the call to `IoRemoveCompletion` returns `STATUS_SUCCESS`.
![](https://images.seebug.org/1679472363525-w331s)
*Figure 16 — afd!AfdNotifyRemoveIoCompletion check return nt!IoRemoveIoCompletion*
## Triggering Arbitrary Write-Where
Now that we can reach the vulnerable code, we can fill the appropriate field in our structure with an arbitrary address to write to. The value that we write to the address comes from an integer whose pointer is passed into the call to `IoRemoveIoCompletion`. `IoRemoveIoCompletion` sets the value of this integer to the return value of a call to `KeRemoveQueueEx`.
![](https://images.seebug.org/1679472383349-w331s)
*Figure 17 — nt!KeRemoveQueueEx return value*
![](https://images.seebug.org/1679472397472-w331s)
*Figure 18 — nt!KeRemoveQueueEx return use*
In our proof of concept, this write value is always equal to `0x1`. We speculated that the return value of `KeRemoveQueueEx` is the number of items removed from the queue, but did not investigate further. At this point, we had the primitive we needed and moved on to finishing the exploit chain. We later confirmed that this guess was correct, and the write value can be arbitrarily incremented by additional calls to `NtSetIoCompletion` on the `IoCompletionObject`.
## LPE with IORING
With the ability to write a fixed value (0x1) at an arbitrary kernel address, we proceeded to turn this into a full arbitrary kernel Read/Write. Because this vulnerability affects the latest versions of Windows 11([22H2](https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information)), we chose to leverage a [Windows I/O ring](https://learn.microsoft.com/en-us/windows/win32/api/ioringapi/) object corruption to create our primitive. [Yarden Shafir](https://twitter.com/yarden_shafir) has written a number of excellent posts on Windows I/O rings and also developed and disclosed the [primitive](https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/) that we leveraged in our exploit chain. As far as we are aware this is the first instance where this primitive has been used in a public exploit.
When an I/O Ring is initialized by a user two separate structures are created, one in user space and one in kernel space. These structures are shown below.
The kernel object maps to `nt!_IORING_OBJECT` and is shown below.
![](https://images.seebug.org/1679472422151-w331s)
*Figure 19 — nt!_IORING_OBJECT initialization*
Note that the kernel object has two fields, `RegBuffersCount` and `RegBuffers`, which are zeroed on initialization. The count indicates how may I/O operations can possibly be queued for the I/O ring. The other parameter is a pointer to a list of the currently queued operations.
On the user space side, when calling `kernelbase!CreateIoRing` you get back an I/O Ring handle on success. This handle is a pointer to an undocumented structure (HIORING). Our definition of this structure was obtained from the research done by Yarden Shafir.
```
typedef struct _HIORING {
HANDLE handle;
NT_IORING_INFO Info;
ULONG IoRingKernelAcceptedVersion;
PVOID RegBufferArray;
ULONG BufferArraySize;
PVOID Unknown;
ULONG FileHandlesCount;
ULONG SubQueueHead;
ULONG SubQueueTail;
};
```
If a vulnerability, such as the one covered in this blog post, allows you to update the `RegBuffersCount` and `RegBuffers` fields, then it is possible to use standard I/O Ring APIs to read and write kernel memory.
As we saw above, we are able to use the vulnerability to write `0x1` at any kernel address that we like. To set up the I/O ring primitive we can simply trigger the vulnerability twice.
In the first trigger we set the `RegBufferCount` to `0x1`.
![](https://images.seebug.org/1679472468573-w331s)
*Figure 20 — nt!_IORING_OBJECT first time triggering the bug*
And in the second trigger we set `RegBuffers` to an address that we can allocate in user space (like `0x0000000100000000`).
![](https://images.seebug.org/1679472487021-w331s)
*Figure 21 — nt!_IORING_OBJECT second time triggering the bug*
All that remains is to queue I/O operations by writing pointers to forged `nt!_IOP_MC_BUFFER_ENTRY` structures at the user space address (`0x100000000`). The number of entries should be equal to `RegBuffersCount`. This process is highlighted in the diagram below.
![](https://images.seebug.org/1679472504347-w331s)
*Figure 22 — Setting up user space for I/O Ring kernel R/W primitive*
One such `nt!_IOP_MC_BUFFER_ENTRY` is shown in the screenshot below. Note that the destination of the operation is a kernel address (`0xfffff8052831da20`) and that the size of the operation, in this case, is `0x8` bytes. It is not possible to tell from the structure if this is a read or write operation. The direction of the operation depends on which API was used to queue the I/O request. Using `kernelbase!BuildIoRingReadFile` results in an arbitrary kernel write and `kernelbase!BuildIoRingWriteFile` results in an arbitrary kernel read.
![](https://images.seebug.org/1679472524647-w331s)
*Figure 23 — Example faked I/O Ring operation*
To perform an arbitrary write, an I/O operation is tasked to read data from a file handle and write that data to a Kernel address.
![](https://images.seebug.org/1679472542053-w331s)
*Figure 24 — I/O Ring arbitrary write*
Conversely, to perform an arbitrary read, an I/O operation is tasked to read data at a kernel address and write that data to a file handle.
![](https://images.seebug.org/1679472557765-w331s)
*Figure 25 – I/O Ring arbitrary read*
## Demo
With the primitive set up all that remains is using some standard kernel post-exploitation techniques to leak the token of an elevated process like System (PID 4) and overwrite the token of a different process.
## Exploitation In the Wild
After the public release of our [exploit code](https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768), an employee from 360 Icesword Lab [disclosed](https://twitter.com/flame36987044/status/1633659037761036290?s=20) publicly for the first time, that they discovered a sample exploiting this vulnerability in the wild (ITW) earlier this year. The technique utilized by the ITW sample differed from ours. The attacker triggers the vulnerability using the corresponding Winsock API function, `ProcessSocketNotifications`, instead of calling into the `afd.sys` driver directly, like in our exploit.
The official statement from 360 Icesword Lab is as follows:
“360 IceSword Lab focuses on APT detection and defense. Based on our 0day vulnerability radar system, we discovered an exploit sample of CVE-2023-21768 in the wild in January this year, which differs from the exploits announced by [@chompie1337](https://twitter.com/chompie1337) and [@FuzzySec](https://twitter.com/FuzzySec) in that it is exploited through system mechanisms and vulnerability features. The exploit is related to `NtSetIoCompletion` and `ProcessSocketNotifications`, `ProcessSocketNotifications` gets the number of times `NtSetIoCompletion` is called, so we use this to change the privilege count.”
##
## Conclusion and Final Reflections
You may notice that in some parts of the reverse engineering our analysis is superficial. It’s sometimes helpful to only observe some relevant state changes and treat portions of the program as a black box, to avoid getting led down an irrelevant rabbit hole. This allowed us to turn around an exploit quickly, even though maximizing the completion speed was not our goal.
Additionally, we conducted a patch diffing review of all the reported vulnerabilities in `afd.sys` indicated as “Exploitation More Likely”. Our review revealed that all except two of the vulnerabilities were a result of improper validation of pointers passed in from user mode. This shows that having a historical knowledge of past vulnerabilities, particularly within a specific target, can be fruitful for finding new vulnerabilities. When the code base is expanded – the same mistakes are likely to be repeated. Remember, new C code == new bugs . As evidenced by the discovery of the aforementioned vulnerability being exploited in the wild, it is safe to say that attackers are closely monitoring new code base additions as well.
The [lack of support](https://github.com/microsoft/MSRC-Security-Research/blob/master/papers/2020/Evaluating the feasibility of enabling SMAP for the Windows kernel.pdf) for Supervisor Mode Access Protection ([SMAP)](https://en.wikipedia.org/wiki/Supervisor_Mode_Access_Prevention) in the Windows kernel leaves us with plentiful options to construct new data-only exploit primitives. These primitives aren’t feasible in other operating systems that support SMAP. For example, consider [CVE-2021-41073](https://chompie.rip/Blog+Posts/Put+an+io_uring+on+it+-+Exploiting+the+Linux+Kernel), a vulnerability in Linux’s implementation of I/O Ring pre-registered buffers, (the same feature we abuse in Windows for a R/W primitive). This vulnerability can allow overwriting a kernel pointer for a registered buffer, but it cannot be used to construct an arbitrary R/W primitive because if the pointer is replaced with a user pointer, and the kernel tries to read or write there, the system will crash.
Despite best efforts by Microsoft to [kill beloved exploit primitives](https://twitter.com/33y0re/status/1597640404748599296), there are bound to be new primitives to be discovered that take their place. We were able to exploit the latest version of Windows 11 22H2 without encountering any mitigations or constraints from Virtualization Based Security features such as [HVCI](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-hvci-enablement).
## References
- [MSRC (CVE-2023-21768](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2023-21768)
- [I/O Rings – When One I/O Operation is Not Enough](https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/) ([@yarden_shafir](https://twitter.com/yarden_shafir))
- [IoRing vs. io_uring: a comparison of Windows and Linux implementations](https://windows-internals.com/ioring-vs-io_uring-a-comparison-of-windows-and-linux-implementations/) ([@yarden_shafir](https://twitter.com/yarden_shafir))
- [One Year to I/O Ring: What Changed?](https://windows-internals.com/one-year-to-i-o-ring-what-changed/) ([@yarden_shafir](https://twitter.com/yarden_shafir))
- [One I/O Ring to Rule Them All: A Full Read/Write Exploit Primitive on Windows 11](https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/) ([@yarden_shafir](https://twitter.com/yarden_shafir))
- [Arbitrary Kernel RW using IORING’s](https://knifecoat.com/Posts/Arbitrary+Kernel+RW+using+IORING's) ([@FuzzySec](https://twitter.com/FuzzySec))
- [NTSockets – Downloading a file via HTTP using the NtCreateFile and NtDeviceIoControlFile syscalls](https://www.x86matthew.com/view_post?id=ntsockets) ([@x86matthew](https://twitter.com/x86matthew))
- [Reverse Engineering AFD.sys](https://recon.cx/2015/slides/recon2015-20-steven-vittitoe-Reverse-Engineering-Windows-AFD-sys.pdf) ([@bool101](https://twitter.com/bool101))
- [Microsoft Windows Ancillary Function Driver for WinSock privilege escalation CVE-2023-21768 Vulnerability Report](https://exchange.xforce.ibmcloud.com/vulnerabilities/243235)
Unavailable Comments