Table of contents

Table of contents
  1. Introduction
  2. Obtaining the IP packet
  3. Printing IP header content
    1. Testing

Introduction

This tutorial will walk you through how to parse and print an IPv4 header using libpcap. It should serve as a follow up to the ARP printing task that we have started in Lab 04.

You will be mainly working on the implementation of the parse_ip function in src/print_ip.c.

Obtaining the IP packet

Let’s check out the main loop in src/main.c, reproduced here it is for your convenience.

  // MAIN LOOP: keep getting packets until error happens or we are done.
while((rc = pcap_next_ex(handle, &hdr, &pkt)) >= 0) {
  // extract ethernet header and field
  eth_hdr = (struct ether_header *)pkt;
  eth_type_field = ntohs(eth_hdr->ether_type);

  // Check if it's an ARP packet
  if(eth_type_field == ETHERTYPE_ARP) {
    parse_arp(pkt, hdr, handle);
  } else {
    print_log("(%s) Got a packet of len %d that is not an ARP packet!\n", fmt_ts(&hdr->ts), hdr->len);
  }
}

Right now, we are only checking if the packet is an ARP packet, we need next to check if it is an IP packet. So now, we will add a check to see if it’s an IPv4 packet by looking at eth_type. You can check out the source code defining the different constants for each protocol type here. However, here is the relevant part for your reference:

#define ETHERTYPE_IP       0x0800		/* IP */
#define ETHERTYPE_ARP      0x0806		/* Address resolution */
#define ETHERTYPE_REVARP   0x8035		/* Reverse ARP */
#define ETHERTYPE_IPV6     0x86dd		/* IP protocol version 6 */
#define ETHERTYPE_LOOPBACK 0x9000		/* used to test interfaces */

You are looking for IPv4, so we check EHTERTYPE_IP which has the value of 0x0800. These values are constant and are defined in the Ethernet standards, you can find a bit more information about the Ethernet header here.

So now, to catch an IPv4 packet, we just need to handle those packets that match the ETHERTYPE_IP. For now, we will just print that we have received an IPv4 packet and the timestamp at which we got it, something like the following:

else if(eth_type_field == ETHERTYPE_IP) {
  print_log("(%s) Got an IPv4 packet of length %d\n", fmt_ts(&hdr->ts), hdr->len);
}

Let’s simply test this one out. Compile the source code, then grab two terminals, one on attacker and another on hostA. Run the code on attacker (Note that the first line below simply shows you where you should be in the directory tree).

$ sudo ./bin/sniff

Then, from hostA, ping attacker with a single packet (note that we have turned off pings, so you probably won’t get a reply).

(hostA:/)
$ ping -c1 attacker

You should start seeing IPv4 packets showing up with your custom printout above, but we’d like to do more and parse the packet.

Printing IP header content

Let’s now print the fields of the IPv4 packet, we have already created a function to do so called parse_ip under include/print_ip.h and src/print_ip.c.

Since we will need the IPv4 header structure, add the following to the top of the file:

#include <netinet/ip.h>
#include <netinet/in.h>

You can check out the source code here, and here’s the packet header again for convenience.

struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
  unsigned int ihl:4;
  unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
  unsigned int version:4;
  unsigned int ihl:4;
#else
# error  "Please fix <bits/endian.h>"
#endif
  uint8_t tos;
  uint16_t tot_len;
  uint16_t id;
  uint16_t frag_off;
  uint8_t ttl;
  uint8_t protocol;
  uint16_t check;
  uint32_t saddr;
  uint32_t daddr;
  /*The options start here. */
};

Also, here are the different constants for the IPv4 protocols:

ValueConstantProtocol
1IPPROTO_ICMPICMP
6IPPROTO_TCPTCP
17IPPROTO_UDPUDP

Finally, to help you out with printing these things, here’s a small helper function to convert between them:

static const char *ip_proto_to_str(struct iphdr *ip) {
  switch(ip->protocol) {
    case IPPROTO_ICMP:
      return "ICMP";
      break;
    case IPPROTO_TCP:
      return "TCP";
      break;
    case IPPROTO_UDP:
      return "UDP";
      break;
    default:
      return "UNKNOWN";
  }
}

You can place this function at the top of the print_ip.c file if you so desire.

First, we need to extract the header form the packet, so we should start reading after the Ethernet header, thus we must move into the packet by sizeof(struct ether_header) bytes.

struct iphdr *ip = (struct iphdr *)(pkt + sizeof(struct ether_header));

That will simply move the packet pointer pkt by sizeof(struct ether_header) bytes, and read those bytes as a struct iphdr. Next, we can start printing things.

Note that you must use ntohs and ntohl for reading anything that is larger than a byte, otherwise, you will get incorrect results. No need to do anything for values that are a byte or less.

Here’s what my printing routine looks like:

int parse_ip(const u_char *pkt, struct pcap_pkthdr *hdr, pcap_t *handle) {
  struct iphdr *ip = (struct iphdr *)(pkt + sizeof(struct ether_header));
  print_log("(%s) Received an IPv4 packet:\n", fmt_ts(&hdr->ts));

  printf("+---------------------------------------------------------+\n");
  printf(" %-20s %-20s \n",   "Field",          "Value");
  printf(" %-20s %-20x \n",   "Version",        ip->version);
  printf(" %-20s 0x%-20x \n", "ID",             ntohs(ip->id));
  printf(" %-20s %-20u \n",   "TTL",            ip->ttl);
  printf(" %-20s %-20u \n",   "Protocol",       ip->protocol);
  printf(" %-20s %-20s \n",   "Parsed Prot",    ip_proto_to_str(ip));
  printf(" %-20s %-20s \n",   "Source IP",      ip_to_str(&ip->saddr));
  printf(" %-20s %-20s \n",   "Destination IP", ip_to_str(&ip->daddr));
  printf("+---------------------------------------------------------+\n");
}

It simply prints some fields of the packet header, it is pretty self-explanatory. Note that the %-20s syntax in the format specifier asks C to do two things to what we print:

  1. Make sure what we print is at least 20 characters long, it appends spaces if it is less.
  2. Make sure that what we print is left-aligned.

It just allows for easy visual stuff, nothing much.

Finally, replace the print statement we added in the first part (for printing the packet length) with a call to the function above (parse_ip(pkt, hdr, handle);), and let’s give it a try.

Testing

Use a similar setup for the tutorial (sniff on attacker and ping on the host machine) to visualize your output. Verify that the output makes sense by contrasting the value you see with those obtained from Wireshark.

Here’s a sample run from my machine:

$ sudo ./bin/sniff
[LOG:pcap_util.c:find_pcap_dev:35] Starting sniffer on interface eth0
[LOG:pcap_util.c:find_pcap_dev:73] Setup done successfully and ready for listening...
[LOG:print_arp.c:parse_arp:46] (19:49:22.987430) Who has 10.10.0.10? tell 10.10.0.4!
                From 66:94:91:83:fe:9f to 00:00:00:00:00:00.
[LOG:print_arp.c:parse_arp:52] (19:49:22.987462) 10.10.0.10 is at 26:c8:57:7e:68:8b
[LOG:print_ip.c:parse_ip:42] (19:49:22.987479) Received an IPv4 packet:
+---------------------------------------------------------+
 Field                Value
 Version              4
 ID                   0xfb51
 TTL                  64
 Protocol             1
 Parsed Prot          ICMP
 Source IP            10.10.0.4
 Destination IP       10.10.0.10
+---------------------------------------------------------+
[LOG:print_ip.c:parse_ip:42] (19:49:22.987503) Received an IPv4 packet:
+---------------------------------------------------------+
 Field                Value
 Version              4
 ID                   0x8eec
 TTL                  64
 Protocol             1
 Parsed Prot          ICMP
 Source IP            10.10.0.10
 Destination IP       10.10.0.4
+---------------------------------------------------------+

Experiment with this function a bit more and try some other fields. Then, when you ready move on to the ICMP parsing exercise.