blog.champtar.fr

View on GitHub

Layer 2 network security bypass using VLAN 0, LLC/SNAP headers and invalid length

TLDR

You should always drop unknown/unclassified traffic. Using VLAN 0, LLC/SNAP headers and invalid length gives you multiple ways to encapsulate the same L3 payload on Ethernet/Wifi, allowing to bypass some L2 network security control implementations like IPv6 Router Advertisement Guard.

Intro

Following my previous article on VLAN 0, I had two itches to scratch.

First, after reading Proper isolation of a Linux bridge article from Vincent Bernat some time ago and seeing:

$ cat /proc/net/ptype
Type Device      Function
0800          ip_rcv
0011          llc_rcv [llc]
0004          llc_rcv [llc]
0806          arp_rcv
86dd          ipv6_rcv

I wanted to understand what were those llc packets and what I could do with them, but it took me a long time and some luck to craft some useful packets with LLC/SNAP headers.

Second, I wanted to test bypassing layer 2 security on managed switches. As I was staying home for the end of 2020 (COVID …), I decided to buy myself one of the cheapest Cisco switches with IPv6 first-hop security (CBS350-8T-E-2G), and was able to bypass its IPv6 RA Guard implementation using VLAN 0 and/or LLC/SNAP headers.

After reporting my findings to Cisco PSIRT, I reported these same bypasses to Juniper SIRT, but I didn’t have time to continue reporting to all network vendors and coordinate between them. Thankfully I discovered that you can report vulnerabilities that affect multiple vendors to CERT/CC and they will take care of reporting to and coordinating between all vendors.

The packet syntax in this article is the one used by Scapy. If you want to try the examples you might need to replace Dot3() with Dot3(src=get_if_hwaddr(ifname)).

Ethernet frame types

If you want to know everything, the following pages will do a better job than me:

To make it short, an Ethernet frame always starts with a preamble, start frame delimiter, MAC destination, MAC source, then VLAN headers (if used), and then the content depends on which of the 4 frame types you are using:

The 4 Ethernet frame formats

LLC/SNAP with OUI 0x000000 can be called “SNAP RFC1042”, and LLC/SNAP with OUI 0x0000f8 called “SNAP 802.1H”. I found “SNAP 802.1H” by chance while looking at Linux mac80211 code.

Using Scapy notation, here are 3 ways to encapsulate an ICMP Echo request to 192.168.1.2:

Ether()/IP(dst='192.168.1.2')/ICMP()
Dot3()/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()
Dot3()/LLC(ctrl=3)/SNAP(OUI=0x0000f8)/IP(dst='192.168.1.2')/ICMP()

Scapy is computing a lot of values for us, here the same packets expanded a bit:

Ether(type=0x0800)/IP(dst='192.168.1.2')/ICMP()
Dot3()/LLC(dsap=0xaa,ssap=0xaa,ctrl=3)/SNAP(OUI=0x000000,code=0x0800)/IP(dst='192.168.1.2')/ICMP()
Dot3()/LLC(dsap=0xaa,ssap=0xaa,ctrl=3)/SNAP(OUI=0x0000f8,code=0x0800)/IP(dst='192.168.1.2')/ICMP()

RFC1122 - Section 2.3.3 states that: “Every Internet host connected to a 10Mbps Ethernet cable:

After reading section 2.3.3, it’s less surprising to know that Microsoft Windows has a boolean ArpUseEtherSNAP that ‘enables TCP/IP to transmit Ethernet packets using 802.3 SNAP encoding’ and ‘by default, the stack transmits packets in Digital, Intel, Xerox (DIX) Ethernet format (but) it always receives both formats’, ie Windows accepts Ethernet II, “SNAP RFC1042” and “SNAP 802.1H” format.

LLC/SNAP Frames on 802.3 have a maximum size of 1500, so in 2001 Extended Ethernet Frame Size Support RFC was sent out. In short, instead of having the length, use 0x8870 Ethertype. This RFC was never accepted, but Wireshark decodes such frame and Scapy forge them:

Ether()/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

(notice the Ether() instead of Dot3())

To finish in 2003 the IEEE defined “OUI Extended Ethertype”, Ethertype 0x88B7 followed by 3 octets OUI and 2 octets protocol. We could say it’s LLC/SNAP without the LLC header.

Ether(type=0x88B7)/SNAP(OUI=0x000000)/IP(dst='192.168.1.2')/ICMP()
Ether(type=0x88B7)/SNAP(OUI=0x0000f8)/IP(dst='192.168.1.2')/ICMP()

And of course OUI Extended Ethertype can be encapsulated inside LLC/SNAP

Dot3()/LLC(ctrl=3)/SNAP(OUI=0x000000,code=0x88b7)/SNAP(OUI=0x000000)/IP(dst='192.168.1.2')/ICMP()

Invalid 802.3 length

In an Ethernet frame, the 2 bytes after the source MAC can either be a length or an EtherType. Values greater than or equal to 1536 (0x0600) indicates that the field is an EtherType and the frame an Ethernet II frame. Values less than or equal to 1500 (0x05dc) indicates that the field is the length and the frame is Novell raw, 802.2 LLC or 802.2 LLC/SNAP. Values between 1500 and 1536, exclusive, are undefined.

The 802.3 length is redundant with the frame length, so a simple implementation can ignore if the value is coherent and just do:

if (pkt[12:13] >= 1536):
    handle_ethernet2()
elif (pkt[14:15] == 0xaaaa):
    handle_llc_snap()
elif (pkt[14:15] == 0xffff):
    handle_novell_raw()
else:
    handle_llc()

Microsoft Windows accept 802.3 headers with any length between 0 and 1535 (0x05ff) inclusive. On it’s own this is not an issue, but some devices doing L2 security ignore packets with length between 1501 and 1535 or with length that is not coherent with the frame length (they are not valid 802.3 packets after all), meaning they let rogue packets thru that are then accepted by Windows.

Here 2 simple example:

# bypass hyperv
Dot3(src=get_if_hwaddr(ifname),len=0)/LLC(ctrl=3)/SNAP()/ra
# bypass cisco
Dot3(src=get_if_hwaddr(ifname),len=0x05ff)/LLC(ctrl=3)/SNAP()/ra

Frame conversion between 802.3/Ethernet and 802.11

If you open Wireshark, look at some packets on your wired interface and some packets on your wireless, you will likely see that all packets are using Ethernet II headers. You might also have seen in the past TCP packets bigger than the interface MTU. For some (good) reasons, your OS and Wireshark are lying to you. If you want to see what real 802.11 traffic looks like, Wireshark wiki has some 802.11 sample captures.

Linux accepts IP packets with multiple VLAN 0 headers (see previous write-up) but not with LLC/SNAP encapsulation, what if we could combine both ?

When a frame is forwarded from 802.3/Ethernet to 802.11 (Wifi), the layer 2 part of the frame needs to be rewritten. Linux 802.11 wireless stack (mac80211) accepts frames with both Ethernet II and 802.2 LLC/SNAP encapsulation as input, so both

Ether()/IP(dst='192.168.1.2')/ICMP()
Dot3()/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

become something that looks like

Dot11()/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

and if we mix VLAN 0 between LLC/SNAP and IP headers

Dot3()/LLC(ctrl=3)/SNAP()/Dot1Q(vlan=0)/IP(dst='192.168.1.2')/ICMP()

becomes something like

Dot11()/LLC(ctrl=3)/SNAP()/Dot1Q(vlan=0)/IP(dst='192.168.1.2')/ICMP()

For 802.3, VLAN headers should be between the source MAC and the length, whereas for all other 802 networks, VLAN headers are after the LLC/SNAP header. By putting VLAN headers after the LLC/SNAP header for 802.3, we can bypass some L2 filtering, and still be accepted by most OS.

Ethernet II / 802.3 / 802.11 / VLAN 0

If we send the following packet using Scapy on a wireless interface

Ether()/Dot1Q(vlan=0,type=len(LLC()/SNAP()/IP()/ICMP()))/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

Linux will actually send

Dot11()/LLC(ctrl=3)/SNAP()/Dot1Q(vlan=0,type=len(LLC()/SNAP()/IP()/ICMP()))/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

and when converted from 802.11 to Ethernet II we end up with

Ether()/Dot1Q(vlan=0,type=len(LLC()/SNAP()/IP()/ICMP()))/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

To finish, Linux will happily ignore the 802.3 length while converting from 802.3 to 802.11, so

Dot3(len=0)/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

becomes a valid packet

Dot11()/LLC(ctrl=3)/SNAP()/IP(dst='192.168.1.2')/ICMP()

How to test your network

As there are a lot of possible combinations, I’ve written a small script that uses Scapy to send many of them: l2-security-whack-a-mole.py. Perform the attacks via both wired and wireless interfaces. An example to send Router Advertisements

sudo python3 l2-security-whack-a-mole.py -i eth0 --i-want-to-break-my-network ipv6_ra <IPv6 Link Local addr of eth0> ff02::1

Going deeper

While researching for those L2 attacks, I stumbled upon an awesome research from 2013 injecting specially crafted packets at L1 to confuse some switches and NICs: Fully arbitrary 802.3 packet injection Maximizing the Ethernet attack surface

Impacted software / hardware

Timeline

Acknowledgments