DEV Community

Cover image for IP spoofing: Theory and implementation
Conner
Conner

Posted on

IP spoofing: Theory and implementation

Theory

Background

When using the internet your devices are assigned a unique 32 bit (IPv4) or 128 bit (IPv6) identifier called an IP address.
You can find the IP address assigned to your network by clicking here.

Think of your IP address as the from: field one would write on an envelope, and the destination IP address as the to:.

If you wanted to pretend to be someone else when sending mail, you can easily erase your address and write a different one, this concept is referred to in this article as spoofing.

As it turns out, the internet works in the same way, on every chunk of data your network sends out conforming to the IP protocol, a header precedes your message containing the from and to fields of the internet. This header is written by your device.

The internet has its foundations in the IP protocol, but most traffic uses TCP/IP or UDP/IP protocol stacks, meaning a TCP or UDP header is included in our message. TCP is important in verifying the source IP of a message, and doesn't enable sustained IP spoofing.

Research

The IP suite is quite complex and has a long history, but overall it defines a set of rules for a software implementation, called the IP stack.

The Internet Protocol itself is used to relay datagrams consisting of a header and data payload. The header section consists of the source IP address, destination IP address, and other relevant fields needed to route and verify the packet. The data payload consists of the data being sent.

In order to view the technical information on what an IPv4 header contains, take a look at the Internetwork Protocol Specification.

In the specification, an IP header is defined on page 15 as the following:
Header Contents

It's a bit strange to decipher but each - character denotes 1 bit, and each field is read from starting ! to ending !

For example, Fragment Offset is 13 bits, and there are 13 - characters from its preceding ! and subsequent !

On page 16 of the specification, we find a few interesting definitions:
Source Address Field Definition
The source address field definition, which we intend to change, specifically the last 3 octets (24 bits).
Checksum Header Definition
And the checksum field description, which we will also have to change in order for our datagram to be considered valid.

Implementation Plans

Before we get started with the implementation, something important to note is that we cannot feasibly spoof an IP with a TCP/IP stack. This is not possible because TCP connections begin with a 3 way handshake between sender and receiver, and who will the receiver of our message try to complete the handshake with? Our spoofed IP and not our actual one.

However we can use a spoofed IP in the UDP/IP stack, the UDP protocol does not care about a handshake, and will accept communication in a single direction, sender to receiver.

NOTE: Spoofing an IP does not actually require TCP or UDP as a higher level protocol. But we intend to use UDP because it supports port specification, making it easier to test our program.

Now that we've done our research, we have the information needed to determine what we want to change. But how will we modify an IP header?

Before we go searching for information on how an IP header can be modified in code, let's design our program.

Program Design

Our program will use C, as it suits a low-level project like this. It will accept a single argument, the destination address.
All other configuration can be done in code and recompiled.
$ ./ipspoofer 127.0.0.1

It will use the UDP protocol in order to send data, since a 3 way handshake cannot be established under TCP IP spoofing.

API Research

During research the following libraries were found that allow us to modify IP headers:

libpcap
A packet capture library intended for the analysis of incoming and outgoing TCP/IP packets on a device, most likely a network device located in /dev/. However it doesn't seem to be intended for packet modification. The windows port is winpcap.

libnet
Libnet describes itself as "An API created to help with the contruction and injection of network packets [...]
Libnet includes packet creation at the IP layer and at the link layer as well as a host of supplementary and complementary functionality".

raw sockets (unix kernel API)
If you've ever invoked the socket() function using the socket library for a linux system, you've probably either used SOCK_STREAM or SOCK_DGRAM as they correspond to TCP and UDP respectively.
There are however a few more socket types, the one to be used in our code is SOCK_RAW. This socket type allows us to construct our protocol header manually, for protocol we will use IPPROTO_RAW.
This means we will construct the IP and UDP headers and supply them to the kernel.

To summarize the design, we are writing C code to create a program that will construct a UDP/IP datagram, then forward it to its destination IP. We will use raw sockets and syscalls provided by linux kernel as they are much simpler than utilizing a 3rd party library.

The caveat to an implementation using raw sockets is that we will have to create our IP and UDP headers manually, as opposed to simply modifying and injecting captured packets, as can be done with libpcap and libnet.

Implementation

Let's get started writing code, the full source for this project is available here. But we will go over the main bits.

NOTE: Code was tested on Linux, FreeBSD, and MacOS. It compiles on all 3 platforms but does not send datagrams on MacOS.

After including the necessary headers, we will create our socket with the following arguments:

int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
Enter fullscreen mode Exit fullscreen mode

AF_INET means we will be using IPv4.
SOCK_RAW means we will be constructing our protocol header.
IPPROTO_RAW means we supply all headers from the IP protocol to the higher level protocols we are using.

We will use 2 predefined structs for our headers, ip_header and udp_header, included from netinet/ip.h and netinet/udp.h.

// IP header
struct ip ip_header;
ip_header.ip_hl = sizeof(struct ip) / 4; // Header length is size of header in 32bit words, always 5.
ip_header.ip_v = 4;                      // IPv4
ip_header.ip_tos = 0;                    // Type of service, See RFC for explanation.
ip_header.ip_len = htons(sizeof(struct ip) + sizeof(struct udphdr) + datalen);
ip_header.ip_id = 0;                     // Can be incremented each time by setting datagram[4] to an unsigned short.
ip_header.ip_off = 0;                    // Fragment offset, see RFC for explanation.
ip_header.ip_ttl = IPDEFTTL;             // Time to live, default 60.
ip_header.ip_p = IPPROTO_UDP;            // Using UDP protocol.
ip_header.ip_sum = 0;                    // Checksum, set by kernel.

// Source IP
struct in_addr src_ip;
src_ip.s_addr = inet_addr(src_addr);
ip_header.ip_src = src_ip;

// Destination IP
struct in_addr dst_ip;
dst_ip.s_addr = inet_addr(dest_addr);
ip_header.ip_dst = dst_ip;

// UDP Header
struct udphdr udp_header;
udp_header.uh_sport = htons(src_port);                          // Source port.
udp_header.uh_dport = htons(dest_port);                         // Destination port.
udp_header.uh_ulen = htons(sizeof(struct udphdr) + datalen);    // Length of data + udp header length.
udp_header.uh_sum = 0;                                          // udp checksum (not set by us or kernel).
Enter fullscreen mode Exit fullscreen mode

To construct the datagram to be sent, we need to fill a buffer with data, starting with the IP header, then UDP header, and finally the data payload.
We can copy a struct into memory with memcpy().

// Construct datagram
int datagram_size = sizeof(struct ip) + sizeof(struct udphdr) + datalen;
unsigned char datagram[datagram_size];
memcpy(datagram, &ip_header, sizeof(struct ip));
memcpy(datagram+sizeof(struct ip), &udp_header, sizeof(struct udphdr));
Enter fullscreen mode Exit fullscreen mode

Finally, we fill out a struct sockaddr_in to hold info about where we are sending this datagram, and send it repeatedly with sendto() until SIGTERM is sent via ctrl+c.

// sendto() destination
struct sockaddr_in destaddr;
destaddr.sin_family = AF_INET;
destaddr.sin_port = htons(dest_port);
destaddr.sin_addr.s_addr = inet_addr(dest_addr);

// Send until SIGTERM
for(;;) {
    sendto(s, datagram, datagram_size, 0,(struct sockaddr*)&destaddr, sizeof(destaddr));
    sleep(1);
}
Enter fullscreen mode Exit fullscreen mode

Thats an overall gist of the code, there's more needed to make this work, and full explanation is inside the source code available at the github link at the top of this section.

View from the Receiver

To verify that our datagrams are being sent, we can use netcat to listen on an open port and display incoming traffic.
$ nc -lvu <port>
-l - Listen (run as a receiver).
-v - Verbose (will show incoming connection IP).
-u - Use UDP.

Run our program and we should see the following:

Connection from 1.2.3.4 2000 received!
...random data...
Enter fullscreen mode Exit fullscreen mode

NOTE: MacOS has no verbose output, you will only see the data.

Wireshark Analysis

In order to verify traffic is being sent by our device, Wireshark can be used to capture and analyze outgoing packets.
UDP Packets sent seen in wireshark
If all is well, wireshark should look like this, a blue UDP protocol packet with the source being our spoofed address.

Packet headers and data seen in wireshark
Inspecting the packet, we can see the contents above.
Everything looks good, source IP, destination IP, and ports are set. Checksum was set by kernel and is verified.

Resolving Bugs

MTU

If packet_size is set larger than the MTU, the datagrams will not be sent, and will not appear in wireshark.

A network device MTU refers to its Maximum Transmission Unit, which is the maximum amount of data that can be sent in one IP datagram without fragmenting. Typically an MTU is 1500.

You can find your network device MTU with ifconfig | grep mtu on MacOS and FreeBSD, and with ip addr | grep mtu on linux.

You can also set the MTU of a device with:
ip link set dev <interface> mtu <size> (Linux)
ifconfig <interface> mtu <size> (FreeBSD, MacOS)

MacOS

This code does not work on MacOS, Wireshark analysis implies the kernel is overwriting our source IP and removing our UDP header. The MacOS kernel also does not calculate our checksum for us, which may be the reasoning behind this.

Top comments (0)