Module 8: Detection, CET & Nighthawk
Real-world deployment, hardware security compatibility, detection strategies, and the Nighthawk “Evanesco” production implementation.
Module Objective
Understand how FunctionPeekaboo relates to Intel CET (Control-flow Enforcement Technology) and Shadow Stacks, why it is compatible while Ekko/Zilean are not, how MDSec’s Nighthawk C2 implements this technique in production as “Evanesco,” and what detection strategies defenders can employ against per-function self-masking.
1. Intel CET and Shadow Stacks
Intel Control-flow Enforcement Technology (CET) is a hardware-based security feature in recent Intel processors (11th gen+ Alder Lake and later) that prevents Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP) attacks. It has two components:
| CET Component | What It Does | Mechanism |
|---|---|---|
| Shadow Stack | Maintains a hardware-protected copy of return addresses | On CALL, return address is pushed to both the normal stack and the shadow stack. On RET, both are compared. If they differ → #CP (Control Protection) exception |
| Indirect Branch Tracking (IBT) | Ensures indirect branches (JMP/CALL through registers or memory) land on valid targets | Indirect branches must land on an ENDBR64 instruction. If not → #CP exception |
CET is increasingly enabled by default in Windows 11 and recent builds of Windows Server. Any evasion technique that manipulates return addresses or uses ROP-like chains will fail on CET-enabled systems.
2. Why Ekko/Zilean/FOLIAGE Break Under CET
Traditional sleep obfuscation techniques fundamentally conflict with CET’s Shadow Stack:
The ROP Problem
Ekko, Zilean, and FOLIAGE use callback-based execution chains (timer callbacks, APC queuing, NtContinue context manipulation) that effectively construct ROP chains — sequences of return addresses that redirect execution through system functions. The Shadow Stack detects that these return addresses were never pushed by a legitimate CALL instruction and raises a #CP exception, killing the process.
| Technique | How It Chains Execution | CET Compatible? |
|---|---|---|
| Ekko | Timer callbacks with crafted context records; uses NtContinue to set RSP to a gadget chain | No — Shadow Stack mismatch on RET from manipulated stack |
| Zilean | APC queue with context manipulation | No — same issue with return address validation |
| FOLIAGE | APC + NtContinue with RSP pivoting | No — RSP pivot causes Shadow Stack desync |
| FunctionPeekaboo | Normal CALL/RET flow through prologue/epilogue stubs | Yes — all calls and returns are legitimate |
3. FunctionPeekaboo’s CET Compatibility
FunctionPeekaboo is fully compatible with CET because it uses legitimate control flow:
Why It Works Under CET
- Normal CALL/RET: The prologue stub calls the handler via a
CALLinstruction, which pushes the return address to both stacks. The handler returns viaRET, which validates against the Shadow Stack. Both match. - No stack pivoting: FunctionPeekaboo never changes RSP to point to a fabricated stack. The stack grows and shrinks naturally through push/pop and CALL/RET.
- No ROP chains: There are no gadget chains. The handler is a single contiguous function called normally.
- ENDBR64 at targets: The LLVM compiler automatically inserts
ENDBR64instructions at indirect branch targets when CET-IBT is enabled. FunctionPeekaboo’s stubs are direct calls, so IBT is not affected. - Transparent to the CPU: From the processor’s perspective, the CALL/RET flow is perfectly normal. The fact that the called code (handler) changes memory permissions and XOR’s bytes is invisible to CET.
4. Nighthawk C2 and “Evanesco”
MDSec’s Nighthawk is a commercial C2 framework designed for red team operations. Version 0.3.3 introduced “Evanesco” — a production implementation of FunctionPeekaboo’s per-function self-masking concept.
Nighthawk Evanesco Features
| Feature | PoC (FunctionPeekaboo) | Production (Nighthawk Evanesco) |
|---|---|---|
| Masking scope | Functions with peekaboo attribute | All implant functions automatically |
| Thread safety | Basic (single-byte IsEncrypted flag) | Reference counting, mutex protection for shared functions |
| Exception handling | Not addressed in PoC | Custom unwind handlers ensure re-encryption on exceptions |
| VirtualProtect avoidance | Uses kernel32 import | Direct syscalls (NtProtectVirtualMemory) to bypass hooks |
| Section names | .funcmeta, .stub (descriptive) | Randomized or merged into existing sections |
| Key strength | Single-byte XOR | Potentially stronger masking (multi-byte or rolling XOR) |
| Performance | Proof of concept | Optimized XOR engine, minimal syscall overhead |
| Sleep integration | Separate (manual combination) | Integrated with Nighthawk’s sleep obfuscation |
The “Evanesco” Name
Named after the vanishing spell in the Harry Potter series, Evanesco makes implant code “vanish” from memory scanners. Each function only appears when actively called — the rest of the time, it’s XOR’d garbage that matches no known signatures.
5. Detection Strategies for Defenders
Understanding FunctionPeekaboo from a defensive perspective is essential for blue team operations. Several detection approaches can identify per-function self-masking:
5.1 VirtualProtect Monitoring
Behavioral Pattern
FunctionPeekaboo generates a characteristic pattern of VirtualProtect calls: rapid RX → RW → RX transitions on small memory regions within the .text section. This pattern is unusual for legitimate software — most programs never change the permissions of their own code sections after loading.
Detection Rule (Pseudocode)rule FunctionPeekaboo_VProtect:
trigger: NtProtectVirtualMemory
conditions:
- target address is within a PE .text section
- protection changed from PAGE_EXECUTE_READ to PAGE_READWRITE
- followed within 1ms by PAGE_READWRITE to PAGE_EXECUTE_READ
- same small region (< 0x2000 bytes)
- occurs repeatedly during process lifetime
action: alert "Potential per-function self-masking detected"
5.2 Custom PE Section Detection
| Indicator | Detection Method |
|---|---|
.funcmeta section name | PE section name scan (static analysis, YARA) |
.stub section name | PE section name scan |
Entry point not in .text | Validate AddressOfEntryPoint falls within the .text section |
| RW data section with structured entries | Entropy and structure analysis of non-standard sections |
5.3 TEB UserReserved Monitoring
TEB Field Writes
Most legitimate applications never write to the TEB UserReserved fields (offsets 0x1478-0x1488). Monitoring writes to these fields can indicate FunctionPeekaboo or similar techniques. This requires kernel-level or hardware-assisted monitoring (e.g., data breakpoints, page guard pages on TEB).
5.4 Timing-Based Detection
The handler adds ~4-20μs overhead to every function call. While individually undetectable, statistical analysis of function call timing across many calls might reveal the overhead pattern. This is a research-level detection approach, not currently deployed by any known EDR.
5.5 Memory Scan Timing
Aggressive Scanning Strategy
The strongest countermeasure is high-frequency memory scanning during process activity periods (not just sleep). If the scanner catches the ~2% window when a function is decrypted, it can extract signatures. This requires scanning every few hundred microseconds, which has significant performance cost but is feasible for targeted investigation.
6. YARA Rules for Detection
YARArule FunctionPeekaboo_Sections {
meta:
description = "Detects PE with FunctionPeekaboo custom sections"
author = "Blue Team"
condition:
uint16(0) == 0x5A4D and
for any i in (0..pe.number_of_sections - 1): (
pe.sections[i].name == ".funcmeta" or
pe.sections[i].name == ".stub"
)
}
rule FunctionPeekaboo_EP_Anomaly {
meta:
description = "Entry point outside .text section"
condition:
uint16(0) == 0x5A4D and
not for any i in (0..pe.number_of_sections - 1): (
pe.sections[i].name == ".text" and
pe.entry_point >= pe.sections[i].virtual_address and
pe.entry_point < pe.sections[i].virtual_address +
pe.sections[i].virtual_size
)
}
rule FunctionPeekaboo_Stub_Pattern {
meta:
description = "Detects the CALL/POP PIC pattern followed by register saves"
strings:
// CALL +5 (E8 00000000) followed by POP RBX (5B)
// then multiple PUSH instructions
$pic_trick = { E8 00 00 00 00 5B 50 51 52 41 50 41 51 }
condition:
uint16(0) == 0x5A4D and $pic_trick
}
7. Comparison with Other Evasion Techniques
| Technique | Scope | When Active | CET Safe | Implementation |
|---|---|---|---|---|
| FunctionPeekaboo | Per-function | Always (active + sleep) | Yes | Compiler (LLVM) |
| Ekko | Entire image | Sleep only | No | Runtime (code) |
| Zilean | Entire image | Sleep only | No | Runtime (code) |
| FOLIAGE | Entire image | Sleep only | No | Runtime (code) |
| Gargoyle | Entire image | Sleep only | No | Runtime (timer ROP) |
| Module Stomping | Entire DLL | Persistent | Yes | Loader technique |
| PE Header Wiping | Headers only | Persistent | Yes | Post-load cleanup |
8. Limitations and Future Work
Current Limitations
| Limitation | Impact | Potential Solution |
|---|---|---|
| On-disk cleartext | Static analysis sees unmasked functions before first run | Combine with packing or on-disk encryption |
| VirtualProtect footprint | EDR can monitor permission changes | Direct syscalls, avoiding kernel32 imports |
| Thread safety gaps | Race conditions with same function across threads | Per-function reference counting |
| Exception path leaks | Exceptions may leave functions decrypted | Custom SEH/VEH handlers for cleanup |
| Custom section names | Trivially signaturable | Merge into existing sections or randomize names |
| XOR weakness | Single-byte XOR is easily reversed | Multi-byte keys, rolling XOR, or lightweight ciphers |
| Build complexity | Requires custom LLVM build | Distribute pre-built toolchain |
9. Operational Deployment Considerations
Red Team Deployment Checklist
- Attribute selection: Mark sensitive functions (C2 comms, credential access, lateral movement, persistence). Leave hot-loop and utility functions unmasked for performance.
- Combine with sleep obfuscation: Use FOLIAGE or similar during sleep for 100% coverage in the sleep window. Accept ~98% during active execution.
- Test on CET hardware: Verify compatibility on Intel 12th/13th/14th gen (Alder Lake+) with CET enabled in Windows 11.
- Rename sections: Change
.funcmetaand.stubto innocuous names before deployment. - Use direct syscalls: Replace
VirtualProtectimports withNtProtectVirtualMemorysyscall stubs to avoid user-mode hooks. - Thread safety: If the implant is multithreaded, implement reference counting for shared functions.
- Test exception paths: Ensure SEH handlers are in place for all peekaboo functions that might throw.
10. Course Summary
What You’ve Learned
| Module | Key Takeaway |
|---|---|
| 1 | Memory scanners exploit the sleep window; traditional sleep obfuscation protects during sleep but not during active execution |
| 2 | LLVM’s modular backend allows custom passes at the PreEmit stage, after all optimizations |
| 3 | Custom PE sections (.funcmeta, .stub) store metadata and initialization code |
| 4 | X86RetModPass identifies attributed functions and injects stubs at every entry and exit point |
| 5 | Prologue (0x46 bytes) decrypts on call; epilogue re-encrypts on return; CALL/POP enables PIC |
| 6 | The handler (~380 bytes) performs PE parsing, .funcmeta lookup, VirtualProtect transitions, and byte-level XOR |
| 7 | Initialization encrypts all functions before main(); steady-state maintains ~98% encryption coverage |
| 8 | CET-compatible (unlike Ekko/Zilean/FOLIAGE); Nighthawk Evanesco is the production implementation |
Knowledge Check
Q1: Why is FunctionPeekaboo compatible with Intel CET while Ekko is not?
Q2: What is the most effective detection approach for FunctionPeekaboo?
Q3: What is Nighthawk’s “Evanesco” feature?