© Jiewen Yao and Vincent Zimmer 2020
J. Yao, V. ZimmerBuilding Secure Firmwarehttps://doi.org/10.1007/978-1-4842-6106-4_21

21. Security Unit Test

Jiewen Yao1  and Vincent Zimmer2
(1)
Shanghai, China
(2)
Issaquah, WA, USA
 

After we finish all of the code development work, we need to perform security testing. Usually, the developers need to perform security unit testing and then deliver the code to the test team. At this point, the test team needs to perform more thorough system-level security testing. There are many books that introduce software testing techniques. We will not repeat those contents here. Instead, in this chapter, we will focus on the firmware-specific security unit testing from a developer’s perspective. In the next chapter, we will focus on the firmware-specific system security testing from a test engineer’s perspective.

Security Unit Testing Plan

In Chapter 2, we discussed the whole idea of proactive firmware secure development and described the threat model. The security unit test should focus on the threat model to see if the identified threats are properly mitigated. We also described eight high-risk areas in the firmware and provided guidance for security code review. Table 21-1 shows the security unit test for those areas.
Table 21-1

Security Unit Test for Eight High-Risk Areas

Category

Security Unit Test

External input

Identify the external input – SMM communication buffer, UEFI variable, firmware image and capsule image, boot BMP file, PE/COFF image such as an OS bootloader and PCI option ROM, file system, disk image, network packet, and so on.

Identify the function to handle the external input – boundary function.Identify the function to perform the check for the external input – check function.

Test the check function with different input.

Verify the output of the check function.

Race condition

Identify the critical resource – SMM communication buffer, locked silicon registers, and so on.

Identify the function to handle the critical resource.Create a race condition scenario to access the critical resources.

Verify the result of the critical resource.

Hardware input

Identify the hardware inputs – read/write silicon register, memory mapped input/output (MMIO) base address register (BAR), USB descriptor, Bluetooth Low Energy (BLE) advertisement data, DMA, cache, and so on.

Identify the function to handle the hardware input.

Identify the function to perform the check for the hardware input – check function – or identify the function to prevent the attack, lock function. Emulate the hardware input such as MMIO BAR, USB, BLE, and so on; and test the check function. Verify the output of the check function.

Verify the lock function to perform the lock correctly, such as DMA or SMM cache and so on.

Secret handling

Identify the secret/password.

Identify the function to use the secret.

Identify the function to clear the secret after use.

Ensure the secret is cleared in all paths.

Identify the function to hash or encrypt the secret if the secret needs to be saved.

Identify the default secret/password handling.

Verify that the side channel guidelines are followed.

Register lock

Identify the registers to be locked, such as flash lock and SMM lock.

Identify the function to lock the register.

Identify the policy to control the lock, such as a policy protocol or variable.

Ensure the end user cannot control the policy.

Ensure the lock happens in all boot paths.

Ensure the lock happens before any third-party code runs.

Secure configuration

Identify the policy to control the security configuration, such as secure boot and measured boot.

Ensure that the unauthorized end user cannot update the secure configuration.Ensure the default configuration is secure.

Replay/rollback

Identify the secure version number (SVN) or lowest supported version number (LSN).

Identify the storage for the SVN or LSN.

Ensure the unauthorized end user cannot update the SVN or LSN.Identify the function to perform the version check.

Cryptography

Identify the cryptographic usages in the firmware, such as signature verification or data encryption.

Identify the cryptographic algorithm used in the firmware.

Ensure that the cryptographic algorithm follows the latest guidelines from the government and corporations. Do not use any deprecated algorithm.

Ensure the key length follows the latest guidelines from the government and corporations. Do not use small lengths.

Identify the key provision function and the key revocation function.

Identify the key storage.

Identify the function to protect the key storage for integrity and/or confidentiality. Only authorized users can update the key.

Verify if the side channel guideline is followed for the confidentiality of the key.

Ensure the signature verification function can handle cases for a missing key or signature.

Care must be taken for solutions that may involve multiple high-risk areas. Taking SMM-based firmware update as an example, the firmware update image is an external input (#1). The image is put into an SMM communication buffer which may have a potential Time-of-Check/Time-of-Use attack (#2). The flash update function needs to unlock the flash, which leaves open the possibility of a race condition attack (#2). The SMM environment should be protected to prevent a DMA or SMM cache attack (#3). The flash device should be locked during boot to prevent any unauthorized updates (#5). The firmware update function should check the SVN of the firmware to prevent a rollback attack (#7). The new firmware image must be signed with a proper cryptographic algorithm with proper length key (#8).

In order to verify if the developed code follows the general secure coding practices, which we introduced in Chapter 14, static code analysis or dynamic code analysis can be used. In Chapter 16, we introduced the different compiler defensive technologies to eliminate potential vulnerabilities, including Klocwork and Coverity static analysis tools, and we also described the address sanitizer and Undefined Behavior Sanitizer.

Advanced Security Unit Testing

In general, for security unit testing, we may need to write a test function to call the target function with different input in order to assess if the target function works properly, especially to test the target function’s ability to handle both external input (#1) and hardware input (#3). One of the biggest problems with this approach is that we don’t know if we have created enough input to cover all cases.

Traditionally, equivalence class partitioning (ECP) is a good approach to remove the redundancy by dividing the input data into a reasonable number of partitions. Then we only need to design test cases to cover all partitions. However, although ECP is necessary, it is not sufficient for the security unit testing because the perquisite of ECP is that we are able to define a set of partitions that cover all cases. This is true for simple functions, but in practice, it might be hard to define a complete set of ECP partitions for a complicated data structure, such as a file system, an executable file format, or a network packet. Therefore, we need some other approach.

Fuzzing

Fuzzing, also known as fuzz testing , is an automated testing technique. The fuzzing tool can generate invalid, unexpected, or random data automatically as the input value for a target function invocation. One of the greatest advantages of fuzzing is that it frees the developer from creating equivalent class partitions or test cases. However, fuzzing has a test oracle problem – a test must be able to distinguish expected (normal) from unexpected (buggy) program behavior. The unexpected behavior includes the following items:
  1. 1)

    Crash, hang, or ASSERT is a simple and objective measurement for fuzzing. If those behaviors are observed, then there must be something wrong in the target function.

     
  2. 2)

    Fuzzing might also trigger some other hard-to-detect problems, such as a buffer out of bound access, use-after-free, or memory leak. In order to catch those failures, fuzzing is usually enabled together with other sanitizers, such as address sanitizer, undefined behavior sanitizer, memory sanitizer, or leak sanitizer.

     
  3. 3)

    Last but not least, fuzzing may cause the program to return unexpected or wrong results. As such, the developer may need to write assertions for the final result or the final state of the program.

     

Fuzzing is a testing technique. It is independent of the execution environment. It can be applied to OS applications, OS kernels, or even firmware. Today, most of the fuzzing tools, such as Peach, American Fuzzy Lop (AFL), and LibFuzzer, run in the OS environment. But firmware runs in a special execution environment, and this environment does not provide the same OS-level application program interface (API) or standard C library. This begs the question: How do we run the fuzzing tool to test the firmware code?

Table 21-2 lists different firmware fuzzing mechanisms .
Table 21-2

Firmware Fuzzing Mechanisms

Mechanism

Effort

Pros

Cons

1. Run firmware function and fuzzing tool in the firmware environment.

Port the fuzzing tool to run in the firmware.

No need to modify the firmware code.

Big effort to port a fuzzing tool.

May have environment limitations, such as memory size, storage size, and CPU processing power.

2. Run firmware function in the firmware environment and fuzz tool in the OS.

a. Create a firmware agent and an OS agent for fuzzing data transmission between firmware and OS via the network.

No need to modify the fuzzing tool.

No need to modify the firmware code.

Time-consuming in fuzzing data transmission.

 

b. Use a hypervisor for data communication between the firmware function and OS fuzzing tool.

Faster than fuzzing data transmission via network.

Need to enable a hypervisor to run the target firmware.

3. Run the firmware function and fuzzing tool in the OS environment.

a. Port the firmware function to the OS and perform fuzzing of it in the OS.

No need to modify the fuzzing tool.

Can fully leverage the instrumented code with the fuzzing compiler.

Fastest solution.

Needs effort to create an OS stub for the firmware function.

 

b. Analyze the firmware binary and perform fuzzing of the target function.

No porting of the firmware is needed.

Binary analysis is time-consuming.

Needs to create a stub function as well.

  1. 1)

    The first option is to port the fuzzing tool from the OS to the firmware environment and run it in the firmware environment. See Figure 21-1. If the fuzzing tool depends upon an OS API, we need to implement a firmware version. Unfortunately, the porting effort is large, based upon our analysis. It also means that continuous porting is required if we need to keep the fuzzing tool up to date. At the same time, we have seen different fuzzing tools created to deal with different scenarios. It is hard to port all of those tools. Even if we can port all of the fuzzing tools, it still might be hard to run the tool because of the limitations of the firmware execution environment, such as ROM size, SRAM size, DRAM size, storage size, and CPU processing power. The fuzzing tool may fail to run or run at a very slow speed.

     
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig1_HTML.png
Figure 21-1

Firmware Fuzzing with Fuzz Tool Running in Firmware

  1. 2)

    The second option is to create an agent in the firmware and let the firmware agent accept the fuzzing data from the OS. An OS fuzzing agent invokes the fuzzing tool to generate the fuzzing data and transfers the data from the OS to the firmware agent. See Figure 21-2. However, we might run into performance problems because some time is wasted in the communication between the OS and UEFI. Fuzzing usually requires a large amount of data generation and usually needs to run for several days. If the time is wasted in the data communication, then the fuzzing is not efficient.

    One possible variant is to enable a hypervisor to run both the target firmware and the OS fuzzing tool on one local machine. It might be faster than communication via the network, but we need to pursue a porting effort in order to let the hypervisor run the firmware.

     
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig2_HTML.png
Figure 21-2

Firmware Fuzzing with Fuzz Agent Communication

  1. 3)

    The third option is to make the firmware target function run in the OS environment and use the OS fuzzing tool directly. Because the firmware function may rely on the firmware-specific services which might not be available in the OS, we need to create the OS stub function to provide this service. If this can be achieved, we can reuse the OS fuzzing tool directly. See Figure 21-3. The advantage of this approach is that we can fully leverage the capability of an OS fuzzing tool, such as instrumented mode, and the host system resources, including memory size, disk storage. and the host CPU execution power. This is the fastest solution.

    One possible variant of this option is that we may fuzz a specific firmware function in a binary in cases where we don’t have the source code. Herein, we must carefully analyze the firmware binary and provide all dependent services. The advantage of this approach is that we can fuzz any firmware binary without having the source code, but we lose the benefit of instrumented fuzzing, and it is much slower than with the source code.

     
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig3_HTML.png
Figure 21-3

Firmware Fuzzing with Firmware Code Running in OS

For EDK II UEFI firmware, we introduced the host-based firmware analyzer (HBFA) feature to provide the capability to build a specific EDK II firmware target function as an OS application and run it with a fuzzing tool. Figure 21-4 shows the HBFA-based fuzzing use case design. The firmware function to be tested is built with the OS stub function and the test main function.
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig4_HTML.png
Figure 21-4

HBFA Fuzz Case Design

Step 1: We run the test application with the OS fuzzing tool.

Step 2: The fuzzing tool generates fuzz data and feeds it to the test main function as an input parameter.

Step 3: The test main function sets up the external input data for the firmware function based upon the fuzzing data.

Step 4: The test main function triggers the firmware function to be tested.

Step 5: The firmware function calls the OS stub function to get the external input. Here the external input can be a communication buffer, a signed firmware capsule image, a PE/COFF executable image, a boot image file, a file system, a network packet, a USB descriptor, a Bluetooth LE advertisement data, and so on. Then the firmware function may call the OS stub to write some output data.

Step 6: The test main function can check the output data based upon the test oracle. At the same time, we can enable the sanitizer for the test application to see if there is any violation at runtime.

Symbolic Execution

Some fuzzing tools require known good seeds for mutation, and the fuzz data is generated based upon the seeds. Even if the seeds are optional, the good seeds can help improve the efficiency of the fuzzing. There are several ways to get seeds. Seeds can be ascertained via an empty seed, downloading seeds from the Internet, using the existing system configuration as the seeds, and using a tool to generate the seeds. The last one can be achieved by a symbolic execution tool, such as KLEE.

During a normal execution session, the program reads a concrete input value and proceeds with the execution flow, such as assignment, addition, multiplication, and conditional branch until generating a concrete final answer. However, during a symbolic execution, the program reads a symbolic value and proceeds with the execution flow. It can still do assignment, addition, and multiplication, though. When it reaches a conditional branch, the symbolic execution can proceed with both branches by forking down two paths. Each path gets a copy of the program state to continue executing independently in a symbolic fashion with a path constraint. The path constraint here means the concrete value which makes the program choose a specific path in the branch condition. When a path is terminated, the symbolic execution computes a concrete value by solving the accumulated path constraints on the branch condition. After all paths are terminated, the symbolic execution collects all concrete values. Those values can be thought of as test cases to cover all of the possible paths for the given program.

Consider a function in Listing 21-1. The symbolic execution takes “buf” as a symbolic input value and assign “buf[0] * 4” to “a” and “buf[4] * 8” to “c.” In the first if branch, it evaluates the “buf[0] * 4 < 0” and forks the two paths: buf[0] < 0 and buf[0] >= 0. In the second if branch, it evaluates the “buf[4] * 8 == 24” and forks two paths: buf[4] == 3 and buf[4] != 3. When the program terminates, the symbolic execution can generate three different cases: (buf[0] < 0), (buf[0] >= 0 && buf[4] == 3), and (buf[0] >= 0 && buf[4] != 3). The final result is useful and important to provide the seed for the fuzzing tool. Without that, it might take a long time to let the fuzzing tool generate a value 3 for buf[4] in order to cover the second path.
===========================
int test (int *buf)
{
  int a = buf[0] * 4;
  int c = buf[4] * 8;
  if (a < 0) {
    return -1;
  } else {
    if (c == 24) {
      return 4;
    } else {
      return 1;
    }
  }
}
===========================

Listing 21-1.

Symbolic execution is a way to analyze a program in order to determine what inputs cause each part of a program to execute. After the symbolic execution is finished, it can generate test cases to cover all possible paths for the program. Those test cases can be treated as seeds for the fuzzing tool. Then the fuzzing tool can mutate the fuzzing data based upon those seeds. See Figure 21-5 for the solution to combine the KLEE symbolic execution as the seed generator and the AFL fuzz tool as the fuzzing engine.
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig5_HTML.png
Figure 21-5

AFL Fuzz + KLEE

Symbolic execution is a good method to generate the seed. However, it has limitations. Because symbolic execution needs to fork the state based on each branch condition, the number of states to be maintained grows exponentially with the increase in the program size. For a complex program, it may take a long time to generate the final result for all paths. In some cases, it might run into infinite loops or generate an exception.

Formal Verification

Formal verification is a technology to prove the correctness of code in a system with respect to certain specifications or predefined properties by using formal method of mathematics. The approach of the formal verification is model checking, which is a systematically exhaustive exploration of the mathematical model. Symbolic execution is one implementation method that can be used in the formal verification. Spin is an open-source software verification tool, used for the formal verification of multi-threaded software applications. C bounded model checker (CBMC) is a bounded model checker for C and C++ programs which verifies memory safety (which includes array bounds checks and checks for the safe use of pointers), checks for exceptions, checks for various variants of undefined behavior, and user-specified assertions.

A model checking tool is to check if a given model satisfies a set of program properties. Take Listing 21-2 as an example. This program parses the input argc and argv. At the end of the program, it adds one assertion as the property. When we run CBMC for this program, CBMC will report if the assertion can succeed or not. Besides assertions, CBMC also includes build-in instrumentation options, such as bounds check, pointer check, and overflow check etc to catch the potential memory safety issues.
#define MAX_NUM 100
void process_arg (void *arg)
{
  // do something
}
int main(int argc, void **argv) {
  int x = argc;
  int i = 0;
  while (i < argc && i < MAX_NUM && i >= 0) {
    process_arg (argv[i]);
    i = i + 1;
    x = x - 1;
  }
  __CPROVER_assert(x >= 0, "postcondition");
}
===========================
Listing 21-2

===========================

There are not many software projects passing formal verification, because of the complexity of the software programs. Writing a complete set of program properties is also challenging work. Some operating systems have been verified, such as secure embedded L4 kernel (seL4). Some network stacks are also verified, such as verified HTTPS in project Everest.

Design for Security Test

In order to make the symbolic execution and fuzzing more efficient, we need to consider how we should write a program:
  1. 1)

    We begin by writing small functions and testing the small functions. A small function is easy for code review and testing. For example, in the EDK II UEFI variable driver MdeModulePkg/Universal/Variable/RuntimeDxe/Variable.c, the UpdateVariable() routine has 600 lines of code and 65 “if” branches. One of the “if” branches misses the “else” branch handling and causes the timestamp field to be filled with a wrong value. This issue had been there for several years. We performed several rounds of code reviews and testing, but no one found this problem. Sixty-five “if” branches in one function is overly complicated.

     
  2. 2)

    Next, consider combining all input data verifications into one check function and testing the check function itself. Some programs have multiple functions to accept the external input. We refer to those functions as boundary functions. We notice that the boundary function may perform some sanity checks against the external input and subsequently pass control to the business logic function for data processing. See Figure 21-6. A potential risk of this approach is that each boundary function does the check in its own function. If there is one security issue exposed in one boundary function, we have to review all other boundary functions to see if there is a similar issue. Unfortunately, that is time-consuming and people may make mistakes. For example, the first Microsoft ANI bug issue (CVSS 9.3 HIGH, MS05-002) was found in 2005. But after the fix, the same issue (MS07-017) was found in 2007. They shared the same root cause, and it coincidentally happened in two different functions.

     
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig6_HTML.png
Figure 21-6

Test for the Security Boundary Function

In order to reduce the risk, we should consolidate all external input checks into one single check function. All boundary functions can call this check function to perform the data sanity check. We should perform the security review and security test for this check function in order to ensure it is robust enough to handle all of the different cases. If there is an issue in the check function, we just need to fix this check function in one place. The security test can be focused on the check function. See Figure 21-7.
../images/488723_1_En_21_Chapter/488723_1_En_21_Fig7_HTML.png
Figure 21-7

Test for the Security Checker Function

  1. 3)

    Next, let all external boundary functions call the check function and test the execution flow. It is good to define a centralized check function. We need to ensure that all boundary functions invoke the same check function before transferring control to the business logic function by having a security test for the execution flow.

     

Summary

In this chapter, we introduced the security unit test. The chapter covered the security unit test plan for the eight high-risk areas in the firmware – external input, race conditions, hardware input, secret handling, register locks, secure configuration, replay/rollback, and cryptography. Then we introduced two advanced security unit test methods – fuzzing and symbolic execution – and we provided a suggestion for the check function. In the next chapter, we will introduce the security validation and penetration test methodology for the firmware.

References

Book

[B-1] Brian Chess, Jacob West Secure, Programming with Static Analysis, Addison-Wesley Professional, 2007

[B-2] Michael Sutton, Adam Greene, Pedram Amini, Fuzzing: Brute Force Vulnerability Discovery, Addison-Wesley Professional, 2007

[B-3] James A. Whittaker, Hugh Thompson, How to Break Software Security, Addison-Wesley Professional, 2003

Conference, Journal, and Paper

[P-1] John Neystadt, “Automated Penetration Testing with White-Box Fuzzing,” Microsoft, 2008, available at https://docs.microsoft.com/en-us/previous-versions/software-testing/cc162782(v=msdn.10)

[P-2] Yuriy Bulygin, Oleksandr Bazhaniuk, “Discovering vulnerable UEFI BIOS firmware at scale,” in 44CON 2017, available at https://github.com/abazhaniuk/Publications/blob/master/2017/44CON_2017/Bulygin_Bazhaniuk_44con.pdf

[P-3] Sugumar Govindarajan, John Loucaides, “The Whole is Greater,” in NIST CSRC 2015, https://csrc.nist.gov/CSRC/media/Presentations/The-Whole-is-Greater/images-media/day1_trusted-computing_200-250.pdf

[P-4] Brian Richardson, Chris Wu, Jiewen Yao, Vincent J. Zimmer, “Using HBFA to Improve Platform Resiliency,” Intel White paper, 2019, https://software.intel.com/sites/default/files/managed/6a/4c/Intel_UsingHBFAtoImprovePlatformResiliency.pdf

[P-5] Marek Zmysłowski, “Feeding the Fuzzers with KLEE,” in KLEE workshop 2018, available at https://srg.doc.ic.ac.uk/klee18/talks/Zmyslowski-Feeding-the-Fuzzers-with-KLEE.pdf

[P-6] Oleksandr Bazhaniuk, John Loucaides, Lee Rosenbaum, Mark R. Tuttle, Vincent Zimmer, “Symbolic execution for BIOS security,” in USENIX WOOT 2015, available at www.markrtuttle.com/data/papers/bazhaniuk-loucaides-rosenbaum-tuttle-zimmer-woot15.pdf

[P-7] Zhenkun Yang, Yuriy Viktorov, Jin Yang, Jiewen Yao, Vincent Zimmer, “UEFI Firmware Fuzzing with Simics Virtual Platform,” in DAC 2020, available at http://web.cecs.pdx.edu/~zhenkun/pub/uefi-fuzzing-dac20.pdf

[P-8] Christopher Domas, “God Mode Unlocked Hardware Backdoors in X86 CPUs,” in BlackHat 2018, available at http://i.blackhat.com/us-18/Thu-August-9/us-18-Domas-God-Mode-Unlocked-Hardware-Backdoors-In-x86-CPUs.pdf

[P-9] Byron Cook, Kareem Khazem, Daniel Kroening, Serdar Tasiran, Michael Tautschnig, Mark R. Tuttle, “Model checking boot code from AWS data centers,” Formal Methods in System Design, 2020, https://link.springer.com/content/pdf/10.1007/s10703-020-00344-2.pdf

Web

[W-1] Using HBFA to Improve Platform Resiliency,

https://software.intel.com/en-us/blogs/2019/02/25/using-host-based-analysis-to-improve-firmware-resiliency

[W-2] HBFA, https://github.com/tianocore/edk2-staging/tree/HBFA

[W-3] libfuzzer, https://llvm.org/docs/LibFuzzer.html

[W-4] Peach fuzzer, http://community.peachfuzzer.com/v3/Test.html

[W-5] AFL fuzzer, http://lcamtuf.coredump.cx/afl/

[W-6] KLEE, https://klee.github.io/

[W-7] STP, http://stp.github.io/

[W-8] Z3, https://github.com/z3prover/z3

[W-9] metaSMT, https://www.informatik.uni-bremen.de/agra/eng/metasmt.php

[W-10] mini SAT, http://minisat.se/

[W-11] Verifying Multi-threaded Software with Spin, http://spinroot.com/spin/whatispin.html

[W-12] Bounded Model Checking for Software, https://www.cprover.org/cbmc/

[W-13] Low-Level Bounded Model Checker (LLBMC), http://llbmc.org/

[W-14] Symbiotic, https://github.com/staticafi/symbiotic

[W-15] seL4, https://sel4.systems/

[W-16] Project Everest, www.microsoft.com/en-us/research/project/project-everest-verified-secure-implementations-https-ecosystem/, https://project-everest.github.io/

[W-17] Microsoft Security Bulletin MS05-002, available at https://docs.microsoft.com/en-us/security-updates/securitybulletins/2005/ms05-002

[W-18] Microsoft Security Bulletin MS07-017, available at https://docs.microsoft.com/en-us/security-updates/securitybulletins/2007/ms07-017

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.156.122