Semmle security advisory: U-Boot, 13 vulnerabilities leading to potential remote code execution Date: 5/23/2019 Status: Developers contacted, patch proposed and waiting for CVEs Author(s): Fermin J. Serna, Pavel Avgustinov and Kevin Backhouse CVE(s) assigned: 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 1.- Overview: Das U-Boot [1] (commonly known as “universal boot loader”) is a popular primary bootloader widely used in embedded devices to fetch from different sources and run the next stage code, commonly but not limited to a Linux Kernel. It is commonly used by IoT devices but also Kindle and ARM ChromeOS devices. U-Boot supports fetching the next stage code from different file partition formats (ext4 as an example) but also from the network (TFTP and NFS). Please note, U-boot supports verified boot [2] where the image fetched is checked for tampering, mitigating the risks of using insecure cleartext protocols such as TFTP and NFS, so any vulnerability before the signature check could mean a device jailbreak. The first vulnerability was found via source code review and we used Semmle’s LGTM.com and QL to find the others. We used the following query [3] that gave us a very manageable list of 9 results to follow up manually. We ended up identifying 7 different memcpy-ish call sites with attacked controlled source buffer and unchecked lengths. Additionally, after some code review we found 5 new additional stack based buffer overflows and an extra read out of bounds vulnerability. 2. - Technical description: 2.1. - Unbound memcpy with unvalidated length at nfs_readlink_reply (x2) The problem exists at the nfs_readlink_reply [4] function that parses an nfs reply coming from the network. It parses 4 bytes and without any further validations it uses them as length for a memcpy in two different locations. 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 *)&(rpc_pkt.u.reply.data[2 + nfsv3_data_offset])) != '/') { int pathlen; strcat(nfs_path, "/"); pathlen = strlen(nfs_path); memcpy(nfs_path + pathlen, (uchar *)&(rpc_pkt.u.reply.data[2 + nfsv3_data_offset]), rlen); nfs_path[pathlen + rlen] = 0; } else { memcpy(nfs_path, (uchar *)&(rpc_pkt.u.reply.data[2 + nfsv3_data_offset]), rlen); nfs_path[rlen] = 0; } return 0; } The destination buffer nfs_path [5] is a global one that can hold up to 2048 bytes. 2.2. - Unbound memcpy with a failed length check at nfs_lookup_reply The problem exists at the nfs_lookup_reply [6] function that again parses an nfs reply coming from the network. It parses 4 bytes and uses them as length for a memcpy in two different locations. A length check happens to make sure it is not bigger than the allocated buffer. Unfortunately this check can be bypassed with a negative value that would lead later to a large buffer overflow. 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 destination buffer nfs_path [7] is a global one that can hold up to 64 bytes. 2.3.- Unbound memcpy with a failed length check at nfs_read_reply/store_block (x2) The problem exists at the nfs_read_reply [8] function when reading a file and storing it into other medium (flash or physical memory) for later processing. Again, the data and length is fully controlled by the attacker and never validated. static int nfs_read_reply(uchar *pkt, unsigned len) { [...] if (supported_nfs_versions & NFSV2_FLAG) { rlen = ntohl(rpc_pkt.u.reply.data[18]); <- rlen is attacker controlled could be 0xFFFFFFFF data_ptr = (uchar *)&(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 *) &(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; [...] } Focusing on physical memory part of the store_block function [9], it attempts to reserve some memory using the arch specific function map_physmem, ending up calling phys_to_virt. As you can see below at the x86 [10] implementation, when reserving physical memory it clearly ignores length and gives you a raw pointer without checking if surrounding areas are reserved (or not) for other purposes. static inline void *phys_to_virt(phys_addr_t paddr) { return (void *)(unsigned long)paddr; } Later at store_block there is a memcpy buffer overrun with attacker controlled source and length. 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); [...] } Similar problem potentially exists with the flash_write code path. 2.4.- Unbound memcpy when parsing a UDP packet due to integer underflow (x2) The function net_process_received_packet is subject to an integer underflow when using ip->udp_len without validation [11]. Later this field is used for a memcpy at nc_input_packet [12] and any udp packet handler set via net_set_udp_handler (DNS, dhcp, ...). #if defined(CONFIG_NETCONSOLE) && !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, we did not audit all potential udp handlers that are set for different purposes (DNS, Dhcp, …). But we did on the nfs_handler and you can read about it next. 2.5 - Multiple (5) stack based buffer overflow in nfs_handler reply helper functions A code review variant of the previous one, where the integer underflows but also with a large enough udp packet and ip->udp_len we end up calling nfs_handler. In this function, again there is no validation of the length and we end up calling helper functions such as nfs_readlink_reply where it blindly uses the length without validation causing a stack based buffer overflow. static int nfs_readlink_reply(uchar *pkt, unsigned len) { struct rpc_t rpc_pkt; [...] memcpy((unsigned char *)&rpc_pkt, pkt, len); We identified 5 different vulnerable functions subject to the same code pattern leading to a stack based buffer overflow: rpc_lookup_reply nfs_mount_reply nfs_umountall_reply nfs_lookup_reply nfs_readlink_reply 2.6 - Read out-of-bound data at nfs_read_reply Very similar to the previous vulnerabilities discussed at 2.5 where the developers tried to be careful and performing size checks while copy the data that come from the socket. While they checked to prevent the buffer overflow they did not check if there was enough data at the source buffer leading to a potential read out of bounds. static int nfs_read_reply(uchar *pkt, unsigned len) { struct rpc_t rpc_pkt; [...] memcpy(&rpc_pkt.u.data[0], pkt, sizeof(rpc_pkt.u.reply)); An attacker could supply an NFS packet with a read request but with a small packet Request sent to the socket. 3.- Semmle’s QL query used for variant 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 4. - Recommendations: In order to mitigate this vulnerabilities there are only two options: 1) Apply patches as soon as they are released, or 2) While vulnerable, do not use mounting filesystems via NFS. 5.- Timeline: This vulnerability report is subject to our disclosure policy available at https://lgtm.com/security/#disclosure_policy 5/15/2019 - Fermin finds initial two vulnerability and writes a dirty QL query. Uncovers 3 more problematic call sites 5/16/2019 - Pavel brings some QL magic and generalizes the query. Validates (and corrects) Fermin’s finding. Finds some more parsing ip and udp headers. 5/23/2019 - Kevin alerts us about our oversight around a stack based buffer overflow via nfs_handler. 5/23/2019 - After some other priorities, more QL magic and code review we decided to conclude the investigation and contact maintainers via email. 5/24/2019 - Tom Rini acknowledges receiving the security report. 7/19/2019 - Tom Rini requests to make this report public at their public mailing list u-boot@lists.denx.de. 7/22/2019 - In order to avoid a weekend disclosure, Fermin makes the report public at u-boot@lists.denx.de. 5.- References: [1] https://en.wikipedia.org/wiki/Das_U-Boot [2] https://github.com/u-boot/u-boot/blob/master/doc/uImage.FIT/verified-boot.txt [3] https://lgtm.com/query/7709901104691588335/ [4] https://github.com/u-boot/u-boot/blob/11e409284ee593c980e16d93d12c5e19c99b62c0/net/nfs.c#L608 [5] https://github.com/u-boot/u-boot/blob/11e409284ee593c980e16d93d12c5e19c99b62c0/net/nfs.c#L75 [6] https://github.com/u-boot/u-boot/blob/5b4b680cfe29a67171ccbe439c66374cb31faca3/net/nfs.c#L571-L574 [7] https://github.com/u-boot/u-boot/blob/5b4b680cfe29a67171ccbe439c66374cb31faca3/net/nfs.c#L55 [8] https://github.com/u-boot/u-boot/blob/5b4b680cfe29a67171ccbe439c66374cb31faca3/net/nfs.c#L655 [9] https://github.com/u-boot/u-boot/blob/5b4b680cfe29a67171ccbe439c66374cb31faca3/net/nfs.c#L81 [10] https://github.com/u-boot/u-boot/blob/94905e1db8d8d42c4f39f14dbee2f9788390db5e/include/asm-generic/io.h#L30 [11] https://github.com/u-boot/u-boot/blob/5b4b680cfe29a67171ccbe439c66374cb31faca3/net/net.c#L1302-L1314 [12] https://github.com/u-boot/u-boot/blob/94905e1db8d8d42c4f39f14dbee2f9788390db5e/drivers/net/netconsole.c#L134 [13] https://github.com/u-boot/u-boot/blob/5b4b680cfe29a67171ccbe439c66374cb31faca3/net/net.c#L913