Another Brick in the Wall: Uncovering SMM Vulnerabilities in HP Firmware

By Assaf Carlsbad & Itai Liba

Hello and welcome back to yet another post in our blog post series covering UEFI & SMM security. This is the 6th (!) entry in the series, and it’s a good spot to pause for a second and look back to better estimate the vast distance we covered: from the baby steps of merely dumping and peeking at UEFI firmware, through the development of emulation infrastructure for it, and up to the point where we learned how to proactively hunt for SMM vulnerabilities. This post will continue where we left last time and will further explore SMM vulnerabilities, albeit from a slightly different angle.

So far, the SMM bug hunting methodology we came up with is mostly manual and goes roughly as follows:

  1. Obtain a UEFI firmware image of interest, either by dumping it from the SPI flash or, when possible, downloading it directly from the vendor’s website.
  2. Extract the encapsulated SMM binaries via tools such as UEFITool or UEFIExtract.
  3. Open the SMM images one by one in IDA and analyze them using efiXplorer, while keeping a keen eye for vulnerable code patterns like the ones described in the previous part.

Needless to say, this process is extremely slow, inaccurate, and cumbersome. After doing it repetitively over and over again, we were so unsatisfied with it that we decided to take the intuition and rules-of-thumb we developed and codify them into the form of an automated tool. The outcome of this endeavor is an IDA-based vulnerability scanner for SMM binaries we named Brick. For the benefit of the firmware security community, we decided to publish it as an open-source project that is readily available on GitHub.

In this post, we’ll introduce readers to Brick, its internal architecture, and its bug-hunting capabilities. Afterward, we’ll present a case study where we demonstrate how Brick was used to discover 6 different vulnerabilities affecting the firmware of some HP laptops. By doing so, we hope to encourage more people in the community to contribute back to Brick, as well as to educate the readers about the potential strengths (and weaknesses) of automated vulnerability hunting.

Enjoy the read!

Automated SMM Vulnerability Hunting Using Brick

As we said, Brick was developed to pinpoint certain vulnerabilities and anti-patterns inside SMM binaries. To effectively pull this off, its execution lifecycle is split into three different phases: harvest phase, analysis phase, and summary phase. Following is a detailed description of each phase.

Figure 1 – Schematic overview of Brick’s execution lifecycle

Harvest Phase

In the vast majority of cases, it’s most useful to give Brick a complete UEFI firmware image to scan. Doing so allows the researcher to “squeeze” the most vulnerabilities out of it while also gaining a bird-eye view of the code quality of the firmware as a whole. Alas, a typical UEFI firmware image is a complex beast that contains much more than SMM binaries. Among other things, it usually includes other stuff such as

  • Non-SMM executable modules for the different boot phases (PEI/DXE/etc.)
  • Microcode updates for the target CPU to be applied during early boot
  • Various Authenticated Code Modules (ACMs) signed by Intel, such as Boot Guard and BIOS Guard
  • A store for NVRAM variables
  • And much more
Figure 2 – SMM binaries are by no means the only file type stored inside a firmware image

Because of that, our first task is to separate the wheat from the chaff. In Brick’s terminology, this is accomplished by the harvest phase. During this phase, Brick will parse the firmware image and extract out of it just the SMM binaries we’re interested in.

To invoke Brick and kickstart the harvest phase, just pass the full path of the firmware’s image to the Brick.py script:

Figure 3a – The harvest phase in action

Internally, the harvest phase is implemented by offloading most of the actual work to two external tools/libraries:

The reason we use two different solutions for this phase is that we encountered several cases where one of them struggled to properly parse a UEFI image, while the other succeeded without any hurdles. Thus, the strategy of using one of them and falling back to the other in case of failure gives us just the right amount of redundancy we need to successfully handle the vast majority of firmware images encountered in the wild.

At the end of the harvest phase — given that all went well — the output directory should contain several dozens of SMM binaries waiting for further examination.

Figure 3b – The output directory at the end of the harvest phase

Note that in addition to full UEFI firmware images, Brick also supports other input formats in case you want to limit bug hunting to a narrower scope. These include

  • A single executable file (e.g. foo.efi)
  • Directory that contains multiple SMM binaries
  • UEFI capsule update package
  • Various other options (see the source code for the complete list of supported options).

Analysis Phase

At this point, we have a directory filled with the SMM binaries we’re interested in analyzing. The rough idea was to open each SMM binary in IDA and — after the initial autoanalysis completes — run some custom IDAPython scripts on top of it to do the actual bug hunting. This must be done intelligently, as a naive solution for this problem would suffer from two severe downsides:

  1. Analyzing SMM binaries one at a time is not very efficient performance-wise. For this reason, we should strive at parallelizing the whole process while taking advantage of multiple CPU cores.
  2. IDA is mostly used in an interactive fashion, and while there exists a batch mode for non-interactive usage, it’s often overlooked as it’s not very convenient to use.

Luckily for us, it didn’t take us too long to bump into a project called idahunt that solves these exact two problems. Put in the author’s own words:

idahunt is a framework to analyze binaries with IDA Pro and hunt for things in IDA Pro. It is a command-line tool to analyze all executable files recursively from a given folder. It executes IDA in the background so you don’t have to manually open each file. It supports executing external IDA Python scripts.”

Figure 4 – Overview of using idahunt to speed up the scanning process

The IDAPython scripts executed by idahunt on behalf of Brick are known as Brick modules and come in three different flavors:

  • Processing modules, which are in charge of doing some initial preparatory work and handling some of the shenanigans of UEFI.
  • Hunting modules that employ a wide range of heuristics to pinpoint potential vulnerabilities. Usually, there exists a dedicated module for each of the vulnerability classes described earlier.
  • Informational modules emit valuable information about the target image that is not necessarily tied to vulnerabilities. This includes, for example, the list of unrecognized UEFI protocols consumed by the image.
Figure 5 – Overview of the various Brick modules

While developing these Brick modules, we found the raw IDAPython API to be a bit rough at times, so for the most part the modules were developed on top of a wrapper framework called Bip. One of the major highlights of this framework is that it also exposes wrapper functions for the Hex-Rays Decompiler API, which allows writing analysis routines in a fairly high-level notion.

Figure 6 – The analysis phase, running 8 concurrent IDA instances in the background

Summary Phase

After all SMM images in the input directory were scanned, Brick will move on to collect the output emitted by individual modules and merge them into a single, browsable HTML report.

Note that in addition to the scan’s verdict, the report file also includes links to some useful resources such as the annotated IDB file (necessary for validating the correctness of the results), the raw IDA log file (useful for troubleshooting and debugging), as well as a separate report file generated by efiXplorer.

Figure 7 – Portion of a Brick report produced for some firmware image

Case Study – Using Brick to Uncover HP Firmware Vulnerabilities

Throughout the past year, we were using Brick extensively to review various firmware images from almost all leading manufacturers in the industry. So far, this campaign is definitely paying off and has already given birth to no less than 13 different CVEs (see Appendix A). In this case study, we would like to put a spotlight on several such vulnerabilities found while auditing one particular firmware image from HP (version 01.04.01 Rev.A for HP ProBook 440 G8 Notebook). After Brick’s scan was completed, we opened the resulting report file and were faced with a rather intriguing entry:

Figure 8 – The SMM module 0155.efi does not validate certain nested pointers

This entry immediately drew our attention because, if confirmed correct, it means that the SMI handler installed by the SMM image 0155.efi does not validate certain pointers that are nested within its communication buffer. As we explained in the previous post, that in turn implies the handler can be exploited by attackers to corrupt or disclose the contents of SMRAM.

In this section, we’ll elaborate on how Brick managed to find such vulnerability in a completely automated fashion. For that, we’ll walk you through the internal workings of some Brick modules that were involved in making this verdict. Note that due to the medium of a written article, the case study will be presented using snapshots of the IDA database, before and after each module invocation. In reality, however, all modules will be executed automatically one after another, without any user interaction.

Preprocessor

The first Brick module that is called to handle any input file is called the preprocessor. The preprocessor sets up the ground for the next modules in the chain and takes care of the following:

  • Making the .text section read-write, which prevents the decompiler from performing some excessive optimizations.
  • Discovering functions that the initial auto-analysis missed (based on codatify).
  • Scraping the edk2 and edk2-platforms repositories for protocol header files and attempting to import them into the IDA database. The net result is that the database is filled with a plethora of UEFI protocol definitions:
Figure 9 – Some UEFI protocols that were imported from EDK2 by the preprocessor

efiXplorer

Right after the preprocessor, Brick moves on to load and run the efiXplorer plugin. As we mentioned countless times throughout the series, efiXplorer has tons of functionality and serves as the de-facto standard way of analyzing UEFI binaries with IDA. Among other things, it takes care of the following:

  • Locating and renaming known UEFI GUIDs
  • Locating and renaming calls to UEFI boot/runtime services
  • Applying correct types for interface pointers
Figure 10 – Pseudocode from a decompiled function before efiXplorer was invoked Figure 11 – The same function, after efiXplorer analysis

Last but not least, efiXplorer is also capable of locating and renaming SMI handlers. In its recent editions, it prefixes all CommBuffer-based SMIs with SmiHandler’, and all legacy software SMIs with ‘SwSmiHandler’. As can be seen, in the case of 0155.efi, only one SMI handler seems to exist:

Figure 12 – the SMI handler found by efiXplorer

Postprocessor

Following efiXplorer, control is passed to the postprocessor. The postprocessor is a module that is in charge of completing the analysis performed earlier by efiXplorer. Among other things, this includes:

  • Locating SMI handlers that efiXplorer might have missed
  • Fixing the function prototype for some UEFI services such as GetVariable()/SetVariable()
  • Renaming function arguments

In the context of this case study, the most important feature of the postprocessor is the handling of calls to EFI_SMM_ACCESS2_PROTOCOL. In a nutshell, this protocol is used to control the visibility of SMRAM on the platform. As such, it exposes the respective methods to open, close, and lock SMRAM.

Figure 13 – interface definition for EFI_SMM_ACCESS2_PROTOCOL, source: Step to UEFI

In addition to those, this protocol also exposes a method called GetCapabilities(), which can be used by clients to query the memory controlled for the exact location of SMRAM in physical memory. Upon return, this function fills in an array of EFI_SMRAM_DESCRIPTOR structures that informs the caller what regions of SMRAM exist, what is their size, state (open vs. close), etc.

Figure 14 – documentation of the GetCapabilities() function, source: Step to UEFI

In EDK2 and its derived implementations, the common practice is to store these EFI_SMRAM_DESCRIPTORS as global variables so that they could be consumed by other functions in the future. As part of its operation, the postprocessor scans the input file for calls to GetCapabilities() and marks the SMRAM descriptors in a way that will make it easy to recover them afterward. This includes both retyping them as 'EFI_SMRAM_DESCRIPTOR *' as well as renaming them to have a unique, known prefix. The significance of this operation will be clarified shortly.

Figure 15 – Calling GetCapabilities(), before running the postprocessor Figure 16 – Same code, after applying the postprocessor

Reconstructing the CommBuffer

Initially, the type assigned to the CommBuffer in the SMI handler’s signature is VOID *. This is adequate, as the structure of the CommBuffer is not known in advance and it’s the responsibility of the handler to correctly interpret it. Still, figuring out the internal layout of the Communication Buffer will be of great aid because it will let us know whether or not it contains nested pointers.

Usually, such tasks are completed manually as part of the reverse engineering process, but in Brick we needed to pull this off automatically. The two most prominent and successful IDA plugins for doing so are HexRaysPyTools and HexRaysCodeXplorer. Based on our experience, HexRaysPyTools produced more accurate results, while HexRaysCodeXplorer is better suited for non-interactive use. Eventually, the scriptability capabilities of HexRaysCodeXplorer tipped the scale in its favor and so it was incorporated into Brick.

Figure 17 – HexRaysCodeXplorer can be invoked from an IDAPython script

At this stage, all SMI handlers present in the image were already identified so Brick can iterate over them and invoke HexRaysCodeXplorer on the associated CommBuffer to reconstruct its internal structure. Doing so for the SMI handler from 0155.efi yields the following structure, which holds two members (field_18 and field_28) that are presumably pointers by themselves:

Figure 18 – the reconstructed structure of the Comm Buffer

How did HexRaysCodeXplorer get to this conclusion? To answer this question, let’s take a closer look at the handler’s code itself:

Figure 19 – The SMI handler forwards CommBuffer->field_18 to sub_17AC

As can be seen, during the course of its operation, the handler passes CommBuffer->field_18 as the 2nd argument to the function sub_17AC. This function, in turn, forwards it to CopyMem(), where it is used as the destination buffer. Based on the signature of CopyMem(), we know the destination buffer is in fact a pointer. That means the argument for sub_17AC is also a pointer by itself and therefore — due to the transitivity of assignments — CommBuffer->field_18 must be a pointer as well! The same logic also applied to field_28, even though we won’t show it here.

Figure 20 – The 2nd argument is forwarded as the destination buffer for CopyMem()

Resolving SmmIsBufferOutsideSmmValid

Now that it knows the CommBuffer does contain some nested pointers, Brick moves on and checks if these pointers are being sanitized properly. That is a two-fold operation:

  1. Locating the function SmmIsBufferOutsideSmmValid() in the input binary.
  2. If found, check that it is aptly used to sanitize the nested pointers.

Let’s start with resolving SmmIsBufferOutsideSmmValid(). As we mentioned in the previous part, SmmIsBufferOutsideSmmValid() is statically linked to the binary and thus locating it is not a trivial problem. To pull this off, we compiled a heuristic comprised of three conditions. Brick will iterate over all of the functions in the IDA database and try to find a function that matches all three. The heuristic goes as follows:

  1. The function at hand must receive two integer arguments – the first used as the buffer’s address and the second as its size. With the help of Bip’s API, checking for these properties is rather trivial:
    def check_arguments(f: BipFunction):
        # The arguments of the function must match (EFI_PHYSICAL_ADDRESS, UINT64)
        if (f.type.nb_args == 2 and \
            isinstance(f.type.get_arg_type(0), BTypeInt) and \
            isinstance(f.type.get_arg_type(1), BTypeInt)):
            return True
        else:
            return False
    

    Figure 21 – Matching the arguments of SmmIsbufferOutsideSmmValid

  2. The function at hand must return a BOOLEAN value. From the perspective of the decompiler, BOOLEAN values are just plain integers, so if we want to make this distinction we must go over all the return statements in the function and check if the returned value is a member of the set {0,1}. In Bip, this can also be accomplished very easily:
    def check_return_type(f: BipFunction):
        if not isinstance(f.type.return_type, BTypeInt):
            # Return type is not something derived from an integer.
            return False
    
    def inspect_return(node: CNodeStmtReturn):
        if not isinstance(node.ret_val, CNodeExprNum) or node.ret_val.value not in (0, 1):
            # Not a boolean value.
            return False
    
    # Run 'inspect_return' on all return statements in the function.
    return f.hxcfunc.visit_cnode_filterlist(inspect_return, [CNodeStmtReturn])
    

    Figure 22 – Checking the function actually returns a BOOLEAN value

  3. Lastly, we know that SmmIsBufferOutsideSmmValid() uses an array of EFI_SMRAM_DESCRIPTORS to keep track of active SMRAM ranges, so we expect the candidate function to reference at least one of them. Because global EFI_SMRAM_DESCRIPTORS were already marked earlier by the postprocessor, checking for xrefs between the function and the descriptors becomes straightforward:
    def references_smram_descriptor(f: BipFunction):
        # The function must reference at least one SMRAM descriptor.
        for smram_descriptor in BipElt.get_by_prefix('gSmramDescriptor'):
            if f in smram_descriptor.xFuncTo:
                return True
        else:
            # No xref to SMRAM descriptor.
            return False
    

    Figure 23 – Checking the function references an EFI_SMRAM_DESCRIPTOR

Are these heuristics bulletproof and guarantee they will always match SmmIsBufferOutsideSmmValid() in the binary? Of course not! But more often than not they do the trick, and that’s what matters. In the HP case, the heuristics didn’t fail and managed to find a proper match:

Figure 24 – Matching SmmIsBufferOutsideSmmValid()

Nested Pointers Validation

Once SmmIsBufferOutsideSmmValid() is matched, Brick verifies it is being used properly by the SMI handler. For that, it iterates over all calls to SmmIsBufferOutsideSmmValid() and tries to deduce if all nested pointers are being covered by it. In 0155.efi, it notices there is only one call to SmmIsBufferOutsideSmmValid() that is used to validate field_28. That implies no validation takes place over the second nested pointer, namely field_18, so it flags the handler as vulnerable.

Figure 25 – SmmIsBufferOutsideSmmValid validates one field while neglecting the other one

To be fair, we were quite lucky to encounter such a clear-cut case as the one above. If the control flow was a bit more convoluted, there is a decent chance Brick’s verdict would become more ambiguous.

Impact

We already saw that depending on the exact flow the handler takes, it might end up calling sub_17AC. This function gets an argument that is derived from CommBuffer->field_18 and will later forward it as the destination address for CopyMem(). The contents of the CommBuffer are fully controllable by the attacker and, leveraging the missing validation, he or she can craft a buffer whose field_18 points to an arbitrary SMRAM address of their choice. As a result, the SMRAM region pointed to by that address will get corrupted by the time CopyMem() gets called.

How to cause the handler to actually call sub_17AC, and how to promote this memory corruption into an arbitrary code execution in SMM are left as exercises to the diligent reader.

Low SMRAM Corruption

In addition to the nested pointer vulnerability present in 0155.efi, the HP firmware image also suffered from 5 additional, less severe issues that enable attackers to corrupt the low portion of SMRAM. All five vulnerabilities are isomorphic to each other, so we’ll focus on the simplest case found in 017D.efi:

Figure 26 - CommBuffer->field18 is passed from SmiHandler through sub_17AC and ends up at CopyMemFigure 26 – CommBuffer->field18 is passed from SmiHandler through sub_17AC and ends up at CopyMem

As we mentioned in the previous post, these vulnerabilities arise when an SMI handler writes data to the communication buffer without first validating its size. Attackers can place the CommBuffer just below SMRAM, which will cause unintended corruption once the handler performs the write to it.

We also noted that SMI handlers can shield themselves from these problems by performing one or both of the following actions:

  1. Calling SmmIsBufferOutsideSmmValid on the CommBuffer with the exact size expected by the handler.
  2. Dereferencing the provided CommBufferSize argument (a pointer to an integer value holding the size of the buffer), then comparing the result against the expected size.

Therefore, to detect this class of vulnerabilities, Brick searches for SMI handlers that omit both checks. Unlike the previous case, this time the heuristics employed to resolve SmmIsBufferOutsideSmmValid() bore no fruit, so Brick simply assumes it’s absent from the binary and moves on to check if CommBufferSize is being dereferenced. This is achieved by traversing the AST associated with the handler, looking for nodes that correspond to a dereference operation (cot_ptr in the Hex-Rays terminology). The child node of a dereference operation in the tree represents the variable being dereferenced, so Brick can check if it’s CommBufferSize.

Figure 27 – The portion of the AST that corresponds to dereferencing CommBufferSize

If such a pair of nodes is found, it tells us that the C source code for the handler contained the expression: *CommBufferSize, so we can assume the programmer intended to compare that value against some anticipated size.

Figure 28 – The corresponding C source code for the dereference operator

Using Bip, implementing this heuristic is easy and only takes a handful of Python lines:

def dereferences_CommBufferSize(handler: BipFunction):
    # CommBufferSize is the 3rd argument of the SMI handler
    CommBufferSize = handler.hxcfunc.args[2]
if not CommBufferSize._lvar.used:
    # CommBufferSize is not touched at all.
    return False

def inspect_dereference(node: CNodeExprPtr):
    child = node.ops[0].ignore_cast

    if isinstance(child, CNodeExprVar) and child.lvar == CommBufferSize:
        # This is confusing, we return False just to signal the search to stop.
        return False

# Run 'insepct_dereference' on all dereference expressions in the function.
return not handler.hxcfunc.visit_cnode_filterlist(inspect_dereference, [CNodeExprPtr])

Figure 29 – Implementing the heuristic in python

Unfortunately, this heuristic yields no results, so Brick now knows CommBufferSize is not being dereferenced and as a result marks the handler as vulnerable.

Figure 31 - Brick’s assessment of 017D.efiFigure 31 – Brick’s assessment of 017D.efi

Conclusion

As can be judged by the number of CVEs it has already generated, we believe Brick is a very promising project that takes a big step in the right direction of harnessing automation to streamline the bug hunting process. This feeling we have was even reinforced recently when a related project called FwHunt was released. FwHunt attempts to solve the same set of problems as Brick, only using strict rule-sets rather than more relaxed heuristics.

Using automation, rules, heuristics, and other static code analysis techniques to crack through complex problems are very much desirable, but it’s always important to remember that reality is more complex than how we describe it. As such, occasional edge conditions that cause Brick and other automated tools to generate false positives and false negatives from time to time are inevitable.

That is perfectly acceptable, as long as we keep in mind that these tools were never intended to fully replace a human analyst, but rather empower him to handle larger and larger quantities of data. Eventually, it’s not the tool itself that makes the difference, but rather the human being that chooses how to use it, on what targets, and how to interpret its findings.

If you’re interested in learning more about the subject, come attend the upcoming Insomnihack conference, where we will be delivering a talk about some more SMM vulnerabilities, found this time in the Intel codebase.

See you there!

Appendix A – List of CVEs by Brick

CVE ID CVSS score Vendor
CVE-2021-36342 7.5 Dell
CVE-2021-44346 ? Gigabyte
CVE-2021-0157 8.2 Intel
CVE-2021-0158 8.2 Intel
CVE-2021-42055 6.8 ASUS
CVE-2021-3599 6.7 Lenovo
CVE-2021-3786 5.5 Lenovo
CVE-2022-23956 8.2 HP
CVE-2022-23953 7.9 HP
CVE-2022-23954 7.9 HP
CVE-2022-23955 7.9 HP
CVE-2022-23957 7.9 HP
CVE-2022-23958 7.9 HP

Appendix B – References and Further Reading

Article Link: Another Brick in the Wall: Uncovering SMM Vulnerabilities in HP Firmware - SentinelOne