# WerSvc.dll Patch Diff — CVE-2026-20817

| | |
|---|---|
| Binary | WerSvc.dll (Windows Error Reporting Service) |
| Pre-patch version | 10.0.26100.7309 |
| Post-patch version | 10.0.26100.7623 |
| KB | KB5074109 |
| CVE | CVE-2026-20817 — Improper Handling of Insufficient Permissions or Privileges (CWE-280), Elevation of Privilege, CVSS 7.8 |
| Diff tool | ghidriff (Ghidra VersionTrackingDiff engine) |
| Functions changed | 1 with code changes (out of ~460 total, 99.78% avg similarity) |
| Functions added | 2 (WIL Controlled-Feature-Rollout accessors for `Feature_2473284922`) |
| Independent analysis | [itm4n — "CVE-2026-20817 - Windows Error Reporting Service EoP"](https://itm4n.github.io/cve-2026-20817-wersvc-eop/) |
| Proof-of-concept | https://github.com/itm4n/CVEs/tree/master/CVE-2026-20817 |

## Summary

The fix is Microsoft's nuclear option — **remove the feature entirely**. A
Controlled-Feature-Rollout flag (`Feature_2473284922`) is checked at the very
top of `CWerService::SvcElevatedLaunch`, and when enabled the function sets an
error status on the outgoing ALPC message and returns `E_FAIL` (`0x80004005`)
before touching any of the vulnerable code path. No refactoring, no
sanitisation, no validation — the whole "elevated launch" ALPC endpoint just
stops working.

That's a strong signal about the underlying bug class: this wasn't a
correctness issue that could be tightened up, it was a design-level "gaping
hole" (itm4n's word) that shouldn't have existed. Which is exactly what the
pre-patch code confirms.

**Pre-patch behaviour.** `WerSvc` hosts an ALPC server at
`\WindowsErrorReportingServicePort`. When it receives a message with
`MessageFlags == 0x50000000`, the dispatcher (`DispatchPortRequestWorkItem`)
routes it to `SvcElevatedLaunch`. The handler:

1. Calls `OpenSenderProcessThread` to open the ALPC client's process with
   `PROCESS_CREATE_PROCESS | PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION`.
2. Reads a client-supplied File Mapping handle out of the message
   (`_WERSVC_MSG.FileMapping` at `+0x38`), duplicates it locally, and maps a
   view.
3. Reads a wide string command line out of that mapped view.
4. Calls `CreateElevatedProcessAsUser`, which builds a SYSTEM token and calls
   `CreateProcessAsUserW` with `lpApplicationName =
   C:\Windows\System32\WerFault.exe` and `lpCommandLine =` **the
   client-supplied string**.
5. Duplicates the resulting process handle back to the client.

The program (`WerFault.exe`) is fixed and its path is constructed safely with
`GetSystemDirectoryW`, so the ALPC client cannot substitute an arbitrary
binary. **But every command-line option to `WerFault.exe` is attacker
controlled**, and the process runs as SYSTEM with a duplicated parent handle
of the caller's choosing. Any low-privilege user that can open the ALPC port
gets a SYSTEM `WerFault.exe` invocation with controllable arguments — the
weaponisation-to-shell path is itm4n's "further work required", and the MSRC
credit (Denis Faiustov & Ruslan Sayfiev, GMO Cybersecurity by Ierae) implies
one already existed at report time.

**Post-patch behaviour.** Any call to `SvcElevatedLaunch` sets
`MessageFlags = 1` and `LastError = 0xC0000001` (`STATUS_UNSUCCESSFUL`) on the
response message, returns `HRESULT_FROM_NT(STATUS_UNSUCCESSFUL) = 0x80004005`,
and does nothing else. `OpenSenderProcessThread`, `ElevatedProcessStart`,
`CreateElevatedProcessAsUser`, `CreateProcessAsUserW` — none of them get
called anymore when the flag is enabled. Microsoft has kept the flag as a
per-cohort kill switch (so they can roll the behaviour back if something
downstream broke), but the production path is "feature deleted".

**Confidence:** high. The single-function code change, the exact flag-first /
early-return / `E_FAIL` shape, and one-to-one alignment with an independently
published root-cause and PoC (itm4n) leave no room for interpretation.

**Credit.** MSRC credits Denis Faiustov and Ruslan Sayfiev, with GMO
Cybersecurity by Ierae. Independent patch analysis and public PoC by
[itm4n](https://itm4n.github.io/cve-2026-20817-wersvc-eop/).

## Function changed

### CWerService::SvcElevatedLaunch

| | |
|---|---|
| Change type | code, length, called |
| Similarity | 0.83 (b_ratio), 0.998 (m_ratio) |
| Length | 78 → 89 instructions |
| Fix pattern | Feature-flag kill switch at entry; short-circuit return `E_FAIL` |

Before (pre-patch — the ALPC-to-SYSTEM primitive):

```c
// WerSvc.dll 10.0.26100.7309 — VULNERABLE
long CWerService::SvcElevatedLaunch(CWerService *this,
                                    _WERSVC_MSG *param_1,   // input ALPC msg
                                    _WERSVC_MSG *param_2)   // output ALPC msg
{
    local_38 = __security_cookie ^ ...;
    pvVar6 = (HANDLE)0;
    local_c0 = (void *)0;
    local_c8 = (HANDLE)0;
    uVar8 = 0;

    // 1) Open the ALPC client's process
    lVar2 = OpenSenderProcessThread(
        this, param_1,
        (-(uint)(*(int *)(param_1 + 0xc0) != 0) & 0x80) + 0x440,   // PROCESS_CREATE_PROCESS|DUP_HANDLE|QUERY_INFORMATION
        (unique_any<file_handle_traits> *)&local_c0,
        0, NULL, 0);
    if (-1 < lVar2) {
        memset(local_b8, 0, 0x80);
        uVar1 = *(uint *)(param_1 + 0xc0);
        if (uVar1 != 0) {
            // 2) Copy up to 16 client-supplied source handles from msg+0x40
            p_Var7 = param_1 + 0x40;
            do {
                if (*(void **)p_Var7 != NULL) {
                    local_b8[(longlong)pvVar9] = *(void **)p_Var7;
                    pvVar9 = (HANDLE)(ulonglong)(uVar8 + 1);
                }
                p_Var7 = p_Var7 + 8;
            } while (uVar4 < uVar1);
        }
        // 3) Run WerFault.exe as SYSTEM with client-controlled command line
        //    (built from a File Mapping the client supplies at msg+0x38)
        lVar2 = ElevatedProcessStart(
            *(ulong *)(param_1 + 0x30),      // "unknown", must be 1
            local_c0,                         // client process handle
            *(void **)(param_1 + 0x38),      // ATTACKER-CONTROLLED file mapping
            local_b8, uVar8,                  // client source handles + count
            (unique_any<file_handle_traits> *)&local_c8);  // out: new process handle
    }
    pvVar5 = local_c8;
    *(long *)(param_2 + 0x2c) = lVar2;
    if (lVar2 < 0) {
        uVar3 = 0x50000002;
    } else {
        local_c8 = NULL;
        uVar3 = 0x50000001;
        pvVar6 = pvVar5;
    }
    // ... duplicate new process handle back to client, return ...
}
```

After (post-patch — the kill switch):

```c
// WerSvc.dll 10.0.26100.7623 — FIXED
long CWerService::SvcElevatedLaunch(CWerService *this,
                                    _WERSVC_MSG *param_1,
                                    _WERSVC_MSG *param_2)
{
    local_38 = __security_cookie ^ ...;
    bVar2 = wil::details::FeatureImpl<Feature_2473284922>::__private_IsEnabled(&impl);
    pvVar7 = (HANDLE)0;
    if (bVar2) {
        // Feature enabled -> refuse the request outright.
        *(undefined4 *)(param_2 + 0x28) = 1;              // MessageFlags = 1 (error)
        *(undefined4 *)(param_2 + 0x2c) = 0xc0000001;     // LastError    = STATUS_UNSUCCESSFUL
        lVar3 = -0x7fffbffb;                              // = 0x80004005 = E_FAIL
    }
    else {
        // Flag disabled -> original vulnerable body (kept only as a kill-switch rollback path).
        local_c0 = NULL;
        local_c8 = NULL;
        uVar9 = 0;
        lVar3 = OpenSenderProcessThread(...);
        // ... original ElevatedProcessStart / DuplicateHandle path ...
    }
    // ...
}
```

The added `called` list confirms the flag check is the only new external
dependency:

```
+wil::details::FeatureImpl<Feature_2473284922>::__private_IsEnabled
```

Nothing else in the function's downstream call graph changed — same
`OpenSenderProcessThread`, same `ElevatedProcessStart` on the fallback path.
The fix is purely the entry-point gate.

### New functions (feature-flag plumbing for `Feature_2473284922`)

`GetCachedFeatureEnabledState`, `__private_IsEnabled` — standard WIL
Controlled Feature Rollout accessors for the new flag gating the entire
`SvcElevatedLaunch` handler. Two functions, not the usual four —
`GetCurrentFeatureEnabledState` and `ReportUsage` were already present in
this DLL from an earlier flag.

## Exploit / Proof-of-Concept (from itm4n)

itm4n's public PoC ([github.com/itm4n/CVEs/tree/master/CVE-2026-20817](https://github.com/itm4n/CVEs/tree/master/CVE-2026-20817))
demonstrates the primitive — a low-priv process making the WER service
launch `WerFault.exe` as SYSTEM with attacker-controlled arguments. It does
not turn that primitive into a SYSTEM shell.

### The message

`SvcElevatedLaunch` is reached with `MessageFlags = 0x50000000`. The message
layout itm4n reverse-engineered:

```c
typedef struct _WERSVC_MSG_ELEVATED_LAUNCH {
    PORT_MESSAGE PortMessage;         // +0x00  standard ALPC header
    DWORD MessageFlags;               // +0x28  set to 0x50000000 to dispatch here
    DWORD LastError;                  // +0x2C  set on return
    BOOL  Unknown;                    // +0x30  must be 1 (checked in ElevatedProcessStart)
    HANDLE FileMapping;               // +0x38  shared memory containing the WerFault.exe args
    HANDLE SourceHandles[16];         // +0x40  handles duplicated into the new SYSTEM process
    BOOL  CopySourceHandles;          // +0xC0  controls process access rights on client
    HANDLE NewProcessHandle;          // +0xC8  receives handle to the new SYSTEM process
    BYTE  Padding[1192];              // +0xD0  total size must be 0x578 (checked)
} WERSVC_MSG_ELEVATED_LAUNCH;
```

### Trigger sequence

1. **Force WerSvc to start.** The service is demand-start / trigger-started;
   itm4n notes low-priv users can't `sc start` it but that a start trigger
   can be induced. Once running, the ALPC port
   `\WindowsErrorReportingServicePort` is listening.
2. **Prepare the shared-memory command line.** `CreateFileMappingW` + write
   a wide-char command line for `WerFault.exe [attacker args]`. The
   application name (`C:\Windows\System32\WerFault.exe`) is fixed by the
   service — only args are attacker-controlled.
3. **Connect and send.** `NtAlpcConnectPort` to
   `\WindowsErrorReportingServicePort`; build a `WERSVC_MSG_ELEVATED_LAUNCH`
   with `MessageFlags=0x50000000`, `Unknown=1`, `FileMapping = ` handle from
   step 2, correct `PortMessage` sizing, total struct size `0x578`;
   `NtAlpcSendWaitReceivePort` to submit and get back the new SYSTEM process
   handle in `NewProcessHandle`.
4. **Parent-PID spoofing (side effect).** The WER service spoofs the client
   as the parent PID of the new SYSTEM process. Defender flags this as
   suspicious behaviour on modern builds — reliability note, not a
   correctness one.

itm4n's suggested weaponisation path is a combination of `WerFault.exe`
command-line options with James Forshaw's
[GetProcessHandleFromHwnd research](https://projectzero.google/2026/02/gphfh-deep-dive.html)
— left as an exercise in the write-up.

---

<sub>Source: ghidriff diff of WerSvc-2025-12.dll (10.0.26100.7309, pre-patch)
vs WerSvc-2026-01.dll (10.0.26100.7623, post-patch) —
[download pre](/data/patch_diffs/binaries/WerSvc-2025-12.dll) /
[download post](/data/patch_diffs/binaries/WerSvc-2026-01.dll). Independent
patch analysis and PoC:
[itm4n — CVE-2026-20817](https://itm4n.github.io/cve-2026-20817-wersvc-eop/)
· [PoC source](https://github.com/itm4n/CVEs/tree/master/CVE-2026-20817).</sub>
