Summary
Kanji gave us a packet capture and ten questions about a botnet. Most of the answers came from its IRC control channel, while two required a closer look at the ICMP flood sent to the victim.
The capture was around 30 GB and contained almost 29 million packets. My first attempts involved running display filters against the whole file, which meant waiting several minutes each time. Extracting the traffic I needed made the investigation much easier, especially for IRC.
I solved Q2 through Q10. Q1, which asked about the bot’s reported identity, remained unsolved; I’ve included the evidence and the answers I tried below.
Analysis
Finding the C2 protocol
I noticed TCP connections on port 6667 and extracted them into a separate capture:
1
tcpdump -nn -r small.pcap -w irc_only.pcap 'tcp port 6667'
Despite its name, small.pcap was the original large capture. The IRC extract was only about 658 KiB, so opening it in Wireshark was much more manageable.
Inside it, I found NICK, USER, JOIN, PRIVMSG, PING, PONG, and USERHOST messages. One bot registered like this:
1
2
3
NICK Pepe971541
USER nesheo 0 0 :Pepe971541
JOIN #sysnet01
The commands confirmed that the C2 protocol was IRC, answering Q2.
Identifying and counting the bots
The infected clients used nicknames beginning with Pepe, followed by six digits. A later IRC NAMES response listed the channel members:
1
2
3
4
5
6
7
8
9
10
11
Pepe428889
Pepe209833
Pepe851090
Pepe182542
Pepe470427
Pepe166191
Pepe796772
Pepe955217
Pepe858477
Pepe971541
@pepe2
That gave me the nickname prefix for Q3: Pepe.
There were ten matching bot nicknames. I excluded pepe2, the controller issuing commands in the channel, so the answer to Q4 was 10.
Finding the victim and UDP port
The controller sent this command:
1
.udpflood 172.16.96.69 100000 1500 10 161
Other attack commands targeted the same address:
1
2
3
4
.synflood 172.16.96.69 1 1000
.ddos.ack 172.16.96.69 1 1000
.ddos.syn 172.16.96.69 1 1000
.icmpflood 172.16.96.69 1000
The victim for Q5 was 172.16.96.69. The UDP command’s final argument specified destination port 161, answering Q6.
Working out which commands failed
Q7 asked which commands did not work. My first guess came from these responses:
1
2
3
[SCAN]: No Scan thread found.
[DDoS]: No DDoS flood thread found.
[PING]: No Ping flood thread found.
I initially submitted the stop commands .scanstop, .ddos.stop, and .pingstop. That was rejected.
Going back through the timeline, I found that three flood commands completed with zero reported throughput:
1
2
3
.synflood 172.16.96.69 1 1000
.ddos.ack 172.16.96.69 1 1000
.ddos.syn 172.16.96.69 1 1000
Their responses included:
1
2
[SYN]: Done with flood (0KB/sec).
[DDoS]: Done with flood (0KB/sec).
These were the commands the question was looking for. The bots accepted them but reported 0KB/sec, unlike a stop command finding no running thread. The accepted answer was:
1
.synflood+.ddos.ack+.ddos.syn
Isolating the ICMP flood
The next two questions concerned traffic generated after the controller issued:
1
.icmpflood 172.16.96.69 1000
I first tried extracting ICMP types directly from the original capture:
1
2
3
tshark -n -r small.pcap \
-Y 'icmp && ip.addr == 172.16.96.69' \
-T fields -e icmp.type
That still required scanning the entire file. I stopped it and extracted ICMP traffic involving the victim:
1
2
3
tcpdump -nn -r small.pcap \
-w victim_icmp.pcap \
'icmp and host 172.16.96.69'
This extract was still large because the flood accounted for much of the capture. It did, however, let me work on the relevant traffic without repeatedly processing the other protocols.
Counting distinct ICMP types
The flood contained far more than ordinary Echo Requests. The ICMP Type field included values such as:
1
41, 4, 213, 34, 12, 169, 60, 132, 208, 156, 202, 123, ...
I added icmp.type as a Wireshark column, but scrolling through packets wasn’t a useful way to count unique values. I exported the packet list as CSV and tried:
1
cut -d',' -f8 icmp.csv | sort -nu
That produced unexpected strings such as seq=256/1. Some of the quoted CSV fields contained commas, and cut treated those commas as separators too. The eighth piece of a line wasn’t necessarily the eighth CSV field.
A CSV parser avoids that problem. For the export with an ICMP Type column, the count can be reproduced with:
1
2
3
4
5
6
7
8
9
10
11
import csv
types = set()
with open("icmp.csv", newline="") as capture:
for row in csv.DictReader(capture):
value = row["ICMP Type"].strip()
if value:
types.add(int(value))
print("Distinct types:", len(types))
print("Range:", min(types), max(types))
After counting the values correctly, I found every value from 0 through 255: 256 distinct types, answering Q8. That explained the unusual packet descriptions in Wireshark. The flood used the full range of the one-byte Type field, including unassigned values.
Counting replies from the victim
Q9 asked how many ping replies came from the victim. An ICMP Echo Request has type 8; an Echo Reply has type 0.
I used this Wireshark display filter:
1
icmp.type == 0 && ip.src == 172.16.96.69
Only one packet remained, so the answer was 1.
Identifying the operating system
The controller also requested system information with .sysinfo. The responses included:
1
2
[OS]: Windows XP (Service Pack 2) (5.1, Build 2600)
[Current User]: Bob
That answered Q10: Windows XP. The reported user, Bob, also became one of my leads for Q1.
Q1: The Unsolved IRC Identity
The question asked:
What is the bot’s real reported username, and in the order they appear in the pcap by timestamp, all the fake usernames used to bypass the server’s mandatory identity verification?
There were several different names in the traffic: the Windows account, the IRC nickname, the username supplied in USER, its trailing realname field, and the identity returned by the server. I could extract them, but I never found the combination the grader expected.
Registration and Ident
The first registration contained:
1
2
NICK Pepe971541
USER nesheo 0 0 :Pepe971541
The server attempted an Ident check and reported:
1
2
*** Checking Ident
*** No Ident response
The bot later sent USERHOST Pepe971541 and received:
1
Pepe971541=+~nesheo@172.16.84.209
Here, the ~ prefix marked the supplied username as unverified by Ident. The server had still allowed the client to join, which made the question’s wording about mandatory verification difficult to interpret.
The random USER values looked like the intended fake usernames. The unresolved part was what counted as the bot’s “real reported username.”
Answers I tried
My first candidate was the Windows account reported by .sysinfo, followed by the IRC usernames in timestamp order:
1
Bob+nesheo+jpncdwf+vgiyai+rjsctt+kkojpvb+ffyufmw+cfppkja+ozxaru+qdzdtui+chvhvfy
That was rejected. I also tried the following interpretations, without success:
| Attempt | Reason for trying it |
|---|---|
Use kvirc as the first value |
The controller appeared as pepe2!~kvirc@gwsrv-27.labs.acme.io. |
Keep the ~ prefixes |
USERHOST reported names such as ~nesheo. |
Use the generated Pepe names after Bob |
The trailing realname field in USER nesheo 0 0 :Pepe971541 contained the bot nickname. |
The last attempt was:
1
Bob+Pepe971541+Pepe858477+Pepe955217+Pepe796772+Pepe166191+Pepe470427+Pepe182542+Pepe851090+Pepe209833+Pepe428889
None of those guesses resolved the question.
Checking registration order
To check the order precisely, I extracted TCP payloads with their packet timestamps:
1
2
3
4
5
tshark -n -r irc_only.pcap -Y 'tcp.len > 0' \
-T fields \
-e frame.number -e frame.time_epoch -e tcp.stream \
-e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport \
-e tcp.payload > irc_payload.tsv
I then parsed the USER lines. This script works on the complete registration messages present in these packet payloads; it does not reassemble messages split across TCP segments.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import re
from decimal import Decimal
rows = []
with open("irc_payload.tsv", errors="replace") as capture:
for line in capture:
fields = line.rstrip("\n").split("\t", 7)
if len(fields) != 8:
continue
frame, ts, stream, src, sport, dst, dport, payload = fields
try:
text = bytes.fromhex(payload.replace(":", "")).decode("latin-1")
except ValueError:
continue
for message in text.splitlines():
match = re.fullmatch(r"USER\s+(\S+)\s+0\s+0\s+:(.+)", message.strip())
if match:
rows.append((Decimal(ts), match.group(1), match.group(2)))
for timestamp, username, realname in sorted(rows):
print(f"{timestamp:.6f} USERNAME={username:10} REALNAME={realname}")
Each registration appeared twice, only microseconds apart. For example:
1
2
1313665395.048241 USERNAME=nesheo REALNAME=Pepe971541
1313665395.048249 USERNAME=nesheo REALNAME=Pepe971541
Showing just the first occurrence of each pair, the order was:
| First timestamp | USER username | USER realname |
|---|---|---|
1313665395.048241 |
nesheo |
Pepe971541 |
1313665439.809354 |
jpncdwf |
Pepe858477 |
1313665470.253566 |
vgiyai |
Pepe955217 |
1313665496.769499 |
rjsctt |
Pepe796772 |
1313665526.287032 |
kkojpvb |
Pepe166191 |
1313665551.298120 |
ffyufmw |
Pepe470427 |
1313665580.528951 |
cfppkja |
Pepe182542 |
1313665602.027193 |
ozxaru |
Pepe851090 |
1313665622.156572 |
qdzdtui |
Pepe209833 |
1313665645.730030 |
chvhvfy |
Pepe428889 |
Because the question asked for all fake usernames in timestamp order, I also considered whether it expected the duplicate values. I never established the correct first value or whether duplicates belonged in the answer, so Q1 remained unsolved.
Answers
| Question | Answer |
|---|---|
| Q1 — Reported identity and fake usernames | Unsolved |
| Q2 — C2 protocol | IRC |
| Q3 — Bot nickname prefix | Pepe |
| Q4 — Active bots | 10 |
| Q5 — Victim IP | 172.16.96.69 |
| Q6 — UDP destination port | 161 |
| Q7 — Unsuccessful attack commands | .synflood+.ddos.ack+.ddos.syn |
| Q8 — Distinct ICMP types | 256 |
| Q9 — Echo Replies from the victim | 1 |
| Q10 — Operating system | Windows XP |