NtAddAtom
Añade (o incrementa el refcount) una cadena en la tabla de átomos global del kernel y devuelve su ID de 16 bits.
Prototipo
NTSTATUS NtAddAtom( PWSTR AtomName, ULONG Length, PRTL_ATOM Atom );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| AtomName | PWSTR | in | Cadena UTF-16 a registrar. **Sin NUL requerido** — Length está en bytes y el kernel guarda la secuencia cruda. |
| Length | ULONG | in | Tamaño del buffer en bytes. Permite contenido binario arbitrario (primitiva de subida de shellcode Atom Bomb). |
| Atom | PRTL_ATOM | out | Recibe el ID de átomo de 16 bits (RTL_ATOM es USHORT). Rango 0xC000-0xFFFF para átomos de usuario. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x47 | win10-1507 |
| Win10 1607 | 0x47 | win10-1607 |
| Win10 1703 | 0x47 | win10-1703 |
| Win10 1709 | 0x47 | win10-1709 |
| Win10 1803 | 0x47 | win10-1803 |
| Win10 1809 | 0x47 | win10-1809 |
| Win10 1903 | 0x47 | win10-1903 |
| Win10 1909 | 0x47 | win10-1909 |
| Win10 2004 | 0x47 | win10-2004 |
| Win10 20H2 | 0x47 | win10-20h2 |
| Win10 21H1 | 0x47 | win10-21h1 |
| Win10 21H2 | 0x47 | win10-21h2 |
| Win10 22H2 | 0x47 | win10-22h2 |
| Win11 21H2 | 0x47 | win11-21h2 |
| Win11 22H2 | 0x47 | win11-22h2 |
| Win11 23H2 | 0x47 | win11-23h2 |
| Win11 24H2 | 0x47 | win11-24h2 |
| Server 2016 | 0x47 | winserver-2016 |
| Server 2019 | 0x47 | winserver-2019 |
| Server 2022 | 0x47 | winserver-2022 |
| Server 2025 | 0x47 | winserver-2025 |
Módulo del kernel
APIs relacionadas
Stub del syscall
4C 8B D1 mov r10, rcx B8 47 00 00 00 mov eax, 0x47 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
Notas no documentadas
El SSN `0x47` ha sido totalmente estable de 1507 a 24H2 — los átomos son un subsistema viejo que nadie renumera. El kernel mantiene una **tabla de átomos global** independiente del proceso: cualquier proceso puede hacer `NtAddAtom` con un nombre y cualquier otro puede `NtFindAtom` para obtener el mismo ID, y luego `NtQueryInformationAtom` para recuperar el nombre. Los wrappers Win32 `GlobalAddAtom*` / `GlobalFindAtom*` / `GlobalGetAtomName*` rutean por estos syscalls. Crucialmente, `Length` está en **bytes** y el kernel no exige que el buffer sea una cadena Unicode NUL-terminada válida — guarda los bytes tal cual, lo que es la base del Atom Bombing.
Uso común por malware
**Atom Bombing** (enSilo, Tal Liberman, octubre 2016) — la técnica canónica de inyección de código construida sobre este syscall. Etapa 1: escribir shellcode en la tabla de átomos global llamando a `GlobalAddAtomA` (que termina en NtAddAtom) con los bytes del shellcode como 'nombre'. Etapa 2: encolar un APC en el proceso objetivo que llame a `GlobalGetAtomNameA` (NtQueryInformationAtom por dentro); el kernel escribe el 'nombre' del átomo — es decir, el shellcode del atacante — en un buffer del espacio de direcciones del objetivo. Etapa 3: un APC posterior o un gadget ROP transiciona ese buffer a RWX (vía NtSetContextThread + llamadas ROP a `ZwProtectVirtualMemory`). Como **la escritura en el proceso objetivo la realiza el propio kernel**, las funciones usermode hookeadas en el objetivo (NtWriteVirtualMemory, WriteProcessMemory) nunca ven los datos — derrotando a los EDR que dependen de hooks de inyección usermode. Recogido por Dridex (oleada 2017, writeup de FireEye), e incorporado como técnica base en varios RAT privados desde entonces.
Oportunidades de detección
La telemetría sobre NtAddAtom desde usermode es escasa — los átomos son tecnología de los 90 que pocos productos instrumentaron históricamente. Los EDR modernos (CrowdStrike, SentinelOne, Defender) añadieron hacia 2017-2018 reglas conductuales que buscan: (1) llamadas `GlobalAddAtomA/W` con `Length` > ~250 bytes (los átomos reales son nombres cortos de clase de ventana o cadenas DDE); (2) bytes de nombre de átomo que no roundtrip a UTF-16 válido (contenido binario); (3) `GlobalGetAtomNameA` llamado desde un hilo que acaba de despertar vía NtTestAlert / KiUserApcDispatcher en un contexto recién asignado — el patrón APC de Atom Bomb. ETW Microsoft-Windows-Kernel-Audit-API-Calls cubre estos syscalls. WinDbg / forense en vivo: la extensión `!atom` vuelca la tabla global — entradas largas con bytes no imprimibles son un IOC fuerte.
Ejemplos de syscalls directos
asmx64 direct stub
; Direct syscall stub for NtAddAtom (SSN 0x47, all builds)
NtAddAtom PROC
mov r10, rcx ; syscall convention
mov eax, 47h ; SSN
syscall
ret
NtAddAtom ENDPcAtom Bombing — stage 1 shellcode upload
// Atom Bomb stage 1: smuggle shellcode bytes into the global atom table.
// The Length is in bytes and the kernel does not require a NUL terminator,
// so arbitrary binary content rides in as the atom 'name'.
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *pNtAddAtom)(PWSTR, ULONG, PUSHORT);
USHORT AtomBombUpload(const BYTE* shellcode, ULONG cb) {
pNtAddAtom NtAddAtom = (pNtAddAtom)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtAddAtom");
USHORT atomId = 0;
// PWSTR is just a cast — the kernel reads `cb` raw bytes regardless
// of UTF-16 validity. Real Atom Bomb POCs round cb up to even.
NTSTATUS s = NtAddAtom((PWSTR)shellcode,
(cb + 1) & ~1u, // even byte count
&atomId);
return NT_SUCCESS(s) ? atomId : 0;
}rustLightweight global atom (legitimate-style)
// Cargo: ntapi = "0.4", widestring = "1"
// Register a normal short string atom — same syscall, benign use.
// Demonstrates the Length-in-bytes convention.
use ntapi::ntexapi::NtAddAtom;
use widestring::U16CString;
pub unsafe fn add_atom(name: &str) -> Option<u16> {
let w = U16CString::from_str(name).ok()?;
let mut atom: u16 = 0;
let bytes = w.len() * 2; // UTF-16 byte count, excluding NUL
let s = NtAddAtom(w.as_ptr() as _, bytes as u32, &mut atom);
if s >= 0 { Some(atom) } else { None }
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20