Missing BGP Attributes?
I was watching a CBT Nuggets video and the instructor had stated that if a mandatory attribute was missing in a BGP update, the router would tear down the session. After hearing this, I thought to myself, I have to try this.
So, here I am several months later to try it. First, I need to find a way to remove an attribute from an update message. Since it would not be possible to have a router omit a mandatory attribute on purpose, I will have to place a tap on the network between the two routers (neighbors) that I am targeting. I will then listen for a real update message, modify it by removing something mandatory like AS path and then resend it on the wire.
For this lab, I setup two routers, a Juniper vSRX and Cisco XRv, with a hub in the middle where I connect a kali linux box to sniff the traffic and inject my own.
With an external BGP relationship in place and a tcpdump running, I have the SRX export a network into BGP. Below is the update message captured.
Highlighted are the 9 bytes for the AS path that I will remove. A bit of dd magic will help get that done. Keep in mind there is a 40-byte header on the pcap file.
root@kali:~# dd if=capture.pcap bs=1 count=121 of=capture.temp
121+0 records in
121+0 records out
121 bytes copied, 0.00075204 s, 161 kB/s
root@kali:~# dd if=capture.pcap bs=1 skip=130 seek=121 of=capture.temp
11+0 records in
11+0 records out
11 bytes copied, 0.00027413 s, 40.1 kB/s
root@kali:~#
Now we need to modify a few things in the packet using hexeditor. Let’s start at the bottom and work our way up. BGP total path attribute length was 20 but will now be 20 – 9 = 11 or 0B in hex.
BGP length 47 – 9 = 38 or 26 hex.
IP length 87 – 9 = 78 or 4E hex.
At this point the capture still wouldn’t open in wireshark unless we modify the pcap header. We just need to look for the total packet size which is 101 but we’ll look for the hex equivalent which is 65 and change it to be 101 – 9 = 92 or 5C.
Wireshark should now be able to open the capture file. Let’s save it and take a look.
A few things remain to be done. We need to change the TCP sequence numbers to be in line with the current flow. For this, I will capture traffic on the wire and then pause router VMs so they don’t continue to talk while we modify our packet further. Also, we will need to update the IP header checksum as well as the TCP checksum values. First, let’s rewrite new sequence and ack numbers.
Before pausing the simulator, the last sequence number sent by 10.100.1.2 was 3406139802 and the tcp length was 19. That gives us 3406139821 (hex CB 05 91 AD) for the next sequence number. No data was sent by 10.100.1.1 after this latest packet (not shown) so the acknowledgment number remains 703184130 (hex 29 E9 BD 02). Let’s update our capture file with hexeditor.
Saving and reopening in wireshark, we should see the new sequence and ack numbers.
Finally we need to update the checksum values. You can let wireshark do the work and just get the values from there but I have nothing better to do with my time than write a script to calculate the checksums. Besides, at some point, I’d like to have this whole thing automated. Why? Not sure. It’s fun, I guess.
Anyway, the checksums in both IP and TCP are calculated using 1’s compliment. There is a really good YouTube video about it by a guy named Ben Eater. I highly recommend checking out his channel.
For the IP header, the checksum field is replaced by 0000 or just omitted like my script does. Each hextet (totally had to look this word up) in the header is then counted. My script uses unsigned shorts. The sum, if over 65535, is then divided by 65535 and the remainder is subtracted from 65535. This value is then placed in the checksum field.
A similar process is used for the TCP checksum, however, the payload is also counted as well as a pseudo header.
#!/usr/bin/python3 import struct import sys filename = sys.argv[1] pcapoffset = 40 etherheader = 14 + pcapoffset data = open(filename, 'rb').read() #get ip header length if data[etherheader] > 64: ipheaderlength = (data[etherheader] - 64) * 4 else: ipheaderlength = data[etherheader] * 4 ipchksize = ipheaderlength / 2 - 1 #calculate ip checksum ipbitcount = 0 for i in struct.unpack("!" + str(int(ipchksize)) + "H", data[etherheader:etherheader+10] + data[etherheader+12:etherheader+ipheaderlength]): ipbitcount += i if ipbitcount > 65535: ipbitcount = 65535 - ( ipbitcount % 65535 ) else: ipbitcount = 65535 - ipbitcount #start writing new packet newfile = open(filename + '.mod', 'wb') newfile.write(data[:etherheader+10]) #write ether and 10 bytes of ip header newfile.write(struct.pack("!H", ipbitcount)) #write checksum newfile.write(data[etherheader+12:etherheader+ipheaderlength+16]) #write remainder of ip header and start tcp header #start tcp calculation tcpbitcount = 6 #always protocol 6 for i in struct.unpack("!4H", data[etherheader+12:etherheader+20]): tcpbitcount += i tcpheaderlength = data[etherheader+ipheaderlength+12] tcpheaderlength = (tcpheaderlength >> 4) * 4 file = open(filename, 'rb') file.read(etherheader+ipheaderlength) data = file.read(16) trash = file.read(2) #don't need existing checksum for i in struct.unpack("!8H", data): tcpbitcount += i tcpcount = 18 #used for tcplength calculation data = file.read(2) tcpdata = data while data: try: myresult = struct.unpack("!H", data) tcpbitcount = tcpbitcount + myresult[0] except: data = data + b'\x00' myresult = struct.unpack("!H", data) tcpbitcount = tcpbitcount + myresult[0] data = file.read(2) tcpdata += data tcpcount += 2 tcpbitcount += tcpcount if tcpbitcount > 65535: tcpbitcount = 65535 - ( tcpbitcount % 65535 ) else: tcpbitcount = 65535 - tcpbitcount newfile.write(struct.pack("!H", tcpbitcount)) newfile.write(tcpdata)
After running through the script and verifying the checksums, we should be good to send the packet on the wire.
To send the packet, I will use tcpreply while unpausing the routers.
tcpreplay -i eth0 --loop 100 --loopdelay 500 capture.temp.mod
While it didn’t take the peer down, it did cause the route to be removed from the routing table. Below is the log from the cisco router.
RP/0/0/CPU0:Dec 7 20:56:43.869 : bgp[1047]: %ROUTING-BGP-3-MALFORM_UPDATE : Malformed UPDATE message received from neighbor 10.100.1.2 (VRF: default) - message length 38 bytes, error flags 0x00000800, action taken "TreatAsWdr". Error details: "Error 0x00000800, Field "Attr-missing", Attribute 2 (Flags 0x00, Length 0), Data []". NLRIs: [IPv4 Unicast] 10.200.0.0/24
I think next I'll try removing the NLRI to see if that will cause the peer to reset the session. Stay tuned for more. In the meantime, check out this Juniper documentation that explains the different error scenarios. https://www.juniper.net/documentation/us/en/software/junos/bgp/topics/topic-map/bgp-error-messages.html