ghost_sec writes
Offensive Security · Methodology

Bypassing EDR at full volume

Building a custom implant from scratch to evade two enterprise EDR platforms running in their most aggressive detection modes. Static analysis, runtime shellcode inspection, behavioural heuristics, userland hooks, network monitoring: each layer had to fall for the beacon to land.

@ghost_sec EDR evasion methodology Educational write-up

00Context

Most red team engagements treat endpoint detection as something to work around quietly: land a commodity payload, hope the rules are lax, and pivot before the SOC catches up. This engagement took the opposite approach. Two enterprise EDR platforms were deployed in their most aggressive detection configurations, and the objective was to build a bespoke implant that could survive every inspection layer, establish a command-and-control channel, and move laterally through the network without triggering a single alert.

The target environment ran both platforms simultaneously, each tuned to maximum sensitivity. Every binary dropped on disk was scanned statically and at runtime. Every process was monitored for suspicious API call sequences. Every outbound connection was inspected for anomalous patterns. The brief was simple: build something that gets past all of it, or prove that you cannot.

Note. This write-up is for educational purposes only. The techniques described illustrate offensive security methodology and are intended to help defenders understand and improve their detection capabilities.

01The adversary model

Before writing a single line of code, the team mapped every detection layer that stood between a payload landing on disk and a stable C2 session. Each layer uses a different class of analysis, and each required a different evasion strategy. The table below is the threat model we operated against.

EDR checkEvasion techniqueImplementationStatus
Static analysisLanguage-level obfuscationCompiled in Go with stripped debug symbols and informationDone
Runtime shellcode analysisShellcode encryptionPayload encrypted with AES-GCM, decrypted only at execution timeDone
Behavioural analysisTiming delays30-second sleep between each API call to break heuristic sequencesDone
Userland hooksIndirect syscallsDirect transition to kernel via syscall stubs, bypassing ntdll hooksDone
Process integrityProcess injectionQueueUserAPC injection into a legitimate processDone
Network monitoring (process)Inject into trusted processBeacon injected into a browser process, connecting over port 443Done
Network monitoring (system)Encrypted C2 over HTTPSRegistered domains with valid SSL certificates to mask beacon trafficDone
Lateral movement detectionLiving off the landProxy chains to tunnel traffic through compromised hostsDone
Application integrityCode signingSign the binary with a purchased certificateIncomplete
Post-exploitation detectionBespoke toolingCustom Beacon Object Files for credential dumpingIncomplete

Eight of ten layers were defeated. The two incomplete items, code signing and bespoke post-exploitation tooling, were development-pipeline constraints, not detection wins: the techniques are well understood, the implementation simply had not shipped by assessment close.


02The evasion chain

Each layer of defence expects a different kind of evidence. Static analysis looks at the file on disk. Runtime analysis watches what happens in memory. Behavioural heuristics correlate sequences of API calls. Userland hooks intercept specific system functions. Network monitors inspect traffic patterns. Defeating them all means building the implant so that no single layer ever sees enough signal to fire.

Choose the right language. The implant was written in Go. Go binaries are large, structurally complex, and use a runtime that looks nothing like the C/C++ patterns that most detection signatures target. Stripping debug symbols and build information removed the metadata that static engines use for attribution and classification.
Encrypt the shellcode. The payload never existed in cleartext on disk or in memory until the moment it was needed. AES-GCM encryption meant that static scanners saw only ciphertext, and runtime scanners had no recognisable shellcode pattern to match during loading.
Break the behavioural sequence. EDR behavioural engines flag processes that call sensitive APIs in rapid succession: allocate memory, write shellcode, change protections, create a thread. Inserting a 30-second sleep between each call broke the temporal correlation window. To the heuristic engine, each call looked isolated and unremarkable.
Bypass the hooks. Both EDR platforms inject monitoring code into user-mode libraries, intercepting calls to functions like NtAllocateVirtualMemory and NtWriteVirtualMemory before they reach the kernel. Indirect syscalls skip the hooked library entirely, transitioning directly to the kernel via dynamically resolved syscall numbers. The hooks see nothing because they are never called.
Inject into a trusted process. Rather than running as a standalone executable, the implant injected its payload into a legitimate process using QueueUserAPC injection. The target was a browser process, which gave two advantages: the process was already trusted by the EDR and already had legitimate outbound network connections on port 443.
Hide the traffic. The C2 channel connected over port 443 to domains registered for the engagement, each with a valid SSL/TLS certificate. From the perspective of network monitoring, the traffic was indistinguishable from ordinary HTTPS browsing originating from a browser process.
Move laterally without new tooling. Once the initial beacon was established, lateral movement used proxy chains to tunnel traffic through compromised hosts. No new binaries were dropped on interior machines. The attacker's traffic rode existing, trusted network paths.

The implant passed static scanning, survived runtime inspection, evaded behavioural heuristics, bypassed userland hooks, hid inside a trusted process, and spoke over encrypted channels to attacker-controlled domains. Zero alerts fired.


03Static analysis: why Go

Most EDR static engines are tuned to recognise patterns in C and C++ binaries: import tables, string constants, known function prologues, and PE section layouts that match common offensive tooling. Go sidesteps nearly all of these. The Go runtime statically links its own standard library, producing a large binary with thousands of functions that have no relationship to known malware signatures. The calling convention is different. The string representation is different. The binary structure is different.

Stripping debug symbols with standard Go build flags removed function names, source file paths, and build metadata. What landed on disk was a large, opaque binary that looked like any number of legitimate Go applications.

Design decision

Why not Rust, Nim, or C?

Each was considered. C and C++ produce small, efficient binaries but are the best-understood targets for signature engines. Nim was promising but had a smaller ecosystem for the specific syscall and injection techniques needed. Rust's binary structure is less well-known to EDR engines, but Go's runtime complexity provided the strongest passive obfuscation with the least development friction. The choice was pragmatic, not ideological.


04Runtime analysis: keeping shellcode invisible

Static evasion gets the binary past the initial scan. Runtime evasion keeps the payload hidden once the process starts executing. The critical window is between the binary launching and the shellcode being injected into the target process. During that window, the EDR's runtime scanner is watching memory allocations, page protections, and byte patterns.

The shellcode was encrypted with AES-GCM and embedded in the binary as an opaque data blob. At execution time, the implant decrypted the payload into a fresh memory allocation, but not before inserting the 30-second timing delay that broke behavioural correlation. The decrypted shellcode existed in memory only briefly, and only in the context of the injection sequence that immediately consumed it.

AES-GCM was chosen over simpler schemes like XOR because GCM provides authenticated encryption. A corrupted or tampered payload fails decryption cleanly rather than producing garbled but partially executable shellcode, which would trigger heuristic detections on malformed code execution.


05Behavioural analysis: the 30-second rule

Behavioural engines do not look at individual API calls. They look at sequences: allocate RW memory, write a buffer, change to RX, create a remote thread. That sequence, executed in rapid succession, is one of the highest-confidence signals for shellcode injection. It fires in nearly every detection model.

The evasion was simple. A 30-second sleep was inserted between each API call in the injection chain. From the EDR's perspective, the process allocated some memory, then did nothing suspicious for half a minute. Then it wrote some data. Then nothing for another half a minute. Then it changed page protections. Each action, viewed in isolation within the correlation window, was unremarkable.

The cost was speed. The injection sequence that normally completes in milliseconds took several minutes. For a red team operation where stealth matters more than speed, this was an acceptable trade.


06Userland hooks: going underneath

Both EDR platforms monitor sensitive Windows API functions by patching the first few bytes of each function in ntdll.dll with a jump to the EDR's monitoring code. When a process calls NtAllocateVirtualMemory, the call hits the hook first, which logs it, analyses it, and then either allows or blocks it.

Indirect syscalls bypass this entirely. Rather than calling the hooked function in ntdll, the implant resolves the correct syscall number for the current Windows version and issues the syscall instruction directly. The CPU transitions to kernel mode without ever executing the hooked code in user space. The EDR's hooks remain in place but are never triggered.

Technical note

Direct vs. indirect syscalls

A direct syscall embeds the syscall instruction inside the implant's own code. This works but leaves a detectable artefact: the syscall instruction originates from an unusual memory region rather than from ntdll. Some EDR platforms now flag this.

An indirect syscall avoids that by jumping to the legitimate syscall instruction inside ntdll, but after the hook's jump patch, so the hook is still bypassed while the return address points to ntdll's own code. This is the approach used here, implemented in Go with the syscall stubs resolved at runtime.


07Process injection: hiding in plain sight

The implant used QueueUserAPC injection. This technique queues a user-mode asynchronous procedure call to a thread in a target process. When that thread enters an alertable wait state, the queued code executes in the target's context. The target process was a browser, specifically one that was already running, already trusted by the EDR, and already making outbound HTTPS connections.

Injecting into a browser process solved two problems at once. First, the EDR trusted the browser's process integrity, so the injected code ran under the same trust umbrella. Second, the beacon's outbound C2 connections on port 443 blended seamlessly with the browser's existing traffic. No new process appeared, no new network socket opened from an unusual binary.


08Command and control: blending into HTTPS

The C2 infrastructure was designed to be invisible to network monitoring. Domains were registered for the engagement and pointed to the team's C2 servers. Valid SSL/TLS certificates were obtained for each domain. The beacon communicated over port 443 using standard HTTPS, with traffic that was structurally indistinguishable from legitimate web browsing.

Because the beacon ran inside a browser process that was already making HTTPS connections, the EDR's network monitoring saw a known browser process talking to an external domain over an encrypted channel. This is exactly what browsers do thousands of times per day. There was no anomaly to flag.

Infrastructure

C2 domain setup

Registered domains with plausible names. Deployed SSL/TLS certificates. Configured the C2 framework to serve valid HTTPS responses. All beacon traffic encrypted end to end. From the wire, indistinguishable from a user browsing the web.


09Lateral movement: living off the land

Once the initial beacon was established, the engagement moved laterally without dropping new binaries. Traffic was routed through proxy chains, using compromised hosts as relay points. Each hop looked like internal traffic between trusted systems. No new executables were written to disk on interior machines. No new processes were spawned that the EDR had not already blessed.

This is the "living off the land" principle applied to post-exploitation networking: use what is already there, avoid introducing anything new that a detection engine could key on.


10What was not completed

Two items in the evasion matrix remained incomplete at assessment close. Neither represents a detection win for the EDR platforms. Both are well-understood techniques that the engagement timeline did not accommodate.

Code signing

Unsigned binaries are treated with higher suspicion by both EDR platforms and by Windows itself. Signing the implant with a purchased code-signing certificate would have reduced this friction. The procurement process for the certificate was underway but had not completed by the end of the engagement window. The implant ran successfully without it, but a signed binary would have reduced the already-low risk of manual analyst review.

Bespoke Beacon Object Files

Post-exploitation credential access, specifically dumping credentials from memory and the Security Account Manager, relied on existing tooling that was more likely to trigger detection than the custom implant itself. Building bespoke Beacon Object Files for these tasks would have kept the entire kill chain at the same evasion standard. This was planned but not shipped. The credential-dumping phase was conducted with increased operational risk as a result.


11Why it worked

Defensive assumptionWhat actually happenedLesson
Static signatures catch known toolsGo binary matched no known signatures. Stripped metadata eliminated attribution.Signature-based detection fails against bespoke tooling. Organisations must assume custom implants will bypass static analysis.
Runtime scanning detects shellcode patternsAES-GCM encryption meant the payload was ciphertext until the moment of use.Encrypted payloads are invisible to pattern matching. Detection must look at behaviour, not byte patterns.
Behavioural heuristics catch injection sequences30-second delays broke temporal correlation. Each API call appeared isolated.Correlation windows are finite. A patient attacker can space actions outside the detection window.
Userland hooks monitor all API callsIndirect syscalls bypassed hooks entirely. The monitoring code was never executed.User-space hooks are a best-effort control. Kernel-level telemetry is more resilient.
Process trust lists prevent injectionInjecting into a browser gave inherited trust and legitimate network patterns.Trust should be per-thread, not per-process. A trusted process can host untrusted code.
Network monitoring detects C2 channelsValid HTTPS from a browser process to registered domains was indistinguishable from browsing.TLS inspection and domain reputation scoring must complement network monitoring.

12Defensive priorities

This engagement demonstrated that EDR, even at maximum sensitivity, is a necessary but insufficient control. The following priorities would materially increase the cost of this attack.