CVE-2023-4692, CVE-2023-4693: vulnerabilities in the GRUB boot manager

The GRUB boot manager is more an operating system than a boot loader. For example, it has more than 20 file system types supported!

This is a really wide attack surface… And, currently, GRUB is the default choice in the Secure Boot implementation using Microsoft-signed shims (but things are moving forward).

Some time ago, I discovered two vulnerabilities (or three vulnerabilities, if we count a security issue which is almost unexploitable) in the NTFS driver of the GRUB boot manager. And here are some technical details…

A heap overflow (CVE-2023-4692)

The GRUB boot manager has some legacy code with weird programming decisions!

In the NTFS file system, any file occupying more than one cluster can be fragmented. And the master file table (the $MFT file) can be fragmented too.

Reading an extremely fragmented $MFT file is a bit tricky, because this file must be read in multiple iterations, with more mapping pairs (it’s an array of “offset in clusters & length in clusters” tuples) appearing each time. Some third-party NTFS implementations fail when dealing with these cases (one example is here).

To make things easy, GRUB developers decided to reuse a buffer containing data read from the underlying drive to store sector numbers, which are later consumed by different code.

The code in question is (the comments are mine, and only the part that writes to the buffer is quoted):

      // This piece of code is reached when the $ATTRIBUTE_LIST attribute has been observed for a given file.
      if ((at->flags & GRUB_NTFS_AF_MMFT) && (attr == GRUB_NTFS_AT_DATA)) // True when reading the $DATA attribute of the $MFT file.
	{
	  at->flags |= GRUB_NTFS_AF_GPOS; // This means that the buffer used to read from a drive is reused to store sector numbers.
	  at->attr_cur = at->attr_nxt;
	  pa = at->attr_cur; // "pa" points somewhere inside the $ATTRIBUTE_LIST attribute.
	  grub_set_unaligned32 ((char *) pa + 0x10, // Write the "mft_start" value to "pa + 0x10".
				grub_cpu_to_le32 (at->mft->data->mft_start));
	  grub_set_unaligned32 ((char *) pa + 0x14, // A similar write to "pa + 0x14".
				grub_cpu_to_le32 (at->mft->data->mft_start
						  + 1));
	  pa = at->attr_nxt + u16at (pa, 4);
	  while (pa < at->attr_end)
	    {
	      if (*pa != attr)
		break;
	      if (read_attr
		  (at, pa + 0x10,
		   u32at (pa, 0x10) * (at->mft->data->mft_size << GRUB_NTFS_BLK_SHR),
		   at->mft->data->mft_size << GRUB_NTFS_BLK_SHR, 0, 0, 0)) // This is dangerous too! Something can be written to "pa + 0x10" and "pa + 0x14" here...
		return NULL;
	      pa += u16at (pa, 4);
	    }
	  at->attr_nxt = at->attr_cur;
	  at->flags &= ~GRUB_NTFS_AF_GPOS;
	}
      goto retry;

So, the concern is: are the “pa + 0x10” and “pa + 0x14” pointers always within the allocated buffer?

As it turns out, no!

When dealing with well-formed file systems, “pa + 0x14“, and even “pa + 0x18“, are always within the allocated buffer.

But an attacker can produce a malformed NTFS file system, so “pa + 0x10” is beyond the allocated buffer boundary. So, the NTFS driver will write beyond the allocated buffer.

What bytes are written to the “pa + 0x10” and “pa + 0x14” offsets? These are 32-bit sector numbers – non-zero values controlled by on-disk structures (and, thus, by an attacker).

In my tests, this issue affects the internal heap metadata of the GRUB boot manager. Technically, it’s possible to modify bytes beyond the internal heap of GRUB (e.g., when the buffer, “pa“, is the last memory chunk in the memory area currently allocated for the GRUB’s heap).

However, even touching the internal heap metadata has security implications, as shown below.

Errors produced by QEMU, in the UEFI mode: the grub_zalloc() function tries to write null bytes into an invalid memory region (which isn’t allocated to the GRUB boot manager)

Additional information:

  • Vector: AV:L/AC:H/PR:H/UI:N/S:C/C:N/I:H/A:N
  • Score: 5.3
  • Weakness: CWE-122 (Heap-based Buffer Overflow)
  • Date discovered: 2023-05-04

Advisory:

There is an out-of-bounds write in grub-core/fs/ntfs.c. An attacker may leverage this vulnerability by presenting a specially crafted NTFS filesystem image leading to GRUB’s heap metadata corruption. Additionally, in some circumstances, the attack may also corrupt the UEFI firmware heap metadata. As a result arbitrary code execution and secure boot protection bypass may be achieved.

An out-of-bounds read (CVE-2023-4693)

In the NTFS file system, file data is stored either in separate clusters or within a file record. (Such file data is called nonresident or resident, respectively.)

Typically, when a file is small enough, no clusters are allocated to store its data, so the data is stored within the file record describing that file (usually, this happens when the file is smaller than 700 bytes, but the actual limit depends on the file name length and some other conditions). This allows the NTFS file system to save space when storing small files.

In the resident data case, the file data is described using the “offset & length” tuple, the “offset” value points to the first byte of the file data within the corresponding attribute (i.e., the unnamed $DATA attribute) in the file record, and the “length” value describes the data size (or, in other words, the file size).

Since the file record is loaded into the memory for processing (it’s either 1024 or 4096 bytes in length), extracting the resident file data is easy, and here is the corresponding GRUB code (the comments are mine):

  if (pa[8] == 0) // This is true when the $DATA attribute is marked as resident.
    {
      if (ofs + len > u32at (pa, 0x10)) // Check if the read request tells us to read beyond the attribute, the length of the attribute is taken from the on-disk field.
	return grub_error (GRUB_ERR_BAD_FS, "read out of range");
      grub_memcpy (dest, pa + u32at (pa, 0x14) + ofs, len); // Copy the bytes into the destination buffer used to read the file data.
      return 0;
    }

In the code quoted above, all values taken from the “pa” buffer, including those retrieved using the “u32at” accessor, come from the on-disk NTFS structure:

  • pa[8]” is the nonresident flag (when zero, the data is resident);
  • u32at (pa, 0x10)” is the resident data length;
  • u32at (pa, 0x14)” is the resident data offset (relative to the start of the attribute in the file record, “pa“).

(See this document for details, but note that the offset field size is different in this piece of the GRUB code: it’s 32 bits, not 16 bits as actually defined by the file system format.)

So, these on-disk values are attacker-controlled, not properly validated, and it’s possible for an attacker to trick the “grub_memcpy” call into reading arbitrary, attacker-chosen (within the 32-bit space) memory locations, then returning them as file data (in the “dest” buffer).

To exploit this, an attacker must first craft a file system image, by creating a small file, then changing the on-disk fields describing its resident data offset and size to arbitrary values, so they point outside of the file record (in most cases, it’s enough to make the “u32at (pa, 0x14)” value close to 1000, but any larger value would be okay).

The only boundary check performed in the code is bypassed by modifying the on-disk field, “u32at (pa, 0x10)” (actually, this check validates that a reader doesn’t request data beyond the EOF position, it does nothing to validate the actual “offset & length” tuple mentioned before). There are no real checks that the resident data actually fits the file record!

Then, by attaching that file system image (as a loopback device or as a physical drive) and issuing the “cat” command to display a crafted file, an attacker is capable of reading arbitrary memory locations. Unfortunately, there is no way to save the bytes read into a file and there is no way to pass these bytes to the Linux kernel, so the attack vector is physical: the attacker needs to enable the GRUB pager and then simply view the bytes displayed when reading a crafted file.

This allows a physically-present attacker to obtain sensitive information stored in the memory (for example, data from EFI BS variables containing plain-text/obfuscated passwords or password hashes – if these EFI variables are cached/stored in the memory).

And, yes, the attacker has to skip over thousands of pages to find those containing something sensitive… And this is how I made this screenshot:

The “MokPWStore” EFI BS variable, which stores the password hash used to “unlock” the MokManager interface

A video demonstrating the exploitation process is here.

This vulnerability can be mitigated by setting the proper boot order (to block attackers from booting using external media), protecting it with a firmware (UEFI) password (so attackers can’t change the boot order or pick a temporary boot device), setting a password to access the MokManager interface (so, in case of a failure, attackers can’t enroll their EFI applications into the trusted list), and locking an existing GRUB installation with a password.

Additional information:

  • Vector: AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
  • Score: 5.3
  • Weakness: CWE-125 (Out-of-bounds Read)
  • Date discovered: 2023-04-24

Advisory:

There is an out-of-bounds read at grub-core/fs/ntfs.c. A physically present attacker may leverage that by presenting a specially crafted NTFS file system image to read arbitrary memory locations. A successful attack may allow sensitive data cached in memory or EFI variables values to be leaked presenting a high confidentiality risk.

Another out-of-bounds read

This issue is similar to CVE-2023-4693, but the out-of-bounds read occurs when querying the NTFS volume label.

The GRUB code used to read the volume label is (the comments are mine):

  pa = find_attr (&mft->attr, GRUB_NTFS_AT_VOLUME_NAME);
  if ((pa) && (pa[8] == 0) && (u32at (pa, 0x10))) // True when the volume label attribute is found, and its data is resident, and the data length is positive.
    {
      int len;

      len = u32at (pa, 0x10) / 2; // The "len" variable counts 16-bit characters, not bytes.
      pa += u16at (pa, 0x14);
      *label = get_utf8 (pa, len);
    }

Again, there are no checks that “u16at (pa, 0x14)” and “u32at (pa, 0x10)” are within the corresponding file record (which is loaded into the memory for processing).

So, it’s possible to read a volume label from an arbitrary, attacker-chosen (within the 16-bit space) memory location. However, the resulting output is mostly unreadable, because the bytes read are treated as an UTF-16LE string, which is then converted to UTF-8…

There is no easy way to convert that mess back to the raw bytes. So, the issue is minor and there is no CVE number.

One thought on “CVE-2023-4692, CVE-2023-4693: vulnerabilities in the GRUB boot manager

Leave a comment