The flood in the last article is the visible half of a DDoS. Somewhere behind it, something told every participating machine when to start. This article looks for that instruction.
Analogy: The radio frequency
Before botnets moved their control onto encrypted HTTP to blend in, many coordinated through Internet Relay Chat, IRC, the way a gang might all tune to one radio frequency to hear the leader's orders. The operator typed a command into an IRC channel; every infected machine listening on that channel, the "hive", received and acted on it.
Listening on one frequency
find_irc_commands.py
# find_irc_commands.py: look for command traffic on IRC's default port
import dpkt, socket
from open_capture import read_packets
IRC_PORT = 6667
for ts, raw in read_packets("capture.pcap"):
try:
ip = dpkt.ethernet.Ethernet(raw).data
tcp = ip.data
if tcp.dport != IRC_PORT and tcp.sport != IRC_PORT:
continue
if len(tcp.data) == 0:
continue
payload = tcp.data.decode(errors="replace")
if "PRIVMSG" in payload:
print(f"{socket.inet_ntoa(ip.src)} IRC: {payload.strip()}")
except AttributeError:
continue
Run against a small lab capture of a command-and-control server sending the same instruction to three bots, with one unrelated packet on a different port mixed in to confirm the filter actually filters:
Sample output
203.0.113.9 IRC: :c2server PRIVMSG #hive :!flood 10.0.0.5
203.0.113.9 IRC: :c2server PRIVMSG #hive :!flood 10.0.0.5
203.0.113.9 IRC: :c2server PRIVMSG #hive :!flood 10.0.0.5
The unrelated packet, on port 443 and carrying no IRC text at all, never appears. IRC_PORT = 6667 is checked first, against both tcp.sport and tcp.dport, so traffic is only decoded as IRC if it was actually travelling to or from that port; everything else is skipped before the script even tries to read it as text.
Python concept: Filtering before decoding
Filtering on port 6667 first keeps the script from decoding traffic that was never IRC. Checking the cheap, structural fact (which port) before the expensive, content-dependent one (does the text contain PRIVMSG) is a pattern worth keeping: it is faster, and it avoids misreading an unrelated protocol's bytes as if they were IRC text.
PRIVMSG is the IRC command that carries a message to a channel, and is where an operator's instruction to the hive would travel. In the sample above, the same instruction, !flood 10.0.0.5, reaches all three bots within moments of each other. A capture showing many client machines all receiving the same PRIVMSG within moments of each other is strong evidence of coordinated control.
Why it carries weight: Why this carries weight
This script answers a different question than the flood detector in the last article. That one found the attack already happening. This one, run against traffic captured earlier, can show how it started: one message, one operator, fanned out to every participating machine at once. Together, the command and the flood it triggers are the two halves of the same story.
Caution: IRC is one channel among several, not the only one
A modern botnet is just as likely to coordinate over encrypted HTTPS, a messaging platform's API, or a peer-to-peer protocol with no single server to watch at all, specifically to avoid a filter as simple as "watch port 6667". This script catches the IRC case cleanly because the protocol is old and plaintext; it is a lesson in the pattern, filter on the channel, then read the content, more than a tool that will find every modern command-and-control channel on sight.
Check your understanding
Question 1: Using the radio frequency analogy, explain how IRC-based command and control coordinates a botnet.
An operator tunes every infected machine to listen on one shared IRC channel, the way a gang might all tune to one radio frequency. The operator then types a single command into that channel, and every machine listening, the hive, receives and acts on it at once, without the operator having to contact each machine individually.
Question 2: Why does find_irc_commands.py check the port before checking for PRIVMSG in the payload?
Checking the port first is cheap and structural: it immediately rules out the great majority of traffic that was never IRC at all, before the script spends any effort trying to decode its content as text. Checking content first would mean needlessly attempting to read non-IRC payloads as IRC commands.
Question 3: What pattern in a capture would make you suspect coordinated command-and-control traffic, rather than an ordinary IRC conversation?
Many distinct client machines all receiving the identical PRIVMSG content within a short window of each other. An ordinary conversation produces varied messages from different participants over time; a synchronised, identical instruction reaching many machines at once is the shape a command fan-out leaves behind.
Key takeaway: Key takeaway
Filter on the channel before reading the content: checking the port first is what makes find_irc_commands.py fast and safe against the rest of a capture. A command fanned out to many machines in moments is the signature of coordinated control, the half of a DDoS story the flood detector alone never shows.

