NtDeleteAtom
Decrements the reference count of a global atom and removes it when the count reaches zero.
Prototype
NTSTATUS NtDeleteAtom( RTL_ATOM Atom );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| Atom | RTL_ATOM | in | 16-bit atom ID returned by NtAddAtom / NtFindAtom. Must be in the user range 0xC000-0xFFFF for global user atoms. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0xC1 | win10-1507 |
| Win10 1607 | 0xC4 | win10-1607 |
| Win10 1703 | 0xC7 | win10-1703 |
| Win10 1709 | 0xC8 | win10-1709 |
| Win10 1803 | 0xC9 | win10-1803 |
| Win10 1809 | 0xCA | win10-1809 |
| Win10 1903 | 0xCB | win10-1903 |
| Win10 1909 | 0xCB | win10-1909 |
| Win10 2004 | 0xCF | win10-2004 |
| Win10 20H2 | 0xCF | win10-20h2 |
| Win10 21H1 | 0xCF | win10-21h1 |
| Win10 21H2 | 0xD0 | win10-21h2 |
| Win10 22H2 | 0xD0 | win10-22h2 |
| Win11 21H2 | 0xD5 | win11-21h2 |
| Win11 22H2 | 0xD6 | win11-22h2 |
| Win11 23H2 | 0xD6 | win11-23h2 |
| Win11 24H2 | 0xD8 | win11-24h2 |
| Server 2016 | 0xC4 | winserver-2016 |
| Server 2019 | 0xCA | winserver-2019 |
| Server 2022 | 0xD4 | winserver-2022 |
| Server 2025 | 0xD8 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 D8 00 00 00 mov eax, 0xD8 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
The mirror of `NtAddAtom`. Each successful AddAtom on an existing name increments a refcount; DeleteAtom decrements it. The atom is only physically removed from the global table when the count hits zero. The Win32 wrappers `GlobalDeleteAtom` and `DeleteAtom` route through here. There is no per-process bookkeeping — any process that knows the atom ID can decrement the count, which is both the convenience of the API and the cleanup hazard. Returns STATUS_INVALID_HANDLE for an ID outside the user-atom range or for a never-added atom.
Common malware usage
**Atom Bombing cleanup**: enSilo's original PoC (Tal Liberman 2016) and the Dridex 2017 adaptation both call NtDeleteAtom on the smuggled-shellcode atom IDs after the target APC has copied the bytes into the victim's address space. Skipping cleanup leaves the shellcode bytes in the global atom table forever (well, until reboot) — a forensic gift for any analyst who runs `!atom` in WinDbg against a live machine, or who diffs the atom table before and after a suspected injection. Outside Atom Bomb cleanup the signal is essentially zero — `NtDeleteAtom` is fired millions of times a day by legitimate apps that register and tear down window classes, DDE topics and IME strings.
Detection opportunities
On its own, NtDeleteAtom is one of the noisiest syscalls on a Windows desktop — Explorer, every WPF/WinForms app, the input stack, DWM, all churn the atom table constantly. The detection pivot is `NtAddAtom` with anomalous (long, binary) names; if you alert on suspicious AddAtom traffic, the matching DeleteAtom comes for free as part of the same flow. WinDbg `!atom` extension can be scripted via LiveKD / lkd to periodically snapshot the global table and diff for entries whose names are not valid UTF-16 or whose byte length exceeds the typical 32-byte window-class size — those are Atom Bomb residuals. ETW Microsoft-Windows-Kernel-Audit-API-Calls covers the call but at very high volume.
Direct syscall examples
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtDeleteAtom (SSN 0xD8 on Win11 24H2)
NtDeleteAtom PROC
mov r10, rcx ; syscall convention
mov eax, 0D8h ; SSN — varies per build
syscall
ret
NtDeleteAtom ENDPcAtom Bombing cleanup pass
// After NtQueueApcThread has delivered the GlobalGetAtomName APC into the
// target and the shellcode bytes have landed in the victim's buffer,
// scrub the smuggled atom IDs from the global table.
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *pNtDeleteAtom)(USHORT);
VOID AtomBombCleanup(const USHORT* ids, ULONG count) {
pNtDeleteAtom NtDeleteAtom = (pNtDeleteAtom)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDeleteAtom");
for (ULONG i = 0; i < count; ++i) {
// Refcount may be >1 if the OS interned the same string elsewhere
// — call until the kernel returns STATUS_INVALID_HANDLE.
while (NtDeleteAtom(ids[i]) == 0) { /* loop */ }
}
}rustBenign teardown via Win32 wrapper
// Cargo: windows-sys = "0.59" (Win32_System_DataExchange)
// Symmetric DeleteAtom for a string we registered with GlobalAddAtomW.
use windows_sys::Win32::System::DataExchange::GlobalDeleteAtom;
pub unsafe fn drop_atom(id: u16) {
// Returns 0 on success — refcount hit zero.
let _residual = GlobalDeleteAtom(id);
}MITRE ATT&CK mappings
Last verified: 2026-05-20