By Ben Seri, Yuval Sarel, Gal Levy, and Noam Afuta
Part 2: The Armis research team finds a new primitive to bypass firewalls using CVE-2021-24094, and provides a full analysis of this Windows TCP/IP stack vulnerability patched in February 2021.
As detailed in the first part of this two-part blog series, several significant vulnerabilities in the Microsoft Windows TCP/IP stack were patched in Microsoft’s February 2021 Patch Tuesday. This blog series set out to do a deep dive on the vulnerabilities, analyzing their patches, and finding the root cause and the potential ramification of these vulnerabilities.
Part of this analysis led us to discover a new primitive, that leverages CVE-2021-24094 to bypass firewalls, which can lead attackers to target unpatched Windows devices, even when they are shielded from attacks using perimeter security. Applying external solutions to shield off attacks is a common solution in industrial and medical environments where patching sensitive devices is challenging.
As noted in part one, these recent vulnerabilities in the Windows TCP/IP stack came after a year and a half in which multiple research initiatives have focused on the security of embedded TCP/IP stacks — beginning with URGENT/11, a set of 11 vulnerabilities discovery by Armis researchers in the IPnet TCP/IP stack, which is used primarily by VxWorks, the most popular real-time operating system.
This trend keeps evolving, with the recent release of important vulnerabilities in the DNS component of the embedded TCP/IP stacks – NetX and Nucleus NET, and in the DHCP client of FreeBSD. In addition, last week, Microsoft’s April 2021 Patch Tuesday included patches for three new vulnerabilities Microsoft in the Windows TCP/IP stack — two DoS vulnerabilities and an additional information leak vulnerability.
The continued focus on the implementation of networking stacks, which are core to the operating system of any connected device is important, especially in embedded systems that operate industrial controllers, medical devices, or any number of mission-critical systems. As demonstrated in a previous blog, the Armis platform has the ability to passively identify the underlying TCP/IP stack used by embedded devices, providing visibility into devices impacted by vulnerabilities that are found in embedded TCP/IP stacks. This ability can be crucial to organizations that are struggling to identify devices vulnerable to the ongoing stream of vulnerabilities, especially when these are discovered in 3rd party TCP/IP stacks that might be used by hundreds of different vendors, OSes, and applications.
The first part of this blog series focused on the analysis of CVE-2021-24074 – a remote-code-execution (RCE) vulnerability in the parsing of options in IPv4 fragments. This second part will focus on CVE-2021-24094 — one of the two remaining vulnerabilities in the Windows TCP/IP stack, patched by Microsoft in February 2021. Both of the remaining vulnerabilities relate to the implementation of IPv6 fragmentation and can be triggered while a vulnerable device reassembles an IPv6 packet that was recursively fragmented, in a malicious way.
The vulnerability analyzed in this blog (CVE-2021-24094 ) is a use-after-free (UAF) vulnerability that can potentially lead to RCE, while the remaining vulnerability (CVE-2021-24086) is a NULL-dereference bug that leads to denial-of-service (and, as our recreation in the lab showed, causes a BSOD even with Windows firewall turned on). Recently, two detailed blogs [1,2] detailing the NULL-dereference bug had been published, so our analysis will focus on the more severe UAF vulnerability in IPv6 fragmentation. While the official impact of CVE-2021-24094 was determined as RCE (although it is not clear whether this UAF bug can actually lead to RCE), our research has discovered an additional primitive that can leverage this bug, and lead to a firewall bypass – as we’ll detail below.
Let’s begin the analysis with some rudimentary background on IPv6 fragmentation.
IPv6 extension headers are the IPv6 equivalent of IP options in IPv4. They carry additional IP layer information such as fragmentation data or routing information. The extension headers are placed between the basic IPv6 header and the next upper-layer header. While IPv4 options reside within the IPv4 header itself (and can amount to a max of 20 bytes), IPv6 extension headers are built as a chain of headers and do not have a specific size limitation.
To implement this, the IPv6 uses the “next protocol” field (found in IPv4 as well) to append additional headers between the IP layer, and the transport layer above it. In IPv6, this field is called Next Header, and it can either indicate a transport layer protocol (TCP/UDP/ICMP/etc.) or a type of IPv6 extension header.
Each extension header will have a unique format based on its type, but all extension headers will start with a Next Header field. This enables the creation of a chain of extension headers that always starts with the IPv6 header and ends with a Next Header field of a transport layer protocol. This last detail is true with one exception — the fragmentation header.
Wikipedia offers a straight-forward explanation of how the IPv6 fragmentation process should be done the “right” way:
“A packet containing the first fragment of an original (larger) packet consists of five parts: the per-fragment headers (the crucial original headers that are repeatedly used in each fragment), followed by the Fragment extension header containing a zero Offset, then all the remaining original extension headers, then the original upper-layer header (alternatively the ESP header), and a piece of the original payload. Each subsequent packet consists of three parts: the per-fragment headers, followed by the Fragment extension header, and by a part of the original payload as identified by a Fragment Offset.”
This short explanation defines a basic principle of IPv6 fragmentation: Certain extension headers will be included per-fragment (several types of routing headers) — which will be included before the fragmentation header in each fragment, while other types of extension headers will be included after the fragmentation header, as part of the payload portion, and essentially be parsed by the receiving end only once the packet has been fully reassembled.
From the perspective of the receiver, the reassembly process will be as follows: when a stack encounters an IPv6 packet with a fragmentation extension header, it considers all data after it as a fraction of the (soon to be) reassembled packet’s payload. Each fragment’s payload is buffered until the very last fragment is received and the reassembly procedure is triggered (the fragmentation header includes a “More fragments” field, that will be 0 on the last fragment).
The reassembled packet will include the basic IPv6 header and the per-fragment extension headers from the first fragment, and the concatenated payloads of all fragments, which may also begin with a chain of extension headers. The transport layer protocol will be defined by the Next Header field from either the first fragment’s fragment header (if no extension headers are in use in the payload portion) or by the Next Header field of the last extension header in the payload portion.
IPv6 fragmentation, as illustrated in Understanding the IPv6 header
As we will see shortly, the “right” way to do IPv6 fragmentation introduces built-in complications that may go wrong in varying ways. The most fundamental of these potential complications is recursive fragmentation (a.k.a “nested fragmentation,” or “splitting fragmented packets”), which can theoretically occur if the extension headers in the payload portion of the reassembled packet end with an additional fragmentation header. If the receiving stack supports it (and Windows stack unfortunately does), a reassembled packet in this state may also be considered as a fragment of a larger packet, meaning the fragmentation process can be done recursively.
As mentioned above, this vulnerability lies within the IPv6 fragmentation mechanism and can be triggered with a handful of maliciously crafted IPv6 fragment packets. This bug is categorized as an RCE since two pointers in the metadata of the IPv6 fragmentation state aren’t properly handled, resulting in a “Dangling Pointer” scenario.
Theoretically, this state could be exploited by an attacker, in a Use-After-Free exploit that leverages heap shaping or other primitives to reach code execution. In addition, our research has also discovered that this vulnerability can be leveraged to obfuscate traffic, which can lead to a firewall bypass — since the result of this fragmentation bug can lead to a packet being reassembled in a different way, by a guarding firewall, and a target Windows device.
To find the underlying bug, we started by analyzing the binary diff between revisions 804 (released February 9, 2021 — when the bug was resolved) and 746 (the one prior) from build 19041 and found the following additions to the IPv6 reassembly routine (highlighted in green is the added code):
Notice that these changes are very similar to those discussed in the first part of this writeup in Ipv4pReassembleDatagram (even the line updating offset_of_routing_option_in_packet which has no use, practical or logical, in IPv6 handling). Just like in the IPv4 flow, without these lines a state confusion can occur, in which the reassembled packet’s struct may contain information taken from the last processed fragment (instead of the “reassembled header”, pointed to by new_header in the snippet above, that is constructed from the first fragment). As noted above, this state confusion can lead to two different primitives, the first of which is a use-after-free (UAF) primitive.
The bug leads to a state-confusion where ip_header and dst_ip are referring to the last processed fragment instead of the new/reassembled packet. Creating a packet fragmented in a specific way leads to the memory pointed by these pointers to be freed, while they are still referenced in a struct and may be used at a later time (as we’ll see shortly). Let’s discuss in more detail the flow of a fragmented IPv6 packet to understand how we can reach such a condition.
As mentioned earlier, an IPv6 packet will contain the basic IPv6 header, extension headers, and a payload (transport layer). To parse and process each of these elements, the stack needs to process the header, then iterate over the extension headers chain (and parse each of them), and finally, parse the transport layer.
A core function to this flow is IppReceiveHeaderBatch. A simplified version of this function can be seen here:
As we can see, the flow of the function consists of three main stages:
To better understand the flow, let’s take a look at the call stacks for a two-fragment ICMPv6 packet:
As we can see above, when Fragment #2 is processed by Ipv6pReceiveFragment, and the fragment is identified as the last fragment, the reassemble routine is called and the reassembled packet is then re-processed through IppReceiveHeaderBatch. The reassembled packet will contain metadata fields copied from the incoming_packet passed to Ipv6pReassembleDatagram, and in this flow, it will hold the metadata fields of the last fragment.
The patch shown earlier corrected this state-confusion, by setting the metadata fields (ip_header, and dst_ip) to point to a newly allocated header object, prior to passing the reassembled packet to IppReceiveHeaderBatch. In the flow shown above, the last fragment is freed only after IppReceiveHeaderBatch is called. As we’ll see shortly, when recursive fragmentation is in use in a certain way, the call flow changes, and this function will be called after the last fragment is freed, leading to the use-after-free scenario.
Let’s take a look at Ipv6pReassembleDatagram once more, this time we’ll focus on another part of the function.
We notice that IppReceiveHeaderBatch is called when the 4th bit of incoming_packet->flags is set. As we can see at the very beginning of that code snippet, this bit is set in the reassembled_packet, which likely means it marks a packet object that was formed by reassembly.
In the case of a recursive fragmentation, the packet will be processed twice by the function Ipv6ReassembleDatagram: First, after the outer reassembly, where the incoming_packet has this bit cleared, and where the reassembled_packet will have the bit set, and second after going through the stack once again as incoming_packet – where the bit is tested.
Basically, the logic here tries to prevent too many recursive function calls to IppReceiveHeaderBatch, and solves this problem by calling it in a separate thread. This is accomplished by queuing a WorkItem (using IoQueuedWorkItem) that will later lead the callback function IppReassembledReceive to call IppReceiveHeaderBatch from within its context (with the same reassembled packet as an argument). This flow leads to ip_header and dst_ip from the outer reassembled packet to point to freed memory, while pointers to them are still held in the reassembled packet object that will be used in the separate thread.
To see how this happens, we’ll review the call stacks of such a flow. The flow will contain 3 packets – the first is “Fragment #1” (the first fragment of the outer packet) and the other two are fragments that after reassembly will create a “Fragment #2” (the last fragment of the outer packet):
When Fragment #2b is processed, both outer and inner reassembly processes will occur, as shown in the flow charts below. We’ll denote the following colors:
Fragment #2b (reassembled as Fragment #2):
Once the inner reassembly set is completed (in the first call to Ipv6pReassembleDatagram – the second purple block in the diagram above) the 4th bit of Fragment #2 is set. The reassembled packet is then reprocessed via IppReceiveHeaderBatch (now as incoming_packet), and since the received packet completes the reception of the outer reassembly set, Ipv6pReassembleDatagram is called again (in the second yellow block).
Since the 4th bit in the processed packet is set, the call to IppRecieveHeaderBatch with the reassembled outer packet is queued and called via a separate thread. This creates a very likely scenario, that the last call to IppReceiveHeaderBatch will occur after the fragments of the outer reassembly set are cleaned up. This cleanup, among other things, frees the memory pointed to by ip_header and dst_ip of the last fragment of the set, which is also pointed to by the outer reassembled packet object, which will be used in the separate thread.
Despite the likelihood of this use-after-free to occur once a recursively reassembled IPv6 packet is processed, it is unclear how this UAF can be leveraged to create an RCE exploit, since the metadata fields contain packet data, and not pointers.
Nevertheless, we have not ruled out this possibility in our analysis, and Microsoft has categorized this bug as a potential RCE (as mentioned above). And as we’ll see shortly, there is more to this tale of how this state confusion actually impacts the processing of recursively fragmented IPv6 packets — and potentially the intricacies of this confusion might also create primitives that do lead to a successful RCE exploit.
While playing around with some recursively fragmented IPv6 packets (similar to the ones mentioned above — an ICMPv6 Echo Request packet), we noticed that certain packets are being dropped by a vulnerable Windows TCP/IP stack, despite the overwhelming effort the stack is doing to successfully reassemble such packets.
We continued our analysis and came to the conclusion that the state confusion introduced above may lead the stack to believe the reassembled outer packet is also a fragment (which it isn’t), and handle it as such — meaning the stack expects to find an additional fragmentation extension header at the beginning of the packet’s payload, where an ICMPv6 header actually resides. When the stack fails to parse this header as a valid fragmentation header, it drops the packet.
This means, first of all — that prior to the February 9, 2021 patch, the Windows stack did not actually support reassembling certain recursively fragmented IPv6 packets (unless they are maliciously crafted, as we’ll see shortly 😬). What this also means, is that this vulnerability can lead to an additional attacking primitive — where the Windows stack reassembles a packet in a certain way, while other network appliances on the path of the packet (firewalls, security appliances, etc.) reassembles it differently. This can lead to the bypass of important security measures on the perimeter of networks, via this convoluted traffic obfuscation. For example, a specially crafted ICMPv6 echo request packet with a specific payload could be interpreted by the vulnerable stack as a UDP packet.
To understand the mechanics of this primitive, and how it can be used maliciously, let’s back up a bit to understand another aspect of IPv6 packet reassembly. As we described in the background section above, IPv6 contains a Next Header field that exists in both the basic header, and in all of its extension headers (including in the fragmentation extension header), and this field enables the chaining of the various headers, until the last header holds a Next Header field of the transport layer above. Let’s see how this field is used in a simple two-fragments IPv6 packet:
The Next Header field contained in the basic IPv6 header of both fragments is of type IPPROTO_FRAGMENT, since a fragmentation extension header is placed above it. In the payload of the reassembled packet, we have a chain of extension headers, that ultimately point to the transport layer by the Next Header of the last extension header (Ext hdr #n). The Next Header field of the fragmentation headers is of the type of the first extension header in the payload (Ext hdr #1).
When a stack reassembles these fragments, it needs to concatenate the payloads of the fragments, but also to update the Next Header field in the reassembled packet that pointed to the fragmentation header that was stripped during reassembly. In the reassembled packet above, the Next Header field in the basic IPv6 header should now point to the first extension header above it.
This process can look a bit more complicated, when a fragmented IPv6 packet also includes per-fragment extension headers (routing headers, primarily), that can exist in a fragment before the fragment header. The RFC defines the basic IPv6 header + all extension headers that follow it, up to the fragmentation header, as the “unfragmentable” part of a packet, and that during reassembly, the “unfragmentable” part from the first fragment will be the head of the reassembled packet.
The two fragments shown above include such a structure — a routing extension header is added after the basic IPv6 header, and before the fragmentation header, in the first fragment. As we can see, this impacts the use of the various Next Header fields. While reassembling such a packet, the stack now needs to correct the Next Header field of the routing extension, since it used to point to a fragmentation header above it.
When the Windows stack processes the first fragment (in Ipv6pReceiveFragment) it will copy the “unfragmentable part” — the basic IPv6 header, and all extension headers up to the fragmentation header — to the reassembly object, so it can be used as the basis of the reassembled packet. It will also copy the offset of the Next Header that points to the fragmentation header within this “unfragmentable” part, so it can be corrected later on, once reassembly is completed.
As we noted earlier, the basic bug that leads to the state confusion is that certain metadata fields in the reassembly packets’ structure were not properly updated, and we’re left with data taken from the last processed fragment, rather than from the first. The state-confusion manifests (as seen earlier) only when recursive reassembly is being performed, in which the definition of “first fragment” suddenly has two different meanings (The first fragment of an outer set, and another first fragment of an inner set).
The logic described above, in which the “unfragmentable” part of a fragment is copied from the first fragment to the reassembly object, to be later used to fix the Next Header field, is implemented in the function Ipv6pReceiveFragment:
The field frag_next_header represents the Next Header field from the fragmentation header of the first fragment, and the field offset_of_last_next_hdr represent the offset within the “unfragmentable” part where a Next Header field points to the fragmentation header. The basic IPv6 header from the incoming_packet is also copied to the reassembly struct at this stage.
When the last fragment of a reassembly set is received, the Ipv6pReassembleDatagram function is called. In the snippet below we can see how the basic IPv6 header of the reassembled packet is being prepared, and how the Next Header field is being corrected (highlighted in green is the part of the code added in the patch).
As we see in the code above, the IPv6 header of the reassembled packet is taken from the ip_header field in the reassembly object, and prior to the patch, this field pointed to the header of the first fragment of the reassembly set, as we’ve seen in the previous function Ipv6pReceiveFragment. The Next Header field in the reassembled packet is corrected, using the metadata fields extracted at the previous function as well (offset_of_last_next_hdr, frag_next_hdr), and the “unfragmentable” part (extension headers from the first fragment) are also copied to the reassembled packet at this stage (not shown in the snippet above.
The state confusion bug manifests when recursive fragmentation is involved — this function is called for the last fragment of an inner reassembly set, and the resulting fragment (of an outer set) is passed again to Ipv6pReceiveFragment (the variable reassembled_packet in the snippet above becomes the incoming_packet of Ipv6pReceiveFragment). Looking back at the snippet of Ipv6pReceiveFragment, we see that it now interprets the input packet as a first fragment, and copies the ip_header of this packet object to the outer reassembly object — and this ip_header actually points to the last fragment of the inner set. Understanding the ramifications of this rabbit hole is not trivial. Let’s review an example flow of a recursively reassembled packet.
This packet will be comprised of three fragments, the first two (Fragment #1a and #1b) will be reassembled as the first half of an outer reassembly set (Fragment #1), and a third fragment which will be the second half of the outer reassembly set (Fragment #2):
Let’s see how these fragments look throughout the various stages of their reassembly:
When Fragment #1b is processed, the reassemble function (Ipv6ReassembleDatagram) is called. The IP header for the inner reassembled packet is correctly taken from the first fragment (Fragment #1a), and the Next Header field for the inner reassembled packet is properly corrected. However, due to the bug, the ip_header field of the inner reassembled packet object is not updated, and it points to the IP header of Fragment #1b (the last processed fragment when the reassembly set was complete). This packet object (which is also the first fragment of the outer set) is passed to Ipv6pReceiveFragment as an input_packet, where the ip_header of the outer reassembly object is prepared.
Lastly, when Fragment #2 is processed, the outer reassembly set is complete, and the IP header for the outer packet (the “original” packet) is constructed using the ip_header field stored in the outer reassembly object. The state-confusion now fully manifests, since this IP header is from Fragment #2b, which still has a Next Header field of IPPROTO_FRAGMENT. The state-confusion is also supported by the fact that the first fragment of the outer set (Fragment #1) also includes a per-fragment extension header (a routing header), that is the final header prior to the fragmentation header. So the offset_of_last_nxt_hdr, prepared when this fragment was processed, actually points to the Next Header field of this routing extension header. The offset is used to correct the Next Header field of this header, while the Next Header field in the basic IPv6 header remains unfixed as IPPROTO_FRAGMENT.
The result of this state-confusion is that the final, outer reassembled packet is still considered as a fragment, and is then reprocessed through Ipv6pReceiveFragment! This function will attempt to parse the first extension header of this packet (Ext hdr #1) as if it was a fragmentation header (which it isn’t). This is a classic type-confusion bug – a data structure of a specific type is parsed as a different type. Some precision in crafting the actual data of the packet could enable a desired encapsulation primitive, as we’ll shortly see.
The first thing to consider is that the final, recursively reassembled packet needs to be a valid one. The strength of the primitive relies on the fact that the packet will be parsed validly by most stacks, but differently (and also validly) on a vulnerable Windows stack.
The first extension header will be parsed as a fragmentation header, in any case, so the whole packet will be considered a fragment. Because we want to see how this packet could be parsed as something meaningful by the stack (TCP/UDP for example) we’ll need to see how to avoid that obstacle.
The most simple solution is to forge a one-fragment reassembly set. This option is supported in IPv6 by sending a fragment extension header where both the M bit and the fragment offset are set to zero. In this case, the fragment handler will extract the next protocol from the Next Header of the fragment extension header, and move on to parse the next header, dispatched per the extracted next protocol (essentially skipping over the fragmentation extension header).
Another thing to remember is that the whole packet is attacker-controlled. This means that we’re looking for an extension header to take Ext hdr #1’s place, in a way that it will be parsed correctly by any stack, and will also conform to the requirements we just mentioned. A convenient candidate is the Routing Extension Header. Let’s take a look at both headers’ structures to see how to accommodate our requirements:
The field Fragment Offset and the M bit of the fragmentation header align with the Routing Type and Segments Left fields of a routing header — the Windows stack will interpret bytes 2-3 of the header as the first, and other stacks will interpret these fields as the latter. Setting these bytes to zero will accommodate both interpretations. When the header will be parsed as a fragment header, the stack will skip over the header by 8 bytes (the size of the fragmentation header).
In this offset (8) the payload portion of the routing header resides — which is completely attacker-controlled and is not validated by the extension header processing function. This enables an attacker to add another extension header that will be parsed only by a vulnerable Windows stack.
As described above, we can place an additional extension header after the first extension header (parsed as a fragment header by a vulnerable stack, and a routing header by other stacks). This additional header can also be a Routing Extension Header, since this header contains 16-bytes of IPv6 addresses which are not validated within the processing function. This allows us to advance the offset of the next header, and align the two interpretations of the reassembled packet, so the final payloads of both packets align. Let’s examine the constructed packet at this point (the following rows represent the exact same buffer of data, interpreted by standard stack vs. a vulnerable Windows stack):
To finalize this example, let’s see how we can encapsulate a TCP transport layer within a valid ICMP Echo Request fragmented packet. In this case, we will set IPPROTO_X to be IPPROTO_TCP, and IPPROTO_X Header will be a valid TCP header. The basic size of TCP header is 20 bytes, but using TCP options enables a lot of flexibility for aligning the payloads of the packets. In the above table, we left two IPv6 addresses space (32 bytes) for the TCP header, meaning that we need to pad the TCP header with 12 bytes of TCP options.
Moreover, as the original packet in our example was chosen to be an ICMP Echo Request, we will need to absorb another 8 bytes (the ICMP Echo Request header size) within our TCP options. Setting the first padding bytes of the TCP options to zero (the EOF option) allows us to fully control the remaining option bytes, since any processing stack will ignore TCP options that follow the EOF option. In these controlled options bytes we will place the ICMP header. See the constructed packet in the following table (again, the table rows represent the exact same buffer, processed differently):
As we can see, we encapsulated a fully controlled TCP packet. A Wireshark example can display the concept:
We can see our three fragments sent in order and are interpreted by Wireshark as an ICMP Echo request sent to a vulnerable Windows stack. Surprisingly, the Windows device responds with a TCP SYN-ACK packet from port 445, a result of an encapsulated hidden SYN packet to port 445.
Unlike the bug reviewed in our previous blog (CVE-2021-24074), this blog focused on a different type of bug (a use-after-free). However, both bugs were the result of the complexity of IP fragmentation. While this bug holds the potential of leading to remote-code-execution, our research showed how it can actually also lead to traffic obfuscation, leading to a firewall bypass.
It is important to understand the wide array of impacts that can result from bugs in TCP/IP stacks. They can range from denial-of-service to information leaks and even to remote-code execution, but also — to strangely sophisticated methods that can be used to bypass security controls and important network mitigations. These bugs can be found in embedded stacks (as we’ve discovered in URGENT/11, in the IPnet stack), but they can also be found in the stack used by the most popular PC operating system — Windows.
Having such complex fragmentation mechanisms, as is found in IPv6, is probably an overkill that should have been prevented at the RFC level. Interestingly enough, an IETF IPv6 Working Group actually considered to deprecate IPv6 fragmentation altogether, in a draft RFC from 2013, but this draft wasn’t accepted. It seems IPv6 will remain to be a fertile ground for research, and potential vulnerabilities for the time being.
Sign up to receive the latest news