How to Build a Packet Sniffer in C — From Scratch
Archit ChoudharyBy the end of this blog post, you will have made a packet sniffer in C. It is a program that
- Tells you which packets are flowing in and out of your network card,
- Notes how many of each type there are, and
- Logs it to a file on your computer.
If you know the basics of C and can't wait to get your hands on something more interesting, this is the perfect project for you. Keep reading to learn why hackers sniff packets, the basics of TCP/IP networking, and how to code network programs in C.
First Steps in Networking
First, let's be clear on definitions. Packet sniffing is the act of capturing and analysing network traffic. Network traffic is the raw traffic that is moving between devices. Devices are... yeah no.
Basically, if you are new to networking, you have to know that any two computers communicate by sending bytes to each other in units called packets. And there needs to be a structure to this, otherwise the bytes don't mean nothing. Thankfully, the structure is simple. A regular packet consists of the following, in the same order: Ethernet header, IP header, TCP/UDP header, and application information (HTTP requests etc.). There are other types of packets that are not TCP/UDP; for example, an ICMP packet just contains: Ethernet header, IP header, ICMP payload. Ethernet, IP, TCP, UDP, ICMP etc. are called protocols.
That's it! That's a packet. There's a lot of details but we'll get there when it comes up. I also think teachers introduce people to these details way too early; just write some network programs and learn about networking as you stop understanding.
Why Sniff Packets? Why in C?
There's many reasons to sniff packets as a hacker. Here's a few:
- Unencrypted creds: If the packet contains unencrypted info like passwords and credit card info, bingo.
- Hijack session cookies: When you log into a website, the website stores browser cookies on your computer so you don't need to log in again. Well, if someone intercepts your session packets, they can steal these cookies and on some occasions log in as you. This can't lead to anything good... for you.
- Passive recon: By analysing captured packets, you can silently observe what devices are active on a network, who's talking to whom, what protocols are used, and what software is running. Even if you don't go for the kill, gathering intel is still a step in the right direction.
- Reversing: If you're trying to reverse an app but don't have access to any of the app's network code or API docs, you can run the app, sniff the network traffic generated by the app, and try to understand how it interacts with servers.
- MITM: This is a big one. Basically, you intercept communication between two computers and extract all the emails, messages, and hashes. You can also modify the traffic (e.g. edit the emails), and inject malware. Fun.
But Wireshark already exists. Why make your own? Well, firstly, you can never go wrong with C. It's essential in a hacker's toolkit to be able to work at the level of C code (and honestly even Assembly). Moreover, learning how to implement something so fundamental in cybersecurity by yourself, in a language like C, will enable you to make your own tools and programs when you need to. This is the goal.
Let's start.
The Sniffer
The Basic Version
To begin, you need to install the libpcap-dev
library:
sudo apt update
sudo apt install libpcap-dev
The term pcap
stands for "packet capture"; you will see it everywhere in network security. Once that's installed, we can start coding.
Yes, this is a picture; it's better if you type it yourself. This might look like a lot but it's deceptively simple. Before we get into the details, let me explain what's happening conceptually:
- We "open a connection" to the network device (the string
dev
) usingpcap_open_live()
. If the connection can't be opened (handle == NULL
), we throw a tantrum and exit. - Assuming the network device is accessible, we start the packet capture loop using
pcap_loop()
. - The loop calls the callback function
packet_handler()
every time a packet is captured (this is passed as a parameter topcap_loop()
). - Once the loop ends, we "close the connection" to the network card using
pcap_close()
.
That's it. This is enough to understand the code at a high level; read and re-read through the code until you can see this. Now, you may have the details.
-
Constants:
PCAP_ERR_BUF
andBUFSIZE
are constants defined inpcap.h
. The former defines the size of the error buffer used by the network device connection (see next bullet) and the latter is used for the connection (handle
) buffer itself. -
Error Buffer: The string
errbuf
(aka buffer) is used to store potential error messages that result from the network device connectionhandle
. Indeed, it is passed as an argument topcap_open_live()
. -
Network IF Connection: The 1 argument in
pcap_open_live()
enables promiscuous mode, meaning it collects all packets, even those that your computer has not sent/received itself. The 1000 is the read timeout. Every 1000ms (or 1s)libpcap
is forced by this to update you on whether packets were received or not. -
Capture Loop: The last argument to
pcap_loop()
is irrelevant for now. The10
just means it listens for 10 packets. It can be replaced with any number. If we replace it with 0, then the loop continues indefinitely until the program is stopped manually. -
Packet Handler: The second
header
argument is of a special type (pcap_pkthdr
defined inpcap.h
) that contains as elements the timestamp (header->ts
), the original packet length that was sent (header->len
), and the actual packet length that we managed to capture (header->caplen
). Thepacket
argument contains the bytes that comprise the actual packet. These are unsigned (u_char
) because raw packet data is never negative. The first argument is irrelevant for now.
We can already test this by running the program in one terminal and doing network shenanigans on another. Make sure to compile first and then run it as root; you can't play with the network card as a mere mortal. Link libpcap
when compiling like in the picture below.
Let's do something a little more interesting with the packets.
Detecting IP Packets
IP packets are a subcategory of all network packets that have a specific kind of Ethernet header. Specifically, the EtherType is 0x0800 (2 bytes). Try to guess how the packet handler checks for the EtherType in the following updated version:
We have added some more libraries and a couple new functions. Here are the changes:
-
Eth/IP Headers: We include header files from
netinet
to get theiphdr
andethhdr
structs. This makes extracting the Ethernet and IP headers from thepacket
easy; just typecasting. To get the IP header, we use the fact that it comes in the packet after the Ethernet header is over, which is why we add an offset to the pointer. -
Detecting IP: The value
eth->h_proto
is the EtherType tag in the Ethernet header, and the constantETH_P_IP
is 0x0800 (as explained above). The functionntohs()
stands for "Network to Host (Short)". It converts the input from the network byte order (big endian) to the host byte order (big or little endian) if needed. Short means 16-bit.
We didn't change anything else. Let's check that this works:
The output makes sense because all ICMP packets (which the ping
command sends) are also IP packets.
But IP packets have more information, like IP addresses. Additionally, as mentioned earlier, the header
also has length and time information, so let's print those as well for each packet.
The end result looks like this:
Here's a brief explanation:
-
Internet Addresses: The
in_addr
struct stands for "Internet address" and it has a fields_addr
to store an IPv4 address in byte form. Theinet_ntoa()
function then converts it to a decimal, human-readable string. -
Time: I hate time formatting. Anyway, the packet header has a timestamp (
header->ts
), of which we get the value in seconds-since-Unix-epoch (header->ts.tv_sec
). Butctime()
needs its input inconst time_t*
units, so we get the address (&header->ts.tv_sec
) and then typecast it.
When we test again, this is the result:
Beautiful. But there's more! Let's classify IP packets further into TCP, UDP and ICMP, and count how many of each we encounter.
TCP/UDP/ICMP
The IP header has a protocol tag, and we can classify IP packets based on the value of this tag. Take a look:
The protocol number being 6, 17 or 1 corresponds to TCP, UDP and ICMP, respectively.
Logging to a File
Logging all this data to a file is pretty easy if you've done IO in C before. It's quite similar to Python. Here's how you do it:
Here's the result. As you can see, it successfully saves the data:
At this moment it is saving the file as root, which is annoying. This can be fixed, but you can't run the sniffer without superuser privileges anyway, so it makes sense that the logs need it too.
We'll now make the program run indefinitely until we press Ctrl-C. When we do Ctrl-C while a program is running, it sends the interrupt signal — SIGINT — to the program. If no special behaviour is specified in the code via a callback function, the program terminates. But we also need to close the file when we get SIGINT, and print the statistics. So let's add a callback function:
And that's a wrap! We have a very basic packet sniffer.
Next Steps...
In the continuation to this blog post, we will turn our rudimentary packet sniffer into a professional-looking command-line program with even cooler features. We will
- Add an aesthetic terminal UI using the
ncurses
library, - Parse command-line options like your favourite terminal command, and finally,
- Implement more interesting packet capturing and analysis.
In the meanwhile, read up on networking!