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 |
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.
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.
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 |
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
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 UnderflowThe 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
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.
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. |
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. |
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. |
<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. |
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.
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. |
grub rescue> write_word 0x7eb514e 0x90909090 grub rescue> normal |
We've got a fully operative GRUB2. |
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. |
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.
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. |
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 ]
$ 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
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.