Lab 05: Re-implementing ping

Table of contents
  1. Lab 05: Re-implementing ping
  2. Introduction
    1. Logistics
    2. Learning objectives
    3. Getting the config
    4. Generating your .env file
    5. Network topology
  3. Code introduction
    1. Compilation
    2. Step 1: Obtaining the ICMP header
    3. Step 2: Parse the ICMP header
      1. 2.1. Print the receipt of an ICMP Echo packet
      2. 2.2. Send the reply
      3. Step 3: Fixing the problem

Introduction

In this short lab, we will be re implementing the ping utility using C and libpcap. We would like to do so we could learn how to construct and inject packets into an already existing stream.

Logistics

We will continue with the same set of tools from Lab01, these are namely:

  1. Wireshark to visually see packets and protocols.
    • Install this on your local machine, so you can see things visually.
  2. If you are comfortable with command line, you can also use tshark to observe the same packets and protocols, directly on the server machine.

  3. scp or rsync will prove to be useful to obtain packet captures from the server and download them on your local machine. They should be available by default on your Linux distribution that you are running.

Learning objectives

After completing this lab, you should be able to:

  • Inject an ICMP Echo response packet into an existing packet stream.

  • Debug network code using gdb and tcpdump.

Getting the config

You can find the starter setup for this lab under the 05_Lab05 directory of the csse341-labs repository. If you have set up your private repository correctly, you can fetch the latest version of the labs using the following sequence of commands:

  1. Synchronize with the labs remote using git fetch upstream.

  2. Pull the latest changes from the main branch of the class repository:

    $ git pull upstream main
    
  3. Push the starter setup to your repository so you can start modifying it:

    $ git add 05_Lab05
    $ git push origin main
    

Generating your .env file

Before we spin up our containers, there are some configuration variables that we must generate. To do so, please run the gen_env_file.sh script from the lab repository directory as follows:

$ ./gen_env_file.sh

If run correctly, you will find the following new files:

  1. .env (hidden file - use ls -al to see it) contains your UID and GID variables.

  2. connect_*.sh a utility script to connect to each container in this lab.

Network topology

In this lab, we will be working with three machines connected to the same local network. They will live on the same subnet and all can access each other directly. The machines are:

  1. hostA with IP address 10.10.0.4
  2. hostB with IP address 10.10.0.5
  3. attacker with IP address 10.10.0.10

Code introduction

In the volumes/code directory, you will find starter code for packet sniffing using libpcap. It is largely based on the tutorial in the previous lab. You might find the print_ip and print_icmp functions you implemented in the previous lab helpful.

As in the previous lab, we structure the code as follows:

  • The include directory contains definitions for the functions and utility headers.

  • The src directory contains the implementation files. Of particular interest to us are:

    1. src/ping.c: This file contains the main function that listens for packets and captures them. New in this file is the use of the build_filter_expr utility function which builds an expression that filters out packets generated by the container running the executables. This avoid self-referencing packets and entering into infinite loops.
    2. ip_util.c: This file contains a simple function that parses an IPv4 packet and determines what it should do with it. In this lab, we’re only interested in ICMP packet, thus this function filters out IPv4 that are do not contain an ICMP payload.
    3. icmp_util.c: This is where you will be doing most of your implementation. The function defined in this file will handle responding to ICMP Echo Request packets.

You should only need to modify the ip_util.c and icmp_util.c files in this lab.

Compilation

As in the previous lab, we will use cmake as our build system to facilitate the resolution of dependencies. To build your code, you should first generate the appropriate makefiles as follows.

On your server (not a container), navigate to the volumes/code directory and then do the following:

$ mkdir build
$ cd build
$ cmake ..

Once we have generated the build files (you only need to do that once), you can compiled your code on any change using make in the build directory.

Step 1: Obtaining the ICMP header

Your first task is to trigger a call to the parse_icmp function from the parse_ip function. The ping.c file already has code that detects when we receive an IPv4 packets and then calls parse_ip with the appropriate arguments. Feel free to check out the header file include/ip_util.h for a description of the arguments.

You should modify this file to do the following:

  1. Check if the received IPv4 header contains an ICMP payload.
  2. If so, call parse_icmp. The arguments for parse_icmp are the same as parse_ip. We document them in include/icmp_util.h.

Do not run your code yet, we need to adjust the parse_icmp function.

Step 2: Parse the ICMP header

Next, grab two terminal windows, one connected to hostA and another connected to the attacker container, and try to ping the attacker from hostA. You will see that the ping attempt will fail since we configure the attacker machine to ignore ICMP packets. Our goal is to change that.

2.1. Print the receipt of an ICMP Echo packet

First thing to do is to acknowledge the receipt of an ICMP Echo Request, and print from where it is coming from. Edit icmp_util.c to do just that.

You will need to check the type of the ICMP header received, and then just print the originating source IPv4 address of the packet. You can find hints in the comments for step 1 under the TODO label.

Note that to run ./ping, you will need to provide the MAC address of the interface on which you should be running. To do so, you can either write it manually, or you can read it from the system.

The kernel stores each interface’s MAC address in a pseudo-filesystem on Linux under /sys. Specifically, if you read the entry /sys/class/net/eth0/address, you would be accessing the MAC address of eth0.

To run the code, you would do something like:

sudo ./ping $(cat /sys/class/net/eth0/address)

To help save on keystrokes, feel free to put this into a bash script that you can run directly. Create a file called run_ping.sh under the volumes directory and put the following in it:

#!/bin/bash

sudo /volumes/code/build/bin/ping $(cat /sys/class/net/eth0/address)

Then, make that script executable using:

$ chmod u+x run_ping.sh

Now, you can run that script using:

$ ./run_ping.sh

Note that I have created a useful script for you to save on keystrokes. You can find that in build_and_run_ping.sh under the 05_Lab05 directory. This script will check if you need to compile the ping executable, and compile it for you if it is not present (note that it will not detect if you have made modifications to the files and trigger a rebuild, so you need to do that yourself). Then it will connect to the attacker container and launch the ping executable there. Feel free to mess around with this script if you like to.

To run that script, from the server, simple execute it using ./build_and_run_ping.sh.

Once you are ready, launch the ping executable on the attacker machine and issue a ping request from hostA. You should something like the following:

┌──(netsec㉿attacker)-[/volumes/code/build]
└─$ sudo ./bin/ping $(cat /sys/class/net/eth0/address)
[LOG:pcap_util.c:find_pcap_dev:36] Starting sniffer on interface eth0
[LOG:pcap_util.c:find_pcap_dev:74] Setup done successfully and ready for listening...
[LOG:icmp_util.c:parse_icmp:44] Received ICMP Echo Request from 10.10.0.4

Or, using the helper script

$ ./build_and_run_ping.sh
[LOG]: Running the ping executable on the attacker container:
[LOG:pcap_util.c:find_pcap_dev:36] Starting sniffer on interface eth0
[LOG:pcap_util.c:find_pcap_dev:74] Setup done successfully and ready for listening...
[LOG:icmp_util.c:parse_icmp:44] Received ICMP Echo Request from 10.10.0.4

On hostA, the ping request should still be unsuccessful.

2.2. Send the reply

Now, we know that we have found our ICMP Echo Request, so we must send our reply. The steps involved in doing that are following:

  1. Create space of the new packet we’d like to send.

  2. Set the content of the Ethernet header.

  3. Set the content of the IPv4 header.

  4. Set the content of the ICMP header.

  5. Send the packet.

However, it is tedious to do all that every single time. So we’ll do a little hack. Our ICMP Echo Reply looks exactly the same as the Echo Request, except for some fields changed here and there. So we will first copy the old packet into the new one, edit it, and then send it.

Note that there’s a reason why we carried the len field with us all this time. We will need it to know how much memory to allocate.

Note that in what follows, I assume that you can infer the types of the declared variables from their use and thus add missing declarations accordingly.

First, allocate room for the return packet and do some error checking:

retpkt = malloc(len);
if(!retpkt) {
  print_err("PANIC: No more room in memory\n");
  exit(99);
}

Next, copy the packet received into the newly created one:

memcpy(retpkt, pkt, len);

Now, let’s start editing. It is useful to grab the headers, so let’s just do exactly that:

eth_hdr = (struct ether_header*)retpkt;
iphdr =     // TODO: Add code to get the IPv4 header IN THE NEW PACKET.
reticmp =   // TODO: Add code to the ICMP header IN THE NEW PACKET.

Then, let’s start the Ethernet header. This is now going from attacker to hostA, while the one we received came from hostA to attacker, so we’d need to swap the MAC addresses. Note that we have our own MAC address in the eth_addr structure.

Copy source host into destination host:

memcpy(eth_hdr->ether_dhost, eth_hdr->ether_shost,
    sizeof eth_hdr->ether_shost);

Copy our MAC address into the source host:

memcpy(eth_hdr->ether_shost, eth_addr->ether_addr_octet,
    sizeof eth_hdr->ether_shost);

Next, swap the source and destination IPv4 addresses, this is a bit easier since we just swap out 32-bits integers rather than needing to copy memory:

uint32_t tmp_addr = iphdr->daddr;     // save destination address.
iphdr->daddr = iphdr->saddr; // set destination to source.
iphdr->saddr = tmp_addr;     // set source address to previous destination.

Finally, we need to adjust the ICMP header’s type and code fields:

reticmp->type = // TODO: set the appropriate type.
reticmp->code = // TODO: set the appropriate code.

Finally, send the packet and free the memory:

pcap_inject(handle, retpkt, len);
free(retpkt);

Now, compile the code on the server using make, then run it on the attacker machine, and from hostA, try to ping the attacker. It should still be unsuccessful.

On your lab sheet, answer the following questions:

  1. Grab a packet capture from hostA and examine it using tshark or Wireshark. You will see that hostA should have received your Reply packet but it dropped it. If not, then your sending code is not correct!

    Examine the packet and its headers, why did hostA drop the packet?

    Hint: Wireshark will highlight the problem for you, you can’t miss it!

  2. What is the use of the field that caused the problem?

Step 3: Fixing the problem

Finally, let’s fix the problem from step 2. First, read the RFC for the ICMP headers here, specifically focus on the description of each field.

To help you out, util.h contains a function called chksum that computes the required value over a header, starting from the start of header. However, it requires us to pass it the pointer as a pointer to two bytes, instead of a pointer to a header.

For example, to use chksum over the reticmp structure from before, we would do something like:

chksum((uint16_t*)reticmp, /* TODO: Compute the needed length */);

Now, before you send the packet, recompute that field, set it in the ICMP header, and then send the packet. Make sure to do what the RFC tells you to do before the computation. Your code would look something like:

// Do something from the RFC
reticmp->/*field name*/ = chksum((uint16_t*)reticmp, /* some length value */);

Yes the name of the function in util.h tells you exactly what you are looking for!

Finally, we need to do the same for the IPv4 header, you can reuse the same process as earlier, only replace reticmp with iphdr.

// do something similar to above from the RFC
iphdr->/*field name*/ = chksum((uint16_t*)iphdr, /* some length you figure out
                               */);

Once you are ready, recompile and test again, you should see something like the following:

┌──(netsec㉿attacker)-[/volumes]
└─$ ./run_ping.sh
[LOG:pcap_util.c:find_pcap_dev:36] Starting sniffer on interface eth0
[LOG:pcap_util.c:find_pcap_dev:74] Setup done successfully and ready for listening...

and from hostA

┌──(netsec㉿hostA)-[/volumes]
└─$ ping -c3 attacker
PING attacker (10.10.0.10) 56(84) bytes of data.
64 bytes from attacker.local-net (10.10.0.10): icmp_seq=1 ttl=64 time=5.14 ms
64 bytes from attacker.local-net (10.10.0.10): icmp_seq=2 ttl=64 time=3.31 ms
64 bytes from attacker.local-net (10.10.0.10): icmp_seq=3 ttl=64 time=1.69 ms

--- attacker ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.693/3.381/5.143/1.409 ms