display | more...

Packets, flying through the ether. Don't you just wish that you could see what they said?

Elem_125 Productions presents:

The Easy (Cross-platform-ish) Packet Sniffer Writing Guide

With the aid of the pcap library (available for Linux, BSD, Windows and other platforms) we can easily write our own packet sniffers. To aid you in this I will walk through the creation of a very simple sniffer that just looks for the start of HTTP streams and prints out the source, destination, packet size and packet flags.

So without further ado let us get started! I've only tested this on my Linux box, but it should (in theory) compile under gcc on windows and BSD, providing the libpcap library is installed.

First we'll start with a few headers:

#include <pcap.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ether.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <netdb.h>

These headers are mainly to provide the network functions that we require. For more information - read them!

Now some constants:

char ethlen, iplen, tcplen;
void handler(char *, const struct pcap_pkthdr *, const u_char *);

We've now defined three variables ethlen, iplen and tcplen and a function handler. We'll be seeing more of these soon.

Now lets move on the main part of the program!

int main(int argc, char **argv)
{
  int buffsize = 65535;
  int promisc = 1;
  int timeout = 1000;

  char pcap_err[PCAP_ERRBUF_SIZE];
  u_char buffer]255];
  char *dev;
  struct in_addr net, mask;
  pcap_t *pcap_nic;
  struct bpf_program filter;

Ok, what have we done here?? We'll we've now created a few variables,let go through and say what each one is for:

  • buffsize - this define the size of buffer that we'll need later, its been set to 64k.
  • promisc - we'll use this later to pass to an other function, 1 indicates that we'll want to capture packets in promiscuous mode, 0 to capture packets just meant for that interface.
  • timeout - a timeout in milli-seconds, used a bit later.
  • pcap_err[PCAP_ERRBUF_SIZE] - a nice place where we can store pcap errors. PCAP_ERRBUF_SIZE is defined in the pcap header.
  • buffer[255] - its, ummm, a buffer.
  • *dev - A char * where the name of our interface will later be placed.
  • net, mask - two in_addrs, needed later to store our network and masks as IP addresses. For what its worth, in_addr is a struct that just holds an in_addr_t which is just a typedef for a uint32_t. Moral of the story: its just an unsigned 32-bit number.
  • *pcap_nic - where pcap will store its pointer to your network interface.
  • filter - this is a structure of type bpf_program which we user to set filters on the packets we capture, most about this later.

Now on to the next bit of code!

  ethlen = sizeof(struct ether_header);
  iplen = sizeof(struct iphdr);
  tcplen = sizeof(struct tcphdr);

  printf("ethlen: %i\niplen: %i\ntcplen: %i\n", ethlen, iplen, tcplen);

Basicly we've just got the sizes of the ethernet header, the ip header and the tcp header and then print them. We largely need this since they provide the offsets we'll need later to make sure that we look at the right part of the packet.

if(!(dev = pcap_lookupdev(pcap_err)))
    {
      perror(pcap_err);
      exit(-1);
    }

  printf("Dev: %s\n\n", dev);
  
  if((pcap_nic = pcap_open_live(dev, buffsize, promisc, timeout, pcap_err))
     == NULL)
    {
      perror(pcap_err);
      exit(-1);
    }

  if(pcap_lookupnet(dev, &net.s_addr, &mask.s_addr, pcap_err) == -1)
    {
      perror(pcap_err);
      exit(-1);
    }
  printf("net: %s\tmask: %s\n\n",  inet_ntoa(net),  inet_ntoa(mask));

In this section we do the bulk of the pcap setup. We start of by letting pcap select our network interface and then printing out which interface it's selected. pcap then attempts to open this interface for live data. Here we see our promisc, buffersize and timeout variables being used. buffersize is the amount of captured data it can buffer at any one point. promisc sets the interface to promiscuous mode or not. timeout is the mount of time it will wait to open up the interface before it will return an error. pcap_err is the array where it will place the text of any error.

We then let pcap look up the network and mask that we are using, then we print it out.

  if(pcap_compile(pcap_nic, &filter, "tcp src port 80", 0, net.s_addr) == -1)
    {
      perror(pcap_err);
      exit(-1);
    }

  if(pcap_setfilter(pcap_nic, &filter) == -1)
    {
      perror(pcap_err);
      exit(-1);
    }

This is the slightly harder part of our program. We are creating a filter so that we only get packets which are tcp packets and that have a source port of 80 (HTTP). Remember our filter variable? Well nows the time that we use it. The language we use to define our filter is the same as that used for tcpdump filters (funny that... since pcap is what lies under tcpdump...). Here we have "tcp src port 80" so what does it mean? Well its pretty easy to read. tcp - tcp traffic only. src port 80 Only traffic where the source port was port 80. Pretty simple really. For more information on the filter language used man tcpdump is your friend. One we've created our filter we set it with pcap_setfilter

  pcap_loop(pcap_nic, -1, (pcap_handler)handler, buffer);
}

This just starts catching packets, each packet is then sent to the function handler. The -1 will mean that it will capture packets for ever, or untill you press Ctrl-C.

void handler(char *usr, const struct pcap_pkthdr *header, const u_char *pkt)
{
  struct ether_header *ethheader;
  struct iphdr *ipheader;
  struct tcphdr *tcpheader;
  struct in_addr source, dest;

Welcome to the handler function. We've been passed a whole load of data here (usr, header, pkt) but we are going to ignore all of it except for pkt which is the raw packet, ethernet headers and all!

We've defined few variables here, the first three are just containers for holder the ethernet headers, ip headers and tcp headers. The last two (source, dest) are for holding the source and destination IP address. They aren't really needed but they make the code neater

  ethheader = (struct ether_header *)pkt;
  ipheader = (struct iphdr *)(pkt + ethlen);
  tcpheader = (struct tcphdr *)(pkt + ethlen + iplen);

Now you can see why we need the header sizes that we got at the top of main they give us the right offsets so that the right bytes end up in the right places for our headers. Here we are just filling ethheader, ipheader and tcpheader with data. For more information on the format of these structs look in net/ethernet.h, netinet/ip.h and netinet/tcp.h.

  if(tcpheader->syn && tcpheader->ack)
    {
      source.s_addr = ipheader->saddr;
      dest.s_addr = ipheader->daddr;

Since we only want to see the start of each HTTP stream, and not every packet we're only going to look at packets with the SYN and ACK flags set, signaling the start of a TCP/IP stream. We're also extracting the source and destination IPs and storing them in source and dest.

      printf("From: %s \t%i\t", inet_ntoa(source), ntohs(tcpheader->source));
      printf("To: %s \t%i\n", inet_ntoa(dest), ntohs(tcpheader->dest));
      printf("\tLength: %i", ntohs(ipheader->tot_len));
      printf("\n");
      printf("Flags: ");
      if(tcpheader->urg)
	printf("URG");
      if(tcpheader->ack)
	printf("ACK ");
      if(tcpheader->psh)
	printf("PSH ");
      if(tcpheader->rst)
	printf("RST ");
      if(tcpheader->syn)
	printf("SYN ");
      if(tcpheader->fin)
	printf("FIN ");
      printf("\n\n");
    }
    
  return;
}

Ok, this last bit is pretty simple. We start off by printing the source IP and port, remember inet_ntoa converts our 32-bit IP address into the dotted-quad that we know and love. All the data in the packets is stored in network-byte format, so we should not assume anything about the endianness of it, which is why we need to use ntohs to ensure that the endianness is correct for they system on which we are running. So we print out our Source IP and destination IP, along with their ports. Then we print the length of the packet.

Once we've done that we just run through and check what flags have been set. In this case we already know that ACK and SYN will be set. Thats pretty much it, the function then returns.

Sample output:

ethlen: 14
iplen: 20
tcplen: 20
Dev: eth0
 
net: 192.168.1.0        mask: 192.168.1.0
 
From: 216.200.201.214   80      To: 192.168.1.10        33080
Length: 60
Flags: ACK SYN
 
From: 216.200.201.214   80      To: 192.168.1.10        33081
Length: 60
Flags: ACK SYN

Hopefully this should have shown you the very basics of using pcap and given you some ideas about writing packet sniffers. For more information check the following sites:

  • http://tcpdump.org - the TcpDump homepage
  • http://www.faqs.org/rfcs/rfc791.html - the RFC where the IP Protocol is outlined
  • http://www.ietf.org/rfc/rfc0793.txt - the RFC where the TCP Protocol is outlined
  • /usr/include/net/*.h - header files
  • /usr/include/netinet/*.h - header files
  • /usr/include/pcap.h - header files

  • Full code of example below

    
    #include <pcap.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <netinet/ether.h>
    #include <netinet/ip.h>
    #include <netinet/tcp.h>
    #include <sys/socket.h>
    
    char ethlen, iplen, tcplen;
    void handler(char *, const struct pcap_pkthdr *, const u_char *);
    
    int main(int argc, char **argv)
    {
      int buffsize = 65535;
      int promisc = 1;
      int timeout = 1000;
    
      char pcap_err[PCAP_ERRBUF_SIZE];
      u_char buffer[255];
      char *dev;
      struct in_addr net, mask;
      pcap_t *pcap_nic;
      struct bpf_program filter;
    
      ethlen = sizeof(struct ether_header);
      iplen = sizeof(struct iphdr);
      tcplen = sizeof(struct tcphdr);
    
      printf("ethlen: %i\niplen: %i\ntcplen: %i\n",
    	 ethlen, iplen, tcplen);
    
      if(!(dev = pcap_lookupdev(pcap_err)))
        {
          perror(pcap_err);
          exit(-1);
        }
    
      printf("Dev: %s\n\n", dev);
      
      if((pcap_nic = pcap_open_live(dev, buffsize, promisc, timeout, pcap_err))
         == NULL)
        {
          perror(pcap_err);
          exit(-1);
        }
    
      if(pcap_lookupnet(dev, &net.s_addr, &mask.s_addr, pcap_err) == -1)
        {
          perror(pcap_err);
          exit(-1);
        }
      printf("net: %s\tmask: %s\n\n",  inet_ntoa(net),  inet_ntoa(mask));
    
      if(pcap_compile(pcap_nic, &filter, "tcp src port 80", 0, net.s_addr) == -1)
        {
          perror(pcap_err);
          exit(-1);
        }
    
      if(pcap_setfilter(pcap_nic, &filter) == -1)
        {
          perror(pcap_err);
          exit(-1);
        }
    
      pcap_loop(pcap_nic, -1, (pcap_handler)handler, buffer);
    }
    
    void handler(char *usr, const struct pcap_pkthdr *header, const u_char *pkt)
    {
      struct ether_header *ethheader;
      struct iphdr *ipheader;
      struct tcphdr *tcpheader;
      struct in_addr source, dest;
    
      ethheader = (struct ether_header *)pkt;
      ipheader = (struct iphdr *)(pkt + ethlen);
      tcpheader = (struct tcphdr *)(pkt + ethlen + iplen);
      
      if(tcpheader->syn && tcpheader->ack)
        {
          source.s_addr = ipheader->saddr;
          dest.s_addr = ipheader->daddr;
          
          printf("From: %s \t%i\t", inet_ntoa(source), ntohs(tcpheader->source));
          printf("To: %s \t%i\n", inet_ntoa(dest), ntohs(tcpheader->dest));
          printf("\tLength: %i", ntohs(ipheader->tot_len));
          printf("\n");
          printf("Flags: ");
          if(tcpheader->urg)
    	printf("URG");
          if(tcpheader->ack)
    	printf("ACK ");
          if(tcpheader->psh)
    	printf("PSH ");
          if(tcpheader->rst)
    	printf("RST ");
          if(tcpheader->syn)
    	printf("SYN ");
          if(tcpheader->fin)
    	printf("FIN ");
          printf("\n\n");
        }
        
      return;
    }