SOLUTION WITH SIMULATION: Part 2, Topic 1, Lab B: Power Analysis for Password Bypass¶
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: This tutorial will introduce you to breaking devices by determining when a device is performing certain operations. Our target device will be performing a simple password check, and we will demonstrate how to perform a basic power analysis.
LEARNING OUTCOMES:
- How power can be used to determine timing information.
- Plotting multiple iterations while varying input data to find interesting locations.
- Using difference of waveforms to find interesting locations.
- Performing power captures with ChipWhisperer hardware (hardware only)
Prerequisites¶
Hold up! Before you continue, check you've done the following tutorials:
- ☑ Jupyter Notebook Intro (you should be OK with plotting & running blocks).
- ☑ SCA101 Intro (you should have an idea of how to get hardware-specific versions running).
Power Trace Gathering¶
At this point you've got to insert code to perform the power trace capture. There are two options here:
- Capture from physical device.
- Read from a file.
You get to choose your adventure - see the two notebooks with the same name of this, but called (SIMULATED)
or (HARDWARE)
to continue. Inside those notebooks you should get some code to copy into the following section, which will define the capture function.
Be sure you get the "✔️ OK to continue!"
print once you run the next cell, otherwise things will fail later on!
Choose your setup options here:
SCOPETYPE='OPENADC'
PLATFORM='CWLITEARM'
CRYPTO_TARGET='TINYAES128C'
VERSION='HARDWARE'
allowable_exceptions = None
SS_VER = 'SS_VER_2_1'
if VERSION == 'HARDWARE':
#!/usr/bin/env python
# coding: utf-8
#
#
# **THIS IS NOT THE COMPLETE TUTORIAL - see file with (MAIN) in the name. Paste all this code before the first Python block**
# First you'll need to select which hardware setup you have. You'll need to select both a `SCOPETYPE` and a `PLATFORM`. `SCOPETYPE` can either be `'OPENADC'` for the CWLite/CW1200 or `'CWNANO'` for the CWNano. `PLATFORM` is the target device, with `'CWLITEARM'`/`'CW308_STM32F3'` being the best supported option, followed by `'CWLITEXMEGA'`/`'CW308_XMEGA'`, then by `'CWNANO'`. As of CW 5.4, you can select the SimpleSerial version
# used. For example:
#
# ```python
# SCOPETYPE = 'OPENADC'
# PLATFORM = 'CWLITEARM'
# SS_VER = 'SS_VER_2_1'
# ```
# In[ ]:
# This code will connect the scope and do some basic setup. We're now just going to use a special setup script to do this. This script contains the commands we ran seperately before.
# In[ ]:
#!/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, 3))
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, 3))
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)
# The following code will build the firmware for the target.
# In[ ]:
try:
get_ipython().run_cell_magic('bash', '-s "$PLATFORM" "$SS_VER"', 'cd ../../../firmware/mcu/basic-passwdcheck\nmake PLATFORM=$1 CRYPTO_TARGET=NONE SS_VER=$2 -j\n &> /tmp/tmp.txt')
except:
x=open("/tmp/tmp.txt").read(); print(x); raise OSError(x)
# Finally, all that's left is to program the device, which can be done with the following line:
# In[ ]:
cw.program_target(scope, prog, "../../../firmware/mcu/basic-passwdcheck/basic-passwdcheck-{}.hex".format(PLATFORM))
# To make interacting with the hardware easier, let's define a function to attempt a password and return a power trace:
# In[ ]:
def cap_pass_trace(pass_guess):
reset_target(scope)
num_char = target.in_waiting()
while num_char > 0:
target.read(num_char, 10)
time.sleep(0.01)
num_char = target.in_waiting()
scope.arm()
target.write(pass_guess)
ret = scope.capture()
if ret:
print('Timeout happened during acquisition')
trace = scope.get_last_trace()
return trace
# We also don't need all of the default 5000 samples in the trace. 3000 is a good starting point for most targets:
# In[ ]:
scope.adc.samples = 3000
elif VERSION == 'SIMULATED':
#!/usr/bin/env python
# coding: utf-8
# # Power Analysis for Password Bypass - SIMULATED Setup
# ---
# **THIS IS NOT THE COMPLETE TUTORIAL - see file with `(MAIN)` in the name.**
#
# ---
# Sure you don't have hardware, but that doesn't mean we can't have fun! If you check the ChipWhisperer based lab (using hardware), you'll find that the capture function is defined like this:
#
# def cap_pass_trace(pass_guess):
# ret = ""
# reset_target(scope)
# num_char = target.in_waiting()
# while num_char > 0:
# ret += target.read(num_char, 10)
# time.sleep(0.01)
# num_char = target.in_waiting()
#
# scope.arm()
# target.write(pass_guess)
# ret = scope.capture()
# if ret:
# print('Timeout happened during acquisition')
#
# trace = scope.get_last_trace()
# return trace
#
# This sends a password guess to the target device, and returns a power trace associated with the guess in question. So for example you could run:
#
# cap_pass_trace("abcde\n")
#
# To get a power trace of `abcde`.
#
# Instead, we have a function that uses pre-recorded data. Run the following block and it should give you access to a function that uses pre-recorded data. While how you use the function is the same, note the following limitations:
#
# * Not every combination is stored in the system -- instead it stores similar power traces.
# * 100 traces are stored for each guess, and it randomly returns one to still give you the effect of noise.
#
# In[ ]:
from cwtraces import sca101_lab_data
import chipwhisperer as cw
cap_pass_trace = sca101_lab_data["lab2_1"]["cap_pass_trace"]
trace_test = cap_pass_trace("h\n")
#Basic sanity check
assert(len(trace_test) == 3000)
print("✔️ OK to continue!")
# But wait - this lab isn't the one you need to run it in! Instead copy the above block into the lab in the requested section, and you should be ready to rock.
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 1167439540 to 1190218952 scope.clock.adc_src changed from clkgen_x1 to clkgen_x4 scope.clock.adc_freq changed from 62772216 to 29538459 scope.clock.adc_rate changed from 62772216.0 to 29538459.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 SS_VER set to SS_VER_2_1
SS_VER set to SS_VER_2_1
arm-none-eabi-gcc (15:9-2019-q4-0ubuntu1) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
Copyright (C) 2019 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-CWLITEARM
.
Welcome to another exciting ChipWhisperer target build!!
.
.
Compiling:
Compiling:
-en basic-passwdcheck.c ...
-en .././simpleserial/simpleserial.c ...
.
.
Compiling:
.
-en .././hal//stm32f3/stm32f3_hal.c ...
Compiling:
Compiling:
-en .././hal//stm32f3/stm32f3_hal_lowlevel.c ...
-en .././hal//stm32f3/stm32f3_sysmem.c ...
.
Assembling: .././hal//stm32f3/stm32f3_startup.S
arm-none-eabi-gcc -c -mcpu=cortex-m4 -I. -x assembler-with-cpp -mthumb -mfloat-abi=soft -fmessage-length=0 -ffunction-sections -DF_CPU=7372800 -Wa,-gstabs,-adhlns=objdir-CWLITEARM/stm32f3_startup.lst -I.././simpleserial/ -I.././hal/ -I.././hal/ -I.././hal//stm32f3 -I.././hal//stm32f3/CMSIS -I.././hal//stm32f3/CMSIS/core -I.././hal//stm32f3/CMSIS/device -I.././hal//stm32f4/Legacy -I.././simpleserial/ -I.././crypto/ .././hal//stm32f3/stm32f3_startup.S -o objdir-CWLITEARM/stm32f3_startup.o
-e Done!
-e Done!
-e Done!
-e Done!
-e Done!
.
LINKING:
-en basic-passwdcheck-CWLITEARM.elf ...
-e Done!
.
.
Creating load file for Flash: basic-passwdcheck-CWLITEARM.hex
arm-none-eabi-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature basic-passwdcheck-CWLITEARM.elf basic-passwdcheck-CWLITEARM.hex
Creating load file for Flash: basic-passwdcheck-CWLITEARM.bin
arm-none-eabi-objcopy -O binary -R .eeprom -R .fuse -R .lock -R .signature basic-passwdcheck-CWLITEARM.elf basic-passwdcheck-CWLITEARM.bin
.
.
Creating load file for EEPROM: basic-passwdcheck-CWLITEARM.eep
arm-none-eabi-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" \
--change-section-lma .eeprom=0 --no-change-warnings -O ihex basic-passwdcheck-CWLITEARM.elf basic-passwdcheck-CWLITEARM.eep || exit 0
Creating Extended Listing: basic-passwdcheck-CWLITEARM.lss
arm-none-eabi-objdump -h -S -z basic-passwdcheck-CWLITEARM.elf > basic-passwdcheck-CWLITEARM.lss
.
Creating Symbol Table: basic-passwdcheck-CWLITEARM.sym
arm-none-eabi-nm -n basic-passwdcheck-CWLITEARM.elf > basic-passwdcheck-CWLITEARM.sym
Size after:
text data bss dec hex filename
4808 8 1176 5992 1768 basic-passwdcheck-CWLITEARM.elf
+--------------------------------------------------------
+ Default target does full rebuild each time.
+ Specify buildtarget == allquick == to avoid full rebuild
+--------------------------------------------------------
+--------------------------------------------------------
+ Built for platform CW-Lite Arm \(STM32F3\) with:
+ CRYPTO_TARGET = NONE
+ CRYPTO_OPTIONS = AES128C
+--------------------------------------------------------
Detected known STMF32: STM32F302xB(C)/303xB(C) Extended erase (0x44), this can take ten seconds or more Attempting to program 4815 bytes at 0x8000000 STM32F Programming flash...
STM32F Reading flash...
Verified flash OK, 4815 bytes
trace_test = cap_pass_trace("h\n")
#Basic sanity check
assert(len(trace_test) == 3000)
print("✔️ OK to continue!")
WARNING:root:SAM3U Serial buffers OVERRUN - data loss has occurred.
✔️ OK to continue!
Exploration¶
So what can we do with this? While first off - I'm going to cheat, and tell you that we have a preset password that starts with h
, and it's 5 characters long. But that's the only hint so far - what can you do? While first off, let's try plotting a comparison of h
to something else.
If you need a reminder of how to do a plot - see the matplotlib section of the Jupyter Introduction notebook.
The following cell shows you how to capture one power trace with h
sent as a password. From there:
- Try adding the plotting code and see what it looks like.
- Send different passwords to the device. We're only going to look at the difference between a password starting with
h
and something else right now. - Plot the different waveforms.
#Example - capture 'h' - end with newline '\n' as serial protocol expects that
trace_h = cap_pass_trace("h\n")
print(trace_h)
# ###################
# START SOLUTION
# ###################
cw.plot(cap_pass_trace("h\n")) * cw.plot(cap_pass_trace("0\n"))
# ###################
# END SOLUTION
# ###################
[ 0.05761719 -0.1015625 -0.07421875 ... -0.05273438 -0.02148438 -0.01074219]
For reference, the output should look something like this:
If you are using the %matplotlib notebook
magic, you can zoom in at the start. What you want to notice is there is two code paths taken, depending on a correct or incorrect path. Here for example is a correct & incorrect character processed:
OK interesting -- what's next? Let's plot every possible password character we could send.
Our password implementation only recognizes characters in the list abcdefghijklmnopqrstuvwxyz0123456789
, so we're going to limit it to those valid characters for now.
Write some code in the following block that implements the following algorithm:
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(CHARACTER + "\n")
plot(trace)
The above isn't quite valid code - so massage it into place! You also may notice the traces are way too long - you might want to make a more narrow plot that only does the first say 500 samples of the power trace.
📝Plotting Note
If using matplotlib
for plotting, you might need to add a plt.figure()
at the start to make a new figure. Otherwise you might find your plot ends up in the figure above! If you don't see the plots, sometimes a plt.show()
is needed at the end.
# ###################
# START SOLUTION
# ###################
from tqdm.notebook import tqdm
fig = cw.plot()
for c in tqdm('abcdefghijklmnopqrstuvwxyz0123456789'):
trace = cap_pass_trace(c + "\n")
fig *= cw.plot(trace[0:1000])
fig
# ###################
# END SOLUTION
# ###################