This is somewhat of a follow-up to the Nintendo Alarmo blog post from last year. This time the blog post is about the security of the STM32H730 microcontroller series itself, which is also used by the Alarmo. The STM32H730 contains an Arm Cortex-M7 core and 128 Kbytes of internal flash memory. In this blog post we'll take a look at a vulnerability that allows arbitrary code execution in the secure bootloader on STM32H73xxx and STM32H7Bxxx microcontrollers.
STM32H730 Security Overview
Like many other microcontrollers, the STM32H730 supports a feature called readout protection (RDP) to protect the contents of the internal flash from being accessed when a debugger is connected. RDP offers three different levels of protection, which can be set through a configuration register. At RDP level 0 the readout protection is disabled and the contents of the internal flash can be read out using a connected debugger. At RDP level 1 the contents of the flash are made inaccessible as soon as a debugger is connected to the system. At RDP level 2 establishing a connection to a debugger is no longer possible at all. RDP can only be regressed from level 1 back to level 0, doing so will completely erase the internal flash. Once the RDP level is set to 2, it can never be changed back.
In addition to the readout protection, the STM32H730 features a global "secure access mode". When this mode is enabled, a special secure-only area can be defined in the user accessible part of the internal flash, which can only be accessed while the CPU is still running secure code at boot. Attaching a debugger while running such secure code is not possible. This allows separating secure user code from non-secure code, for example to run startup code that handles any sort of secret data, like cryptographic keys, which should not be retrieved after system startup. On the Nintendo Alarmo, the code within the secure-only area sets up the cryptographic engine with a secret key stored in the secure area, then loads a second stage bootloader from external eMMC, verifies its signature, and decrypts it. After jumping to this second stage bootloader using a special function stored in system memory, the secure-only area is no longer accessible.
In order to enable the secure access mode, the SECURITY bit is set in the option bytes using a configuration register. This can be done from unprivileged user code. To clear the SECURITY bit after configuring the secure-only area, a RDP regression is required. This will completely erase the internal flash, including any secure-only areas, allowing the SECURITY bit to be cleared again.
The Secure Bootloader
Once the secure access mode is enabled, the system always boots into a piece of code located in protected system memory, called the secure bootloader. This secure bootloader should not be confused with STMicroelectronics' non-secure bootloader, which allows programming the flash over regular communication ports like USB or UART.
The purpose of the secure bootloader is to securely boot into a secure-only user area which might have been set and/or to run services on startup called root secure services (RSS) (these services will be described in more detail later).
Attaching a debugger is not possible while the CPU is executing the secure bootloader, even with RDP disabled (level 0). The secure bootloader code is located in protected system memory and gets locked out before jumping to regular user code, making it inaccessible.
Because the secure-bootloader's internal behavior is mostly undocumented, I decided to attempt dumping it. Since I didn't want to risk bricking an Alarmo with my experiments, I started with a development board containing a similar STM chip with security protections.
One of the first things I tried was reading the secure bootloader while executing code from a secure-only user area, since both are regarded as "secure code" by the reference manual. Unfortunately, it wasn't this easy. Any attempts to read this code returned only zeroes and sets the read secure error bit RDSERR.
Notably, the secure bootloader doesn't clear its stack (located in DTCM RAM) before branching to the user code, allowing the stack of the secure bootloader to be dumped from user code as soon as it runs. This reveals some return addresses and variables left on the stack. This confirms that the secure bootloader is actually running, but does not allow for much else.
| Stack offset | Value | Description |
|---|---|---|
| -0x00 | 0xFFFFFFFF | |
| -0x04 | 0x00000000 | |
| -0x08 | 0x00000008 | |
| -0x0C | 0x00000008 | |
| -0x10 | 0x1FF06D03 | Return address in system memory |
| -0x14 | 0xCC000007 | |
| -0x18 | 0xAA0038A0 | |
| -0x1C | 0x20000800 | |
| -0x20 | 0xE000EDA0 | |
| -0x24 | 0x00000000 | |
| -0x28 | 0x20004000 | |
| -0x2C | 0x00000000 | |
| -0x30 | 0x1FF04209 | Return address in system memory |
| ... | ... | ... |
Root Secure Services (RSS)
In order to perform some security-critical tasks, like setting up on-the-fly decryption (OTFD) or configuring secure-only areas, the root secure services (RSS) are used. To access RSS functionality, the secure access mode needs to be enabled, since these services are performed by the secure bootloader. A small RSS caller library is located at the end of the system memory. It can be accessed by non-secure user code and provides several functions to access the secure services. The function addresses are stored at hardcoded addresses in ROM to allow for easily calling them. The RSS code itself then runs from within the secure bootloader at startup. In order to run a service, a service ID together with all parameters are written to DTCM RAM and a system reset is performed. After reset, the secure bootloader runs with highest possible privileges and executes the secure service specified by its ID in RAM.
On-the-Fly Decryption (OTFD) Service
One of these RSS services is the on-the-fly decryption (OTFD) service resetAndEncrypt. OTFD is another security feature of the STM32H730 which allows directly mapping an encrypted external SPI-flash in memory space, performing decryption on-the-fly when accessing this memory. The flash can be split into up to four different encrypted regions. The data is encrypted using AES-128 in counter mode, with a key and nonce specified by the user.
In order to encrypt the data to be stored on the flash, the RSS library function resetAndEncrypt is called with pointers to a plaintext buffer, key and nonce stored in RAM. The function then performs a system reset, causing the secure service to encrypt the buffer in-place with the specified key and nonce, before jumping back to the user code after completion. The user code can then write the encrypted data to the external flash and enable on-the-fly decryption.
Gaining code execution in RSS
I have to admit, an anonymous person managed to glitch the chip before I was able to find a way to dump the secure bootloader. This means I actually got access to a dump of the entire protected system memory beforehand, allowing me to take a look at the implementation of these secure services, revealing a vulnerability in one of them.
The vulnerability itself was actually easy to spot and is rather straight forward: The OTFD secure service doesn't check the address of the buffer which will be in-place encrypted by RSS, allowing encryption of a buffer anywhere in memory with a chosen key and nonce. By choosing the right key and nonce pair, the RSS code can encrypt a return address on its stack, transforming it into a valid address in SRAM.
The actual AES128-CTR encryption happens in the CRYPT engine of the microcontroller, using an undocumented set of registers. Luckily, the process itself is described in detail in Reference Manual RM0468 and Application Note AN5281. Half of the 128-bit IV is made up of the 64-bit user-controlled nonce, while the other half contains the region firmware version, region ID, and the current read address.
With the structure of the final IV revealed, it's time to find some key and nonce candidates which produce a valid address in SRAM when encrypting one of the return addresses before jumping to user code. For this I wrote a brute-forcing tool in C which simply encrypts the return address with random key and nonce pairs and checks if the resulting value is a nicely aligned address in SRAM. The source for this tool can be found here.
$ ./bruteforcer
Candidate: 24012000 Key: 1839323a 50458bf1 abd34402 124dad4d Nonce: 591164ac 70f2bb52
Candidate: 24016000 Key: ff0f6ec9 10950afb 929f6a00 da3d42d5 Nonce: 5016ce89 6d800495
Candidate: 24004000 Key: b95154df 05bf505f 08ef0476 b50ae336 Nonce: 43a44da3 92261edc
Dumping the RSS Code
In order to make sure that this actually works, I needed another device with an STM32H730. While I already had the Alarmo, I didn't want to fully erase it. Unfortunately STMicroelectronics doesn't have any development boards with an STM32H730. The closest match would be a discovery kit with an STM32H735, but that costs over a hundred bucks and doesn't even have the right chip. The two options I had were buying a pin-compatible development board like the NUCLEO-H723ZG and resoldering it with an STM32H730 or buying a cheap no-name development board from China. I ended up doing both, having the board from China as a backup in case something didn't end up working after the chip transplant.
All exception handlers in the secure bootloader set a flag to clear the stack and perform a system reset. This nicely allows for testing if an exception has been raised while in the secure bootloader: if the unused area of the stack has been cleared, an exception was raised. And indeed, simply encrypting the return address right after the command handler with a zero key and nonce results in a cleared stack, proving that an exception was triggered while running the secure bootloader.
As a payload to load into SRAM, I wrote a small program which prints "Hello world" over UART. Using the key and nonce found by the bruteforcer, the resetAndEncrypt service can be called, resulting in the message being printed over UART, proving successful code execution.
|
| Output of "Hello world" payload. |
With successful code execution from within the context of the RSS code, I wrote a payload which dumps the secure bootloader code itself over UART. The full dumper code can be found on this GitHub repository, allowing anyone to dump the RSS code from vulnerable devices.
Can this bypass RDP or secure-only areas?
Short answer: No.
Secure-only areas cannot be dumped since the root secure services are disabled once a secure-only area has been set. There is one exception to this: if the first reserved entry in the secure-only area's vector table (offset 0x1C) is set to the magic value 52 53 53 45 ("RSSE") (0x45535352 as a little-endian word), RSS services remain accessible.
While I was initially under the assumption that it's possible to use this to bypass the readout protection, I stumbled across two things which prevented me from dumping the internal flash while readout protection was at level 1.
The first issue is that an external reset (via NRESET) does not clear the RDP status: if RDP was tripped, even the secure-bootloader cannot access the internal flash after an external reset. The only way to reset the readout protection seems to be by performing a full power-cycle, which will also erase any RSS commands in DTCM. A simple external reset keeps the readout protection tripped.
The other issue is due to the OTFD engine itself. After RDP has been tripped, the engine will only return zeroes upon attempting to read from an OTFD region. This also affects the encryption in the RSS code, which is now only writing zeroes over the specified buffer. Additionally it seems to now only write 8-byte aligned buffers and sizes, which still allows overwriting the return address on the stack with zeroes. This would allow jumping to a payload in ITCM RAM, which is mapped at 0x0, but without having the least significant bit of the address (T-bit) set, the Thumb-only CPU will simply abort upon jumping there.
Unless there is a way to perform a full power cycle while preserving the contents of DTCM RAM (where the RSS commands are stored), this technique cannot bypass readout protection.
What now?
Even though this didn't break the security as much as I initially expected, I reported this vulnerability to ST's PSIRT team. I also asked for further comment on the behavior of OTFD encryption under RDP regression, which they declined to comment on. They published a security advisory SA0053 in late September, recommending enabling RDP level 2 on affected devices. I'm not sure if this is really necessary, but maybe ST knows something I don't :)
Disclosure Timeline
06 April 2025: Technical details about the vulnerability and example code provided to ST PSIRT.
12 May 2025: Response from PSIRT acknowledging the vulnerability and asking for further details about potential disclosure. Reply from me, mentioning that all details are planned to be disclosed.
17 June 2025: Follow-up email from me asking for further details and status of the vulnerability.
18 June 2025: Response from ST needing more time to investigate.
29 September 2025: Security advisory published by ST.
13 November 2025: Publication of this blog post with technical details and example code on GitHub.
