Part 1, Topic 3: Clock Glitching to Dump Memory (MAIN)¶
NOTE: This lab references some (commercial) training material on ChipWhisperer.io. You can freely execute and use the lab per the open-source license (including using it in your own courses if you distribute similarly), but you must maintain notice about this source location. Consider joining our training course to enjoy the full experience.
SUMMARY: In the previous lab, we learned how clock glitching can be used to get a microcontroller to skip a password check. This time, we'll look at a more practical example: getting an example bootloader to dump a large chunk of memory.
LEARNING OUTCOMES:
- Applying previous glitch settings to new firmware
- Checking for success and failure when glitching
- Understanding how compiler optimizations can cause devices to behave in strange ways
The Situation¶
Now that we've got our feet wet with glitching, we're going to try something a bit more realistic: an "encrypted" bootloader (it's actually just rot-13, but we'll pretend it's unbreakable encryption), where we make as few assumptions as possible. Our goal will be to get that bootloader to decrypt the data and send it back to us. Here's what we know about the bootloader:
- The
'p'
command is used to write encrypted firmware to the device. It takes in an encrypted ASCII-encoded string, terminated with a newline. Our first chunk of firmware is"516261276720736265747267206762206f686c207a76797821"
. - It does something to it (presumably unencrypts it, authenticates it, etc. and writes it to memory)
- It sends back an error code of
"r000000\n"
Of immediate interest is that error code. That's the only time the bootloader communicates back with us, so attacking there is a good place to start. One thing that we'll assume is that we've got a trigger right before the error code is sent back to us. This is just a simple trigger_high()
call, but we could also trigger on an IO line (better with the CW1200 Pro) or with a SAD trigger on a power trace (CW1200 Pro only). We've got a place to start, but let's see if we can learn more about the bootloader first.
We recommend using SimpleSerial V2 for this as, though the firmware doesn't use the simpleserial protocol, the faster baud rate will help speed up glitching.
SCOPETYPE = 'OPENADC'
PLATFORM = 'CWLITEXMEGA'
SS_VER='SS_VER_2_1'
VERSION = 'HARDWARE'
CRYPTO_TARGET = 'TINYAES128C'
allowable_exceptions = None
%%bash -s "$PLATFORM" "$SS_VER"
cd ../../../firmware/mcu/bootloader-glitch
make PLATFORM=$1 CRYPTO_TARGET=NONE -j SS_VER=$2
SS_VER set to SS_VER_2_1
SS_VER set to SS_VER_2_1
avr-gcc (GCC) 5.4.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
mkdir -p objdir-CWLITEXMEGA
.
Welcome to another exciting ChipWhisperer target build!!
.
.
.
Compiling:
Compiling:
Compiling:
-en bootloader.c ...
-en decryption.c ...
-en .././simpleserial/simpleserial.c ...
.
.
Compiling:
Compiling:
-en .././hal//xmega/XMEGA_AES_driver.c ...
-e Done!
-en .././hal//xmega/uart.c ...
.
.
-e Done!
Compiling:
Compiling:
-en .././hal//xmega/usart_driver.c ...
-en .././hal//xmega/xmega_hal.c ...
-e Done!
-e Done!
-e Done!
-e Done!
-e Done!
.
LINKING:
-en bootloader-CWLITEXMEGA.elf ...
-e Done!
.
.
Creating load file for Flash: bootloader-CWLITEXMEGA.hex
.
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature bootloader-CWLITEXMEGA.elf bootloader-CWLITEXMEGA.hex
Creating load file for Flash: bootloader-CWLITEXMEGA.bin
avr-objcopy -O binary -R .eeprom -R .fuse -R .lock -R .signature bootloader-CWLITEXMEGA.elf bootloader-CWLITEXMEGA.bin
Creating load file for EEPROM: bootloader-CWLITEXMEGA.eep
avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" \
--change-section-lma .eeprom=0 --no-change-warnings -O ihex bootloader-CWLITEXMEGA.elf bootloader-CWLITEXMEGA.eep || exit 0
.
.
Creating Extended Listing: bootloader-CWLITEXMEGA.lss
avr-objdump -h -S -z bootloader-CWLITEXMEGA.elf > bootloader-CWLITEXMEGA.lss
Creating Symbol Table: bootloader-CWLITEXMEGA.sym
avr-nm -n bootloader-CWLITEXMEGA.elf > bootloader-CWLITEXMEGA.sym
Size after:
text data bss dec hex filename
2414 124 202 2740 ab4 bootloader-CWLITEXMEGA.elf
+--------------------------------------------------------
+ Default target does full rebuild each time.
+ Specify buildtarget == allquick == to avoid full rebuild
+--------------------------------------------------------
+--------------------------------------------------------
+ Built for platform CW-Lite XMEGA with:
+ CRYPTO_TARGET = NONE
+ CRYPTO_OPTIONS = AES128C
+--------------------------------------------------------
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
import chipwhisperer as cw
try:
if not scope.connectStatus:
scope.con()
except NameError:
scope = cw.scope(hw_location=(5, 4))
try:
if SS_VER == "SS_VER_2_1":
target_type = cw.targets.SimpleSerial2
elif SS_VER == "SS_VER_2_0":
raise OSError("SS_VER_2_0 is deprecated. Use SS_VER_2_1")
else:
target_type = cw.targets.SimpleSerial
except:
SS_VER="SS_VER_1_1"
target_type = cw.targets.SimpleSerial
try:
target = cw.target(scope, target_type)
except:
print("INFO: Caught exception on reconnecting to target - attempting to reconnect to scope first.")
print("INFO: This is a work-around when USB has died without Python knowing. Ignore errors above this line.")
scope = cw.scope(hw_location=(5, 4))
target = cw.target(scope, target_type)
print("INFO: Found ChipWhisperer😍")
# In[ ]:
if "STM" in PLATFORM or PLATFORM == "CWLITEARM" or PLATFORM == "CWNANO":
prog = cw.programmers.STM32FProgrammer
elif PLATFORM == "CW303" or PLATFORM == "CWLITEXMEGA":
prog = cw.programmers.XMEGAProgrammer
elif "neorv32" in PLATFORM.lower():
prog = cw.programmers.NEORV32Programmer
elif PLATFORM == "CW308_SAM4S" or PLATFORM == "CWHUSKY":
prog = cw.programmers.SAM4SProgrammer
else:
prog = None
# In[ ]:
import time
time.sleep(0.05)
scope.default_setup()
def reset_target(scope):
if PLATFORM == "CW303" or PLATFORM == "CWLITEXMEGA":
scope.io.pdic = 'low'
time.sleep(0.1)
scope.io.pdic = 'high_z' #XMEGA doesn't like pdic driven high
time.sleep(0.1) #xmega needs more startup time
elif "neorv32" in PLATFORM.lower():
raise IOError("Default iCE40 neorv32 build does not have external reset - reprogram device to reset")
elif PLATFORM == "CW308_SAM4S" or PLATFORM == "CWHUSKY":
scope.io.nrst = 'low'
time.sleep(0.25)
scope.io.nrst = 'high_z'
time.sleep(0.25)
else:
scope.io.nrst = 'low'
time.sleep(0.05)
scope.io.nrst = 'high_z'
time.sleep(0.05)
INFO: Found ChipWhisperer😍
scope.gain.mode changed from low to high scope.gain.gain changed from 0 to 30 scope.gain.db changed from 5.5 to 24.8359375 scope.adc.basic_mode changed from low to rising_edge scope.adc.samples changed from 24400 to 5000 scope.adc.trig_count changed from 10898586 to 21921108 scope.clock.adc_src changed from clkgen_x1 to clkgen_x4 scope.clock.adc_freq changed from 20252357 to 29906490 scope.clock.adc_rate changed from 20252357.0 to 29906490.0 scope.clock.clkgen_div changed from 1 to 26 scope.clock.clkgen_freq changed from 192000000.0 to 7384615.384615385 scope.io.tio1 changed from serial_tx to serial_rx scope.io.tio2 changed from serial_rx to serial_tx scope.io.hs2 changed from None to clkgen scope.io.tio_states changed from (1, 0, 0, 0) to (1, 1, 0, 0) scope.glitch.mmcm_locked changed from True to False
fw_path = "../../../firmware/mcu/bootloader-glitch/bootloader-{}.hex".format(PLATFORM)
cw.program_target(scope, prog, fw_path)
XMEGA Programming flash...
XMEGA Reading flash...
Verified flash OK, 2537 bytes
The first thing we'll do is some simple power analysis to see what the device is doing when it sends data back to us. Serial communication is pretty slow, so set the ChipWhisperer to capture around 24k samples with a "x1" ADC clock.
def reboot_flush():
reset_target(scope)
#Flush garbage too
target.flush()
scope.clock.adc_src = "clkgen_x1"
reboot_flush()
scope.adc.samples = 24000
Next, capture a power trace. The string "p516261276720736265747267206762206f686c207a76797821\n"
will send the bootloader the first chunk of code and plot it. If you don't see the full serial message, you can increase scope.adc.decimate
, which will throw out every nth ADC sample.
scope.adc.timeout = 3
scope.arm()
target.write("p516261276720736265747267206762206f686c207a76797821\n")
ret = scope.capture()
if ret:
print("Timeout")
trace = scope.get_last_trace()
cw.plot(trace)