
Nowadays, cars rely on an interconnected network of computers in addition to mechanical systems. A modern vehicle can have more than 50 embedded systems handling braking, steering, and sensors, among other tasks. These devices run on firmware, which needs updates to patch flaws and improve safety. But an update system also creates a new way for things to go wrong. For instance, someone could maliciously modify an update, downgrade to a vulnerable version, or make the device run untrusted code.
At MIT’s Beaver Works Summer Institute, I addressed this problem on a smaller scale with a secure bootloader for a Tiva microcontroller modeled as an automotive control unit. Though it didn’t control a real vehicle, it forced me to work on the same question: how do you safely update firmware on a small embedded device when the update channel and even the hardware itself could be attacked?
This was a group project, and my main work was on the architecture, the firmware file format, and the bootloader verification logic. We submitted our project as part of an embedded Capture The Flag (eCTF) mini-competition, where teams earned points by finding and exploiting vulnerabilities in each other’s designs.
I worked to design a bootloader in C, paired with a Python packaging and updater tool. The Python side encrypted and packaged the firmware; the bootloader received it over UART, verified its integrity and version, and finally wrote it to flash.
Security goals#
The microcontroller is a device an attacker can physically access, connect to over UART, and potentially try to access using JTAG. With those things in mind, the bootloader needed to accomplish:
- Confidentiality: firmware shouldn’t be readable from the update file, and it shouldn’t be easy to extract from the device.
- Integrity and authenticity: the device should only install and run firmware created with the trusted key.
- Anti-rollback: the bootloader should reject older firmware versions than the one currently installed.
All of these features needed to be carefully implemented within the hardware constraints of a tiny Tiva microcontroller, which had low memory and computational limits.
Architecture#
The update system is divided into two parts: a trusted build/protection pipeline for creating valid firmware updates, and an update path to simply deliver those updates to the board.
bl_build.pybuilds the bootloader, generates the ChaCha20-Poly1305 key, writes it to the protected build output, and compiles the same key into the bootloader binary.fw_protect.pyuses that key to package firmware into the protected update format.fw_update.pysends an already-protected firmware bundle to the board over UART in 16-byte chunks, without knowledge of the key.bootloader.cruns on the Tiva microcontroller and decides whether the received bundle should be installed using cryptographic verification.
Cryptography#
To prevent firmware contents from being read, I used ChaCha20-Poly1305, a single authenticated encryption scheme that provides confidentiality through a stream cipher. The Poly1305 authentication tag provides integrity, allowing the bootloader to confirm the encrypted content matches the original firmware produced by the developer. Its high efficiency provides an advantage on low-powered microcontrollers like the Tiva boards.
During updates, the bootloader:
- Decrypts metadata and firmware blocks
- Uses Poly1305 tags to authenticate integrity
- Rejects any block failing verification
Firmware file format#
The bundle produced by fw_protect.py stores the metadata and firmware as separate blocks, each with its own nonce and authentication tag.
The encrypted firmware file contains three major sections:
Metadata block#
| Length | 16 bytes | 12 bytes | 1028 bytes |
|---|---|---|---|
| Type | Tag | Nonce | Encrypted metadata |
Encrypted metadata block#
| Length | 2 bytes | 1024 bytes | 2 bytes |
|---|---|---|---|
| Type | Version | Message | Firmware length |
Firmware block#
| Length | 16 bytes | 12 bytes | 2 bytes | 2 bytes | Variable |
|---|---|---|---|---|---|
| Type | Tag | Nonce | Padding | Encrypted version | Encrypted firmware |
Bootloader workflow#
Once the protected firmware bundle reaches the Tiva over UART, the bootloader handles the installation. It verifies the update before modifying the main firmware region in flash.
Metadata verification#
The bootloader first receives the encrypted metadata block. This block contains the firmware version, firmware length, and release message. The bootloader decrypts it with ChaCha20-Poly1305 and rejects the update if the authentication tag does not pass verification.
After decrypting the metadata, the bootloader performs validation before it continues. The firmware length must fit within the allowed flash region and the incoming version must be at least as new as the currently installed version.
Firmware verification#
If the metadata passed verification, the bootloader continues receiving the encrypted firmware block. This block contains another copy of the firmware version followed by the firmware contents. The bootloader decrypts and authenticates the block, then compares the version inside the firmware block against the version from the metadata block.
This duplicate version check prevents attackers from mixing metadata from one update with firmware from another update.
Flash installation#
The bootloader writes the update to flash after both blocks pass verification. It stores the version number, copies the release message, and writes the decrypted firmware into the firmware region.
If any check fails, the bootloader returns an error code and clears temporary buffers, instead of installing the update.
Challenges and limitations#
Microcontrollers often have limited memory, making it impossible to store encrypted firmware in flash that is decrypted on-the-fly into RAM, and perform cryptographic operations that require larger memory buffers. Without making major memory optimizations, I was forced to store the installed firmware unencrypted in flash.
To prevent attackers from connecting via JTAG and dumping decrypted firmware, I locked all debugging functionality on the debug interface. Once locked, reading memory through the debug interface requires a full chip erase, which also wipes the firmware.
