FreeRTOS TCP/IP Stack Vulnerabilities – The Details

Researcher: Ori Karliner (@oriHCX)

Following our blog from last month, this blog will cover the technical details of our findings.

If you suspect that any of your devices are affected by these vulnerabilities and want our assessment, contact us at freertos@zimperium.com.

General information

Before we dive into the vulnerabilities, there are some important things to keep in mind.

The FreeRTOS+TCP TCP/IP stack is highly configurable. Some of the vulnerabilities only exist for specific configurations.

All packets are stored using the NetworkBufferDescriptor_t struct:

FreeRTOS+TCP offers two allocation schemes for storing the packets:

BufferAllocation_1.c – All packet buffers are statically allocated by the network driver in compile time, at the maximal size a packet can have (ipTOTAL_ETHERNET_FRAME_SIZE). BufferAllocation_2.c – Packet buffers are allocated dynamically from the heap according to the needed size. Packet buffers have a minimal size – baMINIMAL_BUFFER_SIZE. This size depends on the configuration of the TCP/IP stack, but it is either the size of a TCP Packet or an ARP Packet.

Also, in some implementations, the packet buffer might be allocated by the networking driver in a different way. In that case, the impact of the vulnerabilities will differ accordingly.

Depending on the configuration and allocation scheme that is used, some of the vulnerabilities might have different impact levels.

I recommend reading the source code together with this disclosure to fully understand the vulnerabilities, and whether or not they are present in your configuration of FreeRTOS+TCP.

CVE-2018-16528 – AWS secure connectivity modules – mbedTLS context corruption

The MQTT agent and GGD modules misuse the API of mbedTLS, creating a corrupt mbedTLS context object. The corrupt object may lead to remote code execution.

The MQTT and Greengrass discovery modules are vulnerable in different code flows.

For this disclosure, I will focus on the flow where an MQTT agent connects to a broker – prvSetupConnection.

The function sets up the TLS connection to a certified server.

It initializes the socket, configures the TLS parameters, and then connects to the server:

SOCKETS_Connect internally calls TLS_Connect, that wraps the mbedTLS library:

It loops until the TLS handshake is done, or until an error has occurred.

Back to prvSetupConnection, we can see that if SOCKETS_Connect fails, prvGracefulSocketClose is called.

This function internally cleans up the connection and closes the socket:

It first receives any data that’s left in the socket, and then it closes it.

SOCKETS_Recv once again wraps TLS_Recv, that wraps mbedTLS:

This flow is very problematic, as SOCKETS_Connect might have failed because the TLS handshake failed, and now we are using the same mbedTLS context (pCtx->mbedSslCtx) for reading data (mbedtls_ssl_read).

mbedtls_ssl_handshake might have left the TLS context in a corrupted state, and comments on the mbedTLS API explicitly warn users about this scenario:

We have found a code flow within mbedTLS that will cause an heap buffer to be free’d twice because of this API misuse, but we will not cover it in this disclosure.

Impact

An attacker can achieve remote code execution.

CVE-2018-16522 – AWS secure connectivity modules – uninitialized pointer free

The SOCKETS_SetSockOpt function of the AWS secure sockets module can cause an uninitialized pointer to be free’d.

The flaw only exists in cases where the user supplies a list of 2 or more ALPN protocols.

The user supplies a list of ALPN protocols, and SOCKETS_SetSockOpt saves a copy of them within the socket context.

At first, the Protocol count field is set according to the user supplied length:

An array of pointers is allocated at the pxContext->ppcAlpnProtocols field, each of these pointers will point to a single protocol name:

Then, the buffers for each protocol name are allocated and assigned to this pointer array:

The problem is that the ppcAlpnProtocols pointer array is not cleaned up (memset to 0) after allocation, which means it will contain data from previous allocations on the heap.

As we can see, if the allocation of a protocol name buffer fails, the code will exit, but only the pointer to that specific buffer will be NULL.

If we have 2 ALPN protocols, and the buffer allocation for the first fails, the pointer to the second remains uninitialized, while the ulAlpnProtocolsCount remains at 3.

Later on, when the Socket is closed, we free this array of pointers:

Which means an uninitialized pointer (ppcAlpnProtocols[1]) will be free’d.

Impact

Combined with a heap exhaustion primitive, an attacker can achieve remote code execution.

CVE-2018-16526 – usGenerateProtocolChecksum memory corruption

The problematic flow beings in prvProcessIPPacket, whilst parsing IP Options:

The code doesn’t handle IP Options at all and simply removes them from the packet by using memmove and truncating the xDataLength field of the network buffer.

The problem here, is that the pxIPHeader->ucVersionHeaderLength field is not updated accordingly, to represent the new IP header size.

This field is also used when sending packets, in usGenerateProtocolChecksum:

This function is used to generate or check the checksum of the upper layer protocol (TCP\UDP\ICMP\…).

The ucVersionHeaderLength field is used to determine where the next layer begins (pxProtPack), so that the calculated checksum can be placed at the right offset according to the used protocol.

For example, if the outgoing packet is a TCP packet:

First, the checksum field is zero’d:

And then the calculated checksum is assigned to this field:

In many cases a Tx packet is built over the Rx packet buffer, or at least based on it’s IP header.

If the Rx packet had IP options, we know that the code had already removed them, but that it didn’t adjust the xIPHeader->ucVersionHeaderLength field accordingly.

That means that when generating the protocol checksum for the transmitted packet, pxProtPack will point to the protocol header as if the IP options were still there.

We can cause the protocol checksum to be generated at an offset of up to 40 bytes from it’s real position in the packet, since 40 is the maximal size of IP options.

In cases where the packet buffer was shrinked, or in cases where the Tx packet is based on the IP header of a previous Rx packet, this 40 byte offset may point out of the buffers bounds, and lead to a memory corruption.

There are multiple code flows that can be used for exploitation, but we will not cover them in this disclosure.

Impact

An attacker can leak information.

(Only with BufferAllocation_2.c) An attacker can achieve remote code execution on the device.

CVE-2018-16525 – DNS\LLMNR memory corruption\information leak

Multiple bugs make it possible for an attacker to corrupt memory or leak information by sending DNS\LLMNR packets to the device.

The same function (ulDNSHandlePacket) is used to parse both DNS and LLMNR queries\replies.

The first bug is at the prvProcessIPPacket function, whilst dispatching a UDP packet:

Before the UDP packet is dispatched through xProcessReceivedUDPPacket, the total length of the packet (xDataLength) is recalculated to represent only the size of the UDP payload.

The bug here is that nothing is done to validate that the peer supplied a valid length in the UDP header (xUDPHeader.usLength), meaning that an attacker can cause xDataLength to be either smaller or larger than the real payload length.

The next bug is at prvParseDNSReply, whilst parsing a DNS\LLMNR request.

Generally, the code at prvParseDNSReply does nothing to validate that parsed data is within the received packet’s boundaries.

In case a DNS query of type HOST and class IN was received for the device’s hostname (xApplicationDNSQueryHook), the code responds to that request.

First of all, a larger buffer must be allocated for the response, since the response is appended to the original DNS packet:

The code calculates the new size by adding to xDataLength the size of all needed packet headers (UDP, IP, ETH), and the size of the LLMNR response (16), and allocates a new buffer using pxDuplicateNetworkBufferWithDescriptor.

As we saw, an attacker can cause xDataLength to be either smaller or larger than the real payload size. That means that when the response buffer is allocated, it might be too small to contain the original packet, or that it might be larger, causing pxDuplicateNetworkBufferWithDescriptor to copy out of bounds data from the original buffer into the new one (Data that can later be leaked back to the attacker).

To understand the implications of this bug, let’s observe the code that builds the response:

As we can see, xOffset1 is used to calculate the offset of the end of the DNS packet, where we want to insert our response. We later use it to build the LLMNR answer using pxAnswer.

For the dynamic buffer allocation scheme (BufferAllocation_2.c), we can cause pxDuplicateNetworkBufferWithDescriptor to allocate a buffer that’s too small to contain the response.

That means an attacker can overrun data at a chosen offset (xOffset1) from the allocated network buffer, with the LLMNRAnswer_t fields.

For the static buffer allocation scheme (BufferAllocation_1.c), we can cause pxDuplicateNetworkBufferWithDescriptor to corrupt memory outside the new buffer:

We have full control over the xDataLength of the original packet buffer (because of the UDP code bug).

Since buffer have a constant size in this allocation scheme, by supplying an xDataLength that’s larger than the static buffer size, we can overflow it.

This vulnerability can also be used to leak information, if an attacker adds another DNS query with truncated length to the packet:

The only problem an attacker will face is that usType and usClass are both not in his control (Because they are read out of bounds), and they must both be 1 for an LLMNR response to be sent back.

This can be achieved by using some sort of heap shaping, and will yield in all data between the network buffer and the shaped data offset being sent back to the attacker.

Impact

An attacker can achieve remote code execution.

An attacker can leak information.

CVE-2018-16599 – NBNS memory corruption\information leak

prvTreatNBNS, just like prvParseDNSReply, doesn’t check if xDataLength is large enough to contain the parsed NBNS packet, Thus permitting an attacker to read data out of bounds.

Also, just like prvParseDNSReply, pxDuplicateNetworkBufferWithDescriptor is used to allocate the Tx packet, and xDataLength is controllable by the attacker (via the UDP length field bug):

That means that on the static buffer allocation scheme (BufferAllocation_1.c), an attacker can corrupt memory by supplying an xDataLength that’s larger than the static buffer size.

Impact

An attacker can leak information.

(Only with BufferAllocation_1.c) An attacker can achieve remote code execution.

CVE-2018-16601 – IP DoS\Memory corruption

The function prvProcessIPPacket removes the IP options field of an Rx packet if it is present.

The size of the IP header is not validated at any point:

If an attacker supplies an IP header length field (uxHeaderLength) that’s larger than the whole packet (pxNetworkBuffer->xDataLength) minus ipSIZE_OF_IPv4_HEADER (20), the computation of xMoveLen will cause an integer underflow.

This will cause memmove to move a huge chunk of data, covering the entire 32 bit memory area, with a very little range to play with.

Impact

An attacker can cause a DoS.

In some cases, an attacker can achieve remote code execution.

CVE-2018-16523, CVE-2018-16524 -TCP Options information leak\DoS

The function prvCheckOptions checks the TCP options supplied within an Rx TCP packet, if there are any:

The code doesn’t validate that the packet buffer is large enough to contain the TCP options (pxNetworkBuffer->xDataLength):

pucLast is not checked at all, to see if it fits within the received packet, and the while loop that iterates TCP options only checks whether the current option begins within valid range (pucPtr < pucLast), but nothing is done to ensure that it ends within the valid range.

Each one of the TCP option handlers can access data out of bounds, and in some cases an attacker can get this data back (thus leaking information).

The TCP_OPT_MSS handler also contains a DoS vulnerability:

When recalculating the RxWindowLength, if an attacker supplies an MSS of 0, this line will divide by 0 thus raising an exception and causing a DoS.

Impact

An attacker can cause a DoS.

An attacker can leak information.

CVE-2018-16603 -TCP information leak

xProcessReceivedTCPPacket doesn’t validate that the received frame is large enough to contain a TCP header. Fields of the TCP header can be accessed out of bounds:

If an attacker sends an IP packet with TCP type, but doesn’t include the TCP header, xLocalPort and xRemotePort will be read out of bounds, and an RST packet using these ports will be sent back with prvTCPSendReset, thus leaking data back to the attacker.

Impact

An attacker can leak information.

CVE-2018-16602 – DHCP information leak

The prvProcessDHCPReplies function doesn’t validate that a packet is large enough to be a valid DHCP packet.

Also, whilst walking the DHCP option fields, length checks aren’t done correctly for the DHCP options:

As no check is done to see if pucByte[1] can even be accessed, and pucByte + ucLength is not checked against pucLastByte.

By making the code read some DHCP parameters out of bounds (For example the offered IP address) an attacker can later observe these values, thus leaking information.

Impact

An attacker can leak information.

CVE-2018-16600 – ARP information leak

eARPProcessPacket doesn’t validate that the received frame is large enough to be an ARP packet:

Fields of the ARP packet are accessed without doing proper boundary checking, which means out of bounds data can be accessed.

An attacker can leak information by observing the responses for truncated ARP packets.

Impact

An attacker can leak information.

CVE-2018-16527 – ICMP information leak

prvProcessICMPPacket doesn’t validate that the received frame is large enough to be an ICMP packet, and can access fields from this packet out of bounds:

The attacker can leak information by observing the response to a truncated ICMP echo request.

Impact

An attacker can leak information.

CVE-2018-16598 – DNS Poisoning

The code takes no steps to prevent DNS Poisoning, As any DNS answer the device receives will be parsed fully, without checking if it matches an outgoing DNS query.

We can see that at the function xProcessReceivedUDPPacket:

ulDNSHandlePacket simply wraps prvParseDNSReply.

prvParseDNSReply will parse the DNS response, and update the DNS cache accordingly:

Impact

An attacker can poison the DNS cache of the device.