As was promised in the last posts, today we will discuss the development risks in the (de)fragmentation feature. From a security stand-point this is a Zero-Sum Game: a developer’s nightmare is a researcher’s goldmine, and defragmentation is a goldmine that seems to always payoff.
Fragmentation and Defragmentation are two complementary features often seen in network protocols / drivers when a message it too big for the underlying network protocols / channels. During the sending operation a fragmentation will occur: the message will be split into several smaller messages, called fragments, and each message will be sent independently. During the receive operation a defragmentation will occur: the fragments will be merged together to form the original message.
When referring to security vulnerabilities in a fragmentation process, one usually refers to the defragmentation process, as receiving the possibly malicious fragments is much more risky than splitting a large message into fragments during the send operation.
There are several implementation and design decision than impact the complexity of a fragmentation feature, and they are:
- Does the receiver supports fragments reordering? (For example: IPv4, IPv6)
- Can the overall message length be known before the last fragment arrives?
- Do the different fragments can vary in sizes?
- Does the header includes any redundant length fields?
- Can a fragment’s index in the fragments stream be easily derived?
- Is there are overlapping policy?
- Is the fragments coalescing is done on each fragment? or only at the end?
In the following chapters we will discuss the implications of each answer, in hope to highlight the pitfalls that a defragmentation feature may contain.
Knowing the message’s size:
In almost every proprietary protocol, the fragment’s header will include the size of the overall message. During the 1st fragment this size will be sanitized, and during the rest of the fragments this size should be checked to be consistent (== check). However, there are exceptions in which we know the overall size only when the last fragment tells us we finished, such is the case in the IPv4 and IPv6 protocols. Although this case might be simple if no reordering was supported, it becomes extremely more error-prone if reordering is supported.
When knowing the overall size we can preform the following actions:
- Sanitize the message size against some known limits (minimum and maximum)
- Allocate a large buffer that will contain the incoming fragments
- Each fragment will be placed in it’s location right after it is sanitized
- Some metadata data-structure will tell us when we have finished
When the size is not known in advance, there are several common practices:
- A buffer will be allocated according to the maximum message size
- A list will hold the ordered fragments until a buffer will be allocated
It can be easily shown that when no reordering is involved an implementation will be much simpler:
- Fragments arrive one-after-the-other: no overlapping is possible
- Each fragment should be size checked against the taken capacity and the overall capacity, and that’s it
However, when a network can reorder our fragments, and we are told to support that scenario, a lot of new questions arise:
- How do we know if an arrived fragment is in a legal place?
- It can be placed AFTER the message’s end
- It can be placed before/after it’s respective position
- It can be overlap an existing/future fragment
- How do know we finished receiving all of the fragments?
- Do we count each fragment?
- Do we wait for a last fragment?
- How do we handle “holes” between fragments?
- What should be done when overlapping is detected?
This brings us to the next implementation decision: overlapping policy.
When receiving a stream of fragments, an adversary can rearrange them in a way that a fragment might collide/override/overlap an existing fragment. Since this scenario can rarely be legitimate by any standard, the only problem is that protocols tend to over-complicate themselves when defining this case. In other cases, “generic” programmers might try to support such a case, thus introducing over-complicated implementations.
For example, IPv4 implementations might vary in their overlapping policy (a good way for OS fingerprinting) and the IPv6 standard strictly defined that overlapping should be ignored. Since most OS implementations simply modified the IPv4 implementation to support IPv6, most implementations can still be used for OS fingerprinting nevertheless.
Overlapping can make most implementations complicated: now keeping track on “holes” in the built message can be a tedious task.
Managing the Holes:
When fragments can arrive out-of-order there will be “holes” in the logical message. In case that each fragment is copied to the allocated buffer on it’s receive, these will be actual holes in the constructed message. When managed incorrectly, a message might be considered “finished” when it still contains holes of uninitialized data, opening a risk of leaking memory to the program/network.
A common hole management technique, used in the firewire kernel driver, is a list of ranges. Each fragment is checked against the always ordered list, and is accepted only if it is contained in an existing hole.
We have seen that there is a very long list of design questions that arise when a defragmentation feature should be supported. From the most simple implementation that only supports in-order fragments, each on a const size except for the last, to the most complicated IPv4 implementations that can even cause OS fingerprinting. Fragmentation is a complicated network feature, that is commonly implemented without the needed attention, thus making it a great source for security vulnerabilities.