Inside the LogoFAIL PoC: From Integer Overflow to Arbitrary Code Execution
The Binarly REsearch team investigates how crashes of firmware parsers can be leveraged by attackers to achieve arbitrary code execution with firmware privileges during the boot process.
In the firmware ecosystem, addressing and promptly rectifying any bug is crucial, as even a single flaw can significantly compromise the security of the entire platform. In this blog post, we prove the severity of the bugs we identified while researching LogoFAIL by developing a PoC on a real device with modern firmware security features enabled (i.e. Intel Boot Guard and Secure Boot).
In particular, we demonstrate how we turned one of the crashes found by our fuzzer into arbitrary code execution during the DXE phase.
After presenting LogoFAIL at Black Hat Europe and H2HC, we received quite a lot of positive feedback from the community and we noted improvements and corrections to our work. In particular, during our talks, we claimed that none of the image libraries used in modern UEFI firmware were ever tested by OEM or IBVs. This was a pure speculation based on our knowledge and on the results of our experiments, and we should have made that much clearer.
Indeed, a few days after our presentations, the developers of Target Software Fuzzer for SIMICS (TSFFS) published a tutorial that shows how TSFFS can be used to fuzz the BMP parser included in EDK2 reference code. The tutorial also includes a link to an existing fuzzer for this parser that was developed as part of HBFA. As we were not aware of this existing fuzzer, we would like to thank the TSFFS authors for bringing this to our attention.
Our fuzzer found multiple crashes on the test device — a Lenovo ThinkCentre M70s Gen 2 — so we need to select one crash as a starting point to create a proof-of-concept.
We analyzed the different root causes of the identified bugs, and finally we selected an integer overflow in the PNG parser. We selected this bug because it affects a file format that is “easy” to work with, and also because it easily leads to a heap overflow with arbitrary content and arbitrary size.
Before delving into the details of the exploitation technique we used, the image above provides a high level overview of how this PNG parser works. First, the parser locates the different PNG chunks of the image and from the IHDR chunk, it extracts global information regarding the image, such as the width and height, while all the IDAT chunks, which contain the actual PNG image data, are concatenated into a single buffer (called
Compressed IDAT chunks in the figure). The parser then allocates all the buffers needed for processing the image, and it finally uncompresses the IDAT chunks into another buffer which we call
This snippet above shows the root cause of the selected crash: an integer overflow on the size used to allocate
PngWidth is related to the width contained in the IHDR chunk, an attacker can increase this value so that the 32-bit multiplication result will overflow (e.g.,
2 * 0x80000040 = 0x80). Because of this integer overflow, the attacker can allocate an OutputBuffer that is not large enough to contain the uncompressed data, which directly leads to a heap overflow with attacker-controlled data during the PNG uncompression phase.
But what can be corrupted with this heap overflow? To answer this question, we went back to the drawing board and dissected how the UEFI heap works.
UEFI Heap Internals
The UEFI heap is pool-based, meaning that it maintains a set of pools for different memory types — the picture above shows the pool associated with the
EfiBootServicesData memory type. Each pool contains a set of free memory chunks (represented by the
POOL_FREE object) which are distributed by size and used to fulfill memory allocations requests. For example, when
EFI_BOOT_SERVICES->AllocatePool is called, the heap manager selects the correct free list depending on the requested allocation size, unlinks the first
POOL_FREE element, and returns an allocated chunk to its caller. The allocated chunk is wrapped with a
POOL_TAIL object, so when
EFI_BOOT_SERVICES->FreePool is called on a chunk, the heap manager can retrieve all the metadata needed to place back this chunk in the appropriate free list.
UEFI Heap Overflow
As is often common in heap allocators, allocated chunks and free chunks co-exist in the same memory regions. For this reason, an attacker exploiting a heap overflow can either corrupt an allocated chunk or a free one, depending on what is stored after the overflowing buffer.
Unfortunately, we do not have any debugging capabilities on the device under test: Intel Direct Connect Interface (DCI) is not available on newer CPUs, and Intel Boot Guard prevents any debugging stubs to be injected in the firmware runtime. For this reason, we cannot inspect the state of heap memory when the overflow occurs, and thus we don’t know what can be overwritten and corrupted with the overflow.
One peculiarity of UEFI is that the memory used during the boot process is not cleared when the control is passed to the operating system so it survives after boot and can be inspected from the operating system. The screenshot below shows that
OutputBuffer can be found by searching for the pattern “
BRLYBRLY” that results from uncompressing the PNG data chunks. At a first glance, this insight seems to identify what is stored after OutputBuffer.
However, this memory content is not a snapshot of the memory present when the overflow happens, but rather what was left when the control was passed to the OS. For this reason, the chunk starting at address
0x82c83f90 is not what is present at overflow time and can be corrupted, but rather the last object that was allocated there by UEFI code.
To overcome this problem, we use a second insight that we discovered while reading the source code of
EFI_BOOT_SERVICES->FreePool. As we can see in the following snippet,
FreePool immediately returns an error when the signature of the chunk to be freed doesn’t match
POOL_HEAD_SIGNATURE (defined as “
This means that by corrupting the signature of the chunk allocated after
OutputBuffer, we can force
FreePool to return an error so that the chunk is not put back in the free lists and thus not reused for future memory allocations. This technique, which we haven’t seen documented elsewhere, effectively preserves the content of an allocated chunk so that an attacker can inspect it from the OS.
Heap Feng Shui
As a quick summary, by using the techniques explained in the previous sections, we can inspect from the OS what chunk is allocated after
OutputBuffer and so what object can be corrupted. The only steps left are to find a good target for corruption and leverage them to get code execution.
Heap overflows often require strong control over the state of the heap. Before the actual overflow, the attacker usually does multiple allocations and deallocations in arbitrary sequence in order to “massage” the heap and choose which object is allocated after the overflowing one. In our scenario, despite lacking strong primitives, we discovered that the state of the heap can be influenced by including certain PNG chunks (e.g., PLTE and gAMA) in the final PNG image and varying their sizes.
We tried this basic heap massaging technique with different sequences of PNG chunks and stopped after a few tries when we found that OutputBuffer got allocated before a PROTOCOL_ENTRY object.
We immediately knew that the
PROTOCOL_ENTRY object was going to be a powerful target for corruption, as protocols are a core concept in
PROTOCOL_ENTRY contain multiple pointers to EFI protocol objects, and lots of them in turn contain function pointers.
PROTOCOL_ENTRY object is used to represent an EFI protocol and everything that revolves around it. The previous screenshot shows the definition of
PROTOCOL_ENTRY as taken from the EDK2 reference code. The field
ProtocolID is used to store the GUID of the protocol represented by this protocol entry. As shown in the next diagram, the
AllEntries field is instead used by the UEFI reference code to keep track of the different
PROTOCOL_ENTRYs in a circular doubly-linked list rooted at the
mProtocolDatabase global variable.
This list is quite important as it is traversed for example by
EFI_BOOT_SERVICES->LocateProtocol to search for a specific protocol. From the Protocols field of
PROTOCOL_ENTRY we can reach its
PROTOCOL_INTERFACE which contains a pointer to the actual interface installed by
EFI_BOOT_SERVICES->InstallProtocolInterface, while the Notify field points to a
PROTOCOL_NOTIFY object used by the UEFI event system. Since both these last two fields eventually point to a function pointers, an attacker that is able to corrupt a
PROTOCOL_NOTIFY can therefore overwrite any of them to achieve arbitrary code execution when the protocol interface or the notification callback handler function are invoked.
UEFI Event System
For our proof-of-concept we decided to (ab)use the EFI Event System. Among many other things, this system allows a module to register a callback handler that is invoked when a specific protocol is installed. This is usually done by UEFI code by calling the functions
EFI_BOOT_SERVICES->RegisterProtocolNotify which creates and prepares the
IEVENT objects. When the specified protocol is installed, the function
CoreNotifyProtocolEntry will queue the event to be notified in a global list pointed by
gEventQueue. The actual dispatch of the pending events, and thus the invocation of the callback handler, will happen when the task priority is set to a value lower than the notification priority specified in the
Our proof-of-concept mimics an event registration by crafting the
IEVENT objects in memory and setting all the connections between these objects. In this way, when the protocol stored in
PROTOCOL_ENTRY during the overflow will be installed, the event dispatchers will invoke our callback handler and thus we achieve arbitrary code execution.
As a last note, as target protocol GUID in the protocol entry, we used a protocol that is installed just after the logo parsing function is called. This GUID can either be found by reverse engineering or by reconstructing with some basic memory forensics techniques the list of installed protocols (rooted at
mProtocolDatabase) from the UEFI memory left after boot.
NVRAM + Shellcode + Second Stage
The last bit we need to figure out is where to redirect the control flow after the callback handler is invoked. This step is made very easy because of a common misconfiguration that doesn’t remove executable permissions from the memory used to map NVRAM variables. In other words, we can simply store a shellcode inside an NVRAM variable and that memory will be both executable and always mapped at the same address!
Once arbitrary code execution is achieved during the DXE phase, it’s a game-over for platform security. From this stage, we have full control over the memory and the disk of the target device, including the operating system that will be started.
In particular, it’s very easy to start a bootkit like BlackLotus and modify the OS that will be started after boot. We prove this point by extending our initial proof-of-concept to have more advanced behavior. In particular, our final PoC is able to disable SecureBoot (this is very straightforward as it only requires setting to
gSecurity2 global variable) and load and execute a second stage from the disk. The second stage replaces the current NTFS driver with one with write support so that files can be written into the Windows file system.
Proof of Concept
The following video shows how the exploitation for LogoFAIL looks on the device under test. After logging in, we start a terminal with Admin privileges. We then check that Secure Boot and Intel Boot Guard (Verified Boot) are enabled, and run the LogoFAIL proof of concept. The PoC prepares all the needed UEFI objects, saves the malicious PNG file inside an NVRAM variable, and finally restarts the devices. During the boot process, the system firmware will parse the injected PNG and trigger the heap overflow which will result in the memory corruption we discussed in the previous sections.
In the proof-of-concept video, we demonstrate arbitrary code execution during DXE by printing a message on the display and by showing that our advanced payload is able to create a file on the Windows file system.
Quite interestingly, the UEFI heap of the EDK2 reference code implements a security mitigation against heap overflows called Heap Guard that would completely hinder the proof-of-concept discussed in this post. This protection works by putting an unmapped memory page before and after each allocation, so any overflow can be immediately blocked.
However, we found a comment in EDK2 sources saying that Heap Guard is only intended for debugging purposes and should not be enabled in any product. This comment matches our own experience since we haven’t seen any system in the wild with this mitigation enabled.
Similarly, the reference code also contains the logic to disable the execution of code stored in the NVRAM area, which can be done by simply setting the page tables correctly. The only systems where we found this mitigation enabled and properly configured are ARM-based systems and some servers. This would still not stop the exploitation, as arbitrary code execution can still be likely achieved with more advanced exploitation techniques (e.g. ROP).
In this blog post we showed how we turned an integer overflow into heap overflow and arbitrary code execution. The techniques we discussed in this blog are very generic and deeply tied with the UEFI ecosystem, so they can be likely applied to exploit any heap overflow but also will be very hard, if not impossible, to fix without enabling stronger defenses tailored for heap corruptions (e.g. Heap Guard). This is also one of the first heap exploits documented in UEFI, so we don’t rule out that in the future easier and more stable techniques could be discovered, lowering even more the bar of exploitation.