How to Build a Packet Sniffer in C — From Scratch

Archit Choudhary

By 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) using pcap_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 to pcap_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 and BUFSIZE are constants defined in pcap.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 connection handle. Indeed, it is passed as an argument to pcap_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. The 10 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 in pcap.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). The packet 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 the iphdr and ethhdr structs. This makes extracting the Ethernet and IP headers from the packet 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 constant ETH_P_IP is 0x0800 (as explained above). The function ntohs() 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 field s_addr to store an IPv4 address in byte form. The inet_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). But ctime() needs its input in const 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!

Back to blog

Leave a comment