Summary
Earlier this week, our friends at ESET Martin Smolar and Peter Strýček published technical documentation of a new UEFI bootkit called “Bootkitty” targeting certain Linux Ubuntu configurations. While this appears to be a proof-of-concept rather than an active threat, Bootkitty signals a major shift as attackers expand bootkit attacks beyond the Windows ecosystem – (BlackLotus and MoonBounce). The operating system bootloaders present a vast attack surface that is often overlooked by defenders, and the constant growth in complexity only makes it worse. The vulnerabilities discovered by Binarly REsearch, such as LogoFAIL and PKfail, only serve to demonstrate that.
Bootkitty, which was uploaded to VirusTotal earlier this month, is the first example of a bootkit capable of infecting the Linux kernel and suggests that threat actors may be actively developing Linux versions of the kinds of bootkits seen on Windows machines.
Separate from the ESET publication, a second article on Bootkitty was publicly shared by @MatheuzSecurity, triggering a full Binarly research investigation into this new threat.
This article complements the analysis released by ESET, focusing on the rootkit capabilities of the malware and mentions the discovery of an open directory with malware samples.
The author added a screenshot of the open directory:
One detail immediately raised eyebrows: The two image files – logofail.bmp and logofail_fake.bmp – are clearly named after our LogoFAIL research which was presented last year at BlackHat EU 23. The fact that the “fake” version was only 7.7KB, and the logofail.bmp file was 16MB was a lightbulb moment that this was something serious.
We quickly located this server and to our surprise… it was still up!
We downloaded all the files and set off to answer the following questions:
Given that the articles from ESET and from humzak711 already covered the bootkit (.efi) and rootkit files (.ko and .so), we decided to focus on the BMP files. We quickly discarded the logofail_fake.bmp
file, after a cursory analysis showed nothing unusual in its header and content, plus all the image viewers we tested were able to open and display this file without any problems.
Our attention instead focused on the logofail.bmp
file, a 16MB file that revealed some unusual patterns just by looking at the hexdump:
This file looked suspicious for several reasons:
0xfffffd00
(-768) and 0x0
, respectively.We decided to set aside the first two suspicions (we will come back to this later in the article) and took a deep dive into the shellcode and the certificate strings as they appeared to be the more promising venues.
We loaded logofail.bmp
in IDA and disassembled it from offset 0x1010186
, where the 90 90 90..
in the hexdump indicated the presence of code (NOP sled). This revealed the following valid assembly code:
After checking all the references contained in the shellcode, we determined that the call [rax + 58h]
at 0x10101FA
corresponds to:
gRT->SetVariable(L"MokList", &MokDatabaseGuid, EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_NON_VOLATILE, 0x3b3, &Data);
This means that the shellcode is setting the MokList
variable with some rogue content (pointed by the Data variable above).
After dumping Data, we find that it matches the structure of the EFI_SIGNATURE_LIST
, which is used in UEFI to store certificates and hashes (it’s usually found in the context of UEFI Secure Boot components). After parsing this structure and extracting the certificate it contained, we found that it matches the certificate extracted from WinCertificate data of bootkit.efi file found by ESET!
The variable MokList
is set with a rogue certificate by the shellcode because this variable is used during the boot process (by shim) to verify the second stage bootloader, which exactly corresponds to bootkit.efi
in the case of Bootkitty.
In other words, the shellcode will be executed through this sequence of steps:
Steps 1-6 – Preparing the boot environment for the further exploitation
Steps 5-7 –Exploiting LogoFAIL vulnerability
Steps 8-9 – Deploying malicious bootloader and replacing the boot logo
This version of the Bootkitty will pass the verification process (at Step 3) by design.
To summarize, we’ve identified a shellcode designed to tamper with the contents of MokList, ensuring the bootkit is trusted by shim. However, one critical question remains: how is this shellcode executed in the first place?
Going back to the filename of the image, logofail.bmp is a clear reference to our LogoFAIL research that made international headlines last December. In addition, the width and the height of the BMP file reminded us clearly of BRLY-LOGOFAIL-2023-002, one of the vulnerabilities we reported last year as part of our LogoFAIL research. (See Insyde advisory).
This vulnerability affects the BmpDecoderDxe
module shipped within Insyde-based UEFI firmware. As the image below shows, the root cause of BRLY-LOGOFAIL-2023-002 is that when PixelHeight
is 0, the variable Blt
will be initialized from &BltBuffer[PixelWidth * (-1)]
, meaning that Blt
will point PixelWidth * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL)
bytes before BltBuffer
(which will be equal to PixelWidth * 4
). Since Blt is then used as a target for a write (Blt->Red = BmpColorMap..
) which depends on attacker-controlled data, the attacker can achieve an arbitrary write primitive from this vulnerability.
Given all the above, we just decided to run the malicious BMP file with the harness that we created during our LogoFAIL research and we got a crash!
After a quick investigation, we realise that our advisory only describes the root cause of the vulnerability and a simple trigger that allows an attacker to control the arbitrary write location below the BltBuffer
. However, it is more difficult to achieve code execution this way, as the interesting data to overwrite, such as UEFI modules code and data, will be located above BltBuffer
, which is itself located in the UEFI heap.
In order to obtain a controllable positive offset relative to BltBuffer
, the exploit repeatedly enters the block where SecondByte == 2
. After the last execution of this block, the variable Blt
will point outside BltBuffer
, exactly at BltBuffer + 0x6b0e20c
. By following the dynamic traces, we observe that the Blt
variable is used for an arbitrary write just a few lines later (at lines 77-79). The written data depends on the BmpColorMap
. When this block is entered, SecondByte
is 3, so the inner loop will be executed three times, writing the values ff e2 90, 45 46 47
and 0 2 ff
.
While the last two sets of writes appear to be just random data, disassembling the first three bytes reveals valid X86 code:
$ echo -ne "\xff\xe2\x90" | ndisasm -b64 -
00000000 FFE2 jmp rdx
00000002 90 nop
At this stage, we believe this is more than just a coincidence: BltBuffer + 0x6b0e20c
must point to a memory region containing code and so the exploit is overwriting legitimate code with the trampoline instruction jmp rdx
there. After considering where this memory region could be located, we made a key observation that made everything come together: the ultimate goal of the exploit is to jump to the shellcode stored in the BMP. Therefore, the jmp rdx
trampoline must be placed at a point in the execution where rdx points to the shellcode. So we can assume that the code of the RLE8ToBlt
function itself should be selected for such a patch.
As a side note, we pondered one key question here: how did the attackers manage to predict the offset relative to the BltBuffer
(located in the UEFI heap) to get directly to the chosen location in the code? In our experience, we have repeatedly found that it is not difficult: the location of the buffer in the heap will be the same from boot to boot (with the same boot configuration). Just like the base address of the BmpDecoderDxe
module.
As one possible option, the attackers could obtain the base address of the BmpDecoderDxe
module via the EFI_BMP_DECODER_PROTOCOL
protocol (e.g. by calling LocateProtocol()
from UEFIShell), so they will obtain the address of the protocol interface located inside the module.
At the same time, they could scan the physical memory for the contents of the BMP file (since most data remains in physical memory after boot, and it can be scanned even in the early stages of the operating system's work!)
Back to the code, by looking at the assembly code where the write happens, we can see that after the loop is completed, rdx
(which in source code is the RLE8Image
) will actually point to the shellcode! So if the jmp rdx
patch is written at that point, then the execution is correctly diverted to the shellcode.
This finding is further corroborated by another fact: the first instructions executed by the shellcode are some "strange-looking” mov
instructions (shown in the next screenshot). At this point r8
will hold Blt
, which is the code address where the three writes just happened. The mov constants must then be instructions and by disassembling them, we see they match exactly the instructions that were just overwritten, which are present in the original BmpDecoder
module (shown at offset 0x824
in the listing above).
In other words, the shellcode restores the original instructions. This operation ensures that the BmpDecoder
code is restored to its original state, so that it will work correctly if executed again after the exploit landed. Moreover, it also hides the exploit activity as it effectively clears all the hooking traces of the bootkit for BmpDecoderDxe
code.
The last clue that links the shellcode with the BmpDecoderDxe
file is that the shellcode epilogue matches the epilogue of the vulnerable function:
This artifact can also be easily explained: since the execution is diverted to the shellcode with a jump instruction, a matching epilogue ensures that the shellcode can safely and directly return to the caller of the vulnerable function.
It’s very important to note that the patch developed by Insyde Software (you can read our full analysis of LogoFAIL patches in this research report) is effective at stopping the exploitation. This patch correctly adds multiple checks for the size of BltBuffer
and the BMP Height and Width so that the vulnerability is removed.
However, we still need to understand if we can find unpatched devices where the exploit chain could work. To answer this question with certainty, we would need to run the exploit on each device to verify whether BltBuffer + 0x6b0e20c
points to the expected address in the BmpDecoderDxe
module, and this is nearly impossible without having access to the actual device.
However, there is another offset hardcoded in the shellcode (0x748
) which is used to calculate the address of the EFI_SYSTEM_TABLE structure.
seg000:0000000001010192 mov dword ptr [r8-0Ch], 0EBC2FF48h
seg000:000000000101019A mov dword ptr [r8-8], 2B60F36h
seg000:00000000010101A2 mov dword ptr [r8-4], 2568D48h
seg000:00000000010101AA mov r15, [r8+748h] ; gST
...
seg000:00000000010101CA mov rax, [r15+58h] ; gST->RuntimeServices
...
seg000:00000000010101FA call qword ptr [rax+58h] ; gRT->SetVariable
In summary, from the previous code, we can see that [r8 + 748h
] must contain a pointer to the gST
so SetVariable will be properly resolved. Based on this fact, we searched in our dataset of firmware images for all those BmpDecoderDxe
modules where the code stored at 0x748
before the &gST
exactly matches the code expected by the shellcode (48FFC2EB360FB602488D5602
).
This search resulted in two modules where this constraint is satisfied:
These two modules were distributed in hundreds of firmware images for Acer, HP, Fujitsu and Lenovo devices. The full list of potentially affected devices was still quite long, so we wanted to find any other clues that could help us shorten it.
To answer this question, we loaded the bootkit.efi
file in IDA and immediately noticed that the main bootkit function references variable names and paths that are very familiar to us during our LogoFAIL research. These strings were found in Lenovo devices based on Insyde that were affected by LogoFAIL (more information can be found in the advisory BRLY-2023-006 under the section “Vulnerability description (Insyde firmware)”).
This added some new findings about Bootkitty that were not present in previous publications. In particular we can see two custom NVRAM variables name and an ESP path being referenced:
LBLDVC
which contains, amongst other things, the CRC32 of the logoLBLDESP
contains instead other meta information about the logo, such as its resolution and the formatEFI\lenovo\logo\mylogo_1920x1080.bmp
which is the file containing the logoFrom the second screenshot we can instead learn that after execution the bootkit will:
LBLDVC
and LBLDESP
with new contentThis new finding restricts the intended bootkit target device to Lenovo. We queried our dataset again, to find all devices that use these logo specific NVRAM variables and that contain a BmpDecoderDxe
module matching the bootkit constraints. This query results in the following list of devices:
In the left column, we report the version details of devices that meet the bootkit constraint and use logo-related variables. The majority of firmware images don’t correspond to the latest version released by Lenovo. As we can see in the right column, those latest versions are not vulnerable anymore to LogoFAIL, so on updated devices the bootkit would not work.
There are however 10 devices for which the latest version affected by the bootkit is the same as the latest version released by Lenovo. In other words, these devices are still vulnerable to LogoFAIL and the bootkit would likely be functional there.
Customers using the Binarly Transparency Platform automatically receive code-guided detection to proactively mitigate both the LogoFAIL vulnerabilities and malicious components of Bootkitty.
It’s been more than a year since we first sounded the alarm about LogoFAIL and yet, many affected parties remain vulnerable to one or more variants of the LogoFAIL vulnerabilities. Bootkitty serves as a stark reminder of the consequences of when these vulnerabilities are not adequately addressed or when fixes are not properly deployed to devices in the field.