NtCallEnclave
Transfère l'exécution du code hôte VTL0 vers une routine à l'intérieur d'une enclave initialisée.
Prototype
NTSTATUS NtCallEnclave( PVOID Routine, PVOID Parameter, BOOLEAN WaitForThread, PVOID *ReturnValue );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| Routine | PVOID | in | Adresse VTL1 de la fonction d'entrée de l'enclave. Doit se trouver dans la plage de l'enclave initialisée et être exportée via la config enclave. |
| Parameter | PVOID | in | Paramètre opaque unique passé à la routine d'enclave dans RCX. Typiquement un pointeur vers un buffer de requête alloué côté hôte. |
| WaitForThread | BOOLEAN | in | TRUE bloque jusqu'à ce qu'un thread d'enclave libre soit disponible ; FALSE retourne STATUS_ENCLAVE_NOT_TERMINATED si tous les threads réservés sont occupés. |
| ReturnValue | PVOID* | out | Reçoit la valeur PVOID retournée par la routine d'enclave (son RAX au retour vers VTL0). |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | 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 |
Module noyau
APIs liées
Stub du syscall
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
Notes non documentées
`NtCallEnclave` est l'appel de **bascule de monde**. En interne il exécute un `EENTER` sur SGX ou un `VMCALL` vers le Secure Kernel pour VBS — les deux transfèrent in fine le CPU logique du thread hôte vers VTL1 avec contexte stack/registres sauvegardé. La routine d'enclave s'exécute avec une stack *fraîche* à l'intérieur de la plage réservée de l'enclave ; elle ne peut jamais toucher directement la mémoire hôte VTL0. Pour échanger des données, l'enclave lit `Parameter` (une adresse VTL0 que le Secure Kernel marshale à l'entrée) et retourne une valeur via `*ReturnValue`. La réentrance est bornée par `ThreadCount` issu de `NtInitializeEnclave`. Le wrapper user-mode `CallEnclave` est un fin shim autour de ce syscall — il récupère `Routine` depuis la table d'exports de l'enclave.
Usage courant par les malwares
C'est l'appel qui **exécute du code attaquant en VTL1**, mais seulement après que la chaîne create/load/initialize a passé les vérifications de signature. Voies d'abus réalistes : (1) appeler dans une enclave *signée Microsoft* avec des paramètres ciblant une vulnérabilité connue — l'enclave exécute alors la logique attaquante en aveuglant l'EDR VTL0 ; (2) appeler dans une enclave *loader de recherche* (ex. le stub Yuste / Soriano-Salvador) qui accepte intentionnellement un pointeur de shellcode arbitraire dans `Parameter` et saute dessus dans VTL1. Une fois dedans, l'enclave peut utiliser `NtCallEnclave` avec `Routine = adresse hors enclave` pour rappeler l'hôte (le mécanisme `CallEnclave` est bidirectionnel) ; c'est la voie utilisée par la recherche Recon pour émettre des syscalls normaux (ex. `NtAllocateVirtualMemory`) depuis l'enclave. L'EDR voit le syscall hôte mais ne peut l'attribuer au thread enclave.
Opportunités de détection
La télémétrie directe depuis l'intérieur de l'enclave est impossible depuis VTL0 — c'est par conception. Pivoter plutôt sur (a) la *fréquence* de `NtCallEnclave` par processus (les trustlets légitimes l'appellent à cadence stable ; un outil attaquant montre souvent un pic au moment de l'attaque), (b) le pattern *callout* : une enclave qui se retourne immédiatement pour rappeler VTL0 et invoquer `NtAllocateVirtualMemory`/`NtWriteVirtualMemory` est suspecte. Le provider ETW `Microsoft-Windows-Kernel-Memory` (event 5) trace les entrées/sorties d'enclave mais n'est pas activé par défaut — l'activer sur les endpoints sensibles. Côté driver noyau, `KeRegisterEnclaveCallback` n'existe *pas* ; vous ne pouvez pas intercepter l'appel enclave depuis un driver classique. Le signal doit venir de la séquence syscall hôte.
Exemples de syscalls directs
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)
}Mappings MITRE ATT&CK
Last verified: 2026-05-20