Automated Unlocking of nRF51 Series SoCs with nrfsec
April 20, 2020
Recently, while conducting an assessment for a product based on the nRF51822 System on Chip (SoC), I found my target’s debug interface was locked – standard stuff. Reading up on the nRF51 series SoCs revealed that this is how these chips are designed. It’s always possible to perform a full memory recovery/dump, even if read back protection is enabled.
I wanted to build on what others have discovered, extending the attack to completely and automatically bypass the memory protection mechanism offered by these SoCs. Beyond reading memory, I also wanted to unlock the device to support interactive debug sessions with my target.
This post outlines the theory behind the shortcomings of the debug interface memory read back protection mechanism and introduces a simple tool (nrfsec), to automate the entire process using low-cost debugging hardware.
Starting from a device with read back protection enabled, I was able to:
- Dump all memory including ROM, RAM, UICR and FICR.
- Automate delayed memory reads to get populated RAM and Peripheral images.
- Patch the extracted UICR to disable read back protection (aka locked status).
- Wipe the device, reprogram the UICR and ROM and disable read back protection.
- Establish an interactive debugging session.
nrfsec can be found here.
The nRF51 series chips from Nordic Semiconductor are a very popular line of low-cost Bluetooth Low Energy (BLE) SoCs. The nRF51 series chips can be programmed and debugged via the internal Serial Wire Debug (SWD) interface. The SWD interface is provided by the internal ARM Cortex-M0 processor present inside all nRF51 SoCs. SWD is a common interface included with ARM processors and many processors already providing a JTAG interface. The two required SWD connections are SWDIO (data) and SWCLK (clock), which are interchangeable with the TMS and TCK connections used in JTAG.
Nordic has implemented a Memory Protection Unit (MPU) for nRF51 series chips to protect internal memory from being read. By enabling protect all (PALL) in the User Information Control Register (UICR) the SWD interface will no longer have access to any memory regions. The only way to clear the PALL setting from the UICR is to instruct the Non-Volatile Memory Controller (NVMC) to erase all memory.
The datasheet tells us when PALL is enabled:
“Only the CPU can do fetches from code memory, and these will always be granted.”
The interesting bit is that the Serial Wire Debug interface cannot be disabled on these chips. So even though you can lock down memory regions, you cannot prevent the SWD from accessing core registers and accepting SWD commands that load and inspect system registers, etc.
I will be using pyswd here for demonstration. Pyswd is a nice Python module for interacting with the popular ST-Link SWD interface originally designed for STM32 processors. Additionally, a tool I will introduce later leverages pyswd for interacting with the target device.
For an nRF51 target board I will be using the Adafruit BLE sniffer USB dongle, which contains an nRF51822 SoC. The ST-Link and the nRF51822 SoC are connected by three wires to complete the SWD interface (GND, SWDIO and SWCLK).
You can see my setup here, forming what I call a Circle of Awesome (CoA).
Memory Protection Unit (MPU)
The BLE sniffer firmware comes unprotected and the SWD debugger is given full access to all memory regions. Let’s kick this off by using pyswd to observe the MPU status by performing a memory read. Reading 32 bytes from 0x00000000 returns valid data.
We can then write to a few configuration registers to engage the MPU and prevent reading memory over SWD:
- The Non-Volatile Memory Controller (NVMC) will only allow for read operations after a reset. We must first tell the NVMC to allow for writing to other memory regions by setting a few bits in the associated CONFIG register. Writing 0x00000001 to 0x4001E504 will set the NVMC to allow memory writes.
- Next we’ll set the Read Back Protection Configuration (RBPCONF) register in the User Information Configuration Registers (UICR), which is the last step to locking down the MPU. This is accomplished by writing 0x00000000 to 0x10001004.
Once the processor is reset, the MPU will enable read back protection. Let’s confirm that worked by attempting the read that we successfully performed earlier.
Notice we can still use the SWD interface to execute commands, except now zeros are returned for all memory read operations. This confirms that the MPU is a real thing. Any developer can enable it and at face value it prevents memory reads via SWD.
One Gadget to Rule Them All
Even with the MPU enabled we can still send SWD commands, which grant full access to all the system registers… yep!
We can now reset and halt the nRF51 and inspect the system registers on the target and observe what occurs on a reset.
So, it seems the Program Counter (PC) and Stack Pointer have been initialized with values. These values are actually defined for ARM for Cortex-M0 processors and are documented in the Interrupt Vector Table, which resides at the fixed address of 0x00000000. The first value in memory is the initial stack pointer and the second location is the reset vector. The reset vector is the address the PC will jump to after a reset condition. This provides us with our first bit of useful information: the contents of two known memory locations. We know the value of 0x00000000 is in the SP register and that 0x00000004 is in PC. Given this discovery and the fact that we can control all the system registers, we can now proceed with the fun stuff.
What if the existing program code on the target contained an instruction like the following load instruction, which takes two arguments – the register to load, and an address (specified by another register)?
LDR R2, [R0]
This instruction will load R2 with 4 bytes from the address currently specified by the R0 register. Using this single instruction, we can dump the entire contents of memory on a protected device. I am going to refer to this useful instruction as a “read gadget,” as we are using an existing instruction to do things it was not intended to do.
So how do we find the read gadget when we don’t have a firmware image and the processor has been locked down?
We can brute force the program space and use the known memory values discovered previously to confirm our gadget. You can’t actually do any useful programming with the ARM Thumb instruction set without loading values with offsets specified by registers, so there are plenty of gadgets for all. We just need to find one. Note that we should reset the processor before performing our search. This will set the NVMC to read-only mode again and will prevent our Rambo-style gadget hunt from altering the current contents of memory on our target.
A reasonable location to start our hunt is at the reset handler address as there must be some instructions near there we can abuse. The method is:
- Set the PC register to 0x00002C78, which is the reset handler address.
- Load all the general purpose registers (R0-R12) with 0x00000000, which is the address we know the memory contents of (0x20003248).
- Step the processor.
- Check if any registers contain the expected target value of 0x20003248.
- If one of the registers contains the desired value, this address contains a valid read gadget. If 0x20003248 does not appear in the registers, increment PC by 2 bytes (16-bit Thumb instructions) and repeat from step 2. PC has already been incremented when we stepped the processor, but you get the point.
- The register which contains 0x20003248 in our read gadget output register can now be iterated though all the general purpose registers and input 0x00000000 into only a single register at a time, while setting PC to our suspected read gadget location. This process will isolate the input register that specifies the address our gadget is reading from.
Let’scement this idea this idea using diagrams. Imagine we do not yet know the contents of flash memory, but the actual content is shown here for illustration. We load up PC with the reset handler address, 0x00002C78. All other registers are loaded with 0x00000000:
When we step the processor we don’t see any of the general purpose registers populated with the desired value of 0x20003248.
We reload all general purpose registers with 0x00000000 and step the processor again. This time the result is as follows:
The R2 register contains the data we know is stored at address 0x00000000, and we’ve successfully found our read gadget. The R2 register contains the 4 bytes of memory we’re requesting. Rewinding PC back to 0x00002C7A and running through the remaining registers will reveal the correct register to load the address to read from, which in our case is R0.
At this point we’ve located our read gadget, we know the correct register to put our read address into as input and what register the resulting data from that address will be stored in as output after we execute an instruction from the read gadget.
Now that you know how this attack works at a low level, you can forget all of it because I wrote a tool to automate the entire process. I call it nrfsec and it can be installed easily with pip.
$ pip3 install nrfsec
A quick info check will ensure that nrfsec is able to communicate with both the debugger and the target. The output for the info command will also specify if the target is currently locked with some additional interesting target information.
Specifying the verbose flag here will dump the previously mentioned information plus the full contents of both the UICR and the Factory Information Control Register (FICR). All the information displayed here can be found by interpreting registers contained within the UICR and FICR.
nrfsec will automatically find a useable read gadget and dump all memory on a locked target. nrfsec will store all the extracted images in ./fw of the current working directory. The below example can be used to automatically read all memory regions by parsing memory specifications located in the FICR.
These images can then be loaded into any disassembler. IDA Pro is shown here as an example.
An advantage of performing a complete memory extraction like this is there’s no longer a need to rebase the images, as we have read memory beginning at 0x00000000 which will likely contain the soft device code. We can now see references from application code reading and writing to the UICR and various peripherals.
You can optionally specify a delay flag to have nrfsec let the target run for a specified length of time before proceeding with the memory read. This is excellent for interacting with the target via Bluetooth to populate RAM and peripheral registers. nrfsec also provides start and end flags for reading from specific areas in memory.
The unlock sub-command will perform the following steps:
- Read all memory regions (most importantly, ROM and UICR) and save the images.
- Perform a full target erase; this will enable writing to the UICR again.
- Patch the UICR image extracted during step 1 to disable read back protection.
- Re-flash the ROM and patched UICR back to the target.
Why go through the trouble of unlocking the target when we already have a complete memory dump? Well, now regular SWD calls work again and we can invoke an interactive debug session. The image below shows a GDB session before and after an unlocking procedure on the same target.
OpenOCD supports the ST-Link interface and a GDB session like the one shown above can be established as follows.
(gdb) target extended-remote 127.0.0.1:3333
(gdb) layout asm
You can even debug your target now directly in IDA by specifying the remote debug port.
Issuing the lock sub-command will enable read back protection on the target. This was useful in developing nrfsec but can also be used if you simply want to lock your target (if it isn’t already locked).
nrfsec is available for download at GitHub. It can automate the entire outlined process for you, letting you uncover the internal working of any nRF51 based product.