# Root Cause Analysis — CVE-2023-21768

## Summary

| Field | Value |
|---|---|
| **CVE** | CVE-2023-21768 |
| **Binary** | afd.sys (Windows Ancillary Function Driver for WinSock) |
| **Component** | AfdNotifyRemoveIoCompletion — called from AfdNotifySock (IOCTL 0x12127) |
| **Bug Class** | Missing PreviousMode Validation / Arbitrary Write-Where (CWE-269) |
| **Impact** | Elevation of Privilege — Local to SYSTEM |
| **CVSS 3.1** | 7.8 (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| **Exploited ITW** | Yes — 360 IceSword Lab discovered ITW sample (January 2023) |
| **Patch** | January 2023 Patch Tuesday (KB5022303 Win11 22H2) |

## BinDiff Summary

### Versions Compared

- **Pre-patch**: afd.sys 10.0.22621.608 (December 2022)
- **Post-patch**: afd.sys 10.0.22621.1105 (January 2023)

### Changed Functions

Only one function was modified between the two versions:

| Function | Change Type | Description |
|---|---|---|
| `AfdNotifyRemoveIoCompletion` | Code (PreviousMode check added) | Added conditional branch: if PreviousMode != 0 (user-mode), call ProbeForWrite on the target pointer before writing |

The fact that only a single function changed made root cause identification trivial from the BinDiff output.

## Vulnerability Overview

The Windows AFD module is the kernel entry point for the Winsock API. IOCTL 0x12127
dispatches to `AfdNotifySock`, which is the last entry in the `AfdImmediateCallDispatch`
table — indicating it was recently added code. `AfdNotifySock` calls
`AfdNotifyRemoveIoCompletion`, which writes a value to a pointer from the user's input
structure without validating that the pointer resides in user-mode address space.

## Root Cause — Missing PreviousMode Check

### The Vulnerable Code (pre-patch)

In `AfdNotifyRemoveIoCompletion`, the function writes a value to a pointer at `field_0x18`
of the input structure unconditionally:

```c
// Pre-patch: No PreviousMode check
// 'pStruct' is the user-supplied AFD_NOTIFYSOCK_STRUCT
// field_0x18 contains a pointer (attacker-controlled)
// 'writeValue' comes from KeRemoveQueueEx return value (0x1)

*pStruct->field_0x18 = writeValue;  // ARBITRARY WRITE — no address validation
```

### The Patch (post-patch)

```c
// Post-patch: PreviousMode check added
if (PreviousMode != KernelMode) {
    // User-mode caller — validate pointer is in user space
    ProbeForWrite(pStruct->field_0x18, sizeof(ULONG), sizeof(ULONG));
}
*pStruct->field_0x18 = writeValue;
```

The patch adds a check: if `PreviousMode` is non-zero (indicating the syscall originated
from user mode), `ProbeForWrite` is called to ensure the target pointer falls within the
user-mode address range. If the pointer is a kernel address, `ProbeForWrite` raises an
exception, preventing the write.

### The Write Value

The value written to the target address comes from `IoRemoveCompletion` → `KeRemoveQueueEx`.
This return value equals the number of items removed from the I/O completion queue. By
calling `NtSetIoCompletion` once before triggering the vulnerability, the write value is
`0x1`. Additional `NtSetIoCompletion` calls can increment this value arbitrarily.

## Call Chain to Vulnerability

```
User mode: DeviceIoControl(hSocket, IOCTL_AFD_NOTIFY_SOCK, ...)
                │
                ▼
afd!AfdNotifySock                    [IOCTL 0x12127 dispatch]
    │
    ├── Check: InputBufferLength == 0x30
    ├── Check: ObReferenceObjectByHandle(pStruct->field_0x00) → IoCompletionObject
    ├── Loop:  Copy data from user-mode pointers in struct
    │
    └── Call: AfdNotifyRemoveIoCompletion(PreviousMode, ?, pStruct)
                │
                ├── Check: pStruct->dwLen != 0
                ├── ProbeForWrite(pStruct->pData2, dwLen * 0x20, ...)
                ├── IoRemoveCompletion(IoCompletionObject, ..., timeout)
                │       └── KeRemoveQueueEx → returns count (write value)
                │
                └── BUG: *pStruct->field_0x18 = writeValue
                         ↑ No PreviousMode check on this pointer
```

## AFD_NOTIFYSOCK_STRUCT Layout

```c
typedef struct _AFD_NOTIFYSOCK_STRUCT {
    HANDLE  hIoCompletion;   // +0x00: Valid IoCompletionObject handle
    PVOID   field_0x08;      // +0x08: User-mode pointer (validated)
    PVOID   field_0x10;      // +0x10: User-mode pointer (validated)
    PVOID   pWriteTarget;    // +0x18: WRITE TARGET — kernel address for exploit
    ULONG   dwCount;         // +0x20: Loop counter (set to 1)
    ULONG   dwLen;           // +0x24: Must be non-zero (set to 1)
    PVOID   pData2;          // +0x28: User-mode pointer for ProbeForWrite
} AFD_NOTIFYSOCK_STRUCT;     // Total size: 0x30 bytes
```

### Key Constraints

1. `hIoCompletion` must be a valid `IoCompletionObject` handle (created via `NtCreateIoCompletion`)
2. At least one completion record must be queued (via `NtSetIoCompletion`) for `IoRemoveCompletion` to succeed
3. `field_0x08` and `field_0x10` must point to valid user-mode memory
4. `dwLen` must be non-zero (so `ProbeForWrite` size = `dwLen * 0x20`)
5. `pData2` must be a valid user-mode address
6. Total struct size must be exactly `0x30` bytes

## Exploitation — I/O Ring Primitive

### Overview

IBM X-Force Red used the arbitrary write-where (value = `0x1` at any kernel address) to corrupt
a kernel `IORING_OBJECT` structure, achieving full arbitrary kernel R/W.

### IORING_OBJECT Corruption

```
IORING_OBJECT (kernel structure)
┌────────────────────────────────┐
│  ... (other fields) ...        │
│  +0x??: RegBuffersCount = 0    │ ← Trigger 1: write 0x1 here
│  +0x??: RegBuffers = NULL      │ ← Trigger 2: write 0x1 here
│  ... (other fields) ...        │    (address 0x0000000100000000)
└────────────────────────────────┘

After two triggers:
  RegBuffersCount = 1
  RegBuffers = 0x0000000100000000 (user-allocatable address)
```

### Step-by-Step

1. **Create I/O Ring**: Call `CreateIoRing` → get `HIORING` handle and kernel `IORING_OBJECT` address
2. **Leak kernel address**: Use `NtQuerySystemInformation` or similar to find `IORING_OBJECT` in kernel
3. **First trigger**: Write `0x1` to `IORING_OBJECT.RegBuffersCount`
4. **Second trigger**: Write `0x1` to `IORING_OBJECT.RegBuffers` (sets it to `0x0000000100000000`)
5. **Allocate user page**: `VirtualAlloc` at address `0x100000000`
6. **Place forged entries**: Write `IOP_MC_BUFFER_ENTRY` structures at that address
7. **Arbitrary kernel read**: `BuildIoRingWriteFile` with kernel address → reads kernel memory to file
8. **Arbitrary kernel write**: `BuildIoRingReadFile` with kernel address → writes file data to kernel memory
9. **Token swap**: Read SYSTEM (PID 4) token, overwrite current process token → SYSTEM shell

### Why SMAP Doesn't Protect

Windows 11 does not support Supervisor Mode Access Prevention (SMAP). This allows the
kernel to freely read from and write to user-mode pages, making the I/O Ring primitive
possible. On Linux (which supports SMAP), the equivalent `io_uring` attack would crash
the system.

## ITW Exploitation

360 IceSword Lab discovered an ITW sample in January 2023 that used `ProcessSocketNotifications`
(the corresponding Winsock API) instead of direct `DeviceIoControl` to reach the vulnerable code.
The ITW technique used the write value (incremented via multiple `NtSetIoCompletion` calls) to
directly modify privilege counts, rather than the I/O Ring approach.

## Detection

### YARA — Exploit Artifacts

```yara
rule CVE_2023_21768_Exploit_Indicators
{
    meta:
        description = "Detects compiled exploit artifacts for CVE-2023-21768 afd.sys LPE"
        cve         = "CVE-2023-21768"
        author      = "OnlyFm252"

    strings:
        $ioctl_code  = { 27 21 01 00 }
        $s_ntcreateio = "NtCreateIoCompletion" ascii wide
        $s_ntsetio    = "NtSetIoCompletion" ascii wide
        $s_ioring     = "CreateIoRing" ascii wide
        $s_afd        = "\\Device\\Afd" ascii wide
        $s_proc_sock  = "ProcessSocketNotifications" ascii wide

        $hex_regbuf_count = { 52 65 67 42 75 66 66 65 72 73 43 6F 75 6E 74 }
        $ntdll_import = "ntdll" ascii wide nocase

    condition:
        uint16(0) == 0x5A4D and
        (
            ($ioctl_code and ($s_ntcreateio or $s_ntsetio)) or
            ($s_afd and ($s_ntcreateio or $s_ntsetio) and $s_ioring) or
            ($s_proc_sock and ($s_ntcreateio or $s_ntsetio))
        )
}
```

### Sigma — Suspicious AFD Socket Notification Activity

```yaml
title: CVE-2023-21768 AFD WinSock LPE Exploitation Attempt
id: a1b2c3d4-e5f6-7890-abcd-ef0123456789
status: experimental
description: >
    Detects potential exploitation of CVE-2023-21768 via suspicious process
    behavior patterns — non-system process loading ntdll for NtCreateIoCompletion
    followed by privilege escalation indicators.
author: OnlyFm252
date: 2026/07/26
references:
    - https://www.ibm.com/think/x-force/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock
    - https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768
    - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21768
logsource:
    product: windows
    category: process_creation
detection:
    selection_parent:
        ParentIntegrityLevel|contains:
            - 'Medium'
            - 'Low'
    selection_child:
        IntegrityLevel: 'System'
        User|contains: 'SYSTEM'
    timeframe: 5s
    condition: selection_parent | near selection_child
level: high
tags:
    - attack.privilege_escalation
    - attack.t1068
    - cve.2023.21768
```

### Sysmon Configuration

```xml
<Sysmon schemaversion="4.90">
  <EventFiltering>
    <!-- Event 1: Detect processes that may exploit CVE-2023-21768 -->
    <ProcessCreate onmatch="include">
      <!-- Detect known PoC filenames -->
      <Image condition="contains any">CVE-2023-21768;afd_exploit;WinSock_LPE</Image>
    </ProcessCreate>

    <!-- Event 7: Detect ntdll loading patterns (NtCreateIoCompletion is in ntdll) -->
    <ImageLoad onmatch="include">
      <ImageLoaded condition="end with">\afd.sys</ImageLoaded>
    </ImageLoad>

    <!-- Event 13: Detect I/O Ring creation artifacts -->
    <RegistryEvent onmatch="include">
      <TargetObject condition="contains">IoRing</TargetObject>
    </RegistryEvent>

    <!-- Event 1: Detect privilege escalation from medium to SYSTEM -->
    <ProcessCreate onmatch="include">
      <IntegrityLevel condition="is">System</IntegrityLevel>
      <ParentCommandLine condition="contains any">cmd;powershell;exploit</ParentCommandLine>
    </ProcessCreate>
  </EventFiltering>
</Sysmon>
```

## References

- [IBM X-Force Red — Patch Tuesday → Exploit Wednesday: Pwning Windows AFD for WinSock](https://www.ibm.com/think/x-force/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock)
- [X-Force Red Exploit Code](https://github.com/xforcered/Windows_LPE_AFD_CVE-2023-21768)
- [Yarden Shafir — One I/O Ring to Rule Them All](https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/)
- [Steven Vittitoe — Reverse Engineering AFD.sys (Recon 2015)](https://recon.cx/2015/slides/recon2015-20-steven-vittitoe-Reverse-Engineering-Windows-AFD-sys.pdf)
- [MSRC Advisory — CVE-2023-21768](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21768)
