CS177: Project 2 - Ping of Death (15% of project score)

Project Goals


The goals of this project are:

  • to craft raw IP (and ICMP) packets
  • to trigger a remote denial-of-service vulnerability

Administrative Information


The project is an individual project. It is due on Tuesday, May 2, 2023, 23:59:59 PST (no deadline extensions; late flags will not be accepted).

Trigger a remote crash in a vulnerable network stack


The goal of this project is to launch a remote denial-of-service attack that crashes a user-mode TCP/IP network stack. This attack is very similar to the famous ping of death (PoD) attack, which was first discovered in the 1990s, and which allowed attackers to send a fragmented ping (ICMP) packet and remotely crash vulnerable versions of Windows 95.

As defined in RFC 791, the maximum packet length of an IPv4 packet, including the IP header (which has 20 bytes) is 65,535 (2^16 − 1) bytes. This is due to the fact that IPv4 uses a 16-bit wide IP header field to describe the total packet length. The attack exploits the fact that it is possible to abuse IP packet fragmentation to create an IP packet that, after reassembly, violates this maximum size. When the victim TCP/IP stack does not perform proper input validation checks or does not reserve a buffer in memory that is large enough to accommodate such (abnormally) large packets, it is possible that a memory overflow occurs.

When fragmentation is performed, each IP fragment needs to carry information about which part of the original IP packet it contains. This information is kept in the Fragment Offset field, in the IP header. The field is 13 bits long, and contains the offset of the data in the current IP fragment, in the original IP packet. The offset is given in units of 8 bytes. This allows a maximum offset of 65,528 ((2^13-1)*8). This means that an IP fragment with the maximum offset should have data no larger than 7 bytes. However, a malicious user can send an IP fragment with the maximum offset and with much more data than 8 bytes (as large as the maximum transmission unit -- MTU -- of the physical layer allows it to be). When the receiver assembles all IP fragments, it will end up with an IP packet which is larger than 65,535 bytes. This may overflow memory buffers that the receiver allocated for the packet, which can cause various problems and crashes.

As is evident from the description above, the "Ping of Death" problem has actually nothing to do with ICMP. Instead, the ICMP packet is just used as payload for the IP packet, big enough to exploit the problem. It is a problem in the reassembly process of IP fragments. To fix the security, one has to add checks to the reassembly process. The check for each incoming IP fragment must ensure that the sum of "Fragment Offset" and "Total Length" fields in the IP header of each IP fragment is smaller or equal to 65,535. If the sum is greater, then the packet is invalid, and the IP fragment should be ignored.

For this project, we remotely run a (user mode) TCP/IP stack implementation that is vulnerable to the aforementioned IP fragment reassembly problem. Your goal is to send a fragmented ICMP (ping) packet that, when the fragments are reassembled, will lead to a buffer overflow in the target (the network stack).

Like in the previous challenge, you will first go to our CTFd site and launch your service instance. The site will give you the address and destination port where your server is running and listening for your connections. Then, you can start to implement your client. The server is not directly the vulnerable network stack. Instead, it is a TCP bridge. That is, you will have to create and send raw network packets to the server (over a TCP connection to the destination port). The server will take your packets and inject them into the vulnerable network stack. When you don't crash the network stack, it might respond with one or more packets, and our TCP bridge will make sure to send these packets back to you. When you manage to inject the proper packets (fragments), then you will receive back a packet that contains the flag. When you get this flag, just submit it via CTFd.

When you craft your packets, you have to build the entire IPv4 packet, including the header. Make sure that you understand the IPv4 header fields, and make sure that your 16-bit and 32-bit numbers have the right byte order. For this challenge, you should set the source address for the packets that you create to 192.168.222.1. You should set the destination IP address of your packets to 192.168.222.2.

In addition to IPv4, you will also need to have a look at the Internet Control Message Protocol (ICMP). Specifically, after you crafted the IPv4 header, you want to craft the proper ICMP header (and payload) for a ping packet. The ICMP packet will be encapsulated in the IP packet (in other words, the ICMP packet will be the payload of the IP packet).

Note that the MTU (maximum transmission unit) of the underlying link layer for our network stack is 1,500 bytes. This is the same as for Ethernet. This means that the maximum size of each packet or fragment that you send us cannot exceed 1,500 bytes. Of course, when you fragment the IP packet, the size of the reassembled packet can be larger. But not the size of each individual fragment. If you send us packets that are larger, they will be dropped by the network stack.

When you exchange packets with the server, you will need to wrap your raw packets in a simple protocol. We will use a mechanism that is a similar (and simplified) version of the first project. Specifically, when you exchange packets between the client and the server, follow this format:

  1. m_len (unsigned short): This is the length, in bytes, of the following packet. Given that the MTU is 1,500 bytes, the m_len value should never exceed this limit.
  2. packet (unsigned byte[]): These are the raw bytes of the packet.

This format is used both for packets sent from your client to the server, and from the server to the client. Note that we don't use protobufs for this project. You can (and will have to) send multiple packets (or fragments) to the server. The server can send multiple packets back to your client. You can use your connection to exchange packets until you close it, or until the server encounters an error and closes the connection.

When you manage to send packets that crash the network stack, you will receive the flag in the payload of a packet that is sent to you from the server. You can distinguish the flag from a regular IPv4 packet because it will not have a valid IPv4 header. Instead, you will receive a string that contains your flag in the standard format CS177{text}.

In some cases, we will send you back an error. In this case, the response that you will receive from the server will start with ERROR, and it will also not be a valid IPv4 packet.

Implementation


The project might sound daunting at first glance. This section contains a number of hints and directions on how you could approach the problem.

It might be easiest to first make sure that you can send a valid ICMP ping (Echo Request) packet to the network stack and get back an response (Echo Reply). To this end, create an ICMP packet of Type 8 and set its destination IP address to (192.168.222.2). When you get back the reply (ICMP packet of Type 0), then you have already achieved a lot. You know that you have created a valid IP header and a valid ICMP payload, you managed to send this packet to the server, and you have received an answer back from the vulnerable network stack.

In the next step, add fragmentation to the mix. Take the ping packet that you have just sent, and break it into two (or more fragments). Then, send all these fragments to the server. If you get back a response to your ping packet, you know that you can create and send valid fragments. You are almost there.

In the third step, you are ready for the exploit. Create fragments that, when assembled, exceed the maximum size of a valid IP packet. You don't have to overflow the maximum size by a lot. The vulnerable network stack is set up so that any IP packet with a size, after reassembly, of greater than 65,540 bytes should trigger the overflow and result in the server sending you the flag. However, you don't need to cut it very close. Instead, make the packet a bit larger and overflow the buffer by 64 or 128 bytes. But also make sure that the packet is not too large, as the network stack could discard fragments that are too large. By sticking with ranges between 65,600 and 66,000 for the total size of the reassembled packet, you should have a decent bit of room to play with that should always result in success (in our tests, the hard ranges that worked were from 65,493 to 66,592 bytes).

If you do not get the flag sent to you in your interactions, you should keep the connection open and listen for packets. The network stack will send out ICMP error messages if things go wrong. For example, if the reassembly fails and times out because one of your packets was not accepted, after about 20 seconds you should receive an ICMP error message of Type 11 (which is Time Exceeded). Again, check out the Wikipedia article on ICMP for details.

Again, you can use any programming language to implement your client. It probably makes sense to leverage your client that you implemented for the first project as a starting point. We do recommend that you use Python.

It will be a valuable exercise for you to craft your packets by hand, since this requires you to really understand the different network protocols and their headers in a fair bit of detail. However, there are many libraries out there that make "packet crafting" easier. A very good library for Python is called scapy. Scapy is a packet manipulation library that can abstract away some of the nitty-gritty details of building packets byte-by-byte. But it is also a library that has a bit of a learning curve.