Embedded Software Security Testing Guide
Embedded Software Security Testing Guide
Embedded Software
Security Testing
1/13 page
What to Expect
2/13 page
Practices for Embedded Software
Security Testing
In embedded software, we need to make sure that growing complexity and
software dependency come at no cost to security.
Many languages used in embedded, such as C/C++ are dependent on the hardware
and operating system and can’t be cross-compiled. This leads to two significant
problems:
3/13 page
1. Although the Public API documentation is available, you need to write
plenty of test harnesses, which is incredibly time-consuming.
Moreover, security tests have to prevent the application from crashing under all
circumstances. They should cover all relevant states and behaviors, including edge
cases caused by unexpected or erroneous inputs.
There are many potential attack vectors that can be exploited in an embedded
system, such as buffer overflows, memory corruption, and code injection. It can be
difficult to identify all the ways that an attacker could potentially exploit a system due
to the vast number of vulnerabilities. Coverage-guided testing can help uncover many
of these vulnerabilities.
4/13 page
3. Time and Resource Constraints
There are proven-effective approaches for embedded software testing at different
stages of the development process, such as unit tests, regression tests, HW/SW
integration and integration tests. However, time and resource constraints may force
teams to omit some of these testing efforts or run them on a reduced scale.
Simulating real-world usage requires testing a system under various conditions and
with different input types. Automated testing tools can help you ensure sufficient
testing despite time constraints.
5/13 page
7. Security vs. Safety
Embedded systems require a balance of security and safety. However, these two
objectives can be conflicting. Safety focuses on fail-safe behaviour and reliability,
while security focuses on protecting sensitive data from unauthorized access.
However, the complexity introduced by security features can impede the testing
process and impact safety. Proper tooling is necessary for embedded software teams
to identify and address both topics.
Two popular methods for performing static code analysis in C++ are linting tools and
utilizing compiler tools.
6/13 page
Linting
Linting is a process of checking source code for bugs, defects, and formatting issues,
such as unused libraries in C/C++ or other languages. It's useful for finding errors
when updating compilers and ensuring style guidelines are followed to prevent
miscommunication on software projects. There are specialized linters for C++, such
as Clang-tidy, which finds and automatically fixes issues, and the IntelliSense Code
Linter developed by Microsoft for Visual C++.
• Black-box approaches for dynamic analysis do not require the source code of
an application to test it
• Code can be executed on both production and development environments for
comparison
• Detecting runtime issues
• Testing with real integrations and dependencies
Two popular methods for dynamic code analysis are unit testing and fuzz testing.
7/13 page
Unit Testing
Unit testing is a technique in which automated tests are written by developers to
evaluate the functionality of a specific unit of code (e.g., a function or class). There
are various approaches to unit testing, such as test-driven development, during
which tests are written first to guide the development process. Another approach is
using unit tests as regression tests to identify bugs introduced into previously
working code. Unit testing is particularly useful for C/C++ developers working on
large projects, as it allows you to test specific portions of the codebase without
having to compile the entire project. In C++ for example, CppUnit, Catch2, and Boost
are popular solutions for unit testing. Visual Studio also provides a built-in C++
testing solution.
Fuzz Testing
Fuzz testing is a dynamic analysis method that involves feeding invalid or random
data, known as "fuzzy" data, into the software under test and observing how it
behaves. Similar to unit testing, the software is tested under various scenarios. The
key difference is that unit testing is deterministic, meaning that the expected
outcome is known in advance, while fuzz testing is non-deterministic or exploratory,
meaning that the expected outcome is not necessarily known ahead of time. This
makes fuzz testing useful for uncovering bugs and security vulnerabilities that may
not have been identified through other testing methods.
8/13 page
Automating Test Case Creation With Fuzz Data
In my experience, writing test cases that adequately cover relevant program states
and behaviours manually takes time and bears the risk of missing things. A mocking
approach based on fuzz data is a proven effective way to speed things up and
pinpoint issues more accurately. Instead of manually trying to come up with test
cases, this approach uses fuzz data to automatically generate invalid, unexpected or
random data as input for your embedded application. This way, you can simulate the
behaviour of your embedded software units under realistic circumstances.
To make sure that your fuzzer is not just throwing random test inputs at your
application, you should opt for tools that are capable of leveraging information about
the software under test to refine fuzz data. Such a so-called “white-box” (or grey-box)
fuzzing approach enables your fuzzer to maximize test coverage based on runtime
information from previous inputs and thereby trigger (almost) all relevant program
states.
This approach to embedded software security testing is much faster and more
accurate than any form of manual testing or dumb fuzzing. Below I explained this in
more detail.
9/13 page
Mocking on Steroids: Testing Positive and Negative Criteria
With Fuzz Data
To test an application with dynamic inputs, you would usually have to compile and
run it first. But with embedded software, you often have the problem that it only runs
on specific hardware or that the application requires input from external sources. For
this reason, mocking/simulating these dependencies is needed for DAST, IAST, or
fuzzing.
A bare-bones approach to testing embedded systems can also be mocking for the
hardware-dependent functions to return static values. However, this setup is
basically blind, as it lacks runtime context, causing it to miss many possible
behaviours, resulting in comparably low code coverage.
Two Excel sheets can be enough to generate a fuzz test that will achieve high code coverage
while using fuzz data to simulate the input from outer sources
10/13 page
The Secret Sauce to Fuzzing Embedded Software:
Instrumentation
One of the reasons why modern fuzzing technology is so effective is that it can
leverage feedback from previous test inputs to increase code coverage. To achieve
this, the source code has to be instrumented using sanitizers. A sanitizer is a
software library used to compile code with the goal of making programs crash more
often. By receiving information about the program under test from these sanitizers,
the fuzzing engine can constantly craft more effective test inputs.
Your fuzzer then starts by calling functions from your Public API documentation file
in random order, with random parameters. Through instrumentation, the fuzzer can
then gather feedback about the covered code. Based on this feedback, it can actively
adapt and mutate the called functions and parameters to increase code coverage
and trigger more interesting program states. This setup will allow you to generate
test cases you might not have thought of that can traverse large parts of the tested
code. With modern open-source tooling, fuzz testing can be simplified to a point
where it only takes a few commands to have a fuzzer up and running within your CLI.
11/13 page
3 Reasons Why You Should Fuzz Embedded Systems
In my experience, fuzzing is one of the most effective ways to test embedded
systems. Especially because the margin of error is very small in many embedded
industries, as software defects do not only affect the functionality of systems, they
can have a physical impact on our lives (e.g., in automotive brake systems).
Example: If you find a bug in a JSON parser during unit testing, you can most likely
fix it in a couple of minutes. This is relatively easy compared to fixing a crash that
was possibly caused by a specific user input that only affects one certain
component. So do yourself a favour and fix the bugs before they pop up in
production.
12/13 page
Enterprise Best Practice to Deal With Complexity in
Embedded Software: CI/CD-Integrated Fuzz Testing
Many of the things that make fuzzing so useful are available within open-source
tooling. In fact, you could have your own fuzz test running locally in less than five
minutes by using our open-source CLI tool for C/C++.
Anyways, if you want to find out more about how you can set up fuzz testing for
large automotive projects, you can book a personal demo with one of my colleagues
or check out our product page. If you want to get started right away, I recommend
checking out CI Fuzz CLI.
CI Fuzz CLI is an open-source solution that lets you run feedback-based fuzz tests from your
command line with three simple commands: https://www.code-intelligence.com/cli-tool
13/13 page