← Back to blog

The Sleep Encryption Triad: Zero Cleartext in Memory

· Krait Team · 6 min read

Modern EDRs don’t just scan processes at load time. They periodically walk process memory looking for IOCs — strings, config patterns, known shellcode sequences, suspicious return addresses. The most vulnerable window is during sleep, when the implant is idle and its memory is static. A single cleartext URL or API string in a heap allocation is enough for a memory scanner to flag the process.

Most C2 frameworks address this by encrypting the implant’s code section during sleep. That’s necessary but insufficient. An implant’s memory footprint spans three distinct regions, and leaving any one of them cleartext defeats the purpose of encrypting the others.

Krait encrypts all three simultaneously. We call it the sleep encryption triad.

The Three Regions

1. Code — Ekko and Foliage

The implant’s .text section contains the executable code. During sleep, this section is the primary target for signature-based memory scanners like pe-sieve and Moneta.

Krait ships two sleep encryption engines with different behavioral fingerprints:

Ekko uses a timer-queue ROP chain. A VirtualAlloc’d shellcode stub calls NtProtectVirtualMemory to flip the code section to RW, then SystemFunction032 (the undocumented RC4 wrapper in ntdll) to encrypt it, then NtDelayExecution to sleep. On wake, the process reverses: decrypt, then restore RX permissions. During the entire sleep window, no page in the implant has execute permission.

Foliage takes a different approach. Instead of a shellcode stub, it creates a suspended thread and builds a chain of seven CONTEXT structures, each representing one step of the encrypt-sleep-decrypt sequence. The chain is queued via NtQueueApcThread and executed through NtContinue. The result is identical — encrypted code during sleep — but the API call sequence and thread behavior are completely different. If an EDR builds a behavioral signature for Ekko’s timer-queue pattern, switching to Foliage changes the fingerprint entirely.

Both engines use indirect syscalls for all Nt operations within the stub, so even the encryption mechanism itself doesn’t trigger userland API hooks.

2. Data — Sleep Mask

The implant’s global data — C2 configuration, poll URLs, pipe names, child relay tables, SOCKS channel state — lives in writable memory regions that are just as scannable as code. A memory scanner doesn’t need to find shellcode if it can find a hardcoded C2 URL or a named pipe pattern.

Krait’s sleep mask registers up to 16 memory regions at runtime via SleepMaskRegister(). Before entering Ekko or Foliage sleep, SleepMaskApply() generates a random 16-byte XOR key (via rand_s) and XORs every registered region. On wake, the same key reverses the operation. The key is zeroed immediately after unmasking.

The registered regions include g_config (all C2 settings), g_children (the P2P relay table), the SOCKS channel table, and httpConfig (the current poll URL and headers). After masking, a memory scan of the implant’s data sections finds only random bytes.

3. Heap — Heap Encryption

The process heap is the most overlooked attack surface. Every HeapAlloc call leaves data in heap blocks that persist even after the pointer is freed — unless the memory is explicitly zeroed. API name strings from resolution, partial HTTP responses, BOF output buffers, DNS query fragments — all of these accumulate in the heap across poll cycles.

Krait’s heap encryption walks every non-default process heap during the sleep transition. Before encryption, all non-implant threads are suspended via CreateToolhelp32Snapshot + SuspendThread to prevent heap corruption from concurrent access. Each heap block is XOR’d with a per-cycle key derived from QueryPerformanceCounter. On wake, threads are resumed after decryption completes.

The thread suspension step is critical. Many implementations skip it, leading to intermittent heap corruption when a background thread (loader lock, COM, WMI) accesses the heap mid-encryption. Krait suspends all threads except the poll thread before touching the heap, and resumes them in reverse order after decryption.

The Coordination

The three layers don’t operate independently. They execute in a precise sequence:

PeFluctSuspend()           // pause PE fluctuation timer
SleepMaskApply(MASK)       // XOR data regions
HeapEncryptApply(ENCRYPT)  // XOR heap blocks (threads suspended)
EkkoSleep(interval)        // encrypt .text + sleep
HeapEncryptApply(DECRYPT)  // XOR heap blocks (threads resumed)
SleepMaskApply(UNMASK)     // XOR data regions
PeFluctResume()            // restart PE fluctuation timer

During the sleep window, the implant’s memory state is:

  • .text section: RC4-encrypted, pages marked RW (no execute)
  • Global data: XOR-encrypted with random key
  • Heap: XOR-encrypted with random key, all non-poll threads suspended
  • Stack: XOR-encrypted from RSP to StackBase (when stack encryption is enabled)

A memory scanner examining the process during this window finds no executable code, no cleartext strings, no recognizable data structures, and no meaningful return addresses. The process looks like a dormant application with encrypted private pages — which, functionally, is exactly what it is.

The Fourth Layer: PE Fluctuation

Krait adds an optional fourth dimension that extends encryption beyond sleep intervals. PE Fluctuation uses a timer callback and a Vectored Exception Handler (VEH) to keep the .text section encrypted even between polls.

The timer fires every 500ms and RC4-encrypts .text. When the implant needs to execute code, the access violation triggers the VEH handler, which decrypts the faulting page. Only the actively executing page is cleartext at any moment. The VEH handler and timer callback live in a separate .pef section so they survive the .text encryption.

This means that even a point-in-time memory scan between poll cycles — not during sleep, but during the active execution window — has a 500ms race condition to catch cleartext code. Combined with the sleep triad, the implant’s memory is encrypted for the vast majority of its lifetime.

Why This Matters

Individual encryption techniques are well-documented. What matters operationally is whether they compose into a system that eliminates the entire memory attack surface, not just the most obvious one.

A C2 that encrypts .text during sleep but leaves heap allocations full of URL strings hasn’t solved the problem — it’s shifted the detection to a different address range. A C2 that encrypts the heap but leaves the stack full of return addresses into private memory has given the EDR a different signal to key on.

The triad approach treats memory encryption as a coverage problem. Code, data, heap, and stack are four distinct surfaces, and the operational guarantee is that all four are encrypted during every sleep cycle. No cleartext survives.

That’s the standard Krait holds itself to: during sleep, a memory forensics examiner should find nothing to indicate that the process is anything other than what it claims to be.