Custom C2 Development #1 - Rust C2 Framework Architecture Review
Author
CloakCat
Time
12 min read
Read by
80
Prologue
While working through a Red Team Lab, I hit a chapter focused on executing custom TTPs. Following along with Sliver, I kept asking myself: "How does this C2 actually work under the hood?" The answer that surfaced was obvious — build one from scratch.
Cobalt Strike, Sliver, Havoc, Brute Ratel — the C2 landscape already spans everything from open-source to commercial. The motivation to build anyway was clear: understand the internals by feel, and identify which components to modify to evade specific defensive controls. The primary objective of this project was never the artifact itself — it was the hands-on experience of tracing exactly which modifications bypass which detection mechanisms.
This post covers the technical decisions, architectural choices, and honest retrospective from designing and implementing CloakCat, a Rust-based C2 framework.
Disclaimer: CloakCat is developed strictly for security research and education. Deployment against any unauthorized environment is prohibited.
1. Benchmark Target: Cobalt Strike
CloakCat's design is explicitly benchmarked against Cobalt Strike — the de facto standard in the red team industry. The primary goal is to reproduce its core operational workflow, and in doing so, develop a deep understanding of how commercial C2 infrastructure actually functions.
The specific Cobalt Strike design elements used as reference:
Beacon Architecture — An async, check-in-based communication model. Sleep + Jitter randomizes the beacon interval; pending tasks are batched and retrieved on each check-in. CloakCat follows this model directly.
Malleable C2 Profiles — The concept of fully customizing HTTP traffic appearance via configuration. This idea of blending into legitimate traffic became the foundation of CloakCat's Health Profile Camouflage system and the TOML-based profile engine.
Operator-Server-Beacon Three-Tier Architecture — Operators connect to the team server; the team server manages beacons. CloakCat mirrors this exactly: cat-cli (operator) → cat-server (team server) → cat-agent (beacon).
BOF (Beacon Object Files) — Cobalt Strike's mechanism for executing compiled .o files in-memory without touching disk. CloakCat implements a COFF parser and in-memory loader targeting compatibility with the existing BOF ecosystem.
The goal is not feature parity with Cobalt Strike. Matching the polish of a commercial tool through solo development is unrealistic. The focus is on understanding how the core workflows operate, not replicating the entire surface area.
2. Why Rust
The realistic language options for a C2 framework narrow to C/C++, Go, Rust, and C#. Each has well-understood tradeoffs. Rust was chosen for a combination of technical advantages and personal motivation.
Technical Advantages
Memory safety at the intersection of OPSEC. A C2 agent runs on target systems for extended periods. A memory bug in C/C++ translates directly to an agent crash — and crashes hand the blue team an artifact. Rust's ownership model eliminates entire classes of these bugs at compile time.
Native binary with minimal runtime footprint. Go is a reasonable choice (Sliver is Go-based), but Go binaries carry a runtime, carry a recognizable binary signature, and run large. Rust can go all the way to no_std, minimizing both binary size and detection surface.
Async ecosystem. The tokio + axum combination handles thousands of concurrent beacon connections on the server side without issue. It's not quite as ergonomic as Go's goroutines, but the performance is comparable.
Cross-compilation. Using the cross toolchain, Windows and macOS target binaries can be built from Linux via Docker. For a C2 that needs to deploy agents across heterogeneous environments, this is a meaningful operational advantage.
Honest Drawbacks
Windows API calls are painful. The core functionality of a C2 agent — token manipulation, process injection, lateral movement — is entirely Windows API-dependent. In Rust, every call goes through the windows-sys crate inside unsafe blocks via FFI. Compared to C/C++, the friction is real.
rust// Token impersonation in Rust — unsafe is unavoidable
unsafe {
let mut token: HANDLE = std::ptr::null_mut();
OpenProcessToken(process, TOKEN_DUPLICATE, &mut token);
DuplicateTokenEx(token, MAXIMUM_ALLOWED, std::ptr::null(),
SecurityImpersonation, TokenPrimary, &mut dup_token);
ImpersonateLoggedOnUser(dup_token);
}
Steep learning curve. Ownership, lifetimes, and the trait system consistently create situations where fighting the compiler takes longer than implementing the actual C2 logic — especially in async code where lifetime errors compound.
Gap with the BOF ecosystem. Existing BOF tooling is written in C and expects Cobalt Strike's BeaconAPI. Implementing a compatible shim layer in Rust is achievable, but the complexity overhead versus writing it in C is non-trivial.
Personal Motivation
Honestly, the technical rationale wasn't the only driver. Rust was already in active development, and the motivation to level up through a real, complex project was significant. A C2 framework covers networking, cryptography, systems programming, and async concurrency in a single codebase — it's an ideal forcing function for Rust proficiency.
3. Architecture
High-Level Structure
CloakCat is structured as a Cargo workspace consisting of four crates.
cloakcat-protocol/ ← Shared types, crypto, protocol definitions (lowest-level dependency)
cat-server/ ← Team server: listeners, beacon management, DB, REST API
cat-agent/ ← Beacon implant: executes on target
cat-cli/ ← Operator CLI (catctl)
Dependency flow is strictly unidirectional: cloakcat-protocol ← cat-server / cat-agent / cat-cli. The protocol crate sits at the bottom of the dependency graph; the other three components reference it. No circular dependencies — each component builds and tests independently.
[Operator CLI]
↕ REST API (X-Operator-Token)
[Team Server] ←──── PostgreSQL
↕ HTTP/S (X-Agent-Token + HMAC)
[Beacon Agent] ← Target host
Communication Protocol
Agent-server communication is reduced to three endpoints.
POST /v1/register — Initial agent registration
GET /v1/poll/{agent_id} — Task retrieval (long-poll)
POST /v1/result/{agent_id} — Result submission (HMAC-signed)
Long-polling was chosen over WebSockets for two reasons: it traverses firewalls more reliably, and it's indistinguishable from standard HTTPS traffic at the network layer. Beacons hold connections with a hold=45 parameter — up to 45 seconds — and receive tasks immediately when queued.
Authentication and Cryptography
Authentication is split into two layers.
rust// Agent auth: keys derived from SHARED_TOKEN via HKDF
let auth_key = derive_auth_key(shared_token.as_bytes()); // registration + polling
let hmac_key = derive_hmac_key(shared_token.as_bytes()); // result signing
The initial design used a single SHARED_TOKEN for both authentication and HMAC signing. An architecture review surfaced the structural problem: compromise of one context exposes the other. Migrated to HKDF (HMAC-based Key Derivation Function) to derive purpose-specific keys.
Result submissions are integrity-verified via HMAC-SHA256.
rust// Null-byte delimiters between fields — ambiguous boundaries enable substitution attacks
let msg = format!("{}\x00{}\x00{}", agent_id, cmd_id, stdout);
let signature = hmac_sign(&hmac_key, &msg);
All secret comparisons use constant-time evaluation. Standard == is vulnerable to timing side-channel attacks.
rust// ✗ Vulnerable to timing attacks
if expected == signature { ... }
// ✓ Constant-time comparison
use subtle::ConstantTimeEq;
if expected.as_bytes().ct_eq(signature.as_bytes()).into() { ... }
Server Internal Structure
The server is organized into explicit layers.
handlers.rs — HTTP request extraction and response construction (thin layer)
service.rs — Domain logic: validation, state management, DB calls
error.rs — ServerError enum + HTTP status code mapping
middleware.rs — Agent and operator authentication middleware
db.rs — sqlx queries (compile-time verified)
The original handlers.rs was a 459-line monolith mixing HTTP parsing, business logic, and DB calls. Post-refactor, handlers are a ~180-line thin wrapper; domain logic lives in service.rs.
Error handling was migrated from blanket anyhow::Result to a typed error enum.
rust#[derive(Debug)]
pub enum ServerError {
NotFound(String),
Unauthorized(String),
BadRequest(String),
Conflict(String),
Db(sqlx::Error),
Internal(anyhow::Error),
}
impl IntoResponse for ServerError {
fn into_response(self) -> Response {
let (status, msg) = match &self {
Self::NotFound(m) => (StatusCode::NOT_FOUND, m.clone()),
Self::Unauthorized(m) => (StatusCode::UNAUTHORIZED, m.clone()),
// ...
};
(status, Json(json!({ "error": msg }))).into_response()
}
}
This eliminated downcast_ref::<sqlx::Error>() chains and produces semantically correct HTTP status codes per error variant automatically.
4. Design Patterns and Structural Decisions
Trait-Based Extensibility
Extensibility is the operational lifespan of a C2 framework. Adding new listener protocols or transport channels should require minimal changes to existing code.
Transport trait — Abstracts the agent's communication channel.
rust#[async_trait]
pub trait Transport: Send + Sync {
async fn register(&self, req: &RegisterRequest) -> Result<RegisterResponse>;
async fn poll(&self, agent_id: &str, hold: u64) -> Result<Option<Command>>;
async fn send_result(&self, result: &TaskResult) -> Result<()>;
}
Currently only HttpTransport is implemented. The design means that adding DNS or SMB transports requires no modifications to beacon.rs's main loop — implement the trait and inject it.
ListenerProfile trait — Abstracts server-side listener profiles.
rustpub trait ListenerProfile: Send + Sync {
fn name(&self) -> &str;
fn base_path(&self) -> &str;
fn validate_request(&self, path: &str, headers: &HeaderMap) -> bool;
}
Previously, adding a new profile required touching constants.rs, paths.rs, validation.rs, and routes.rs — four files. Post-refactor, a single implementation wires routes and validation automatically.
CLI Structure: clap derive + Module Separation
The CLI targets a msfconsole-style REPL. The original implementation was a 554-line commands.rs dispatching 17 commands through a single match cmd.as_str() chain. This was migrated to clap derive with per-module command handlers.
rust#[derive(Subcommand)]
pub enum Commands {
/// List active agents
Agents,
/// Interact with a specific agent
Interact { agent_id: String },
/// Retrieve task results
Results {
#[arg(long)]
agent: String,
#[arg(long)]
full: bool, // output without truncation
},
// ...
}
Gains: Five instances of duplicated argument parsing were eliminated. --help is generated automatically. Adding a new command is a single-file addition.
Caveat: clap is not designed for REPL environments. On parse errors, clap attempts to exit the process — requires wrapping with try_parse_from to intercept. A proper msfconsole-style implementation needs an additional context stack (MainContext → BeaconContext).
Directory Layout
cat-server/src/
├── main.rs # Entry point, AppState initialization
├── error.rs # ServerError enum
├── service.rs # Domain logic layer
├── handlers.rs # HTTP thin wrapper
├── middleware.rs # Authentication middleware
├── routes.rs # /v1 route groups
├── validation.rs # Profile validation
├── state.rs # AppState + Notify + View types
├── db.rs # sqlx queries
└── tunnel/ # SOCKS5 tunneling (Phase 4)
cat-agent/src/
├── main.rs # Entry point
├── config.rs # Build-time embedded configuration
├── beacon.rs # Main loop (Transport trait dependency)
├── exec.rs # Command execution (timeout + size limits)
├── host.rs # Host enumeration
├── transport/ # Transport trait + HttpTransport
└── tasks/ # Command handlers (fs, token, lateral...)
What works: File-level responsibilities are unambiguous. New feature placement is obvious.
What needs work: cat-server/state.rs is accumulating AppState, View types, and Notify maps in a single file. As server complexity grows, this needs to split into a state/ directory.
5. Key Lessons from Implementation
Security Code Demands Consistency
The most critical finding from the architecture review was inconsistency in HMAC comparison. The server middleware correctly used ring::constant_time::verify_slices_are_equal, while crypto.rs used standard == for the same class of operation. Two implementations of the same security primitive in the same codebase.
This is a classic "fixed it in one place, missed it in another" failure pattern. Security-critical utilities must be defined once and referenced everywhere. No exceptions.
The Hidden DB Load of Long-Polling
The initial poll implementation was a busy-wait loop querying the database every 300ms. At 10 agents polling for 120 seconds each, that's up to 4,000 queries. Migrated to tokio::sync::Notify — the handler sleeps until a task is inserted.
rust// Before: DB query every 300ms (expensive)
loop {
if let Some(cmd) = db::get_pending_command(&pool, agent_id).await? {
return Ok(Some(cmd));
}
tokio::time::sleep(Duration::from_millis(300)).await;
}
// After: Notify-based (wakes only on task insertion)
tokio::select! {
_ = notify.notified() => {
db::get_pending_command(&pool, agent_id).await
}
_ = tokio::time::sleep(hold_duration) => Ok(None)
}
Agent Safeguards Are Non-Negotiable
Without execution timeouts, a single sleep 999999 command permanently blocks the agent. Without stdout size limits, yes | head -c 1G exhausts memory. These defensive controls are not polish — they are critical path, day-one requirements.
rust// 300s timeout + 1MB output cap
let result = tokio::time::timeout(
Duration::from_secs(300),
cmd.output()
).await;
let stdout = truncate_output(&raw_stdout, 1_048_576); // 1MB hard cap
6. Current Gaps and Roadmap
Current Gaps
Encryption posture. HMAC-based integrity verification is in place, but end-to-end traffic encryption (ECDH + AES-256-GCM) is not yet implemented. The current architecture relies on HTTPS — strip TLS and the payload is plaintext.
Windows post-exploitation primitives. Token manipulation, lateral movement, and process injection — Cobalt Strike's core post-ex capability set — are still in development. Without these, realistic adversary simulation scenarios can't be reproduced.
Detection evasion. Agent binaries currently build without obfuscation or packing. No sleep masking, no direct syscall implementation, no ETW patching. A modern EDR will flag this immediately.
Test coverage. Unit tests exist for crypto.rs, but integration coverage is thin across the board. An end-to-end agent-server test pipeline is needed.
Operator experience. The CLI is functionally operational but doesn't approach the UX of Cobalt Strike's GUI or Sliver's TUI.
Roadmap
| Phase | Scope | Status |
|---|---|---|
| Phase 0–3 | Security hardening + architecture refactor | Complete |
| Phase 4 | File transfer + SOCKS5 proxying | In progress |
| Phase 5 | Windows token manipulation + lateral movement | Planned |
| Phase 6 | BOF (Beacon Object File) loader | Planned |
| Phase 7 | Malleable C2 profiles + HTTPS hardening | Planned |
Phase 5 completion should enable reproduction of most standard lab scenarios with CloakCat. Phase 6 unlocks interoperability with the existing offensive tooling ecosystem — Mimikatz, Rubeus, and compatible BOF tooling.
7. Closing
The most significant realization from building a C2 from scratch is that using a tool and building a tool represent entirely different levels of understanding.
Typing socks5 start in Sliver opens a proxy. It doesn't explain how tunnel data is multiplexed over the C2 channel, or how the SOCKS5 handshake is relayed through to the beacon. That only surfaces when you implement it.
Running jump psexec in Cobalt Strike executes lateral movement. It doesn't explain the SCM open, the service creation, the binary copy to ADMIN$, the service start — the full Windows API call chain. Only by writing that chain yourself do you develop a concrete model of why it gets detected and what exactly you'd need to change to reduce that detection surface.
CloakCat is currently a fraction of Cobalt Strike's capability set. But the understanding built while implementing that fraction cannot be obtained by operating the full tool. That's the point of the project.
Development continues — Phase-by-phase progress, technical implementation details, and the inevitable lessons from hitting walls will be documented here as the project moves forward.
CloakCat is under active development as an open-source project for security education and research.