NtCallEnclave
Transitions execution from VTL0 host code into a routine inside an initialised enclave.
Prototype
NTSTATUS NtCallEnclave( PVOID Routine, PVOID Parameter, BOOLEAN WaitForThread, PVOID *ReturnValue );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| Routine | PVOID | in | VTL1 address of the enclave entry function. Must lie inside the initialised enclave's range and be exported via the enclave config. |
| Parameter | PVOID | in | Single opaque parameter passed to the enclave routine in RCX. Typically a pointer to a host-allocated request buffer. |
| WaitForThread | BOOLEAN | in | TRUE blocks until a free enclave thread is available; FALSE returns STATUS_ENCLAVE_NOT_TERMINATED if all reserved threads are busy. |
| ReturnValue | PVOID* | out | Receives the PVOID value returned from the enclave routine (its RAX on return into VTL0). |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1709 | 0x8E | win10-1709 |
| Win10 1803 | 0x8F | win10-1803 |
| Win10 1809 | 0x8F | win10-1809 |
| Win10 1903 | 0x8F | win10-1903 |
| Win10 1909 | 0x8F | win10-1909 |
| Win10 2004 | 0x91 | win10-2004 |
| Win10 20H2 | 0x91 | win10-20h2 |
| Win10 21H1 | 0x91 | win10-21h1 |
| Win10 21H2 | 0x91 | win10-21h2 |
| Win10 22H2 | 0x91 | win10-22h2 |
| Win11 21H2 | 0x91 | win11-21h2 |
| Win11 22H2 | 0x91 | win11-22h2 |
| Win11 23H2 | 0x91 | win11-23h2 |
| Win11 24H2 | 0x93 | win11-24h2 |
| Server 2019 | 0x8F | winserver-2019 |
| Server 2022 | 0x91 | winserver-2022 |
| Server 2025 | 0x93 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 93 00 00 00 mov eax, 0x93 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
`NtCallEnclave` is the **world-switch** call. Internally it performs an `EENTER` on SGX or a `VMCALL` into the Secure Kernel for VBS — both ultimately transfer the host thread's logical CPU into VTL1 with a saved-stack-and-registers context. The enclave routine runs with a *fresh* stack inside the enclave's reserved range; it can never touch host-VTL0 memory directly. To get data in and out, the enclave reads `Parameter` (a VTL0 address that the Secure Kernel marshals on entry) and returns a value via `*ReturnValue`. Reentrance is bounded by `ThreadCount` from `NtInitializeEnclave`. The user-mode wrapper `CallEnclave` is a thin shim around this syscall — it picks up `Routine` from the enclave's export table.
Common malware usage
This is the call that **executes attacker code in VTL1**, but only after the create/load/initialize chain has passed signature checks. Realistic abuse paths: (1) call into a *Microsoft-signed* enclave with crafted parameters that trigger a known vulnerability — the enclave then runs attacker logic with VTL0-EDR blindness; (2) call into a *research-loader* enclave (e.g. the Yuste / Soriano-Salvador stub) that intentionally accepts an arbitrary shellcode pointer in `Parameter` and jumps to it inside VTL1. Once inside, the enclave can use `NtCallEnclave` with `Routine = 0x... outside enclave` to call *back* into the host (it's bidirectional via the `CallEnclave` mechanism); this is the path that Recon-class research uses to issue normal syscalls (e.g. `NtAllocateVirtualMemory`) from within the enclave. EDR sees the host syscall but cannot attribute it to the enclave thread.
Detection opportunities
Direct telemetry from inside the enclave is impossible from VTL0 — that is by design. Pivot instead on (a) the *frequency* of `NtCallEnclave` from a process (legitimate trustlets call it on a steady cadence; an attacker tool often shows a burst at attack time), (b) the *callout* pattern: an enclave that immediately turns around and calls back into VTL0 to invoke `NtAllocateVirtualMemory`/`NtWriteVirtualMemory` is suspicious. The ETW provider `Microsoft-Windows-Kernel-Memory` (event 5) traces enclave entries/exits but is not enabled by default — turn it on for high-value endpoints. From a kernel driver perspective, `KeRegisterEnclaveCallback` does *not* exist; you cannot intercept the enclave call from a normal driver. The signal must come from the host syscall sequence.
Direct syscall examples
cVBS enclave bring-up (step 4 — call into VTL1)
// Final step of a minimal enclave session: invoke a named export inside the enclave.
// 'enclaveBase' came from CreateEnclave; 'EnclaveEntry' is exported by the signed enclave DLL.
#include <windows.h>
typedef int (*PENCLAVE_ENTRY)(void *param);
int InvokeEnclave(PVOID enclaveBase, PVOID hostRequest) {
PENCLAVE_ENTRY entry = (PENCLAVE_ENTRY)GetProcAddressForCaller(
enclaveBase, "EnclaveEntry");
PVOID retVal = NULL;
if (!CallEnclave(entry, hostRequest, TRUE /*WaitForThread*/, &retVal)) {
return -1;
}
return (int)(INT_PTR)retVal;
}asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtCallEnclave (SSN 0x93 on Win11 24H2 / Server 2025)
; SSN was 0x91 from Win10 2004 through Win11 23H2 — stable for ~5 years.
NtCallEnclave PROC
mov r10, rcx ; Routine
mov eax, 93h ; SSN
syscall
ret
NtCallEnclave ENDPrustwindows-sys CallEnclave
// Cargo: windows-sys = { version = "0.59", features = ["Win32_System_Threading"] }
use std::ptr::null_mut;
use windows_sys::core::BOOL;
extern "system" {
fn CallEnclave(
routine: *const core::ffi::c_void,
param: *mut core::ffi::c_void,
wait_for_thread: BOOL,
ret: *mut *mut core::ffi::c_void,
) -> BOOL;
}
pub unsafe fn call_enclave_entry(
routine: *const core::ffi::c_void,
request: *mut core::ffi::c_void,
) -> Option<*mut core::ffi::c_void> {
let mut ret: *mut core::ffi::c_void = null_mut();
if CallEnclave(routine, request, 1 /*TRUE*/, &mut ret) == 0 {
return None;
}
Some(ret)
}MITRE ATT&CK mappings
Last verified: 2026-05-20