Skip to content

NRF52 Fault Injection to Bypass APPROTECT (CVE-2020-27211) ​

Our company’s in-house product uses Nordic nRF52 as a root of trust, with secrets stored on-chip. With APPROTECT (AP PROTECT) enabled, SWD should not be able to read Flash/RAM even if an attacker has full physical access to the device.

This write-up documents a security assessment of a known VCORE glitch at early power-up that can disturb the APPROTECT lock sequence, temporarily restoring debug access long enough for OpenOCD/GDB attach and firmware extraction. It focuses on: feasibility, required effort/cost, and practical risk for our product.

Goal: With APPROTECT enabled (SWD read-protected), inject a voltage glitch on DEC1/VCORE early in power-up to disturb the AHB-AP locking process, temporarily restoring debug access for this power cycle, enabling OpenOCD/GDB attach and firmware dump.

Demo video: https://b23.tv/A95MaCd


Background: APPROTECT and Readout Protection ​

APPROTECT blocks the debug module from talking to the CPU when enabled, denying SWD debug access.

From an ARM CoreSight / DAP perspective, there are two main ports:

  • CTRL-AP (Control Access Port): management-oriented, not dependent on AHB-AP; used for recover/eraseall.
  • AHB-AP (AHB Access Port): the real debug/memory port via SWD (Flash/RAM/register read/write, halt/run, breakpoints).

APPROTECT makes AHB-AP unavailable when protected, so OpenOCD typically reports “no MEM-AP / cannot control core.”

debug_and_trace_overview

Enabling APPROTECT

Debug readout protection is controlled by UICR.APPROTECT at 0x10001208: 0xFFFFFFFF = off; 0xFFFFFF00 = on. After first write, power-cycle/reset is required to take effect.

enable_approtect

Enable paths: (1) write UICR in firmware via NVMC then reset; (2) program an image containing UICR segment or directly write the word via tools, e.g. OpenOCD flash fillw 0x10001208 0xFFFFFF00 or nrfjprog --memwr 0x10001208 --val 0xFFFFFF00.

Example in firmware (effective after first write + reset):

c
#include "nrf_nvmc.h"

void SWD_protect(void)
{
    if (NRF_UICR->APPROTECT == 0xFFFFFFFF)
    {
        nrf_nvmc_write_word((uint32_t)&(NRF_UICR->APPROTECT), 0xFFFFFF00);
        nrf_delay_ms(1000);
        NVIC_SystemReset();
    }
}

How to disable APPROTECT

Officially “recover” equals eraseall: trigger ERASEALL via CTRL-AP or nrfjprog -f NRF52 --recover, wiping Flash + RAM. This experiment shows that without recover/erase and with 0x10001208 = 0xFFFFFF00, a temporary AHB-AP (MEM-AP) window can be opened for the current power cycle.

With APPROTECT off you can read firmware/RAM directly (e.g., J-Flash/Segger); the glitch aims to restore that window without clearing protection bits.

jmem_read

Vulnerability Overview ​

CVE-2020-27211 abuses early power-on hardware init: a state machine (not BootROM) reads UICR.APPROTECT and locks AHB-AP. If we glitch DEC1 (VCORE) then, Flash reads or latching can fail, leaving AHB-AP unlocked for this cycle. Observing DEC1/DEC4 power patterns to align the glitch with Flash read improves success; glitch must be moderate—too weak: no effect; too strong: crash.


Hardware Analysis ​

Glitch Injection Point ​

Target point: DEC1 (VCORE)

VCORE stability depends on its decoupling capacitor, so a short-to-ground glitch on DEC1 is most effective. nRF52840 uses two-stage regulation (REG0/REG1); DEC1 controls CPU/debug init, making it the best target. Watch DEC1 and DEC4 power traces—DEC4 shows system power modes—to align with NVMC Flash/UICR reads. Remove DEC1’s decoupling cap (C5 on official DK) to raise sensitivity.

soc_datasheet

Board differences: your “C5” may not be C5

On a clone nRF52840-DK, silkscreen/cap numbering differed: official C5 (DEC1 cap) was actually C7 on my board. Mistake: don’t assume “short a cap to GND and the chip reboots” proves the point; that might just short another rail. Use PCB tracing, silkscreen comparison, and VCORE power-up waveform to verify.

BGA package means you can’t probe DEC1 pin directly. I compared public DK photos and found the matching cap location; cloned boards kept the same layout order. I wired the identified C7 pad as the injection point.

C5_examplemy_board

Practical tip: remove the DEC1 decoupling cap. Without removal, VCORE won’t dip enough and the scope shows no real sag. After removal, solder a wire to that pad as VCORE glitch point; avoid stray strands touching neighbors.

dongledongle2

nRF52840 dongle pinout (tested):

  • White: DEC1 (to ChipWhisperer glitch)
  • Red: VCC (to ChipWhisperer power)
  • Black: GND (shared with scope/CW/J-LINK)
  • Orange/Brown: SWD IO/CLK (to J-LINK)
  • Purple: reset (to ChipWhisperer tio3)
connect_liveconnect_live2

Wiring Topology ​

Using ChipWhisperer Pro + CW506 splitter to connect target, scope, J-LINK, and ChipWhisperer for automated power-cycle/glitch/verify.

  • Power: CW target_pwr → target VCC (3.3V to avoid double-power with J-Link)
  • Glitch: CW glitch → VCORE/DEC1 (scope CH1 on same point)
  • Trigger: CW tio3 ← target RESET (power-on reset as trigger)
  • SWD: J-Link SWDIO/SWDCLK/GND → target SWDIO/SWDCLK/GND

Diagram:

text
nrf52 5v vcc <--> cw vcc (3.3v)
nrf52 swd(io/clk/gnd) <--> jlink swd(io/clk/gnd)
nrf52 gnd <--> cw gnd <--> scope gnd
nrf52 vcore <--> cw glitch <--> scope ch1
nrf52 reset <--> cw tio3 (reset)
connect

Common ground (easy but critical)

I repeated Joe Grand’s pitfall on Trezor STM32: forgot common ground, leading to either no effect or over-glitch crashes. Without ground sharing:

fail_injection

With ground shared, you can raise repeat without browning out and the glitch is clearer.

Timing and Parameters ​

AHB-AP decides APPROTECT by reading UICR at boot; that read leaves a power trace signature. Aligning the glitch to that window saves time versus blind ext_offset sweeping.

AHB_AP_start

Observed phases:

  • Flash activity: early Flash/UICR reads
  • CPU execute: after fetch/execute starts, DEC1 trace changes visibly

Aim near Flash/NVMC init for higher success than random scans.

Best injection point: Scope DEC1 (yellow); ~1s after power-on (blue) there’s a >100mV dip—inject on that falling edge.

injection_point

Sweeping injection

Key ChipWhisperer params:

  • ext_offset: delay from trigger to glitch out (controls stage hit)
  • repeat: glitch duration/strength (effective short-to-GND length)
  • width: pulse width/shape per glitch

Use RESET as trigger + sweep ext_offset. With 100MHz clock (10ns period), ext_offset = 169500 ≈ 1.695ms. Mind the two RESET edges (power drop/rise) and choose the correct reference.

Width & repeat

width default 10.15625, max 49; repeat controls duration. Even with width=40, repeat=100 (messy waveform), it can still work.

Repeat=1 vs repeat=49:

repeat1repeat2

Automated Scanning ​

To efficiently find a working window, scan ext_offset and repeat/width, after each glitch do a small OpenOCD read; stop on success.

python
import chipwhisperer as cw
import os, time

scope = cw.scope()

scope.clock.clkgen_freq = 100E6
scope.glitch.clk_src = "clkgen"
scope.glitch.resetDCMs()
scope.glitch.output = "enable_only"

scope.io.tio3 = 'high_z'
scope.trigger.triggers = 'tio3'
scope.glitch.trigger_src = "ext_continuous"

scope.glitch.ext_offset = 366500
scope.glitch.repeat = 100
scope.io.glitch_lp = False
scope.io.glitch_hp = True

print("Initial glitch config:", scope.glitch)

scan_running = True
while scan_running:
    scope.glitch.ext_offset += 100
    for i in range(1, 100, 10):
        scope.glitch.repeat = i * 10
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] offset={scope.glitch.ext_offset}, repeat={scope.glitch.repeat}")

        scope.glitch.width = 40
        scope.io.glitch_lp = False
        scope.io.glitch_hp = True

        scope.io.target_pwr = True
        time.sleep(2)

        exit_status = os.system(
            'openocd -s /usr/local/share/openocd/scripts '
            '-f ./interface/jlink.cfg '
            '-c "transport select swd" '
            '-f ./target/nrf52.cfg '
            '-c "init;dump_image nrf52_dumped.bin 0x0 0x100;exit"'
        )

        if exit_status == 0:
            print("✅ success! current params:")
            print(scope.glitch)
            scan_running = False
            raise SystemExit(0)

        time.sleep(2)
        scope.io.target_pwr = False
        time.sleep(0.3)

        if scope.glitch.ext_offset > 380000:
            print("❌ scan ended, no hit")
            scan_running = False
            break

Quick verification (scan stage): only dump 0x0~0x100 to save time:

bash
openocd \
  -s /usr/local/share/openocd/scripts \
  -f ./interface/jlink.cfg \
  -c "transport select swd" \
  -f ./target/nrf52.cfg \
  -c "init;dump_image nrf52_dumped.bin 0x0 0x100;exit"

Full dump (after success): read full Flash (nRF52840 0x80000/0x100000):

bash
openocd \
  -s /usr/local/share/openocd/scripts \
  -f ./interface/jlink.cfg \
  -c "transport select swd" \
  -f ./target/nrf52.cfg \
  -c "init;dump_image nrf52_dumped.bin 0x0 0x80000;exit"
start_injection

Common pitfalls during scanning:

  1. False success: scope shows no sag, OpenOCD still fails—glitch too strong causing a crash.
  2. Board variance: my nRF52840-DK clone never hit; switching to nRF52840 Dongle + new params worked (power-up timing differs).
  3. Soldering issues: stray solder on the injection pad distorts the glitch path.
fail1fail111

Signs of Success & Example Params ​

Waveform: a clear glitch on VCORE with no major post-glitch droop (suggesting APPROTECT lock step was skipped).

Functionally: full Flash/RAM dump via OpenOCD; GDB attach works.

success3success4

Working params on my setup (hardware varies):

  • ext_offset = 169500 (≈1.695ms at 100MHz)
  • repeat = 11
  • width = 10
successsuccess1

Reading UICR.APPROTECT (0x10001208) after success still shows 0xFFFFFF00 (protection on), proving the bypass is temporary for this power cycle, without erasing or modifying protection bits.

success2

Impact ​

  • Firmware extraction / reverse engineering (incl. illegal cloning)
  • Wallets/security devices: if secrets or intermediates sit in RAM, you can read RAM or chain other attacks
  • Extensions: fault injection can aid DFA, secure boot bypass, UART shell gain, etc.

Mitigation Status ​

Newer Nordic parts ship with improved APPROTECT addressing this class. Hard to tell from power traces alone; check chip model/lot vs latest PS (e.g., nRF52840 PS v1.7) and Nordic note: https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/working-with-the-nrf52-series-improved-approtect


References & Acknowledgments ​

Thanks to @yichen from @OSR for help.

References: