U-Boot NFS RCE vulnerability (CVE-2019-14192)

U-Boot NFS RCE vulnerability (CVE-2019-14192)

Original text: https://blog.semmle.com/uboot-rce-nfs-vulnerability/

Translation: Kanxue Translation Team – lipss

Proofreading: Kanxue Translation Team – Nxe

This article is about 13 remote code execution vulnerabilities in the U-Boot bootloader that my colleagues Pavel Avgustinov and Kevin Backhouse and I discovered. These vulnerabilities can be triggered when U-Boot is configured to use the network to obtain boot resources for the next stage.

Please note that this vulnerability has not been patched via https://gitlab.denx.de/u-boot/u-boot and I am making these public at the request of Tom Rini, the main custodian of U-Boot. For more information, please view the schedule below.

MITER has released the following CVEs for these 13 vulnerabilities: CVE-2019-14192, CVE-2019-14193, CVE-2019-14194, CVE-2019-14195, CVE-2019-14196, CVE-2019-14197, CVE- 2019-14198, CVE-2019-14199, CVE-2019-14200, CVE-2019-14201, CVE-2019-14202, CVE-2019-14203 and CVE-2019-14204

What is U-Boot?

Das U-Boot (often called “Universal Bootloader”) is a popular master bootloader that is widely used in embedded devices to get data from different sources and run the next stage of code, usually (but Not limited to) Linux kernel. It is commonly used by IoT, Kindle and ARM ChromeOS devices.

U-Boot supports retrieving next-stage code from different file partition formats (such as ext4), as well as networks (TFTP and NFS). Note that U-boot supports verified boot, where the acquired image is checked whether it has been tampered with. This mitigates the risk of using insecure plaintext protocols such as TFTP and NFS. So any vulnerability before signature checking could mean the device is jailbroken.

I am using U-boot, will it be affected?

These vulnerabilities affect very specific U-Boot configurations, where U-Boot is instructed to use the network. Some of these vulnerabilities exist in the NFS parsing code, while others exist in the generic TCP/IP stack.

This configuration is typically used during diskless IoT deployments and rapid development processes.

What is the impact?

These vulnerabilities allow an attacker within the same network (or controlling a malicious NFS server) to execute code on a U-Boot powered device. Due to the nature of this vulnerability, exploitation does not appear to be very complex, although it can be made more challenging through the use of stack cookies, ASLR, or other runtime and compile-time memory protection mechanisms.

I understand, what are these vulnerabilities?

The first vulnerability was discovered through source code review in 2 very similar incidents, and we used Semmle’s LGTM.com and QL to find additional vulnerabilities. It’s a common memcpy overflow, with attacker-controlled sizes coming from network packets without any validation.

The problem exists in the nfs_readlink_reply function that parses nfs replies from the network. It parses 4 bytes and uses them as the length of memcpy in two different places without further validation.

static int nfs_readlink_reply(uchar *pkt, unsigned len)
{
[...]

/* new path length */
rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]);

if (*((char *) & amp;(rpc_pkt.u.reply.data[2 + nfsv3_data_offset])) != '/') {
int pathlen;

strcat(nfs_path, "/");
pathlen = strlen(nfs_path);
memcpy(nfs_path + pathlen,
(uchar *) & amp;(rpc_pkt.u.reply.data[2 + nfsv3_data_offset]),
rlen);
nfs_path[pathlen + rlen] = 0;
} else {
memcpy(nfs_path,
(uchar *) & amp;(rpc_pkt.u.reply.data[2 + nfsv3_data_offset]),
rlen);
nfs_path[rlen] = 0;
}
return 0;
}

The target buffer nfs_path is a global buffer that can hold up to 2048 bytes.

Variation analysis using QL

The following query gives us 9 lists to track manually. The idea behind the query is to execute it from any helper function (such as ntohl()/ ntohs()…) to the size parameter of memcpy Data flow analysis.

import cpp

import semmle.code.cpp.dataflow.TaintTracking
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis

class NetworkByteOrderTranslation extends Expr {
  NetworkByteOrderTranslation() {
    // On Windows, there are ntoh* functions.
    this.(Call).getTarget().getName().regexpMatch("ntoh(l|ll|s)")
    or
    // On Linux, and in some code bases, these are defined as macros.
    this = any(MacroInvocation mi |
        mi.getOutermostMacroAccess().getMacroName().regexpMatch("(?i)(^|.*_)ntoh(l|ll|s)")
      ).getExpr()
  }
}

class NetworkToMemFuncLength extends TaintTracking::Configuration {
  NetworkToMemFuncLength() { this = "NetworkToMemFuncLength" }

  override predicate isSource(DataFlow::Node source) {
       source.asExpr() instanceof NetworkByteOrderTranslation
  }
  
  override predicate isSink(DataFlow::Node sink) {
    exists (FunctionCall fc |
        fc.getTarget().getName().regexpMatch("memcpy|memmove") and
        fc.getArgument(2) = sink.asExpr() )
  }
 
}

from Expr ntoh, Expr sizeArg, NetworkToMemFuncLength config
where config.hasFlow(DataFlow::exprNode(ntoh), DataFlow::exprNode(sizeArg))
select ntoh.getLocation(), sizeArg

Can we find vulnerabilities caused by similar functions?

Although some data was size-checked between the data flow from source to sink, some data was found to be exploitable. In addition, I also discovered other similar functions through source code auditing.

Failed length check in nfs_lookup_reply results in unbounded memcpy

There is a problem with the nfs_lookup_reply function parsing nfs replies from the network again. 4 bytes are parsed and used as the length of memcpy in two different locations.

Specify the length of a section of code so that the copied section will not be larger than the allocated buffer. However, you can still use negative values to bypass this judgment statement, and buffer overflow can still occur later.

filefh3_length = ntohl(rpc_pkt.u.reply.data\[1]);
if (filefh3_length > NFS3_FHSIZE)
    filefh3_length = NFS3_FHSIZE;

memcpy(filefh, rpc_pkt.u.reply.data + 2, filefh3_length);

The target buffer filefh is a global buffer that can hold up to 64 bytes.

Failed length check in nfs_read_reply/store_block results in unbounded memcpy

This issue exists in the nfs_read_reply function when reading a file and storing it into another medium (flash or physical memory) for later processing. Again, the data and length are fully controlled by the attacker and never verified.

static int nfs_read_reply(uchar *pkt, unsigned len)
{ [...]

if (supported_nfs_versions & amp; NFSV2_FLAG) {
rlen = ntohl(rpc_pkt.u.reply.data[18]); // <-- rlen is attacker-controlled could be 0xFFFFFFFF
data_ptr = (uchar *) & amp;(rpc_pkt.u.reply.data[19]);
} else { /* NFSV3_FLAG */
int nfsv3_data_offset =
nfs3_get_attributes_offset(rpc_pkt.u.reply.data);

/* count value */
rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]); // <-- rlen is attacker-controlled
/* Skip unused values:
EOF: 32 bits value,
data_size: 32 bits value,
*/
data_ptr = (uchar *)
& amp;(rpc_pkt.u.reply.data[4 + nfsv3_data_offset]);
}

if (store_block(data_ptr, nfs_offset, rlen)) // <-- We pass to store_block source and length controlled by the attacker
return -9999;

[...]
}

Focus on the physical memory part of the store_block function, try to reserve some memory using the arch-specific function map_physmem, and eventually call phys_to_virt. As seen in the x86 implementation, the length is obviously ignored when reserving physical memory. The function return value provides the original pointer without adding any additional data to determine whether there is other data retained in the memory area.

static inline void *phys_to_virt(phys_addr_t paddr)
{
        return (void *)(unsigned long)paddr;
}

This was followed by a memcpy buffer overflow of attacker-controlled source and length in store_block.

static inline int store_block(uchar *src, unsigned offset, unsigned len)
{

[...]

 void *ptr = map_sysmem(load_addr + offset, len); // <-- essentially this is ptr = load_addr + offset
 memcpy(ptr, src, len); // <-- unrestricted overflow happens here
 unmap_sysmem(ptr);

[...]

}

A similar problem may exist in the flash_write code path.

Unbounded memcpy occurred while parsing UDP packet due to integer underflow

Function net_process_received_packet using ip->udp_len without verification will cause integer underflow. Thereafter, this field will be used for memcpyat nc_input_packet and all udp packets set via net_set_udp_handler (DNS, dhcp, …) in the processing function.

#if defined(CONFIG_NETCONSOLE) & amp; & amp; !defined(CONFIG_SPL_BUILD)
                nc_input_packet((uchar *)ip + IP_UDP_HDR_SIZE,
                                src_ip,
                                ntohs(ip->udp_dst),
                                ntohs(ip->udp_src),
                                ntohs(ip->udp_len) - UDP_HDR_SIZE); // <- integer underflow
#endif
                /*
                 * IP header OK. Pass the packet to the current handler.
                 */
                (*udp_packet_handler)((uchar *)ip + IP_UDP_HDR_SIZE,
                                      ntohs(ip->udp_dst),
                                      src_ip,
                                      ntohs(ip->udp_src),
                                      ntohs(ip->udp_len) - UDP_HDR_SIZE); // <- integer underflow

Please note that we did not audit all potential UDP handlers set up for different purposes (DNS, DHCP, etc.). However, we do audit nfs_handler as described below.

Multiple stack-based buffer overflows in response helper function nfs_handler

This is a code review variant of the above vulnerability. Here, an integer underflow occurs when parsing a udp packet with a large ip->udp_len parameter, followed by a call to nfs_handler. In this function, there is also no verification of the length, we will call a helper function, such as nfs_readlink_reply. The function blindly uses the length without validating it, causing a stack-based buffer overflow.

static int nfs_readlink_reply(uchar *pkt, unsigned len)
{
    struct rpc_t rpc_pkt;

    [...]

        memcpy((unsigned char *) & amp;rpc_pkt, pkt, len);

We identified 5 different vulnerable functions that follow the same code pattern, leading to a stack-based buffer overflow. In addition to nfs_readlink_reply, there are:

  • rpc_lookup_reply
  • nfs_mount_reply
  • nfs_umountall_reply
  • nfs_lookup_reply

Read out-of-bounds data in nfs_read_reply

This is very similar to previous exploits. Developers try to be cautious by performing size checks when copying data from the socket. When they checked to prevent buffer overflow, they did not check whether there was enough data in the source buffer, resulting in a potential read out-of-bounds access violation.

static int nfs_read_reply(uchar *pkt, unsigned len)
{
    struct rpc_t rpc_pkt;

    [...]

        memcpy( & amp;rpc_pkt.u.data[0], pkt, sizeof(rpc_pkt.u.reply));

An attacker can serve NFS packets with read requests and smaller packet requests sent to the socket.

Any suggestions?

To mitigate these vulnerabilities, there are only two options:

  • Apply the patch as soon as it is released, or
  • Do not mount the file system via NFS or any U-Boot network function while it is vulnerable

Disclosure Schedule

This vulnerability report is subject to the disclosure policy, available here https://lgtm.com/security/#disclosure_policy.

  • May 15, 2019 – Fermín Serna initially discovered two vulnerabilities and wrote a QL query to discover three additional problematic calls.
  • May 16, 2019 – Pavel Avgustinov brought in some QL magic, outlining queries and finding more functions for parsing ip and udp headers.
  • May 23, 2019 – Kevin Backhouse alerts Pavel and Fermín to an oversight regarding a stack-based buffer overflow via nfs_handler.
  • May 23, 2019 – The Semmle security team closed the investigation and contacted the maintainers via email.
  • May 24, 2019 – Tom Rini (U-Boot’s primary custodian) confirms receipt of the security report.
  • July 19, 2019 – Tom Rini requested that this report be made public on his public mailing list [email protected].
  • July 22, 2019 – To avoid disclosure over the weekend, Fermin made the report public at [email protected].