Blog 8 in series: Digital Certificates - Models for Trust and Targets for Misuse
Author: Tomislav Pericin, Chief Software Architect & Co-Founder at ReversingLabs
Authenticode is a Microsoft code signing technology designed to guarantee the origin and integrity of an application. The core principle of its integrity verification system is code immutability. In other words, once the firmware application is signed, its code cannot change without breaking the envelope integrity. This way, the user is guaranteed that the machine is only executing the firmware signed by a trusted party.
Unified Extensible Firmware Interface (UEFI) is a specification that defines the interfaces between the operating system and the platform firmware. Originally developed by Intel, and now supported by an alliance of software and hardware vendors, UEFI quickly became a standard that displaced the legacy basic input/output system (BIOS). UEFI brought order into the hectic world where every motherboard manufacturer developed their own BIOS code, each with its own set of quirks that low-level software developers had to live with.
UEFI represents a huge leap forward for both firmware software development and its security. Secure boot and its chain of trust are the best line of defense our systems have to prevent a magnitude of issues that might arise if a machine is tampered with during startup. Authenticode plays a key role in making this system secure. It ensures that every firmware application and driver the system executes is trusted.
Authenticode is a very well-designed system. The reason it was chosen for firmware signing was that it perfectly fits with the toolchain selected for the UEFI platform. Compilers and linkers that were chosen produced the portable executable format, which in turn supported digital signing through Authenticode. A natural fit, as its use was proven in the field, and it was leveraging decades of engineering experience.
A typical portable executable file consists of headers, followed by an arbitrary number of sections and extra data appended after them. Sections in turn consist of code, data and structures that tell the loader how to correctly map the file in memory. While desktop applications tend to have a larger set of data directories, the UEFI implementation has them reduced to just two: relocations and debugging information. That significantly decreases the portable executable (PE) format parsing complexity, and makes it easier to design a system free of image malformation attacks.
The UEFI image parser is well-implemented, with a strict set of format validation requirements. For that matter, it is far more restrictive than its desktop counterpart. Still, there are a few oversights in its design that make it possible to break the Authenticode security model. Subverting Authenticode security is an exercise in abusing the absence of just a few simple format validation checks.
The UEFI image loader starts the image allocation process by reserving enough memory for the image to be fully loaded. The required memory is expanded by a single page so that the loader has enough space to store the information about debugging symbols. Normally this wouldn’t be required, but depending on the linker, debugging information can be placed inside the image overlay. Since overlay information isn’t loaded in memory, the UEFI loader ensures that it is available by making a copy from the disk to this additional allocated page. This complete logic is sound and would solve the issue perfectly, were it not for lack of a few crucial checks to make sure this process can’t be abused.
The image memory footprint is incorrectly determined by walking back through the list of sections. The last section entry is used to determine how large the image is in memory. It is also used to determine where the additionally allocated page begins. Sum of the last section's virtual address and its virtual size should typically do the trick. However, the UEFI loader never verifies that the section table is ordered correctly. This means that we can select any arbitrary address within the loaded image as the target for the debugging information. Since the UEFI loader copies the debugging information from the debug table to the additional allocated page (or the arbitrary selected address), we now have full control of a memory copy operation.
The code of the UEFI application can be selected as the target address of the arbitrary memory copy. Since the memory copy is arbitrary, any number of bytes can be replaced - even the entire application code, if need be. Targeting the beginning of the memory copy is as simple as creating an additional section. Its virtual address and size combined provide the correct memory copy target pointer.
The first debug table entry can be selected as the source address of the arbitrary memory copy. Its whole content will be copied to the target location. To achieve the Authenticode integrity bypass, it is necessary to have the source data in a location not covered by integrity checks. Since Authenticode allows extending the certificate information for purposes of timestamping and dual-signing, that region of the file would do just fine. Luckily, debug information can also be placed into the overlay, where Authenticode and this extended information reside. So, by making a few edits to the debug table, it can be made to point to a location which doesn’t currently exist, but will exist once the file is signed.
When arbitrary memory copy source and target addresses are selected, the preparation phase is done. The file works normally at this stage, and executes the original code of the application.
Once the UEFI application is signed and its Authenticode signature extended, the new code can be placed in the predetermined region. This code is within the part of the file that isn’t validated by Authenticode integrity checks, and can be arbitrarily changed after the image has been signed. The UEFI loader itself performs the arbitrary memory copy once the application is started. That makes changes to the image in memory, and replaces the signed code with the unsigned one. Since the unsigned code is the one that ends up being executed, the Authenticode integrity checks are completely bypassed.
The following image shows both the original and the modified files, their Authenticode signature validation response, and the behavior of both UEFI applications when they are launched in the EDK2 emulator.
While interesting, this attack isn’t very practical. Because there’s a preparation phase involved, no existing Authenticode signature can be modified without breaking the envelope integrity. Every UEFI application or driver signed before this publication remains just as secure as it ever was. Following the best development practices and secure development lifecycle procedures will still result in organizations publishing safe code.
ReversingLabs solutions understand the dangers of extended Authenticode signatures, and they actively prohibit whitelisting when such cases are encountered. More importantly, they alert the developers that such behavior introduces risk, and that it should be avoided. Those notifications are just a small piece of a larger picture in which ReversingLabs solutions enhance secure code development.
Code signing is here to stay, and doing it properly is more important than ever.
UEFI specification 2.1+
Microsoft Project Mu
06/03/2019 - Contacted Microsoft MSRC
06/05/2019 - MSRC assigns case 52332 to bug submission
06/13/2019 - MSRC successfully reproduces the issue
06/17/2019 - MSRC ask for more details about preparation steps
06/17/2019 - MSRC to coordinate bug patching with TianoCore
06/18/2019 - ReversingLabs provides a more detailed description
07/09/2019 - MSRC reaches out to TianoCore and opens an issue
SignedApp.A.efi - DF9F0587F4D40046C77926205E3E062AAFF1BCCBCE61AC3BCD70095BB4AFF5AF
SignedApp.B.efi - E2672136DDF40393FA734CA286675A4AC5F73EC630A6EFACEA3AEE27BCCEE911