depthcharge.checker

This subpackage provides functionality for building U-Boot “security checker” tooling.

class depthcharge.checker.ConfigChecker(uboot_version: str, enable_builtins=True)

Create a new configuration checker instance.

The uboot_version parameter should list the U-Boot version being inspected. This will be used to inform version-specific checks, such as whether a particular feature has CVEs associated with it.

Examples of acceptable version strings:

  • 2020.10 (A release version)

  • 2021.01-rc1 (A release candidate)

  • v2020.10 (The `v` prefix is ignored)

  • 1.3.0 (U-Boot version prior to use of `YYYY.MM[-rcN]` convention)

With enable_builtins set to its default value of True, the configuration handlers built into Depthcharge will be used.

Set this to False if you want the configuration checker to only report risks identified by handlers registered via register_handler().

register_handler(config_key: str, match, security_risk, user_data=None)

This method allows UBootConfigChecker to be extended in order to report additional configuration-related security risks, beyond those built in to Depthcharge. For instance, one could leverage this to maintain their own out-of-tree collections of SecurityRisk definitions and corresponding handlers specific to certain silicon or product vendors’ U-Boot forks.

Given a configuration item named config_key (e.g. 'CONFIG_SOME_FEATURE'), the match parameter shall determine whether the specified security_risk – an instance of the SecurityRisk class – shall be reported.

The match parameter may be one of the following:

  • A boolean (True or False) value. A value of True corresponds to a configuration value being enabled and False if it is disabled. This should only be used for options that have a boolean state.

  • A string that is compared with the configuration value in question. The comparison is case-sensitive.

  • A compiled regular expression. This can be used to match a value whose format is known in advance, but not easily matched with a string literal.

  • A callable function or method that takes two parameters: the configuration value and the user_data parameter supplied to this method. The registered handler must return True if the security_risk should be reported and False otherwise. This provides the most flexibility, with a bit more complexity.

For SecurityRisk objects that contain affected version ranges, the provided match criteria will only be evaluated if the U-Boot version provided to the ConfigChecker constructor falls within the relevant range.

Handlers may be registered either before or after a call to load(). The latter allows allows function-based handlers to have access to configuration information (by way of user_data) collected when the checker parses the file.

More than one handler can be registered for a given configuration item. All of the handlers will be executed, allowing a single configuration item to have multiple risks associated with it.

load(filename: str) dict

Subclasses implement this function to load the relevant file, parse it, and return a dictionary describing the U-Boot configuration settings.

The keys in this dictionary are the CONFIG_* items ingested from the loaded file. The value associated with each key is a tuple: (cfg_val, source).

The cfg_val is True or False for boolean configuration items. Otherwise, it is a verbatim string from the source file, containing any quotation marks.

The source item is a string describing where cfg_val was obtained from (e.g. line number).

audit()

Audit the loaded file(s) and return a Report instance containing any identified potential security risks.

class depthcharge.checker.UBootConfigChecker(uboot_version: str, enable_builtins=True)

Inspect U-Boot build configurations present in .config files produced by more modern, Kconfig-driven U-Boot builds. (i.e. those produced from make <platform>_defconfig)

load(filename: str) dict

Load and parse the specified U-Boot build configuration file and return a dictionary in the format defined by ConfigChecker.load().

The provided file should be a .config file produced by running make <platform>_defconfig within the U-Boot codebase - not just the platform’s defconfig file, doesn’t include all the configuration items inherited by default settings.

Calling load() multiple times will aggregate the configurations present across all loaded files. When re-defined configuration items are encountered their values are ignored and a warning is printed.

class depthcharge.checker.UBootHeaderChecker(uboot_version: str, include_paths: list, config_defs: Optional[dict] = None, cpp: Optional[str] = None, dummy_headers: Optional[list] = None, enable_builtins=True)

Inspect a platform’s U-Boot build configuration header.

This is applicable for older U-Boot version. See UBootConfigChecker when working with version that have been more fully migrated to Kconfig.

If you find yourself with a version of U-Boot that has some CONFIG_* items defined in Kconfig files, and other in headers – worry not!

You should first use a UBootConfigChecker. Pass the configuration dictionary returned by UBootConfigChecker.load() to this class’s constructor via the config_defs keyword argument. These definitions may provide additional definitions that will be used when preprocessing the target platform header file.

From there, you can invoke each checker’s audit() method and then merge their reports. (See Report.merge().)

The include_paths parameter should contain a list of paths to search for header files when #include directives are encountered.

Should you encounter problematic header files that prevent preprocessing from completed, but are not of interest to you, you can specify their names in the dummy_headers list. This class will automatically create empty files in a temporary directory, placed at higher precedence in the include search path. You may supply eithr single file names (regs.h) or relative paths (asm/arch/imx-regs.h) – whichever is needed to fulfil a failing #include. (Note: Paths with ``..`` are not currently supported.)

By default, this class will invoke the cpp program located in one’s PATH. If you would like to use a different preprocessor program, you may override this using the cpp keyword argument. Depthcharge assumes that it will take the same arguments, however.

load(filename: str) dict

Load and parse the specified U-Boot platform configuration header file and return a dictionary in the format defined by ConfigChecker.load().

Calling load() multiple times with different files will aggregate the configurations present across all loaded files.

class depthcharge.checker.Report

A set of of SecurityRisk objects that can be exported to multiple formats. This class can be used to aggregate results from custom “checkers” and tools.

The number of results currently within the Report can be determined via len(report_instance).

add(risk) bool

Record a new security risk. This argument should be either a SecurityRisk object or a dictionary that can be passed to the SecurityRisk constructor.

Each added SecurityRisk must have a unique identifier in order to be treated as a unique result. If a SecurityRisk with a given identifier is already in the report, no change will be made. The previously added item will remain in place.

This methods returns True if the SecurityRisk was newly added. False is returned when risk is a duplicate of an already-recorded item.

security_risks(high_impact_first=True)

This is a generator that provides each of the SecurityRisk objects contained in the Report.

The default value of high_impact_first=True ensures that the SecurityRisk objects are ordered highest-to-lowest impact.

Bear in mind, however, that “risk” and “impact” are just generalizations – ultimately the target device’s threat model and surrounding context actually define these. (Use your brain, don’t just rely upon automated tools like this one.)

It is also possible to iterate over the report in a highest-impact-first order as follows:

merge(*others)

Merge SecurityRisk items from one or more Report instances (others) into this report.

Items having the same SecurityRisk.identifer value will not be duplicated. Only new items from other will be added. (i.e. it’s a set union)

The |= operator has the same effect.

report |= other
save_csv(filename: str, write_header=True, columns=None, **kwargs)

Write checker results to a CSV file with the name specified by filename.

If filename is None or '-', the CSV is written to stdout.

A header row will be written unless write_header is set to False.

The columns argument controls the order and presence of columns in the produced CSV. This should be provided as a tuple of strings. Below are the supported names. Those followed by an asterisk are note included by default when columns=None

  • Identifier

  • Impact

  • Source

  • Summary

  • Description (*)

  • Recommendation (*)

Any additional keyword arguments are passed to csv.writer().

save_html(filename: str, write_header=True, columns=None, **kwargs)

Write checker results to a simple HTML file with the name specified by filename.

If filename is None or '-', the output is written to stdout.

A header row will be written unless write_header is set to False.

The columns argument controls the order and presence of columns in the produced HTML table. This should be provided as a tuple of strings. Below are the supported names. Those followed by an asterisk are note included by default when columns=None

  • Identifier

  • Impact

  • Source

  • Summary

  • Description (*)

  • Recommendation (*)

Additional keyword arguments are ignored, but not used at this time. This API reserves keyword arguments named with a single underscore prefix (e.g. _foo='bar') for internal use.

save_markdown(filename, **kwargs)

Writer checker results to a Markdown file with the name specified by filename.

Additional keyword arguments are ignored, but not used at this time. This API reserves keyword arguments named with a single underscore prefix (e.g. _foo='bar') for internal use.

class depthcharge.checker.SecurityRisk(identifier, impact, source, summary, description, recommendation, affected_versions: Optional[tuple] = None)

Encapsulates information about a potential security risk.

Important

Identifiers must be unique, as these are used to differentiate one SecurityRisk from another when coalescing results.

property identifier: str

A short string that uniquely identifies a specific security risk.

property summary: str

A concise summary of the security risk, suitable for use as a title heading.

property impact

The potential impact of a security vulnerability, encoded as a SecurityImpact.

property impact_str: str

The potential impact of a security vulnerability, represented as a string of one or more abbreviations.

property source: str

A string denoting the file and/or location that was used to deduce the presence of a potential security risk.

property description: str

A string that provides a more detailed explanation of the potential security risk - what it is, why it matters, and how it could impact stakeholders and users.

property recommendation: str

A string containing high-level and generic guidance for eliminating or mitigating a reported security risk.

This is almost never appropriate as one-size-fits-all guidance; all products and threat models vary in their own unique ways. In many cases, the recommendation is “just turn this off”, which obviously doesn’t account for how any relevant requirements (e.g. for failure analysis of returned units) can be satisfied in a more secure manner.

Instead, consider is this a starting point for further discussion about remediation efforts.

to_dict() dict

Return the SecurityRisk object as a dictionary with the following keys, each associated with a value of the listed property.

classmethod from_dict(d: dict)

Create a SecurityRisk from a dictionary.

Refer to to_dict() for the supported key-value pairs.

applicable_to_version(version: str) bool

Returns True if this security risk may be applicable to the specified U-Boot version, and False otherwise.

class depthcharge.checker.SecurityImpact(value)

Enumerated flags (enum.IntFlag) that describe the impact of a SecurityRisk.

The integer value of each flag roughly increases with impact. Of course, context matters and ultimately the context surrounding a device and its threat model ultimately define this. Think of this as simply an approximation that can be used to sort lists in a reasonable manner.

NONE = 0

No immediate security risk; informational note.

UNKNOWN = 1

The security impact is unclear or otherwise not yet known.

DOS = 2

The system is rendered temporarily inoperable and must be power cycled to recover.

PDOS = 4

The system is rendered permanently inoperable and cannot be recovered by users. In more severe cases, the system may not be recoverable even with vendor support.

INFO_LEAK = 64

Behavior discloses information that may aid in reverse engineering efforts or exploiting security vulnerabilities on the system.

ATTACK_SURFACE = 128

Feature or behavior increases bootloader’s attack surface beyond that which is necessitated by its functional requirements.

RD_MEM = 256

Operation can be abused to read memory at an attacker-controlled address.

LIMITED_WR_MEM = 512

Operation can be abused to perform a limited, but attacker-controlled memory write. The address and/or value written are constrained, or the write is otherwise unlikely to result in arbitrary code execution due to other mitigating factors.

WEAK_AUTH = 1024

Authentication mechanism or underlying algorithm contains known weaknesses or its use is otherwise inconsistent with modern security standards and best practices.

VERIFICATION_BYPASS = 65536

One or more security-critical verifications (or operations) can be bypassed.

EXEC = 8388608

Operation can be abused to directly execute arbitrary code, provided that there exists a means to load code.

WR_MEM = 16777216

Operation can be abused to read write memory at an attacker-controlled address, potentially leading to execution of attacker-supplied code.

describe(html=False) str

Return a string describing the aggregate security impact.