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
Tags: forensics pcap wireshark tshark irc botnet