Lab 05: Re-implementing ping
Table of contents
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:
- Wireshark to visually see packets and protocols.
- Install this on your local machine, so you can see things visually.
If you are comfortable with command line, you can also use
tsharkto observe the same packets and protocols, directly on the server machine.scporrsyncwill 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
gdbandtcpdump.
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:
Synchronize with the labs remote using
git fetch upstream.Pull the latest changes from the
mainbranch of the class repository:$ git pull upstream mainPush 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:
.env(hidden file - usels -alto see it) contains yourUIDandGIDvariables.connect_*.sha 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:
hostAwith IP address10.10.0.4hostBwith IP address10.10.0.5attackerwith IP address10.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
includedirectory contains definitions for the functions and utility headers.The
srcdirectory contains the implementation files. Of particular interest to us are:src/ping.c: This file contains themainfunction that listens for packets and captures them. New in this file is the use of thebuild_filter_exprutility 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.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.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:
- Check if the received IPv4 header contains an ICMP payload.
- If so, call
parse_icmp. The arguments forparse_icmpare the same asparse_ip. We document them ininclude/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.shunder the05_Lab05directory. This script will check if you need to compile thepingexecutable, 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 theattackercontainer and launch thepingexecutable 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:
Create space of the new packet we’d like to send.
Set the content of the Ethernet header.
Set the content of the IPv4 header.
Set the content of the ICMP header.
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:
Grab a packet capture from
hostAand examine it usingtsharkorWireshark. You will see thathostAshould 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
hostAdrop the packet?Hint:
Wiresharkwill highlight the problem for you, you can’t miss it!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