# Playing It SFAPE: Code Execution in Isolated User Mode

SFAPE.dll VBS enclave's repeated Initialize writes a fixed status to an unvalidated host pointer, corrupting the enclave range check and enabling full VTL1 code execution.

- Payout: Research
- Severity: high
- Program: Microsoft
- Write-up by: Doyensec
- Disclosed: 2026-10-12
- CVE: CVE-2026-83498
- Write-up quality: 9/10
- Original: https://blog.doyensec.com/2026/10/13/Sfape.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2026-10-13-sfape-html
- Tags: area:os domain:desktop impact:integrity impact:privesc kind:research platform:research sev:high tech:windows technique:chained technique:race-window technique:source-review vuln:memory-corruption vuln:privilege-escalation vuln:race-condition

**Target:** VBS enclave DLL (SFAPE.dll) loaded by lsass.exe via SFAPM.dll LSA package
**Root cause:** Initialize's already-initialized path writes STATUS_ALREADY_INITIALIZED through request+0x30 without validating the address, letting the host target enclave globals; the IsInsideEnclave range check also uses a wraparound-prone address+size comparison.
**Steps:**
1. Call `Initialize` once legitimately to set g_initialized.
2. Place a repeated-initialization request at RVA 0xE83C so its status field at +0x30 lands on the high DWORD of g_enclaveBase (RVA 0xE86C); call `Initialize` again to write `0xC0000510` there.
3. Corrupted g_enclaveBase makes IsInsideEnclave accept VTL1 pointers.
4. Exploit ClipboardInput TOCTOU: pass a UNICODE_STRING whose Buffer/MaximumLength validate, then during the telemetry callout swap Buffer and Length to get `memset(ptr, 0, length)`.
5. Use memset to zero the high DWORD of the credential-context map pointer at RVA 0xE060, pivoting it into a sprayed VTL0 mirror buffer with synthetic tree nodes.
6. Resolve a real handle to the synthetic node to get read16 via GetCcAttributes (`object = source - 0x2A8`) and write64 via SetCcTimeStamps (`object = ptr - 0x2B0`, plus `write32(ptr - 0x29C, 0xC0000510)` to skip lock acquisition).
7. Overwrite vertdll VertValidTargetTable pointer/count (vertdll+0x2CCB8 / +0x2CCB0) to a replacement table in SFAPE .data authorizing arbitrary functions/gadgets.
8. Add RtlCaptureContext to the VTT, capture live VTL1 CONTEXT, build a fake CONTEXT with RSP = captured RSP - 8, and use NtContinue to call arbitrary VTL1 functions with full argument control.
9. Optionally use EnclaveCopyIntoEnclave/EnclaveCopyOutOfEnclave wrappers (RVA 0x4090/0x40B0, syscall 0x08000019) for clean VTL1 read/write.
**How found:** Surveyed Windows VBS enclaves after prior Edge enclave research; noticed Initialize's early-return path skipped the IsInsideEnclave validation present on first call, then examined the range validator's arithmetic and found the high-DWORD overwrite target at RVA 0xE86C.
**Exploitation:** A VTL0 host process loads the signed SFAPE.dll enclave and drives the chain: fixed write32 corrupts the cached enclave base, ClipboardInput TOCTOU yields memset, map-pointer pivot yields read16/write64, VTT rewrite authorizes arbitrary VTL1 targets, and NtContinue with a captured stack gives full-argument calls into existing VTL1 code, achieving control over the enclave instance.
**Escalation:** Chained four primitives (fixed write32 -> range-check bypass -> memset -> map pivot -> read/write) into VTT hijack and NtContinue-based code reuse, then upgraded to Secure Kernel copy services for cleaner arbitrary VTL1 read/write.
**Insight:** Early-return/error paths in enclave exports often skip the pointer validation done on the happy path; a fixed-value write to a host-controlled offset can still be weaponized by targeting the high DWORD of a cached enclave-base global to invert the range check.

## Proof of concept (copied exactly from the original report)

**Repeated Initialize fixed-value write32 primitive**

```
status = (uint32_t *)((uint8_t *)request + 0x30);

configuration = request->configuration;
callout = request->callout;
context = request->calloutContext;
table = request->callbackTable;

if (g_initialized) {
 *status = STATUS_ALREADY_INITIALIZED; // 0xC0000510
 return status;
}

EnclaveGetEnclaveInformation(..., &g_enclaveInfo);

if (IsInsideEnclave(request, 0x38))
 return STATUS_CONFLICTING_ADDRESSES;
...


On the first call, the function performs extensive validation of the request and many nested callback pointers. On a repeated call, g_initialized is already set, so SFAPE writes the error status through request + 0x30 and returns. Notably, this time there is no address verification - the address can belong to either VTL0 or VTL1. The calling host process controls the request parameter. Therefore, after legitimate initialization, the function gives us the following primitive:

write32(request + 0x30, 0xC0000510)
```

**ClipboardInput TOCTOU memset primitive**

```
// Wrapper
ValidateHostRange(text, sizeof(*text));
ValidateHostRange(text->Buffer, text->MaximumLength);

// Worker
HashClipboardText(text->Buffer, text->Length);
InvokeTelemetryCallout(...); // telemetry callback sync point
SecureZeroMemory(text->Buffer, text->Length);


The descriptor remains in writable VTL0 memory throughout the call. The telemetry callback provides a deterministic synchronization point after hashing but before the final SecureZeroMemory. While the enclave thread is blocked in that callback, the host replaces Buffer and Length, then allows the worker to continue. The final primitive becomes:

memset(ptr, 0, length)
```

**SetCcTimeStamps controlled 64-bit VTL1 write PoC**

```
object = ptr - 0x2B0
write32(ptr - 0x29C, 0xC0000510) // object+0x14: nonzero lock owner
SetCcTimeStamps(real_handle, controlled_qword)
```

