NtUnloadKey
Detaches a previously-loaded registry hive from the configuration manager.
Prototype
NTSTATUS NtUnloadKey( POBJECT_ATTRIBUTES TargetKey );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| TargetKey | POBJECT_ATTRIBUTES | in | Object attributes naming the hive root key to unload, e.g. \Registry\User\TempHive. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x1AA | win10-1507 |
| Win10 1607 | 0x1B3 | win10-1607 |
| Win10 1703 | 0x1B9 | win10-1703 |
| Win10 1709 | 0x1BD | win10-1709 |
| Win10 1803 | 0x1BF | win10-1803 |
| Win10 1809 | 0x1C0 | win10-1809 |
| Win10 1903 | 0x1C1 | win10-1903 |
| Win10 1909 | 0x1C1 | win10-1909 |
| Win10 2004 | 0x1C7 | win10-2004 |
| Win10 20H2 | 0x1C7 | win10-20h2 |
| Win10 21H1 | 0x1C7 | win10-21h1 |
| Win10 21H2 | 0x1C9 | win10-21h2 |
| Win10 22H2 | 0x1C9 | win10-22h2 |
| Win11 21H2 | 0x1D3 | win11-21h2 |
| Win11 22H2 | 0x1D7 | win11-22h2 |
| Win11 23H2 | 0x1D7 | win11-23h2 |
| Win11 24H2 | 0x1DA | win11-24h2 |
| Server 2016 | 0x1B3 | winserver-2016 |
| Server 2019 | 0x1C0 | winserver-2019 |
| Server 2022 | 0x1CF | winserver-2022 |
| Server 2025 | 0x1DA | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 DA 01 00 00 mov eax, 0x1DA 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
NtUnloadKey forcibly detaches a registry hive that was previously attached via NtLoadKey / `reg load`. The caller must hold SeRestorePrivilege; the request fails with STATUS_PRIVILEGE_NOT_HELD otherwise. Internally CmUnloadKey walks the CM_KEY_CONTROL_BLOCK chain and writes back any pending changes before tearing down the hive — so a sloppy unload of a still-in-use hive can race against another thread's open key handles and return STATUS_CANNOT_DELETE. A safer variant NtUnloadKey2 takes an `Flags` argument to force the detach.
Common malware usage
Used as the cleanup half of an offline-hive abuse: an operator stages `reg load HKU\Tmp C:\Users\victim\NTUSER.DAT`, extracts credentials / harvests Run keys from the mounted hive, then issues NtUnloadKey to detach the hive and remove the obvious indicator. The same primitive is used as light anti-forensics — by unloading a hive that a triage tool has just mapped, the implant can starve the analyst's view. Some loaders also unload temporarily-mounted hives used for AppCompat / TypedURL tampering.
Detection opportunities
Sysmon doesn't have a dedicated event for hive unload, but ETW provider Microsoft-Windows-Kernel-Registry emits Event ID 1 (HiveLoad) and Event ID 2 (HiveUnload) with the hive path. Audit subcategory "Registry" (4657 / 4660) does not cover hive load/unload; you need "Other System Events" (4660-class) or the Kernel-Registry ETW provider directly. Defender ATP raises a high-fidelity alert on `reg unload` of hives outside %SystemRoot%\System32\config — anomalous when paired with a preceding hive load against another user's profile.
Direct syscall examples
asmx64 direct stub (Win11 24H2 SSN 0x1DA)
NtUnloadKey PROC
mov r10, rcx
mov eax, 1DAh
syscall
ret
NtUnloadKey ENDPcOffline hive cleanup after credential harvest
// After mounting another user's NTUSER.DAT via NtLoadKey and harvesting,
// detach it so `reg query HKU` no longer reveals the staged hive.
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\Registry\\User\\TempHive");
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, &name, OBJ_CASE_INSENSITIVE, NULL, NULL);
NTSTATUS st = NtUnloadKey(&oa);
if (st == STATUS_CANNOT_DELETE) {
// Some handle is still open; force via NtUnloadKey2 with REG_FORCE_UNLOAD.
}rustwindows-sys + SeRestorePrivilege enablement
// Cargo: windows-sys = "0.59" (Win32_Security, Win32_System_Registry)
// Caller must first enable SeRestorePrivilege via NtAdjustPrivilegesToken.
unsafe {
let mut name = make_unicode_string("\\Registry\\User\\TempHive");
let mut oa = OBJECT_ATTRIBUTES {
Length: size_of::<OBJECT_ATTRIBUTES>() as u32,
ObjectName: &mut name,
Attributes: OBJ_CASE_INSENSITIVE,
..zeroed()
};
let st = NtUnloadKey(&mut oa);
assert!(st == 0, "NtUnloadKey failed: {st:#x}");
}MITRE ATT&CK mappings
Last verified: 2026-05-20