// 1dotq

vulnerability research & low-level Windows internals

Three pre-auth memory corruption bugs in Windows wsdapi.dll

2026-05-19  —  Windows 11 24H2 (Build 26100.8115)  —  FDResPub / TCP 5357
windows wsdapi pre-auth uaf memory-corruption
Bug Type Vector Impact
Bug 1 Use-After-Free WS-Discovery SOAP RCE (controlled vtable dispatch)
Bug 2 Stack Overflow HTTP Pipelining Remote DoS / Potential RCE
Bug 3 Race Condition Session Management Remote DoS / Info Leak

I spent a few weeks digging into the WS-Discovery stack on Windows and found three distinct memory corruption bugs in wsdapi.dll, all triggerable from the network before any authentication. The target service is FDResPub (Function Discovery Resource Publication), which runs inside svchost.exe and listens on TCP port 5357 by default on Windows 11. No credentials. No user interaction. Just a socket.

I'm writing this up because the research process itself is worth documenting — the non-invasive CDB technique, the heap spray logic, how I proved controlled vtable dispatch without writing a single byte of shellcode. The bugs themselves were confirmed on Windows 11 24H2, Build 26100.8115.

Setup

My lab target was a Windows 11 24H2 VM on Proxmox. The attack machine is Kali on the same /24. FDResPub starts automatically; you can verify it's running with:

sc.exe queryex FDResPub

Port 5357 is the WSD HTTP endpoint. You can confirm it's alive with any TCP connection — it speaks HTTP/1.1 and expects SOAP envelopes. The protocol is WS-Discovery (WSD), defined in the OASIS spec, used by Windows for network device announcement and discovery.

For crash capture I configured the target with kernel dump enabled and AutoReboot = 0 so the system doesn't restart before the dump writes. For user-mode crashes, Windows Error Reporting drops a minidump in %LOCALAPPDATA%\CrashDumps automatically.

The key tool is CDB (the command-line WinDbg) in non-invasive mode. The -pv flag attaches without suspending the target process, so FDResPub keeps running and accepting connections while you're attached. That matters because some bugs only trigger under concurrent load.

cdb.exe -pv -p <PID>

I automated the PID resolution and module base address lookup over SSH using a Python script (specter_brain / specter_agent) so I could run CDB commands from Kali without touching the Windows desktop.

Bug 1 — Use-After-Free with controlled vtable dispatch

This one took the longest to understand. The crash shows up as a CFG violation (STATUS_STACK_BUFFER_OVERRUN 0xC0000409) rather than a typical access violation, which is the CFG mitigation kicking in after wsdapi dispatches a vtable call through a freed COM object.

The faulting offset is wsdapi+0x50e5c. What's happening there: the code calls a method on a COM object (CWSDXMLReader) but the object has already been freed. The AddRef that should pin the object before the call is missing — classic refcount bug. After free, the vtable pointer at offset 0 of the object points into whatever replaced the allocation. When CFG validates the dispatch target against its bitmap, it fails and terminates the process.

The crash dump confirms this. The RSI register holds the freed object's address:

rsi = 0x0000021eb4853070   ; freed CWSDXMLReader
[rsi+0] = vtable ptr       ; now pointing at LFH fill (0xfeeefeee region)
call [vtable+0x30]          ; vtable[6] dispatch → CFG violation

Without any spray, the freed memory gets filled with the default LFH pattern (0xfeeefeee), and ntdll+0x4cd90 ends up in the vtable slot. CFG rejects that target and crashes.

The spray is straightforward. CWSDXMLReader is 768 bytes (0x300), solidly within LFH territory (the modern Segment Heap LFH handles up to 16 KB). I hold a set of partial SOAP probe requests open with an oversized Content-Length header — this pins live CWSDXMLReader allocations in the heap — then cycle them to reclaim freed slots with controlled content.

I don't need shellcode to prove this is exploitable. I set a breakpoint at wsdapi+0xf7a5 — a valid function entry point that lives in the CFG bitmap, so dispatching to it won't trigger the CFG check — using CDB:

bu 0x00007ffd895ff7a5 ".echo ==UAF_VTABLE6_CONTROLLED==; r rcx rdx r8 rsi; gc"

The hardcoded address is from my lab — 0x00007ffd895ff7a5 is wsdapi base + 0xf7a5 on my build. Resolve the base for your version with lm m wsdapi in CDB and add 0xf7a5 to get the correct address.

With the spray active, that breakpoint hits 30 out of 30 attempts. Without the spray, the process crashes every time. That's proof of controlled vtable dispatch — an attacker who fills those 768-byte slots with a crafted fake vtable gets a PC-control primitive.

Bug 2 — Stack buffer overflow (GS cookie violation)

This one is noisier and easier to trigger. The crash code is 0xC000071C (GS stack cookie check failure) at wsdapi+0xa6b78. Four crashes in the Event Log across a single fuzzing session.

The trigger is HTTP pipelining at volume. FDResPub handles multiple HTTP requests per connection, and when you pipeline enough requests with specific body sizes, the stack frame at +0xa6b78 gets smashed. I used a 1MB SOAP SUBSCRIBE body with Transfer-Encoding chunked to hold connections open, combined with a burst of concurrent pipelined probes on separate connections.

The GS cookie check means the overflow reaches the cookie before the return address, which triggers the fast-fail. The process terminates before returning. The DoS is guaranteed and reliable — you don't need to win any timing race or get heap layout right, just pipeline enough requests and the service dies. Turning it into RCE is non-trivial because the cookie fires before you control the return address, but the primitive is there if you can bypass or brute-force the cookie. I stopped at DoS — that's sufficient proof for reporting.

Bug 3 — Lock-free session list UAF (AV read)

The third bug is a race condition in the session list management code around wsdapi+0x23e45. The fault address is 0xfffffffffffffff1 — that's -15 sign-extended to 64 bits, which is what you get when a session lookup fails and returns an NTSTATUS or HRESULT error code, and the caller uses that return value directly as a pointer without checking it first.

The trigger is concurrent session creation and destruction. I open N connections, synchronize them to close simultaneously (using a Python threading barrier), and interleave that with new incoming connections. The race window is narrow but reproducible — four crashes across the session, all showing the same fault address pattern.

; wsdapi+0x23e45 (AV read)
; rax = 0xfffffffffffffff1  ; -15 sign-extended — failed lookup returned as pointer
mov rax, [rax+8]            ; → access violation read

The 0xfffffffffffffff1 address is consistent across crashes, which tells me it's not random noise — the same failed lookup produces the same sign-extended value every time, just at the wrong moment in the race. That kind of consistency is useful; it means you have a real window to work with, not a one-in-a-million timing fluke.

Proof approach

For each bug I wanted proof that didn't require shellcode. CDB non-invasive mode is the key tool here. You attach without suspending the process, set a conditional breakpoint at a location you've reasoned the CPU will reach if the bug is triggered, and log register state. If the breakpoint fires with the right register values, that's forensic proof of the code path.

The hardest part of this kind of research when you're working alone is staying disciplined about what you've actually proven vs. what you're assuming. The UAF vtable dispatch is proven — 30/30 breakpoints firing is not coincidence. The GS overflow is proven — four Event Log entries with the same crash offset is not coincidence. The race condition UAF is proven — the deterministic fault address tells you exactly what the code is doing wrong.

What I didn't prove is post-exploitation. I'm not going to, and I don't need to for research purposes. Controlled vtable dispatch at a CFG-valid target with register state pointing at attacker data is the demonstration.

The tooling

Everything ran from Kali over SSH. The Windows-side component (specter_agent.py) is a lightweight HTTP server wrapping CDB, exposing endpoints for running commands against a live process:

GET  /context?pid=<pid>           # lm, !peb, general state
POST /command?pid=<pid>          # arbitrary CDB command
GET  /discover?dll=wsdapi         # resolve base address

The Kali side (specter_brain.py) coordinates the attack traffic, CDB commands, and crash detection. Both are sitting in the repo if you want to look at them.

Target environment

Final note

Three distinct pre-auth memory corruption bugs in one service, one attack surface, no credentials required. The WSD stack is old code that hasn't gotten a lot of attention from the research community, and it shows. If you're looking for a place to dig, FDResPub and the broader Function Discovery infrastructure are worth your time.

I'll write up the spray technique in more detail in a follow-up post — the HOLD+RECLAIM approach for LFH buckets specifically, since most spray write-ups gloss over the details of how you keep slots pinned long enough to do anything useful with them.