Abusing VBS Enclaves: Data-Only Execution and VTL1-Backed Process Hollowing
Share
By Juan Sacco from Exploit Pack
Interested in Windows Kernel? Check out our Technical Training Courses.
What happens when an untrusted VTL0 host gains read/write access to a VBS enclave, then uses data-only techniques as shown in my previous articles and abuses of code already trusted inside VTL1?
That question turned into a complete research journey, long nights, and lots of empty coffee cups.
We started with the smallest possible question: Can we using this enclave reliably move data across the VTL0/VTL1 boundary? Once the answer was yes, the next questions arrived quickly in my head.
- Can we inspect what was already loaded inside the enclave?
- Can we reuse its trusted routines without introducing new executable code?
- And most important: Can we describe an entire sequence of operations using data only?
From there, the research became more ambitious. We moved the control data into an FWA, or Free Writable Area, physical page. We used VTL1 as a protected place to preserve a process image after its original process had disappeared. We then turned the FWA into a physical gate: the session could remain dormant inside VTL1 until an explicit wake request opened the gate and began restoration.
The final step was to make “wake up” mean more than copying the bytes back. We created a suspended replacement process, rebuilt the image preserved inside VTL1, and restored execution through process hollowing.
The result is a process sleep/wake lifecycle backed by VTL1. A selected process has the readable pages of its main image preserved inside the enclave. The original PID is terminated. The wait executes through the enclave runtime and remains controllable through an FWA physical gate. On wake, a suspended replacement process is hollowed, the protected image is reconstructed inside it, and its initial thread is redirected to the restored entry point under a new PID.
The source process is gone during the waiting interval. Its saved image bytes remain in memory protected by VTL1.
How hibernation works: To make this image-level sleep/wake and execution-restoration technique, the targer process receives the PE image preserved in VTL1, its image mapping and PEB state are replaced, and its suspended initial thread is redirected before execution resumes.
Using VBS Enclaves to hide from the OS.
A Virtualization-based Security enclave is a trusted execution environment inside the address space of a host application. The host reserves the enclave and enters approved routines, but the enclave's protected memory belongs to VTL1 and is isolated from the containing VTL0 process and from ordinary VTL0 kernel code.

For context, Windows separates execution into Virtual Trust Levels or VTLs for short:
- VTL0 is the normal Windows environment. Applications, the Windows kernel, and most drivers execute here. Administrator privileges and even kernel-mode execution remain inside VTL0.
- VTL1 is the isolated security environment protected by the Windows hypervisor. It hosts the Secure Kernel and protected workloads such as VBS enclaves. VTL1 can interact with VTL0 under controlled conditions, but VTL0 cannot directly access VTL1-protected memory.
This distinction is important: VTL1 is a separate trust boundary enforced by the hypervisor. Compromising the VTL0 kernel does not grant access to memory protected by VTL1. The hypervisor enforces these memory-access restrictions using Second Level Address Translation (SLAT), so even code executing in Ring 0 at VTL0 cannot overcome the VTL1 security boundary.
Second Level Address Translation or (SLAT) is the hardware-assisted translation layer used by the hypervisor to control how virtualized memory reaches physical memory. Intel calls its implementation Extended Page Tables (EPT), while AMD calls it Rapid Virtualization Indexing (RVI), implemented through Nested Page Tables (NPT).
Under normal paging, the operating system translates a virtual address into a guest-physical address using its page tables. With SLAT enabled, the processor performs a second translation controlled by the hypervisor:
Virtual address -> guest-physical address -> system-physical address
The hypervisor assigns separate SLAT permissions to each Virtual Trust Level. A physical page may therefore be readable and writable from VTL1 while inaccessible from VTL0.
Kernel privileges, meaning executing at Current Privilege Level 0 (or CPL=0), allows VTL0 code to manage the operating system’s page tables. They do not, as mentioned however, VTL0 to override the second-stage translation and access permissions enforced by the hypervisor, this distiction is imporant as it enforces Data Protection and Code Integrity via the enclaves, and we can abuse that.
Microsoft currently describes VBS enclaves as a mechanism for isolating sensitive workloads from both the host application and the rest of the system. On current supported systems, they require VBS and HVCI to be enabled.
The normal host-side lifecycle looks something like this:
- Check
ENCLAVE_TYPE_VBSsupport. - Reserve an enclave with
CreateEnclave. - Load the enclave image and imports with
LoadEnclaveImageW. - Finalize it with
InitializeEnclave. - Enter exported routines through
CallEnclave. - Destroy the reservation with
DeleteEnclave.
Every enclave routine extends the VTL1 security boundary to the interface exposed to its VTL0 host. Any host-controlled pointer, size, or data structure must therefore be treated as untrusted and validated before it is used inside the enclave.
The abuse of an Enclave
Windows tracks enclave ownership and address ranges through VTL0 process metadata. Although the enclave pages remain protected in VTL1, their reservation is represented in structures such as EPROCESS, the enclave table, and the process VAD tree. These structures provide enough information to identify the host process and locate its enclave address range
Our inventory begins at the active process list. For each process, the enclave count in EPROCESS acts as a fast filter. Processes with enclaves expose an enclave table pointer and enclave-tagged Virtual Address Descriptor entries. Walking the VAD tree and reconstructing its high and low VPN fields reveals the base, end, and size of each enclave reservation. When the reservation retains a file object, its image path can also be recovered.
This creates two different views of the target:
- VTL0 management view: process ownership, enclave count, table address, VAD range, and sometimes the backing image path.
-
VTL1 protected view: plaintext pages, loaded runtime modules, stacks, contexts, allocator state, and internal dispatch metadata.

The first view inventories the boundary. The vulnerable enclave interface is what lets the research cross into the second one.
Establishing a research context
Before testing execution, the implementation establishes whether the platform supports VBS enclaves and whether the selected backend initialized correctly. It records the host PID, enclave reservation base, returned enclave context, image source, and the result of a deterministic read/write.
A quick write with a known marker (0xDEADBEEF) through the seal/unseal primitive, reads it back through the opposite direction VTL0 <-> VTL1, and compare. Write success, read success, data equality, and protected-blob size are reported separately. 
From a sealing bug to VTL1 read/write
The enclave used in our research exposes application-specific routines named Init, SealSettings, and UnsealSettings. These are not generic Windows enclave APIs. They belong to the selected enclave image.
Init returns a configuration pointer inside the protected reservation. That gives the host an address anchor in VTL1. More importantly, the sealing interface accepts pointers without correctly enforcing whether each address belongs to trusted enclave memory or untrusted host memory.
That creates two enclave-mediated movements:
- VTL1 read: seal plaintext from a chosen enclave address into an external blob, then unseal that blob into a normal VTL0 buffer.
- VTL1 write: seal controlled VTL0 bytes, then unseal the result to a chosen address inside the enclave.
VTL0 never directly reads a protected page. Instead, trusted enclave code performs the access and copies the result across the boundary. The cryptographic operation becomes a gateway.
The implementation handles transfers in page-sized chunks, allowing the same read/write primitive to move complete images reliably between VTL0 and VTL1.
Calling code that is already inside VTL1
Inside VTL1, the enclave runs with a restricted, enclave-compatible Windows API surface. In our target, that runtime includes ucrtbase_enclave.dll, which supplies functions such as malloc, free, memcpy, and memset, and vertdll.dll, which supplies enclave services such as sealing, context capture, memory management, waiting, and NtContinue. These existing functions became the building blocks of the data-only execution chain.
-
malloc:allocate protected VTL1 storage -
free:release the protected allocation -
memcpy: copy data between VTL1 buffers -
memset:initialize or overwrite VTL1 buffers
CallEnclave only transfers execution to registered enclave entry points. To invoke another routine existing routine internal routine function, we locate an approved valid entry in the enclave’s target table, temporarily redirect it to the desired routine target, execute the call, and then restore the original entry.
The target journal understands absolute 64-bit, reservation-relative 32-bit, and enclave-end-relative 32-bit entries. It requires a unique match and verifies both installation and restoration.

With RtlCaptureContext and NtContinue, the primitive becomes a data-only call sequencer. It captures a live enclave thread context, copies its stack into controlled VTL1 memory, sets the instruction pointer and x64 argument registers for an existing function, and continues execution with the modified context.

The first proof calls memset and memcpy inside VTL1 and checks their output byte for byte. No new executable enclave page is required at this stage: the targets are already trusted runtime code, the controlled material is context, stack, arguments, buffers, and a temporary dispatch entry.
An FWA-backed VTL1 data-only call sequence
The next step separates the call program from ordinary process memory. A verified Free Writable Area, or FWA, physical page is selected and exposed through a temporary user mapping. That page stores a compact sequence header and an array of call descriptors. Each descriptor contains an existing VTL1 target, the x64 argument registers, completion status, and the returned value.
Free Writable Areas are writable gaps in physical-address space that are not assigned to normal operating-system memory allocations. We use an FWA to hide control data outside the virtual address space and allocation history of the controlling process. The page is mapped only when the data must be read or changed, then the temporary mapping is removed. The data remains in physical memory without appearing as an ordinary process allocation.

How it works with FWA: fill one VTL1 buffer with memset, then copy a marker to another VTL1 buffer with memcpy. The sequence executor consumes those descriptors through the captured-context and NtContinue path. After execution, the buffers are read independently and must contain the expected fill and marker.
The FWA page is used as data in a physical backing page. The research implementation saves the original mapping, materializes the alias, verifies the descriptor bytes through that alias, runs the sequence, and restores the original mapping afterward, it's a gateway.
This gives us a reusable data-only model:
- Place targets and arguments in a call-description page.
- Capture a legitimate VTL1 execution context.
- Redirect a valid target only for the duration of the call.
- Continue into code already mapped and trusted in VTL1.
- Record status and results as data.
- Verify output, restore mappings, and restore the target entry.
Sleeping and restoring a VTL0 allocation
The first complete action operates on a controlled allocation in the current process:
- Copies readable pages from the current executable into a VTL0 allocation.
- Copies that allocation to a backup created in VTL1.
- Releases the original VTL0 allocation with
VirtualFree. - Waits inside the enclave runtime with
WaitOnAddress. - Recreates the allocation at its old address.
- Copies the saved bytes back from VTL1 and verifies them.

The implementation checks each invariant separately: backup creation, source copy, free, wait, reallocation at the requested address, restore copy, byte comparison, and cleanup. This establishes that data can leave VTL0, survive only in VTL1 during a timed gap, and return to the same VTL0 address.
Putting a process image (snapshot) to sleep inside VTL1
The next stage takes this control outside the enclave host and applies it to a selected source process.
We walk the main-image range with VirtualQueryEx. Committed pages that are neither guarded nor inaccessible are copied with ReadProcessMemory; unreadable gaps remain zero in the snapshot. The payload is rounded to a page boundary and capped at 64 MB.
An enclave-side malloc then creates a large VTL1 backup. The complete snapshot is written into that allocation through the vulnerable enclave interface in page-sized chunks. Two words placed after the image provide the address and comparison value used by the enclave-side wait.
After the protected copy is complete, the original source process is terminated. The implementation waits for its process object to signal and only continues after Windows confirms that the source PID exited.
At that moment, the original PID no longer exists. The preserved main-image bytes remain inside VTL1.

The wait really executes from the enclave runtime
The pause executes inside the live enclave runtime through a data-only call descriptor targeting vertdll!WaitOnAddress:
target = vertdll!WaitOnAddress
RCX = VTL1 wait address
RDX = VTL1 comparison address
R8 = sizeof(uint32_t)
R9 = timeout in milliseconds
The descriptor executes through the VTL1 context sequencer. During this interval, the source process is gone while its saved image remains in the enclave allocation. The call must return through the sequencer with a successful descriptor status before the wake stage begins.
Waking as a new process
After the VTL1 wait returns, the code reads the protected snapshot back and starts the same executable path with CREATE_SUSPENDED. Windows assigns a new PID.
A separate PAGE_READWRITE allocation is created in that suspended process with VirtualAllocEx. The snapshot recovered from VTL1 is copied into it using WriteProcessMemory, read back using ReadProcessMemory, and compared twice:
- the remote readback must equal the bytes recovered from VTL1;
- the recovered bytes must equal the original source snapshot.
We have to be sure the bytes are correct, meaning not corrupted. Therefor only after both comparisons are checked does the transfer path resume the primary thread.
The workflow looks like this: VTL1 allocation, source copy, source termination, enclave-side wait, suspended process creation, remote allocation, write, readback, two-way verification, thread resume, post-resume observation, handle closure, then VTL1 cleanup.
Giving VTL0 Control over the Wake-Up via FWA Gate
The fixed-delay establishes that the image can survive inside VTL1, but it does not provide an interactive wake-up call. The next stage adds one without moving the protected image out of the enclave: a verified FWA physical page becomes a physical gate shared by the VTL0 controller and the asynchronous sleep session.
The gate is small and holds only a few properties. It begins with a magic value and a version number, followed by the current state, last error, source and replacement PIDs, a wake generation, source-image metadata, the VTL1 backup address and size, and the destination buffer created during restoration.
Empty -> Copying -> Sleeping -> WakeRequested
|
v
Restoring -> Restored
|
v
Failed
The FWA page does not hold the process image, only the gate. The image remains in a separately allocated VTL1 buffer. The gate holds only coordination and observable session metadata. This separatio allows us to ensure that: bulk protected storage stays above the VTL0 boundary, while the physical page provides a compact data-only control channel.
Starting the session creates a detached worker. That worker snapshots the selected main image, allocates the VTL1 backup, copies the pages, terminates the source process, and publishes Sleeping to the gate. The caller returns while the session remains active, so the research interface can be used again to inspect the sleeping PID and request an early wake.

Gate controlled wake-up
A single long enclave call would make the session difficult to control from VTL0. The implementation therefore divides the sleep into 250 millisecond slices. Each slice still executes vertdll!WaitOnAddress through the VTL1 data-only context sequencer. Between slices, the worker reads the FWA gate and checks whether its state changed to WakeRequested, this is core of this technique.
A wake request performs two related actions. It sets the active session's in-memory wake flag and writes WakeRequested to the physical gate, incrementing the wake generation. The generation value makes successive requests visible as distinct control events rather than an ambiguous persistent bit.
The session wakes when either the request is observed or the current one-hour safety ceiling expires. It publishes Restoring, recovers the snapshot from VTL1, creates the new process suspended, transfers and verifies the bytes, resumes the new primary thread, and writes either Restored or Failed to the gate. On success, the gate also records the new PID and remote buffer address.
This changes the research result in an important way. The first sleep/wake path was a synchronous transfer with a predetermined delay. The gate version is an externally controlled state machine: the source can remain absent while protected data waits in VTL1 indefinitely, and a later data-only signal in an FWA page decides when restoration begins.
It also creates a useful forensic timeline. Instead of inferring progress from a blocked thread, an observer with access to the physical page could potentially see copying, sleeping, wake requested, restoration, success, and failure statuses.
From verified transfer to Sleepy Hollow.
The gate controlled sleep establishes protected storage, asynchronous control, and cross-process restoration. The final implementation changes the wake stage itself. Instead of placing the recovered bytes in an auxiliary buffer and allowing the replacement image to start normally, it uses the suspended process as a carrier and replaces its executable image before the initial thread runs.
Process hollowing begins by creating a legitimate process with its primary thread suspended. Windows maps the executable normally but holds the thread before it starts running. The loader-created image is then removed, a replacement PE image is mapped into the process, its headers and sections are restored, relocations are applied when necessary, and the remote PEB is updated with the new image base. Finally, the suspended thread is redirected to the replacement entry point and resumed.
In our implementation, the replacement image is the memory pages preserved inside VTL1 during the sleep interval. When the FWA gate requests a wake-up, those enclave-backed bytes are recovered and used to hollow the suspended replacement process. Execution resumes under a new PID from an image that crossed the dormant interval inside the enclave.


The memory-layout snapshot preserved inside VTL1 becomes the image used to hollow the suspended replacement process. The bytes cross the complete sleep interval inside the enclave before returning to VTL0 for restoration and execution.
Reconstructing the VTL1-preserved PE
After a wake request, the image is read from the VTL1 allocation and compared with the original captured snapshot. Its DOS header, NT signature, PE32+ optional header, entry-point RVA, image size, and section table are validated before the replacement process is modified.
A new instance of the same executable is created with CREATE_SUSPENDED. The implementation queries ProcessBasicInformation, obtains the remote PEB, reads PEB.ImageBaseAddress, and unmaps the loader-created image with NtUnmapViewOfSection.
Memory is then reserved and committed for the preserved image. The original source base is requested first. If that address is unavailable, another base is accepted and the image's base-relocation directory is processed. The current x64 path accepts IMAGE_REL_BASED_DIR64 entries, applies the delta between the captured source base and the new remote base, and updates the optional header's image base.
The reconstructed memory image is written into the hollowed process and read back in full. After this check, the wake is executed and the process is resumed.
Restoring the image contract
Restoring a PE image requires rebuilding the metadata and memory layout Windows expects in the replacement process: sections at their intended virtual addresses, relocations for the selected image base, correct page protections, an updated PEB, and a valid thread context pointing to the restored entry point.
- Update
PEB.ImageBaseAddressto the reconstructed image. - Set the headers read-only.
- Translate each section's
IMAGE_SCN_MEM_READ,IMAGE_SCN_MEM_WRITE, andIMAGE_SCN_MEM_EXECUTEcharacteristics into the corresponding page protection. - Capture the suspended primary-thread context.
- Set the x64 startup argument in
RCXto the restored entry point. - Flush the remote instruction cache.
- Apply the thread context and resume the primary thread.
If any validation, unmapping, allocation, relocation, write, verification, protection, PEB update, context update, or resume operation fails, the suspended replacement is terminated. The FWA Gate in that case ends in Failed. A successful result will be Restored together with the new PID and reconstructed image base.
This completes the transition from protected byte transfer to restored execution. During wake-up, the loader’s original mapping is replaced with the memory image preserved inside VTL1, and the initial thread begins execution at the entry point of that restored image.
What wake up means?
It is important to define the result precisely. The research preserves and restores the readable memory image of the source process's main executable. The hollowing path replaces the new process's loader-created image, updates its image-base state, and redirects its initial thread to the restored PE entry point.
The new process receives a new PID and restores the VTL1-preserved executable image as its initial execution path. The restored state covers the executable image, its layout, relocations, memory protections, PEB image base, and primary-thread entry point. Thread histories, additional threads, handles, private heaps, unrelated stacks, loaded-module state, kernel objects, and external resources remain bound to the terminated PID.
Sleepy Hollow is therefore the following actions concatenated:
- capture the source image
- move it into VTL1
- terminate or virtualfree the source PID
- wait from VTL1
- create a new instance
- unmap the suspended replacement image
- relocate, restore, and verify the VTL1-preserved PE
- repair the PEB and section protections
- redirect the initial thread to the restored entry point
- resume execution and return the new PID
What does this means for VTL1 Security?
VBS enclave security is usually discussed in terms of confidentiality: VTL1 protects secrets even when the normal host or kernel is compromised. This research demonstrates the reverse side of that design. If an enclave accepts unsafe pointer relationships, the same protected memory can become storage controlled by a lower-trust caller but hidden from direct VTL0 inspection.
- Separation: the source PID can terminate while its image snapshot remains allocated in VTL1.
- Protection: the timed wait is dispatched to an existing VTL1 runtime routine instead of a normal host sleep.
- Cross-process transfer: bytes captured from one process are preserved through the enclave and verified inside a newly created process with a different PID.
- Execution restoration: the suspended replacement can be hollowed and redirected to the PE image recovered from VTL1.
- Data-only control: an FWA physical gate controls when the protected image leaves its sleeping state and enters restoration.
The research also shows why blocking arbitrary executable allocation in VTL1 is not the end of the analysis. Existing enclave runtime functions remain useful. With data-only control of contexts, stacks, arguments, and accepted call targets, trusted code can be composed into operations the enclave developer never intended.
Detection
The hypervisor can enforce VTL ownership of memory, but it cannot correct an enclave's pointer-validation mistakes. Enclave developers should assume the containing process is hostile and treat every host-provided pointer, length, nested structure, and output location as untrusted.
- Validate whether each buffer must be inside or outside the enclave.
- Reject ranges that overflow, partially overlap, cross an unexpected boundary, or change during use.
- Avoid returning raw enclave addresses or internal layout information.
- Minimize writable metadata that influences valid call targets.
- Authenticate sensitive configuration and fail closed on ambiguous state.
- Think about the ABI functions from a hostile-host mindset, including page boundaries and shared-buffer races.
Detection should also consider the complete lifecycle. Enclave allocation, unusually large seal or unseal operations, repeated short enclave waits, source process termination, suspended process creation, image unmapping, remote PE reconstruction, PEB modification, cross-process writes, thread-context changes, and thread resume form a much stronger signal together than any one event alone.
Bonus track:
One of the strangest results appeared when we called NtQueryInformationThread from VTL1 through the NtContinue execution path using the current-thread pseudo handle, (HANDLE)-2.
The call enters the VTL0 kernel, but it never completes the expected return journey into VTL1. Because the host thread is still waiting for the enclave transition to finish, the process remains visible from VTL0 while making no useful forward progress. Its PID and kernel bookkeeping are still present, but execution is effectively stranded across the trust boundary.
We have then created a VTL0 zombie! This is not zombie in the traditional Unix sense, the process has not exited and is not waiting to be reaped. It is alive from the operating system’s perspective, but its execution has entered a VTL1 path that never returns.

The reproducible trigger is the combination of the NtContinue-driven VTL1 call, NtQueryInformationThread, and the current-thread pseudo handle. The exact reason the return path becomes stranded still requires deeper tracing, so we do not claim that (HANDLE)-2 alone is the root cause. What we can confirm is the observable result: a live VTL0 process whose active execution disappears into VTL1 and never comes back.
Takeaway
This research began with one enclave bug: trusted sealing code accepted the wrong pointers (Discovered by Google project zero). That bug became VTL1 read/write. VTL1 read/write exposed runtime layout and mutable call-target metadata. Existing RtlCaptureContext and NtContinue routines turned controlled data into an FWA-backed call sequence. Enclave-side allocation and WaitOnAddress then provided protected storage and timing across the death of one process and the creation of another. The FWA gate added a versioned, data-only wake signal and an observable session state. Process hollowing completes the chain by restoring execution from the PE image that survived inside VTL1.
The important result is that the VBS boundary itself becomes part of the technique: trusted enclave code is redirected into using VTL1 as an unintended process-image vault, preserving the image across termination and supplying it to a hollowed replacement process.