VTL Secure Calls — NT ↔ Secure Kernel Interface

Last updated: 2026-04-10
Author of primary source: Connor McGarr
Related: Mitigations, Architecture
Tags: vtl, vbs, hvci, hyper-v, secure-kernel, secure-calls, kernel-mode, internals


Summary

“Secure calls” are the mechanism by which the NT kernel (VTL 0) requests services from the Secure Kernel (VTL 1). They underlie key Windows security features: HVCI, Credential Guard, Virtual TPM, Windows Hello. Understanding this interface is essential for:

  • HVCI bypass research
  • Kernel shadow stack analysis (VslAllocateKernelShadowStack is a secure call)
  • Trustlet/IUM process research
  • VBS/VSM security model analysis

Key tool: Connor McGarr’s SkBridge — allows issuing arbitrary secure calls from kernel mode; Vtl1Mon — ETW-based VTL 1 entry/exit monitoring.


Architecture Overview

VTL 0 (NT kernel)
  nt!VslpEnterIumSecureMode()
    → nt!HvlSwitchToVsmVtl1()
      → vmcall (HvCallVtlCall = 0x11)
        → [VM Exit to Hyper-V]
          → HandleVmCall
            → HvCallVtlCall handler
              → LoadVTL1 VMCS
              → vmresume
                → [VTL 1: securekernel!IumInvokeSecureService]
                  → service handled
                  → securekernel!SkpPrepareForNormalCall
                    → vmcall (HvCallVtlReturn = 0x12)
                      → [VM Exit to Hyper-V]
                        → Restore VTL 0 VMCS
                        → vmresume
                          → [VTL 0 resumes after vmcall instruction]

Three components: NT kernel, Hyper-V (VMM), Secure Kernel. Hyper-V brokers the VTL transition — SK does NOT live “in the hypervisor”.


Hypercall Mechanics

A secure call is implemented as vmcall with hypercall code HvCallVtlCall = 0x11:

// NT-side stub: nt!HvlpVsmVtlCallVa
// Issues: vmcall with HvCallVtlCall (0x11) in RCX[15:0]

// Hyper-V VM exit handler dispatches to HvCallVtlCall handler:
// gs:[360h] → current partition
// gs:[368h] → current virtual processor (VP)
// VP+0x3C0  → VTL state array (per-VTL state)
// VTL state+0x14 → current VTL index

On vmcall, Hyper-V:

  1. Captures VM exit reason = VMX_EXIT_REASON_EXECUTE_VMCALL
  2. Identifies hypercall code from VMCS guest RAX
  3. Routes to HvCallVtlCall handler
  4. Increments VTL 0 RIP to next instruction (so return doesn’t re-issue vmcall)
  5. Loads VTL 1 VMCS (vmptrld VTL1_VMCS_ptr)
  6. Issues vmresume → processor starts executing in VTL 1 at VTL 1’s saved RIP

VTL 1’s saved RIP is always securekernel!SkpReturnFromNormalMode — a carefully positioned trampoline that handles any VM entry into VTL 1.


Secure Call Return Path

When the Secure Kernel finishes servicing a secure call:

// securekernel!IumInvokeSecureService →
// → securekernel!SkpPrepareForNormalCall
//   → securekernel!SkpPrepareForReturnToNormalMode
//     → call securekernel!ShvlpVtlReturn  ← pushes return addr onto stack
//        Next instruction after call = securekernel!SkpReturnFromNormalMode
//     → ShvlpVtlReturn issues vmcall (HvCallVtlReturn = 0x12)
//       → VM exit → Hyper-V fixes up VTL 1 RIP to next instruction (= ret)
//       → Loads VTL 0 VMCS → vmresume → VTL 0 continues
//
// Next entry into VTL 1: executes 'ret' → pops stack → lands at SkpReturnFromNormalMode

The call (not jump) to ShvlpVtlReturn is intentional — it stages SkpReturnFromNormalMode on the stack so that the VTL 1 “always-ready” trampoline is correctly positioned for the next VTL 1 entry.


NT-Side Interface: VslpEnterIumSecureMode

NTSTATUS
nt!VslpEnterIumSecureMode(
    _In_    UINT8           OperationType,
    _In_    ULONG64         SecureCallCode,
    _In_    ULONG64         OptionalSecureThreadCookie,
    _Inout_ SECURE_CALL_ARGS *SecureCallArgs
);

OperationType values:

  • 2 = Standard secure service request (most common)
  • 3 = Flush translation buffers (direct path, may generate ETW event)
typedef struct _SECURE_CALL_ARGS {
    SECURE_CALL_RESERVED_FIELD Reserved;  // contains OperationType + SecureCallCode + cookie
    ULONGLONG Field1;   // up to 12 input/output fields
    ULONGLONG Field2;
    ...
    ULONGLONG Field12;
} SECURE_CALL_ARGS;  // 0x68 bytes total

Valid secure call codes: Enumerated in nt!_SKSERVICE symbol. Providing an invalid code triggers a bugcheck.


Secure Threads and Thread Cookies

Some secure calls are dispatched on a specific secure thread (a thread running in VTL 1, usually owned by a trustlet):

// Find secure thread cookies in current session:
lkd> dx -g @$cursession.Processes
  .Where(p => p.Threads.Any(t => t.KernelObject.Tcb.SecureThreadCookie != 0))
  .Last().Threads
  .Where(t => t.KernelObject.Tcb.SecureThreadCookie != 0)
  .Select(t => new { Process, TID, SecureThreadCookie })

// Example: NgcIso.exe (Windows Hello) with cookie 0x15

OptionalSecureThreadCookie routes the secure call to be handled by the thread identified by that cookie in VTL 1.


Parameter Passing: MDL Encapsulation

Many secure calls pass parameters as MDLs (Memory Descriptor Lists):

  1. VTL 0 allocates parameter data, creates MDL locking it into physical memory
  2. MDL virtual address + PFN (physical page number) passed to secure call
  3. VTL 1 creates a second MDL validating the input MDL’s physical pages
  4. VTL 1 maps the VTL 0 MDL into VTL 1 virtual address space via MdlMappedSystemVa
  5. VTL 1 processes parameter via mapped VA
  6. Output written back through same MDL or output fields of SECURE_CALL_ARGS

Security Implications

Attack Surface on Secure Calls

  • Invalid secure call code → bugcheck (no information disclosure, but DoS)
  • Malformed MDL → potential VTL 1 kernel bugs (if SK validation is incomplete)
  • Secure thread cookies are handles — handle table exhaustion may apply
  • SkBridge enables fuzzing the SC interface with arbitrary parameters

HVCI and Secure Calls

The VslAllocateKernelShadowStack secure call (from KiCreateKernelShadowStack) is the mechanism by which the NT kernel gets shadow stack pages. The Secure Kernel allocates these pages in VTL 1-protected memory, then maps them back to VTL 0 as read-only + shadow-stack-writable only. Any HVCI bypass that wants to corrupt shadow stacks must either:

  1. Compromise the secure call interface itself, OR
  2. Find a way to write to VTL 1-protected physical pages from VTL 0

Hyper-V Internals Key Offsets (Windows 24H2)

LocationContent
gs:[0h]Self-pointer (Hyper-V per-CPU data)
gs:[360h]Current partition
gs:[368h]Current virtual processor (VP)
gs:[2C680h]Current VMCS virtual address (enlightened VMCS)
VP + 0x3C0VTL state array
VTL_state + 0x14Current VTL index
Partition + 0x1B0Privilege mask (for hypercall authorization check)

Offsets change between builds — verify with windbgx -z C:\Windows\system32\hvix64.exe.


References

  • Connor McGarr — “Windows Internals: Secure Calls — The Bridge Between NT and Secure Kernel” — 2025-09-06
  • Connor McGarr — SkBridge tool
  • Connor McGarr — Vtl1Mon tool
  • Quarkslab — “A Virtual Journey from Hardware Virtualization to Hyper-V’s Virtual Trust Levels”
  • Saar Amar — “First Steps in Hyper-V Research” — MSRC blog 2018
  • Windows Internals 7th Edition Part 2 — SECURE_CALL_ARGS structure documentation