> Windows Syscalls
ntoskrnl.exeT1068T1106

NtVdmControl

Legacy NT Virtual DOS Machine control entry point — 16-bit DOS/Windows support, defunct on x64.

Prototype

NTSTATUS NtVdmControl(
  VDMSERVICECLASS ServiceClass,
  PVOID           ServiceData
);

Arguments

NameTypeDirDescription
ServiceClassVDMSERVICECLASSinEnum selecting the VDM operation (VdmStartExecution, VdmQueueInterrupt, VdmDelayInterrupt, VdmInitialize, …).
ServiceDataPVOIDin/outPer-service input/output buffer. Layout depends entirely on ServiceClass; mostly opaque to non-NTVDM callers.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
Win10 15070x1B2win10-1507
Win10 16070x1BBwin10-1607
Win10 17030x1C1win10-1703
Win10 17090x1C5win10-1709
Win10 18030x1C7win10-1803
Win10 18090x1C8win10-1809
Win10 19030x1C9win10-1903
Win10 19090x1C9win10-1909
Win10 20040x1CFwin10-2004
Win10 20H20x1CFwin10-20h2
Win10 21H10x1CFwin10-21h1
Win10 21H20x1D1win10-21h2
Win10 22H20x1D1win10-22h2
Win11 21H20x1DBwin11-21h2
Win11 22H20x1DFwin11-22h2
Win11 23H20x1DFwin11-23h2
Win11 24H20x1E2win11-24h2
Server 20160x1BBwinserver-2016
Server 20190x1C8winserver-2019
Server 20220x1D7winserver-2022
Server 20250x1E2winserver-2025

Kernel module

ntoskrnl.exeNtVdmControl

Related APIs

NtCreateProcessNtQueueApcThreadNtRaiseExceptionWow64GetThreadContext

Syscall stub

4C 8B D1            mov r10, rcx
B8 E2 01 00 00      mov eax, 0x1E2
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

NtVdmControl is the kernel back end for the NT Virtual DOS Machine (NTVDM) — the Windows NT subsystem that ran 16-bit MS-DOS and Win16 binaries on 32-bit Windows via ntvdm.exe. NTVDM was *removed in its entirety* on 64-bit editions of Windows, because long-mode CPUs cannot execute the v8086-mode that NTVDM relied on. The syscall stub nevertheless survives in 64-bit ntoskrnl.exe and dispatches into a stub that returns STATUS_NOT_IMPLEMENTED for every ServiceClass on x64. On 32-bit Windows 10/11 (and on WoW64-hosted ntvdm.exe up through 32-bit editions) the call services interrupt queueing, BIOS data area maintenance, and v8086 state transitions for the trapped DOS process. There is no public Win32 wrapper; everything that uses it is internal to ntvdm.exe and the WoW16/NTVDM subsystem.

Common malware usage

Pure historical interest on modern x64. On 32-bit XP and 32-bit Windows 7 NTVDM was a recurring Trojan vector — 16-bit DOS components of mass-mailer families (Bagle, Sober, Bugbear variants) shipped a small DOS stub that, when launched, started ntvdm.exe which then called NtVdmControl as part of its normal initialization. The actual abuse vector was the 16-bit code, not the syscall itself, but the syscall is the choke point. NTVDM has also been the source of multiple privilege escalations: CVE-2010-0232 (Tavis Ormandy's #GP-handler bug, exploited on 32-bit XP/Vista/7) and CVE-2018-8897/8453 (kernel bugs reachable via legacy paths). On 64-bit Windows the syscall is unreachable as a meaningful primitive and there are no in-the-wild abuse reports.

Detection opportunities

On 64-bit Windows, *any* call to NtVdmControl is anomalous — the legitimate caller (ntvdm.exe) does not exist and the syscall returns STATUS_NOT_IMPLEMENTED unconditionally. ETW provider Microsoft-Windows-Kernel-Audit-API-Calls and EDR syscall-tracing hooks can flag invocations directly; the false-positive rate on a modern Windows 11 endpoint is essentially zero. On 32-bit hosts, scope detection to processes other than %SystemRoot%\System32\ntvdm.exe and look for ntvdm.exe parent chains that did not originate from a user double-clicking a .com/.exe MZ-only binary. Sysmon Event ID 1 (process creation) with parent image ntvdm.exe + child of an unusual DOS binary is the practical hunt on legacy systems.

Direct syscall examples

asmx64 direct stub (returns STATUS_NOT_IMPLEMENTED)

; Direct syscall stub for NtVdmControl (SSN 0x1E2, Win11 24H2)
; On x64 always returns 0xC0000002 STATUS_NOT_IMPLEMENTED.
NtVdmControl PROC
    mov  r10, rcx          ; syscall convention
    mov  eax, 1E2h         ; SSN
    syscall
    ret
NtVdmControl ENDP

cHistorical 32-bit XP-era 16-bit Trojan launch (defunct)

// Historical sketch — does not work on any 64-bit Windows.
// 16-bit MZ-only DOS components of early-2000s mass-mailers (Bagle, Sober)
// relied on the Windows shell auto-launching ntvdm.exe when the user
// double-clicked the 16-bit dropper. Inside, ntvdm.exe issued a long
// sequence of NtVdmControl calls to set up interrupt queues and the
// BIOS data area for the trapped DOS task.
//
// On modern x64 Windows the shell refuses to run 16-bit MZ binaries
// ("unsupported 16-bit application") and this whole vector is dead.
//
// Reference: VX-Underground archives, Symantec write-ups on Bagle.A
// and Sober.D from 2003-2004.
typedef NTSTATUS (NTAPI *PFN_NtVdmControl)(ULONG ServiceClass, PVOID ServiceData);
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
PFN_NtVdmControl p = (PFN_NtVdmControl)GetProcAddress(hNtdll, "NtVdmControl");
NTSTATUS s = p(/*VdmInitialize*/ 0, NULL); // STATUS_NOT_IMPLEMENTED on x64

rustProbe to confirm NTVDM absence on x64

// Detection helper: on any x64 Windows this returns STATUS_NOT_IMPLEMENTED.
// Useful inside an EDR self-test or a defensive sanity check.
use std::ffi::CString;
use std::ptr;

type NtVdmControlFn = unsafe extern "system" fn(u32, *mut core::ffi::c_void) -> i32;

fn ntvdm_is_dead() -> bool {
    unsafe {
        let ntdll = libloading::Library::new("ntdll.dll").unwrap();
        let f: libloading::Symbol<NtVdmControlFn> =
            ntdll.get(b"NtVdmControl").unwrap();
        let status = f(0, ptr::null_mut());
        status == 0xC0000002u32 as i32 // STATUS_NOT_IMPLEMENTED
    }
}

MITRE ATT&CK mappings

Last verified: 2026-05-20