Packet in Packet for Cisco HDLC + MPLS

25 Oct 2012

This is a derivative of Travis Goodspeed's excellent work, which he presented at Ruxcon 2012. It should be noted that this is theoretical and still untested, so please let me know if you find any problems with it.

Service Provider and Telco backbones use basic technologies like VLAN and MPLS to segregate traffic. These protocols basically prepend a tag to a packet. While this approach is quite successful at providing private numbering and routing across public clouds, they shouldn't be confused for a security protocol (as some vendors seem to claim) - they leave the packet in tact, and have no way of guaranteeing the integrity of the MPLS packets.

Organisations that employ hosted firewalls should factor into their risk profile the possibility of packets being injected behind it, using this technique or similar.

HDLC protocols (including PPP) have a very simple frame structure - they have a delimiter - 0x7e - which can appear anywhere in the bit stream, followed by a 4-byte header, then the packet and finally a 16-bit CRC (optionally 32-bit for PPP), after which the delimiter will appear again. They cleverly avoid having 0x7e appearing anywhere inside the packet stream by placing a 0-bit after any 5 1-bits in the stream.

While CRC is quite effective at detecting accidental bit-errors, the CRC is easily manipulated so that if just the right bit error occurs, the packet can take on a very different meaning.

To illustrate this, let's construct a packet that opens a pinhole in a firewall to somebody's SIP server:

###[ cHDLC ]###
address= 15
control= 0
protocol = 641
###[ MPLS ]###
 label= 17
 exp= 0
 bottom= 0
 ttl= 57
###[ MPLS ]###
 label= 2049
 exp= 0
 bottom= 1
 ttl= 64
###[ IP ]###
  version= 4
  ihl= None
  tos= 0x0
  len= None
  id= 1
  flags= 
  frag= 0
  ttl= 64
  proto= udp
  chksum= None
  src= 192.168.0.50
  dst= 74.125.237.48
  \options\
###[ UDP ]###
     sport= sip
     dport= 10000
     len= None
     chksum= None
###[ Raw ]###
    load= '\x00'

Payload:

0f000281
39000011 40800801
4500001d0001000040118247c0a800324a7ded30
13c427100009cc7f
00

The MPLS values for the target could be found thanks to Cisco's traceroute extensions for MPLS, or better yet by bribing a network admin ;)

Now to get this to be recognised as a valid packet-in-packet, it would need to have 0x7e before it, and a valid checksum and 0x7e after.

The hardest part for an attacker armed with the knowledge in this document is to get the initial 0x7e into the stream - we can't put 0x7e into the packet, because the HDLC encoder will append a 0-bit after any 5 1-bits.

One solution to this would be to put 0x5e into the payload where we want 0x7e to exist. Since there's likely to be an ATM-scrambler on the outside of the packet (between SDH and HDLC), a disruption to the line would cause some scrambling/truncation, which if it stops somewhere in the first nibble of our not-quite-a-flag, it could cause the receiver to treat our inner packet as valid.

Now, we can simply wrap that payload into a UDP packet ready to send over our own VPN, leaving 2 bytes before our payload with place-holder value - I'll explain what this is for later. You might want to set the UDP checksum to 0 (disabled).

A detail that I glossed over earlier is that there's a checksum at the end of a HDLC packet - a CRC16-CCITT to be specific. We need to ensure that this is the same value for our inner packet as our outer packet - this avoids us from having to put a checksum and end-flag into our packet.

To do this you need to calculate the crc of:

  • The Cisco HDLC header described above (not including the flag)
  • The MPLS tags you expect to be applied to your packet while traversing that link (bribe an engineer, or traceroute if you're lucky)
  • The IP/ICMP packet you create - but not the payload.

Now we want to reset the CRC function - the result of the CRC so far should equal the initial value. This can be brute-forced - just need to try combinations of 2 bytes followed by 0x5e until you get 0xffff - which is the standard starting value for CRC16-CCITT. These two bytes should replace the placeholder value I suggested above.

Now, you just need to increment the TTL and update the IP header checksum of your outer packet, and send across the noisy link, and eventually your packet might hop across VPNs.

Of course, this is all theoretical - I suspect that if there were all clean optic-fibre links within a network (no Microwave), the bit-error required to perform this attack might never reach layer 2.