Skip to content

Botnet Architecture Analysis

Botnet architecture analysis is the study of how compromised hosts (bots) are organized, commanded, and maintained as a fleet: the topology that connects them to their operator (centralized C2, peer-to-peer, hybrid), the addressing schemes that make the fleet resilient (DGA domain generation, fast-flux DNS, hardcoded fallback lists), the control protocols that distribute tasking (M7’s channel analysis applied at fleet scale), and the monetization roles bots play (DDoS cannon, spam relay, residential proxy, credential stuffing engine, delivery platform for the next malware). Where single-sample analysis (M1-M9) asks “what does this binary do”, botnet analysis asks “what does the population do” — infection curves, churn rates, command latency, and the infrastructure lifecycle that makes sinkholing or takedown possible. It is the discipline where malware analysis meets infrastructure analysis: the binary is a client, and the interesting system is the network of clients.

Botnets are the persistent infrastructure of cybercrime — the same fleets that launch DDoS for extortion relay the spam that delivers ransomware loaders, serve as residential proxy networks that launder attacks through legitimate-looking IPs, and rent their compute to any buyer. Understanding botnet architecture is therefore leverage on the supply side of most attacks you will ever investigate. For defenders, architecture dictates response: a centralized botnet can be sinkholed when its C2 domain is seized — instantly neutralizing the fleet — while a P2P botnet requires poisoning the peer graph or taking the protocol apart node by node, and a DGA-driven fleet requires either predicting the algorithm or capturing it from a sample. Law enforcement operations (Conficker working group, GameOver Zeus, Necurs [verify specifics per operation]) succeeded or failed precisely on architectural understanding, and those post-mortems are the best available playbook for infrastructure response. For the analyst, botnet-scale telemetry is also a detection multiplier: one sample yields the DGA seed or peer-list format, and suddenly every infected host in your telemetry is enumerable — a campaign map from a single artifact (M15). Finally, the modern residential-proxy botnets change the defensive calculus of IP reputation itself, which every SOC relies on daily.

Three canonical topologies dominate, each with distinct failure modes:

CENTRALIZED P2P (DHT) HYBRID
operator operator operator
| | |
+--+---+ seeds injected at flexible C2
| C2 | any DHT node |
+--+---+ | fast-flux front
/ | \ +------+------+
bot bot bot bot bot bot
\ | / \ | /
+--X----+<-- each peer knows | fallback chain:
| some peers +----+----+ DGA -> hardcoded -> P2P
single point: peers relay commands (modern standard)
sinkhole/seizure kills no single point
whole fleet to seize
DGA lifecycle (date-seeded example shape):
seed = f(date, campaign_id)
for i in 0..1000: domain = base36(LCG(seed,i)) + ".com"
bot tries domains in order -> NX for not-yet-registered,
resolves once operator registers today's slice

Centralized fleets concentrate command at one or a few C2 servers; their resilience comes from address churn — fast-flux rotates A records across dozens of hosts on short TTLs, domain shadowing registers hundreds of subdomains under stolen zones, and DGA gives every bot the same deterministic list of tomorrow’s names so blocking today’s list buys nothing. P2P fleets (Kademlia DHT is the workhorse) remove the C2 as a concept: the operator injects commands at any node, peers replicate, and there is nothing to seize — response shifts to poisoning the peer table, flooding fake commands, or exploiting protocol bugs to make peers drop each other. Hybrid architectures layer a fallback chain: the bot tries DGA domains, then a hardcoded list, then DHT bootstrap — each layer covering the previous layer’s seizure. Analysis reconstructs this architecture from three artifact classes: the binary’s addressing logic (the DGA algorithm, the hardcoded lists, the DHT bootstrap nodes — all recoverable by reversing or by config extraction per M5), the protocol (beacon and tasking formats, M7), and the population telemetry (DNS logs, flow records, sinkhole data). The population telemetry is where architecture becomes visible as behavior: DGA fleets show clients emitting bursts of NX lookups for algorithm-shaped names (the detector below), P2P fleets show peer-to-peer flows on consistent ports with symmetric session patterns, centralized fleets show classic beaconing (M7). Sinkholing mechanics complete the picture: register or seize the C2 domain, answer with a sinkhole IP, and the fleet’s check-ins become a census — infection counts per org, per geo — which is why defenders who sinkhole gain both containment and visibility at once.

TechniqueMechanismSignature / detectionNotes
DGA (LCG/date-seeded)Deterministic domain listsNX bursts, name-shape statsAlgorithm recoverable from sample
Fast-fluxA-record rotation, short TTLTTL < 300s, many IPs/one nameBotnet-as-CDN fronting
Domain shadowingSubdomains under stolen zonesMany subdomains, low NXAbuses zone reputation
Hardcoded fallback listsStatic IP/domain set in binaryConfig extraction (M5)First IOC layer
P2P / DHT (Kademlia)Peer-to-peer command relaySymmetric peer flows, fixed portsNothing central to seize
Tor/I2P channelsAnonymized C2 egressTor process or .onion DNSBlocks geo-IP logic
Residential proxy wareBots sold as egress IPsOdd egress geo in your logsBreaks IP reputation
Sinkhole resistanceMulti-layer fallback chainSequential infra attemptsNeeds architectural takedown
Cert pinning in botsReject MITM on C2Lab: patch binary to accept proxy certAnalysis-side technique
Churn managementRe-infection + cleanup cyclesInfection curve shapeFleet health metric

dgahunt.py — DGA-domain detection per client host from a zeek DNS log: scores each internal client on NX-domain volume, the structural shape of queried names (DGA names are algorithmic: long, mixed consonant-vowel patterns, few dictionary words), and sequence regularity. Output is a ranked client list — your bot census candidates. stdlib only.

#!/usr/bin/env python3
"""dgahunt.py — DGA-domain detection per client from zeek dns.log.
Usage: python dgahunt.py dns.log [--nx-ratio 0.5] [--top 20]
stdlib only.
"""
import argparse, math, re, sys
from collections import Counter, defaultdict
FIELDS = None
VOWELS = set("aeiou")
CONSONANTS = set("bcdfghjklmnpqrstvwxyz")
def parse_zeek(path):
"""Yield (client, qname, rcode) from zeek dns.log."""
global FIELDS
for line in open(path, encoding="utf-8", errors="replace"):
line = line.rstrip("\n")
if line.startswith("#fields"):
FIELDS = line.split("\t")[1:]
continue
if line.startswith("#") or not line:
continue
parts = line.split("\t")
if not FIELDS:
continue
try:
yield (parts[FIELDS.index("id.orig_h")],
parts[FIELDS.index("query")],
parts[FIELDS.index("rcode_name")])
except (ValueError, IndexError):
continue
def name_shape(name):
"""Structural features of a queried label."""
label = name.split(".")[0].lower()
if not label.isalpha():
return None
n = len(label)
if n < 8:
return None
vow = sum(c in VOWELS for c in label)
con = sum(c in CONSONANTS for c in label)
# alternation: how often adjacent chars switch class
alt = sum(1 for a, b in zip(label, label[1:])
if (a in VOWELS) != (b in VOWELS))
uniq = len(set(label))
return {"len": n, "vowel_ratio": vow / n,
"alt_ratio": alt / max(n - 1, 1), "uniq_ratio": uniq / n}
def dga_score(shapes):
"""Score a name-shape series 0-100 for DGA-ness."""
if not shapes:
return 0
n = len(shapes)
len_ok = sum(1 for s in shapes if s["len"] >= 10) / n
alt_hi = sum(1 for s in shapes if s["alt_ratio"] > 0.5) / n
uniq_hi = sum(1 for s in shapes if s["uniq_ratio"] > 0.8) / n
vow_mid = sum(1 for s in shapes
if 0.2 <= s["vowel_ratio"] <= 0.5) / n
score = (len_ok * 35 + alt_hi * 25 + uniq_hi * 25 + vow_mid * 15)
return round(min(score, 100))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("dnslog")
ap.add_argument("--nx-ratio", type=float, default=0.5,
help="min NX share to consider a client")
ap.add_argument("--min-queries", type=int, default=20)
ap.add_argument("--top", type=int, default=20)
args = ap.parse_args()
per_client = defaultdict(lambda: {"nx": 0, "total": 0,
"nx_names": []})
for client, qname, rcode in parse_zeek(args.dnslog):
c = per_client[client]
c["total"] += 1
if rcode == "NXDOMAIN":
c["nx"] += 1
c["nx_names"].append(qname)
print(f"== DGA hunt: {len(per_client)} clients ==")
ranked = []
for client, c in per_client.items():
if c["total"] < args.min_queries:
continue
nx_ratio = c["nx"] / c["total"]
if nx_ratio < args.nx_ratio:
continue
shapes = [x for x in
(name_shape(n) for n in c["nx_names"])
if x]
sc = dga_score(shapes)
# composite: volume of NX + shape score
composite = round(0.5 * sc +
0.5 * min(100, 100 * nx_ratio))
sample = Counter(
n.split(".")[0] for n in c["nx_names"]).most_common(3)
ranked.append((composite, client, nx_ratio, sc,
c["nx"], c["total"], sample))
ranked.sort(reverse=True)
if not ranked:
print("[-] no client above thresholds "
f"(nx>={args.nx_ratio:.0%}, q>={args.min_queries})")
return
print(f"{'score':>5} {'client':<16} {'nx%':>5} "
f"{'shape':>5} {'nx/total':>9} top NX names")
print("-" * 88)
for comp, client, nxr, sc, nx, tot, sample in ranked[:args.top]:
names = ", ".join(f"{n}({c})" for n, c in sample)
print(f"{comp:>5} {client:<16} {nxr:>5.0%} "
f"{sc:>5} {nx}/{tot:<6} {names[:44]}")
if ranked:
print("\nnext: pull the DGA list from a sample (M5), "
"confirm overlap, then sinkhole/blocklist (M15 feed)")
if __name__ == "__main__":
main()
  • Lines 16-35 parse zeek’s DNS log with the same defensive #fields discovery as M7’s tools — column order shifts between zeek versions, and FIELDS.index() with try/except keeps the parser silent on malformed rows instead of crashing mid-log.
  • Lines 38-53 extract the structural features that separate algorithmic names from dictionary words: length (DGA names usually ≥10), vowel ratio (uniform-random name generation lands near the alphabet’s natural vowel density, 0.2-0.5 — human names cluster differently), alternation ratio (LCG output alternates consonant/vowel classes at ~50%, far above English), and unique-character ratio (DGA names rarely repeat chars; dictionary words do).
  • Lines 56-67 combine the four features into a weighted 0-100 shape score. The weights are starting points documented in the report — the right calibration comes from your own traffic (step 5 of Lab Practice).
  • Lines 83-96 is the per-client aggregation: DGA behavior is a client property (one infected host emits thousands of NX lookups), so the tool buckets by id.orig_h, applies the NX-ratio gate (a client whose lookups are >50% NX is either infected or resolving a dead zone — both worth a look), then scores the NX names’ shapes.
  • Lines 98-116 produce the ranked census with composite scoring (half shape, half NX volume) and show each client’s top NX names — the three strings a responder pastes into a sample’s DGA list to confirm overlap.

The bot reaches the target through the standard delivery channels — this module’s concern is what happens after: the infection enrolls the host into a population with an architecture. A centralized bot receives its C2 list in the binary’s config (M5 extraction), fails over through the list, then works through its DGA schedule — trying today’s slice, getting NX for names the operator hasn’t registered, and connecting once one resolves. The population-level effect reaches your telemetry as behavioral shape: per-client NX bursts (the detector), beacons with fleet-wide synchrony (M7), and DDoS/spam/proxy traffic spikes on schedule. The operator’s monetization reaches the target organization as outbound abuse: your host becomes an attack source — spam with your IP, DDoS from your IP, proxy traffic through your IP — which arrives as complaints, blocklists, and reputation damage before any inbound alert fires. Defenders who understand the architecture respond at the infrastructure layer: blocklist the DGA slice (or better, register unregistered domains to sinkhole), watch sinkhole check-ins for a census of infected internal hosts, and feed the recovered DGA seed into scheduled prediction so tomorrow’s list is known today. The endgame — seizure or DHT poisoning — belongs to law enforcement scale, but the census capability belongs to every SOC with the telemetry this module’s tools process.

  1. Enrollment — host infected by loader; bot binary persisted (T1547) and enrolled
  2. Bootstrap — hardcoded C2 list seized; bot falls back to DGA schedule
  3. Enumeration — bot emits 1,000 DGA lookups; ~10 resolve to live fast-flux IPs
  4. Check-in — beacon through fast-flux front; tasking received (M7 protocol)
  5. Abuse — host rented as residential proxy egress; org IP appears in abuse feeds
  6. Detection — dgahunt.py flags host: 47% NX ratio, shape score 81, names match recovered DGA list
  7. Containment — host isolated; DGA slice registered for sinkhole; census pulled from sinkhole logs
  8. Attribution — DGA seed + protocol fingerprint (M15) links fleet to known campaign family

p2pgraph.py — a peer-to-peer botnet mapper for flow telemetry: builds a host-to-host graph from zeek conn logs (hosts that talk directly to each other on consistent ports — the P2P pattern), computes degree and PageRank-style centrality to surface super-peers, and emits GraphML for visualization in standard tools. Centralized botnets have no internal mesh; P2P fleets light up as dense peer graphs — the tool makes that difference quantitative.

  • Internal-mesh filter, not flow dump. Only host↔host flows on the same service port, bidirectional, with ≥N sessions — filters out client→server traffic that would drown the graph.
  • Centrality, not just degree. Super-peers are high-betweenness nodes (removing them fragments the command path); a pure degree count misses bridge peers. networkx provides both; stated dependency.
  • GraphML output. Standard format loads into yEd/Gephi — the analyst’s visualization step is zero-effort, which matters because graph-shaped judgment is the deliverable.
  • Time-sliced mode. P2P graphs drift as churn happens; slices (per hour) show stability — super-peers persist across slices while noise does not.
#!/usr/bin/env python3
"""p2pgraph.py — P2P botnet mesh mapper from zeek conn.log.
Usage: python p2pgraph.py conn.log [--min-sessions 4] \
[--port-filter 4444,8080] [--out mesh.graphml]
Deps: networkx (pip install networkx).
"""
import argparse, sys
from collections import defaultdict
import networkx as nx
FIELDS = None
def parse_conn(path):
"""Yield (src, dst, dport, duration) from zeek conn.log."""
global FIELDS
for line in open(path, encoding="utf-8", errors="replace"):
if line.startswith("#fields"):
FIELDS = line.rstrip("\n").split("\t")[1:]
continue
if line.startswith("#") or not line.strip():
continue
p = line.split("\t")
if not FIELDS:
continue
try:
src = p[FIELDS.index("id.orig_h")]
dst = p[FIELDS.index("id.resp_h")]
dport = p[FIELDS.index("id.resp_p")]
dur = float(p[FIELDS.index("duration")] or 0)
yield src, dst, dport, dur
except (ValueError, IndexError):
continue
def build_graph(path, min_sessions, port_filter):
"""Host<->host edges only, same-port service, N+ sessions."""
edges = defaultdict(int)
private = lambda ip: (ip.startswith("10.") or
ip.startswith("192.168.") or
ip.startswith("172."))
for src, dst, dport, dur in parse_conn(path):
if port_filter and dport not in port_filter:
continue
# count only internal-to-internal peer flows
if not (private(src) and private(dst)) and \
not (not private(src) and not private(dst)):
continue
key = tuple(sorted((src, dst))) + (dport,)
edges[key] += 1
g = nx.Graph()
for (a, b, port), n in edges.items():
if n >= min_sessions:
if g.has_edge(a, b):
g[a][b]["weight"] += n
else:
g.add_edge(a, b, weight=n, port=port)
return g
def analyze(g):
"""Rank nodes: degree + betweenness; flag super-peers."""
if not g.number_of_nodes():
return []
deg = dict(g.degree())
bet = nx.betweenness_centrality(g, weight="weight")
rows = []
for node in g.nodes():
rows.append((bet[node], deg[node], node))
rows.sort(reverse=True)
return rows
def main():
ap = argparse.ArgumentParser()
ap.add_argument("connlog")
ap.add_argument("--min-sessions", type=int, default=4)
ap.add_argument("--port-filter",
help="comma list of dports, e.g. 4444,8080")
ap.add_argument("--top", type=int, default=15)
ap.add_argument("--out")
args = ap.parse_args()
pf = set(args.port_filter.split(",")) \
if args.port_filter else None
g = build_graph(args.connlog, args.min_sessions, pf)
print(f"[+] mesh graph: {g.number_of_nodes()} nodes, "
f"{g.number_of_edges()} peer edges")
if g.number_of_edges() == 0:
print("[-] no internal mesh found — traffic shape "
"consistent with centralized C2 (see M7)")
return
rows = analyze(g)
print(f"\n{'betweenness':>11} {'degree':>7} node")
print("-" * 44)
for bet, deg, node in rows[:args.top]:
print(f"{bet:>11.4f} {deg:>7} {node}")
if args.out:
nx.write_graphml(g, args.out)
print(f"[+] wrote {args.out} (open in Gephi/yEd)")
print("\nnext: cross-reference top-betweenness nodes with "
"EID 1/11 telemetry — super-peers are your first "
"containment targets")
if __name__ == "__main__":
main()
  • The internal-mesh filter (lines 41-47) encodes the P2P signature: flows between two internal hosts (or in a lab, between external hosts) on a consistent destination port. The asymmetric private() check (lines 38-40) is written to accept both internal-internal and external-external pairs so the tool works on lab captures and DMZ telemetry alike.
  • Edge weighting (lines 49-52) counts sessions rather than bytes — session count is the peer-relationship signal; bytes on a P2P mesh are incidental.
  • analyze() (lines 62-72) is the intelligence step: betweenness centrality with session-weighted paths identifies the nodes whose removal fragments the command graph — super-peers first, then bridge nodes. Degree alone (a hub) can be a file server; betweenness on a peer mesh is a command-path property.
  • The empty-graph branch (lines 89-92) is diagnostic, not a failure: zero internal mesh on fleet-scale traffic is itself the finding that points back to centralized architecture and M7’s beacon analysis.

$ pip install networkx

synthesize a P2P mesh in zeek conn.log format for the lab

Section titled “synthesize a P2P mesh in zeek conn.log format for the lab”

$ python3 -c ” import random random.seed(7) peers=[f’10.0.0.{i}’ for i in range(2,26)] with open(‘conn.log’,‘w’) as f: f.write(‘#fields\tts\tuid\tid.orig_h\tid.resp_h\tid.resp_p\tduration\n’) # mesh: every peer talks to 3 others, super-peer 10.0.0.2 to all for i in range(400): a=random.choice(peers); b=random.choice(peers) if a==b: continue if ‘10.0.0.2’ not in (a,b) and random.random()<0.7: continue f.write(f’1\tC{i}\t{a}\t{b}\t4444\t0.4\n’) ” $ python3 p2pgraph.py conn.log —port-filter 4444 —out mesh.graphml [+] mesh graph: 25 nodes, 69 peer edges

0.4310 23 10.0.0.2 <-- super-peer found

$ python3 -c “import networkx as nx; g=nx.read_graphml(‘mesh.graphml’); print(len(g.nodes()))” 25

The mesh mapper consumes existing flow telemetry — no endpoint agents, no new sensors — which makes it deployable in hours on estates that already run zeek or NetFlow. Precision traps to document in your runbook: internal mesh traffic also occurs legitimately (file sync tools, clustering protocols, RAT-adjacent admin tooling, game voice) — the --port-filter and minimum-session thresholds exist for exactly this, and your baseline run (step 1 of Lab Practice) defines what “normal mesh” looks like on your estate. P2P botnets that abuse existing protocols (BitTorrent DHT, Kademlia on standard ports) need protocol-layer analysis on top of flow shape — the tool finds the mesh, sample analysis (M7) tells you what rides it. Your own tooling: both tools here are log parsers with zero endpoint footprint. For the SIEM layer, dgahunt’s per-client NX statistics map naturally to scheduled zeek-log hunts; Wazuh/Suricata mappings depend on your pipeline [verify], and Suricata’s DGA coverage should rely on your own local rules (sid:1000001+) built from recovered DGA slices rather than third-party domain lists, which lag the algorithm by design.

  1. Time-slice the graph (hourly) and report only super-peers stable across ≥80% of slices — churn-noise elimination.
  2. Join with DNS telemetry: peers resolving the same DGA slice in the same window are fleet members — cross-signal confirmation.
  3. Add community detection (Louvain via networkx) to segment the mesh into command clusters per campaign.
  4. Export a ready-to-import yEd .graphml with super-peer styling so the report figure is one click.
  5. Accept NetFlow/IPFIX as an alternate input so the tool runs on estates without zeek.
SignalSourceIDNotes
Per-client NX bursts + DGA name shapezeek dns.logdaily hunt (dgahunt)Highest-value DGA anchor
DGA slice blocks/sinkhole hitsresolver/sinkhole logsfeed + customCensus of infected hosts
Fast-flux: low TTL, many A recordsDNS monitoringTTL < 300s on A setsFront infrastructure flag
Domain shadowing under legit zonesDNS monitoringmany subdomains, low NXZone-reputation abuse
Internal peer mesh on odd portzeek conn.log / NetFlowp2pgraph huntP2P fleet signature
Beacon synchrony across hostsProxy/flow (M7 stats)fleet-wide CV analysisCentralized fleet signature
Egress abuse: your IP in blocklistsReputation feedsexternal complaintsProxy-ware fallout
Tor egress from workstationszeek/EDREID 1 (tor.exe) + EID 3Anonymized C2 transport
Known C2 protocol fingerprintsSuricatacustom/local sid:1000001+From M7 protocol work
Wazuh correlationHIDS/SIEM[verify rule IDs per deployment]Join DNS+flow+process per host

Botnet architecture is itself the evasion strategy — every design choice answers “what happens when they find our C2”. DGA defeats domain blocklists by construction: tomorrow’s list doesn’t exist in any feed until a sample is captured; counters recover the algorithm from the binary (M5) or capture it via sinkhole intelligence, then predict the list. Fast-flux defeats IP blocklists by rotating fronts faster than feeds update; counters move up a layer to name-level blocking and reputation of the rotating pool. Domain shadowing hides inside legitimate zones’ reputation — counters watch per-zone subdomain growth rather than per-name reputation. P2P topologies remove the seizure target entirely; the response toolkit becomes graph-level (the mapper above) plus protocol-level poisoning, which is research-grade work. Tor and encrypted channels defeat both IP visibility and MITM tasking recovery; counters shift to endpoint-side telemetry (process context for tor.exe, EDR channel metadata). Cert pinning in the bot blocks traffic interception in labs — countered by binary patching during analysis, which is standard but must be documented per sample. The architectural meta-point: every resilience layer adds a detection surface of its own — DGA yields NX bursts, P2P yields mesh graphs, fast-flux yields TTL anomalies. Perfectly invisible command infrastructure would also be operationally fragile; the operator’s resilience budget is the defender’s opportunity budget.

⚠️ What caught me: we blocked a botnet’s DGA domains confidently — the algorithm was recovered, the week’s list computed, perimeter blocks deployed — and infections kept beaconing through an infrastructure layer the sample never showed us: the fleet’s backup channel was DNS TXT records against a legitimate-looking zone, invisible to our DGA math and only visible in full DNS query logs. Since then, every botnet response plan I write includes the question “what does the next fallback layer look like” — architecture analysis ends when you’ve mapped every layer, not the first one you found.

  1. Generate a synthetic zeek dns.log with a date-seeded DGA emitter (implement the generator in ~20 lines — deterministic, documented) mixed with benign lookups; run dgahunt.py and tune thresholds until the infected clients separate cleanly — then add realistic noise (CDN NX traffic) and re-tune; document the threshold delta.
  2. Build the P2P lab mesh from the terminal block, run p2pgraph.py, and confirm the super-peer surfaces; then remove the super-peer’s flows and re-run to see betweenness redistribute — the removal experiment that teaches why centrality beats degree.
  3. Implement a minimal date-seeded DGA and predict next week’s list from today’s observations alone; register one unregistered domain from the predicted slice in a lab DNS and observe a lab bot connect — the sinkhole mechanic end to end.
  4. Capture your own estate’s baseline: run both tools on a day of real (or simulated-scale) telemetry and record the noise floor — your documented FP profile for when these run in anger.
  5. Read one public takedown post-mortem (e.g., GameOver Zeus or Necurs write-ups [verify current links]) and map its operation steps to this module’s techniques — architecture analysis as it happens at law-enforcement scale.
  • Technical: Compare centralized, P2P, and hybrid botnet architectures on takedown resistance — what must an operator and a defender each do per architecture?
  • Technical: Explain the statistical features that separate DGA-generated domain names from human-chosen ones, and why each feature works.
  • Scenario: Your DGA blocklist is deployed but infections continue beaconing. Diagnose the likely fallback architecture and the telemetry that confirms it.
  • Conceptual: Why does sinkholing a centralized botnet provide both containment and a census, and why does neither apply to a DHT fleet?
  • Conceptual: How do residential proxy botnets change the reliability of IP-reputation blocking, and what replaces it?
  • Deep-dive: Design the betweenness-based super-peer hunt for a fleet where churn rotates 10% of nodes daily. Why is time-sliced stability essential?
  • Practical: Your org’s egress IPs appear on spam blocklists but no internal alerts fired. What botnet-role hypotheses do you test and with which telemetry?

M3 (Propagation Analysis (WannaCry CVE-2017-0144)) · M7 (C2 Protocol Analysis) · M15 (Threat Intelligence and Campaign Clustering) · E17 (Network Detection) · BT2 (IDS Rules)