HOLIDAY OFFER: Get the gift of up to $70 of Bitcoin. While supplies last!

Shop now

Donjon | 08/10/2022

Integrating fault injection in development workflows

Fault injection vulnerabilities can be tedious to evaluate even with the right tools and experts. Could we improve this situation by giving appropriate open-source tools to developers?

As a first step toward mitigating fault injection attacks, we introduce a new open-source evaluation tool that painlessly integrates with an IDE or continuous integration testing pipelines. We illustrate the usage of this tool using the Rust programming language.

Table of contents

Fault injection simulation with Rainbow

Fault injection effects are usually simulated as corruptions during register write (bit stuck-at model) or as instructions skips1. Security-critical embedded devices such as smart cards and hardware wallets need to be hardened using these models to guarantee that these effects do not introduce vulnerabilities in their processing.

Electromagnetic fault injection setup using a Scaffold board and a SiliconToaster.

Ledger Donjon has been developing the open-source Python side-channel and fault injection simulator called Rainbow since 2019. We recently added first-class support for simulating fault injection attacks by providing fault models:

  • fault_skip models fault attacks causing one instruction to be skipped during execution,
  • fault_stuck_at models fault attacks causing the destination register of one instruction to be overridden with a faulty value during execution. A “stuck-at zeros” model is often referred to as a bit-reset attack, and a “stuck-at ones” model is often referred to as a bit-set attack.

Using these models with Rainbow makes it possible to quickly simulate the effects of different fault injection attacks without needing access to expensive equipment, as seen above, but also removing uncertainties and measure effects like jitter from the equation. It also opens up the ability to check exhaustively that a given fault model cannot be applied to a given code.

Let’s illustrate the usage of the fault_stuck_at model on the third instruction of a PIN verification process taken from an older Trezor firmware:

We successfully faulted the output of the PIN code comparison function: rather than returning 0 as expected (1874 != 0000), it returned 1.

We can find all instructions vulnerable to a single-fault attack with these fault models if we iterate this fault simulation on every instruction. However, this method cannot find vulnerabilities caused by fault injection effects that are not modelled. Another shortcoming is that we do not expect that firmware developers will write Python code for each piece of critical code they need to harden anytime soon.

Integration in Rust development workflows

Developers are accustomed to using code style checking and testing pipelines in their daily workflows. Taking inspiration from how these tools are used, we propose a new tool called fi_check that checks for potential fault injection vulnerabilities. This tool was designed to be easily embeddable in an IDE or continuous testing pipelines, enabling developers to be alerted by code modifications that introduce single-fault injection vulnerabilities.

We only considered single-fault injection attacks to simplify the problem. A naive generalization to N�-fault injection attacks would exponentially increase the evaluation time. Protecting code against single-fault injections is still an important goal as it makes potential attacks much harder.

Writing fault injection evaluation tests in Rust

Let’s consider that compare_pin is a security-critical function that needs to be hardened against single-fault injection attacks. To define the expected behavior of this function and prepare it for automatic fault injection evaluation, one may append to their Rust source code:

This structure looks like classical Rust tests asserting that the compare_pin function returns false as the PIN codes do not match. fi_check can recognize these tests and evaluates whether it can make compare_pin return true by faulting its instructions. We do not use #[test] macro as we just need the function symbol to exist in the compiled binary to execute it later with Rainbow.

Thanks to this tool, Rust crates can be quickly evaluated for potential vulnerability to single-fault injection by:

  • Adding the rust_fi crate to their project dev-dependencies,
  • Writing fault injections robustness tests using the above structure,
  • Running fi_check.py on the crate.

By default, fi_check.py instantiates a Rainbow emulator configured for ARM targets, but this can be easily changed to target other architectures.

How does it work?

Successful fault injection detection:

We consider a function taking no arguments and returning one Boolean value (true or false). This function logic is written to always returns false by checking an invalid condition2. In theory, this function should always return false. However, if we execute this function on real hardware, it can result in 3 different states:

  • Nominal behavior: the code returned false as expected,
  • Faulted behavior: the code returned true, meaning the disrupted execution caused the check to be skipped,
  • Panicked or crashed: an exception was raised during the execution, such as an out of bounds, or the device got an unexpected instruction and crashed.

Healthy hardware not under extreme conditions should always behave in the nominal behavior. In our case, we want to detect if an attack creating a single fault in the processing would be able to get a faulted behavior without raising an exception or crashing the device.

assert_eq! is a macro that raises a panic if operands differ. We can distinguish between these 3 states by using a modified assert_eq! macro in Rust.

Proposed evaluation algorithm:

We choose one of the proposed fault models, then:

  • We execute the function multiple times, but in each run, we apply the chosen fault model on the i�-th instruction. i� starts from the first instruction and increments until we reach the end of the function.
  • When the function returns true without panicking or crashing, we know which instruction makes the function vulnerable to this fault model.

If the developer is not directly working on assembly code, we use addr2line tool3, which can retrieve which line of code generated the problematic assembly instruction. This requires to compiling the code with debug symbols4.

Examples of code evaluation and mitigation

We will illustrate the usage of this tool with some pieces of code that are vulnerable to single-fault injection attacks once compiled to ARM Cortex-M3 assembly (ARM Thumb). A commonly used function for this kind of benchmark is the critical PIN code comparison.

Example 1: imperative-style PIN code comparison

Let’s consider the following PIN code comparison function written in an imperative-style Rust code:

The compiler outputs the following assembly code:

As expected by the calling convention used by Rust, user_pin array is represented by a pointer in r0 and a size in r1ref_pin array is represented by a pointer in r2 and a size in r3 and the returned value is represented by r0.

We run ./fi_check.py --cli test_fi_simple to check for any interesting faults:

The output indicates vulnerable instructions in the test_fi_simple function, which is the test function calling compare_pin, so we can ignore these. It also indicates that this function is vulnerable to a bit-set fault attack. When looking at the source code, we understand that this is due to the developer initializing the returned value to true, then setting it false during comparison. This vulnerability exploitation consists in setting good=0xFFFFFFFF in the last iteration of the loop, which Rust considers to be equivalent to true.

On a side note, we also observe that the Rust compiler makes the code panic if user_pin array is accessed out of bounds (checked at 0x90) as expected from a memory-safe language.

Hardening through double call and inlining:

This compare_pin function is vulnerable to a simple fault attack. A common mitigation is to simply execute the test twice.

Running an evaluation with fi_check on this function confirms that we successfully hardened it:

Hardening using a protected Boolean type:

Boolean values are usually encoded on the first bit of a register, meaning that “stuck-at” fault injection attacks can flip its value. A method to harden these values against fault injection vulnerabilities is to change the representation of “true” and “false”. We choose the following representation on 32-bit:

This enables us to use the 31 extra bits to do error checking. We implemented these checks as Bool Rust type.

We can then use it in our PIN verification function:

fi_check confirms that this method works:

Example 2: functional-style PIN code comparison

Sometimes it can be difficult to predict how a function will be assembled. For illustration purposes, let’s switch to functional-style code:

The compiler outputs the following assembly code:

Our tool can find 9 vulnerable points, 4 vulnerabilities with the fault_skip model, 3 with the stuck_at_0x0 model and 2 with the stuck_at_0xFFFFFFFF model:

Hardening using a protected Boolean type:

Let’s use the protected Boolean type that we described earlier:

Using the protected Boolean type, we are now down to 2 vulnerable instructions. These last two vulnerabilities are due to an early size check on the input that makes the function return true if one array is empty. In our context, we should handle these cases manually.

Now our tool no longer finds any vulnerable instructions, voilà!

Conclusion

We published the evaluation script and associated Rust crates at https://github.com/Ledger-Donjon/fault_injection_checks_demo/.

We show that we are able to simulate the effect of modelled fault injection attacks using Rainbow. Then we tightly integrate this simulator with the Rust ecosystem to demonstrate a scenario where these evaluations are relatively easy to set up for developers.

To demonstrate the integration of such tools in workflows, we opened a pull request that introduces a vulnerability: https://github.com/Ledger-Donjon/fault_injection_checks_demo/pull/13. The automated checks fail due to fi_check finding a vulnerability.

Such a tool enables developers to design new Rust types hardened against fault injection attacks. We propose an early design of a protected Boolean type and Protected struct that hardens PartialEq traits.

  1. M. Otto, “Fault attacks and countermeasures.” Ph.D. dissertation, University of Paderborn, 2005 
  2. We consider that the compiler does not optimize the condition. This can always be enforced with a few tricks if needed, for example with https://doc.rust-lang.org/std/hint/fn.black_box.html 
  3. From GNU Binutils, available in most GNU/Linux distributions. A cross-platform version can also be installed from https://github.com/gimli-rs/addr2line
  4. We use the release profile in Rust with debug=true. This does not increase the final binary size on flash for embedded binaries. 

Stay in touch

Announcements can be found in our blog. Press contact:
[email protected]

Subscribe to our
newsletter

New coins supported, blog updates and exclusive offers directly in your inbox


Your email address will only be used to send you our newsletter, as well as updates and offers. You can unsubscribe at any time using the link included in the newsletter.

Learn more about how we manage your data and your rights.