← Back to blog

PE Fluctuation: Demand-Paged Encryption for Always-On Protection

· Krait Team · 6 min read

Sleep encryption is the standard answer to memory scanning. Encrypt the implant’s code during idle periods, decrypt when it’s time to work. Ekko, Foliage, UDRL-based approaches — they all follow the same pattern: the code is cleartext while executing and encrypted while sleeping.

This creates a window. Between poll cycles — after the implant decrypts to check in, processes any tasks, and before it re-encrypts to sleep — the code is fully cleartext in memory. For an implant with a 60-second sleep interval that takes 200ms to execute a poll, the code is cleartext for those 200ms every minute. That’s a 0.3% duty cycle where a memory scanner can catch it.

For most engagements, this is an acceptable risk. But against EDR products with continuous memory scanning or point-in-time dump capabilities, that window is exploitable. A defender who knows the approximate poll interval can time their scan to coincide with the active window.

Krait’s PE Fluctuation closes this window. Code is encrypted not just during sleep, but at all times except the specific page currently executing.

How It Works

PE Fluctuation uses two mechanisms that coordinate to maintain encryption during active execution:

A timer callback fires every 500ms and RC4-encrypts the entire .text section. This is the background encryption — it re-arms continuously, ensuring that any code left cleartext from a previous execution pass gets encrypted within half a second.

A Vectored Exception Handler catches access violations when the implant tries to execute an encrypted page. The handler decrypts the faulting page, marks it executable, and returns. Execution continues normally from the now-cleartext page.

The result is a demand-paging model for encrypted code. Only the page actively being executed is cleartext. All other pages in .text are RC4-encrypted. When execution moves to a different page, the new page faults, gets decrypted, and the timer re-encrypts the previous page on its next cycle.

Timer fires (every 500ms):
  → RC4-encrypt entire .text section
  → Set all .text pages to PAGE_READWRITE (no execute)

Code execution hits encrypted page:
  → Access violation (no execute permission)
  → VEH handler catches it
  → Decrypt the faulting page
  → Set page to PAGE_EXECUTE_READ
  → Return EXCEPTION_CONTINUE_EXECUTION
  → Code executes normally

Timer fires again:
  → Re-encrypts everything, including the page we just decrypted

The .pef Section

The timer callback and VEH handler obviously can’t live in .text — they’d be encrypted along with everything else. Krait places them in a separate PE section named .pef (PE Fluctuation). This section is always executable and never encrypted. It contains only the minimal code needed to manage the encryption cycle: the timer callback, the VEH handler, and the RC4 implementation.

The .pef section is small — a few hundred bytes. It’s a fixed, predictable artifact, but its contents (RC4 + VEH dispatch) don’t contain any IOC-worthy patterns. It looks like a legitimate helper section with generic cryptographic code.

Coordination with Sleep Encryption

PE Fluctuation and Ekko sleep encryption both operate on the .text section, so they need to coordinate to avoid double-encryption or corruption. Before entering Ekko sleep, the implant calls PeFluctSuspend():

PeFluctSuspend():
  → Cancel the timer (no more background encryption)
  → If .text is currently encrypted by PeFluct, decrypt it
  → Ekko now has cleartext .text to encrypt with its own RC4 key

EkkoSleep():
  → Encrypt .text with Ekko's key
  → Sleep
  → Decrypt .text with Ekko's key

PeFluctResume():
  → Re-arm the timer (background encryption resumes)
  → Next timer tick re-encrypts .text with PeFluct's key

The handoff is clean: PE Fluctuation owns .text encryption during active execution, Ekko owns it during sleep. At no point is .text double-encrypted or left in an inconsistent state.

What This Actually Buys You

Consider the timeline of a typical implant with a 60-second poll interval:

Without PE Fluctuation:

0.0s  — Wake from Ekko sleep, decrypt .text
0.0s  — .text is cleartext (VULNERABLE)
0.2s  — Poll complete, enter Ekko sleep, encrypt .text
0.2s  — .text encrypted (safe)
60.0s — Wake, decrypt, poll, encrypt

The cleartext window is ~200ms every 60 seconds. Small, but deterministic and exploitable.

With PE Fluctuation:

0.0s   — Wake from Ekko sleep, decrypt .text
0.0s   — PeFluctResume() re-arms timer
0.0s   — .text cleartext, execution begins
0.001s — Executing page 3 of .text
0.5s   — Timer fires: encrypts all .text pages except the one actively faulting
0.2s   — Poll complete, PeFluctSuspend(), enter Ekko sleep

During those 200ms of active execution, only one page (4KB) of .text is cleartext at any moment. The rest of the section is encrypted. A memory scanner that dumps the process during the active window gets one cleartext page and N-1 encrypted pages.

For a typical implant with a 40KB .text section (10 pages), 90% of the code is encrypted even during active execution. The specific page that’s cleartext changes as execution moves through different code paths — it’s a moving target, not a static window.

The Race Condition

PE Fluctuation doesn’t eliminate the cleartext window; it shrinks it from “the entire code section” to “one page, for up to 500ms.” A memory scanner that dumps at exactly the right moment will find one cleartext page.

But this changes the detection calculus. Instead of scanning for a known shellcode signature across the entire code section (a reliable detection), the scanner needs to:

  1. Catch the process during the sub-500ms window when a page is cleartext
  2. Have the signature happen to fall within that specific 4KB page
  3. Complete the scan before the timer re-encrypts the page

This is still theoretically possible but practically difficult, especially for automated scanning that runs on fixed intervals. The detection has moved from “pattern match against static memory” to “win a timing race against a 500ms timer on a per-page basis.”

When to Use It

PE Fluctuation is not free. The VEH handler fires on every page transition during execution, adding microseconds of overhead per fault. The 500ms timer adds periodic CPU wake-ups. For most engagements, standard Ekko/Foliage sleep encryption provides sufficient coverage.

PE Fluctuation is worth enabling when:

  • The target environment runs continuous memory scanning (some EDR products scan process memory on a timer, not just at load time)
  • The engagement requires extended dwell time with short poll intervals (more active windows = more exposure)
  • The SOC is known to use memory forensics tools (Volatility, WinDbg live kernel debugging) that can capture point-in-time snapshots

Enable it with --pe-fluct at build time. It composes cleanly with all other evasion features — sleep encryption, sleep mask, heap encryption, tampered syscalls, stack spoofing. Each addresses a different detection surface; PE Fluctuation specifically addresses the active-execution memory scanning gap that sleep encryption alone leaves open.