|
|
(45 intermediate revisions by 5 users not shown) |
Line 1: |
Line 1: |
− | This tutorial will take you through a complete attack on an encrypted bootloader using AES-256. This demonstrates how to using side-channel power analysis on practical systems, along with discussing how to perform analysis with custom scripts. | + | {{Warningbox|This tutorial has been updated for ChipWhisperer 5 release. If you are using 4.x.x or 3.x.x see the "V4" or "V3" link in the sidebar.}} |
| | | |
− | Whilst the tutorial assumes you will be performing the entire capture of traces along with the attack, it is possible to download the traces if you don't have the hardware, in which case skip section [[#Setting up the Hardware]] and [[#Capturing the Traces]].
| + | {{Infobox tutorial |
| + | |name = A5: Breaking AES-256 Bootloader |
| + | |image = |
| + | |caption = |
| + | |software versions = |
| + | |capture hardware = CW-Lite, CW-Lite 2-Part, CW-Pro |
| + | |Target Device = |
| + | |Target Architecture = XMEGA/Arm |
| + | |Hardware Crypto = No |
| + | |Purchase Hardware = |
| + | }} |
| | | |
− | = Background =
| + | <!-- To edit this, edit Template:Tutorial_boilerplate --> |
− | In the world of microcontrollers, a bootloader is a special piece of firmware that is made to let the user upload new programs into memory. This is especially useful for devices with complex code that may need to be patched or otherwise updated in the future - a bootloader makes it possible for the user to upload a patched version of the firmware onto the micro. The bootloader receives information from a communication line (a USB port, serial port, ethernet port, WiFi connection, etc...) and stores this data into program memory. Once the full firmware has been received, the micro can happily run its updated code.
| + | {{Tutorial boilerplate}} |
| | | |
− | There is one big security issue to worry about with bootloaders. A company may want to stop their customers from writing their own firmware and uploading it onto the micro. For example, this might be for protection reasons - hackers might be able to access parts of the device that weren't meant to be accessed. One way of stopping this is to add encryption. The company can add their own secret signature to the firmware code and encrypt it with a secret key. Then, the bootloader can decrypt the incoming firmware and confirm that the incoming firmware is correctly signed. Users will not know the secret key or the signature tied to the firmware, so they won't be able to "fake" their own.
| + | * Jupyter file: '''PA_Multi_1-Breaking_AES-256_Bootloader.ipynb''' |
| | | |
− | This tutorial will work with a simple AES-256 bootloader. The victim will receive data through a serial connection, decrypt the command, and confirm that the included signature is correct. Then, it will only save the code into memory if the signature check succeeded. To make this system more robust against attacks, the bootloader will use cipher-block chaining (CBC mode). Our goal is to find the secret key and the CBC initialization vector so that we could successfully fake our own firmware.
| |
| | | |
− | == Bootloader Communications Protocol == | + | == XMEGA Target == |
− | The bootloader's communications protocol operates over a serial port at 38400 baud rate. The bootloader is always waiting for new data to be sent in this example; in real life one would typically force the bootloader to enter through a command sequence.
| + | |
| | | |
− | Commands sent to the bootloader look as follows:
| + | See the following for using: |
| + | * ChipWhisperer-Lite Classic (XMEGA) |
| + | * ChipWhisperer-Lite Capture + XMEGA Target on UFO Board (including NAE-SCAPACK-L1/L2 users) |
| + | * ChipWhisperer-Pro + XMEGA Target on UFO Board |
| | | |
− | <pre>
| + | https://chipwhisperer.readthedocs.io/en/latest/tutorials/pa_multi_1-openadc-cwlitexmega.html#tutorial-pa-multi-1-openadc-cwlitexmega |
− | |<-------- Encrypted block (16 bytes) ---------->|
| + | |
− | | |
| + | |
− | +------+------+------+------+------+------+ .... +------+------+------+
| + | |
− | | 0x00 | Signature (4 Bytes) | Data (12 Bytes) | CRC-16 |
| + | |
− | +------+------+------+------+------+------+ .... +------+------+------+
| + | |
− | </pre>
| + | |
| | | |
− | This frame has four parts:
| + | == ChipWhisperer-Lite ARM / STM32F3 Target == |
− | * <code>0x00</code>: 1 byte of fixed header
| + | |
− | * Signature: A secret 4 byte constant. The bootloader will confirm that this signature is correct after decrypting the frame.
| + | |
− | * Data: 12 bytes of the incoming firmware. This system forces us to send the code 12 bytes at a time; more complete bootloaders may allow longer variable-length frames.
| + | |
− | * CRC-16: A 16-bit checksum using the CRC-CCITT polynomial (0x1021). The LSB of the CRC is sent first, followed by the MSB. The bootloader will reply over the serial port, describing whether or not this CRC check was valid.
| + | |
− | As described in the diagram, the 16 byte block is not sent as plaintext. Instead, it is encrypted using AES-256 in CBC mode. This encryption method will be described in the next section.
| + | |
| | | |
− | The bootloader responds to each command with a single byte indicating if the CRC-16 was OK or not:
| + | See the following for using: |
| + | * ChipWhisperer-Lite 32-bit (STM32F3 Target) |
| + | * ChipWhisperer-Lite Capture + STM32F3 Target on UFO Board (including NAE-SCAPACK-L1/L2 users) |
| + | * ChipWhisperer-Pro + STM32F3 Target on UFO Board |
| | | |
− | <pre>
| + | https://chipwhisperer.readthedocs.io/en/latest/tutorials/pa_multi_1-openadc-cwlitearm.html#tutorial-pa-multi-1-openadc-cwlitearm |
− | +------+
| + | |
− | CRC-OK: | 0xA1 |
| + | |
− | +------+
| + | |
| | | |
− | +------+
| + | == ChipWhisperer Nano Target == |
− | CRC Failed: | 0xA4 |
| + | |
− | +------+
| + | |
− | </pre>
| + | |
| | | |
− | Then, after replying to the command, the bootloader veries that the signature is correct. If it matches the expected manufacturer's signature, the 12 bytes of data will be written to flash memory. Otherwise, the data is discarded.
| + | This tutorial is not available for the ChipWhisperer Nano. |
− | | + | |
− | == Details of AES-256 CBC ==
| + | |
− | | + | |
− | The system uses the AES algorithm in Cipher Block Chaining (CBC) mode. In general one avoids using encryption 'as-is' (i.e. Electronic Code Book), since it means any piece of plaintext always maps to the same piece of ciphertext. Cipher Block Chaining ensures that if you encrypted the same thing a bunch of times it would always encrypt to a new piece of ciphertext.
| + | |
− | | + | |
− | You can see another reference on the design of the encryption side; we'll be only talking about the decryption side here. In this case AES-256 CBC mode is used as follows, where the details of the AES-256 Decryption block will be discussed in detail later:
| + | |
− | | + | |
− | [[File:aes256_cbc.png|image]]
| + | |
− | | + | |
− | This diagram shows that the output of the decryption is no longer used directly as the plaintext. Instead, the output is XORed with a 16 byte mask, which is usually taken from the previous ciphertext. Also, the first decryption block has no previous ciphertext to use, so a secret initialization vector (IV) is used instead. If we are going to decrypt the entire ciphertext (including block 0) or correctly generate our own ciphertext, we'll need to find this IV along with the AES key. | + | |
− | | + | |
− | | + | |
− | == Attacking AES-256 ==
| + | |
− | The system in this tutorial uses AES-256 encryption, which has a 256 bit (32 byte) key - twice as large as the 16 byte key we've attacked in previous tutorials. This means that our regular AES-128 CPA attacks won't quite work. However, extending these attacks to AES-256 is fairly straightforward: the theory is explained in detail in [[Extending AES-128 Attacks to AES-256]].
| + | |
− | | + | |
− | As the theory page explains, our AES-256 attack will have 4 steps:
| + | |
− | # Perform a standard attack (as in AES-128 decryption) to determine the first 16 bytes of the key, corresponding to the 14th round encryption key.
| + | |
− | # Using the known 14th round key, calculate the hypothetical outputs of each S-Box from the 13th round using the ciphertext processed by the 14th round, and determine the 16 bytes of the 13th round key manipulated by inverse MixColumns.
| + | |
− | # Perform the MixColumns and ShiftRows operation on the hypothetical key determined above, recovering the 13th round key.
| + | |
− | # Using the AES-256 key schedule, reverse the 13th and 14th round keys to determine the original AES-256 encryption key.
| + | |
− | | + | |
− | = Setting up the Hardware =
| + | |
− | This tutorial uses the [[CW1173 ChipWhisperer-Lite]] hardware. This hardware does not require any special setup - it should be ready to go out-of-the-box.
| + | |
− | | + | |
− | Note that you '''don't need hardware''' to complete the tutorial. Instead, you can download [https://www.assembla.com/spaces/chipwhisperer/wiki/Example_Captures example traces from the ChipWhisperer Site]. Just look for the traces titled ''AVR: AES256 Bootloader (ChipWhisperer Tutorial #A5)''.
| + | |
− | | + | |
− | == Building/Programming the Bootloader ==
| + | |
− | The firmware that implements the bootloader is available inside the ChipWhisperer folder at <code>chipwhisperer\hardware\victims\firmware\bootloader-aes256</code>. If you've uploaded the firmware for any of the other tutorials, the process is identical:
| + | |
− | | + | |
− | # Open the Makefile inside this folder. For the ChipWhisperer Lite, make sure that the line <code>PLATFORM = CW303</code> is uncommented. For other devices, make sure that the appropriate line is uncommented.
| + | |
− | # Open a command prompt/terminal window and navigate to this folder. Enter the command <code>make</code> and ensure that the program is successfully compiled. The output should end with a line like
| + | |
− | #: <pre>Built for platform CW-Lite XMEGA</pre>
| + | |
− | # Open the ChipWhisperer Capture software and connect to your hardware. Open the programmer window (''Tools > CW-Lite XMEGA Programmer''), find the <code>.hex</code> file that you just made, and ''Erase/Program/Verify FLASH''.
| + | |
− | | + | |
− | The firmware is now loaded onto your hardware, and you can continue onto the capture process.
| + | |
− | | + | |
− | = Capturing the Traces =
| + | |
− | Once the hardware is ready, we can capture some traces for our attack using the ChipWhisperer Capture software. If you somehow got to the 5th ''Advanced Tutorial'' without getting this software ready, you can follow the helpful guide at [[Installing ChipWhisperer]].
| + | |
− | | + | |
− | In some of the previous tutorials, we entered all of the capture settings by hand. Since we are civilized humans armed with technology, we can use a script to do all of this setup for us. A pre-written Python script is provided at [[#Appendix A: Capture Script]]. Take a look at this code and notice what it does:
| + | |
− | * it fills in the scope, target, and trace format that we'll use;
| + | |
− | * it connects to the hardware; and
| + | |
− | * it loads all of the hardware parameters for us. Nice!
| + | |
− | Copy this script into a <code>.py</code> file somewhere convenient. Then, perform the following steps to finish the capture:
| + | |
− | # Run the capture script, which will open a ChipWhisperer Capture window with everything connected for us.
| + | |
− | # Open the terminal (''Tools > Terminal'') and connect to the board. While the terminal is open, press the ''Capture 1'' button. A single byte of data should appear in the terminal. This byte will either be <code>a1</code> (CRC failed) or <code>a4</code> (CRC OK). If you see any other responses, something is wrong.
| + | |
− | #: [[File:Tutorial-A5-Capture.PNG|image]]
| + | |
− | # Once you're happy with this, open the General Settings tab and set the Number of Traces. You should need around 100 traces to break AES.
| + | |
− | # Press the ''Capture Many'' button to record the 100 traces. You'll see the new traces plotted on-screen.
| + | |
− | # Once the program is finished capturing the traces, save the project. Put it somewhere memorable and give it a nice name.
| + | |
− | | + | |
− | = Finding the Encryption Key =
| + | |
− | Now that we have our traces, we can go ahead and perform the attack. As described in the background theory, we'll have to do two attacks - one to get the 14th round key, and another (using the first result) to get the 13th round key. Then, we'll do some post-processing to finally get the 256 bit encryption key.
| + | |
− | | + | |
− | == 14th Round Key ==
| + | |
− | We can attack the 14th round key with a standard, no-frills CPA attack:
| + | |
− | | + | |
− | # Open the ChipWhisperer Analyzer program and load the <code>.cwp</code> file with the 13th and 14th round traces. This can be either the <code>aes256_round1413_key0_100.cwp</code> file downloaded or the capture you performed.
| + | |
− | # View and manipulate the trace data with the following steps:
| + | |
− | ## Switch to the ''Trace Output Plot'' tab
| + | |
− | ## Switch to the ''General'' parameter setting tab
| + | |
− | ## Choose the traces to be plotted and press the ''Redraw'' button to draw them
| + | |
− | ## Right-click on the waveform to change options, or left-click and drag to zoom
| + | |
− | ## Use the toolbar to quickly reset the zoom back to original
| + | |
− | ##: [[File:Tutorial-A5-Plot-Traces.PNG|image]]
| + | |
− | ##: Notice that the traces are synchronized for the first 7000 samples, but become unsynchronized later. This fact will be important later in the tutorial.
| + | |
− | # Set up the attack in the ''Attack'' settings tab:
| + | |
− | ## Leave the Crypto Algorithm set to AES-128. (Remember that we're applying the AES-128 attack to half of the AES-256 key!)
| + | |
− | ## Change the Leakage Model to ''HW: AES Inv SBox Output, First Round (Dec)''.
| + | |
− | ## If you're finding the attack very slow, narrow down the attack a bit. Normally, this requires a bit of investigation to determine which ranges of the trace are important. Here, you can use the range from 2900 for 4200. The default settings will also work fine!
| + | |
− | ##: [[File:Tutorial-A5-Hardware-Model.PNG|image]]
| + | |
− | # Note that we do ''not'' know the secret encryption key, so we cannot highlight the correct key automatically. If you want to fix this, the ''Results'' settings tab has a Highlighted Key setting. Change this to Override mode and enter the key <code>ea 79 79 20 c8 71 44 7d 46 62 5f 51 85 c1 3b cb</code>.
| + | |
− | # Finally, run the attack by switching to the ''Results Table'' tab and then hitting the ''Attack'' button.
| + | |
− | | + | |
− | There are a few ways to check the results of the attack. First, the results table will show the best guesses for each subkey. With the highlight override enabled, the red bytes should be the best guesses for every single subkey:
| + | |
− | | + | |
− | [[File:Tutorial-A5-Results-Right-Key.PNG|image]]
| + | |
− | | + | |
− | However, the correct key will still rise to the top even if the wrong bytes are highlighted. The coloring and correlation coefficients in the results table should still make it clear that the top guess is the best one:
| + | |
− | | + | |
− | [[File:Tutorial-A5-Results-Wrong-Key.PNG|image]]
| + | |
− | | + | |
− | Finally, the ''Output vs Point Plot'' shows the correlation against all of the sample points. The spikes on this plot show exactly where the attack was successful (ie: where the sensitive data was leaked):
| + | |
− | | + | |
− | [[File:Aes14round points.png|image]]
| + | |
− | | + | |
− | In any case, we've determined that the correct 14th round key is <code>ea 79 79 20 c8 71 44 7d 46 62 5f 51 85 c1 3b cb</code>.
| + | |
− | | + | |
− | ''NOTE: if you're stuck, a full listing of the attack script is given in [[#Appendix C: AES-256 14th Round Key Script]].''
| + | |
− | | + | |
− | == 13th Round Key ==
| + | |
− | Unfortunately, we cannot use the GUI to attack the 13th round key. The system has no built-in model for round 13 of the AES-256 algorithm. Instead, we can write our own script and insert a custom model into the system. See [[#Appendix D: AES-256 13th Round Key Script]] for complete script used here.
| + | |
− | | + | |
− | The ChipWhisperer Analyzer software uses the settings in the GUI to automatically adjust an attack script. Every time you change a setting in the GUI, the autogenerated script is overwritten. Fpr example, the point range is mapped directly to an API call:
| + | |
− | | + | |
− | [[File:autoscript1.png|image]]
| + | |
− | | + | |
− | If we modified this script directly, it would be very easy for us to accidentally overwrite our custom script from the GUI. Instead, we'll use the autogenerated code to set up a base script, then add in our own attack model. To set up the base script, the procedure is as follows:
| + | |
− | | + | |
− | # Open the ChipWhisperer Analyzer software again and reopen the project file.
| + | |
− | # Recall from the 14th round attack that the trace data becomes unsynchronized around sample 7000. This is due to a non-constant AES implementation: the code does not always take the same amount of time to run for every input. (It's actually possible to do a timing attack on this AES implementation! We'll stick with our CPA attack for now.)
| + | |
− | #: [[File:syncproblems.png|image]]
| + | |
− | # Resynchronize the traces:
| + | |
− | ## In the ''Attack Script Generator'' tab, enable the ''Resync: Sum of Difference'' preprocessing:
| + | |
− | ##: [[File:resyncsad.png|image]]
| + | |
− | ## Enable the module and configure the input points. To start, set the reference points to (9063, 9177) and the input window to (9010, 9080), but don't be afraid to change these ranges:
| + | |
− | ##: [[File:resyncsad2.png|image]]
| + | |
− | ## Redraw the traces and confirm we now have synchronization on the second half:
| + | |
− | ##: [[File:resyncsad3.png|image]]
| + | |
− | | + | |
− | Now, we are ready to make a copy of this script:
| + | |
− | # Click on the auto-generated script
| + | |
− | # Hit ''Copy'' and save the file somewhere
| + | |
− | # Double-click on the description of the new file and give it a better name.
| + | |
− | # Finally, hit ''Set Active'' after clicking on your new file. The result should look like this:
| + | |
− | #: [[File:aes256_customscript.png|image]]
| + | |
− | You can now edit the custom script file using the built-in editor OR with an external editor. In this example, the file would be <code>C:\Users\Colin\AppData\Local\Temp\testaes256.py</code>.
| + | |
− | | + | |
− | The next step is to program our own leakage model. The following Python code models the Hamming weight model of the 13th round S-box:
| + | |
− | | + | |
− | <pre>
| + | |
− | # Imports for AES256 Attack
| + | |
− | from chipwhisperer.analyzer.attacks.models.AES128_8bit import getHW
| + | |
− | from chipwhisperer.analyzer.models.aes.funcs import sbox, inv_sbox, inv_shiftrows, inv_mixcolumns, inv_subbytes
| + | |
− | | + | |
− | class AES256Attack(object):
| + | |
− | numSubKeys = 16
| + | |
− | | + | |
− | @staticmethod
| + | |
− | def leakage(textin, textout, guess, bnum, setting, state):
| + | |
− | if setting == 13:
| + | |
− | knownkey = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]
| + | |
− | xored = [knownkey[i] ^ textin[i] for i in range(0, 16)]
| + | |
− | block = xored
| + | |
− | block = inv_shiftrows(block)
| + | |
− | block = inv_subbytes(block)
| + | |
− | block = inv_mixcolumns(block)
| + | |
− | block = inv_shiftrows(block)
| + | |
− | result = block
| + | |
− | return getHW(inv_sbox((result[bnum] ^ guess)))
| + | |
− | </pre>
| + | |
− | You can look back at the C code of the AES-256 decryption to see how this is implementing the decryption code. Note that because of the Inverse MixColumns operation, we need the entire input ciphertext -- otherwise, we would only need to operate on one byte of the ciphertext.
| + | |
− | | + | |
− | The last step is to perform the attack using this model:
| + | |
− | # Add the above function to your custom script file.
| + | |
− | # Change the <code>setAnalysisAlgorithm</code> in the script to use your custom functions by making the following call:
| + | |
− | #:<pre>self.attack.setAnalysisAlgorithm(CPAProgressive, AES256Attack, 13)</pre>
| + | |
− | # As we did in the 14th round attack, reducing the point range can speed up the attack. For example, to use a smaller range of points, try changing the <code>setPointRange()</code> function call to
| + | |
− | #:<pre>self.attack.setPointRange((8000,10990))</pre>
| + | |
− | # Start the attack! Wait for the attack to complete, and you will determine the 13th round key:
| + | |
− | #: [[File:Tutorial-A5-Results-Round-13.PNG|image]]
| + | |
− | | + | |
− | Note you can check [[#Appendix C AES-256 13th Round Key Script]] for the complete contents of the attack script.
| + | |
− | | + | |
− | Finally, we need to convert this hypothetical key into the actual value of the 13th round key. We can do this by passing the key through ShiftRows and MixColumns to remove the effect of these two functions. This is easy to do in the Python console:
| + | |
− | | + | |
− | <pre>
| + | |
− | >>> from chipwhisperer.analyzer.models.aes.funcs import shiftrows,mixcolumns
| + | |
− | >>> = [0xC6, 0xBD, 0x4E, 0x50, 0xAB, 0xCA, 0x75, 0x77, 0x79, 0x87, 0x96, 0xCA, 0x1C, 0x7F, 0xC5, 0x82]
| + | |
− | >>> key = shiftrows(knownkey)
| + | |
− | >>> key = mixcolumns(key)
| + | |
− | >>> print " ".join(["%02x" % i for i in key])
| + | |
− | c6 6a a6 12 4a ba 4d 04 4a 22 03 54 5b 28 0e 63
| + | |
− | </pre>
| + | |
− | | + | |
− | Our hard work has rewarded us with the 13th round key, which is <code>c6 6a a6 12 4a ba 4d 04 4a 22 03 54 5b 28 0e 63</code>.
| + | |
− | | + | |
− | == Recovering the Encryption Key ==
| + | |
− | Finally, we have enough information to recover the initial encryption key. In AES-256, the initial key is used in the key expansion routine to generate 15 round keys, and we know the key for round 13 and 14. All we need to do now is reverse the key scheduling algorithm to calculate the ''0/1 Round Key'' from the ''13/14 Round Key''.
| + | |
− | | + | |
− | In the ChipWhisperer Analyzer software, a key schedule calculator is provided in ''Tools > AES Key Schedule'':
| + | |
− | | + | |
− | [[File:keyschedule_tool.png|image]]
| + | |
− | | + | |
− | Open this tool and paste the 13/14 round keys, which are
| + | |
− | <pre>
| + | |
− | c6 6a a6 12 4a ba 4d 04 4a 22 03 54 5b 28 0e 63 ea 79 79 20 c8 71 44 7d 46 62 5f 51 85 c1 3b cb
| + | |
− | </pre>
| + | |
− | | + | |
− | Tell the tool that this key is the 13/14 round key; it will automatically display the entire key schedule and the initial encryption key. You should find the initial encryption key is:
| + | |
− | <pre>
| + | |
− | 94 28 5d 4d 6d cf ec 08 d8 ac dd f6 be 25 a4 99 c4 d9 d0 1e c3 40 7e d7 d5 28 d4 09 e9 f0 88 a1
| + | |
− | </pre>
| + | |
− | | + | |
− | Peek into <code>supersecret.h</code>, confirm that this is the right key, and celebrate!
| + | |
− | | + | |
− | = Next Steps =
| + | |
− | If you want to go further with this tutorial, [[Tutorial A5-Bonus: Breaking AES-256 Bootloader]] continues working with the same firmware to find the remaining secrets in the bootloader (the IV and the signature).
| + | |
− | | + | |
− | = Appendix A: Target Code =
| + | |
− | | + | |
− | The following:
| + | |
− | | + | |
− | <pre>#!/usr/bin/python
| + | |
− | # -*- coding: utf-8 -*-
| + | |
− | #
| + | |
− | # Copyright (c) 2013-2014, NewAE Technology Inc
| + | |
− | # All rights reserved.
| + | |
− | #
| + | |
− | # Authors: Colin O'Flynn
| + | |
− | #
| + | |
− | # Find this and more at newae.com - this file is part of the chipwhisperer
| + | |
− | # project, http://www.assembla.com/spaces/chipwhisperer
| + | |
− | #
| + | |
− | # This file is part of chipwhisperer.
| + | |
− | #
| + | |
− | # chipwhisperer is free software: you can redistribute it and/or modify
| + | |
− | # it under the terms of the GNU General Public License as published by
| + | |
− | # the Free Software Foundation, either version 3 of the License, or
| + | |
− | # (at your option) any later version.
| + | |
− | #
| + | |
− | # chipwhisperer is distributed in the hope that it will be useful,
| + | |
− | # but WITHOUT ANY WARRANTY; without even the implied warranty of
| + | |
− | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
| + | |
− | # GNU Lesser General Public License for more details.
| + | |
− | #
| + | |
− | # You should have received a copy of the GNU General Public License
| + | |
− | # along with chipwhisperer. If not, see <http://www.gnu.org/licenses/>.
| + | |
− | #=================================================
| + | |
− | #
| + | |
− | #
| + | |
− | #
| + | |
− | # This example captures data using the ChipWhisperer Rev2 capture hardware.
| + | |
− | # The target is a SimpleSerial board attached to the ChipWhisperer.
| + | |
− | #
| + | |
− | # Data is saved into both a project file and a MATLAB array
| + | |
− | #
| + | |
− | | + | |
− | #Setup path
| + | |
− | import sys
| + | |
− | | + | |
− | import time
| + | |
− | | + | |
− | #Import the ChipWhispererCapture module
| + | |
− | import chipwhisperer.capture.ChipWhispererCapture as cwc
| + | |
− | from chipwhisperer.capture.targets.TargetTemplate import TargetTemplate
| + | |
− | from chipwhisperer.capture.targets.SimpleSerial import SimpleSerial_ChipWhisperer
| + | |
− | | + | |
− | #Check for PySide
| + | |
− | try:
| + | |
− | from PySide.QtCore import *
| + | |
− | from PySide.QtGui import *
| + | |
− | except ImportError:
| + | |
− | print "ERROR: PySide is required for this program"
| + | |
− | sys.exit()
| + | |
− | | + | |
− | import thread
| + | |
− | | + | |
− | import scipy.io as sio
| + | |
− | | + | |
− | exitWhenDone=False
| + | |
− | | + | |
− | def pe():
| + | |
− | QCoreApplication.processEvents()
| + | |
− | | + | |
− | # Class Crc
| + | |
− | #############################################################
| + | |
− | # These CRC routines are copy-pasted from pycrc, which are:
| + | |
− | # Copyright (c) 2006-2013 Thomas Pircher <tehpeh@gmx.net>
| + | |
− | #
| + | |
− | class Crc(object):
| + | |
− | """
| + | |
− | A base class for CRC routines.
| + | |
− | """
| + | |
− | | + | |
− | def __init__(self, width, poly):
| + | |
− | """The Crc constructor.
| + | |
− | | + | |
− | The parameters are as follows:
| + | |
− | width
| + | |
− | poly
| + | |
− | reflect_in
| + | |
− | xor_in
| + | |
− | reflect_out
| + | |
− | xor_out
| + | |
− | """
| + | |
− | self.Width = width
| + | |
− | self.Poly = poly
| + | |
− | | + | |
− | | + | |
− | self.MSB_Mask = 0x1 << (self.Width - 1)
| + | |
− | self.Mask = ((self.MSB_Mask - 1) << 1) | 1
| + | |
− | | + | |
− | self.XorIn = 0x0000
| + | |
− | self.XorOut = 0x0000
| + | |
− | | + | |
− | self.DirectInit = self.XorIn
| + | |
− | self.NonDirectInit = self.__get_nondirect_init(self.XorIn)
| + | |
− | if self.Width < 8:
| + | |
− | self.CrcShift = 8 - self.Width
| + | |
− | else:
| + | |
− | self.CrcShift = 0
| + | |
− | | + | |
− | def __get_nondirect_init(self, init):
| + | |
− | """
| + | |
− | return the non-direct init if the direct algorithm has been selected.
| + | |
− | """
| + | |
− | crc = init
| + | |
− | for i in range(self.Width):
| + | |
− | bit = crc & 0x01
| + | |
− | if bit:
| + | |
− | crc ^= self.Poly
| + | |
− | crc >>= 1
| + | |
− | if bit:
| + | |
− | crc |= self.MSB_Mask
| + | |
− | return crc & self.Mask
| + | |
− | | + | |
− | | + | |
− | def bit_by_bit(self, in_data):
| + | |
− | """
| + | |
− | Classic simple and slow CRC implementation. This function iterates bit
| + | |
− | by bit over the augmented input message and returns the calculated CRC
| + | |
− | value at the end.
| + | |
− | """
| + | |
− | # If the input data is a string, convert to bytes.
| + | |
− | if isinstance(in_data, str):
| + | |
− | in_data = [ord(c) for c in in_data]
| + | |
− | | + | |
− | register = self.NonDirectInit
| + | |
− | for octet in in_data:
| + | |
− | for i in range(8):
| + | |
− | topbit = register & self.MSB_Mask
| + | |
− | register = ((register << 1) & self.Mask) | ((octet >> (7 - i)) & 0x01)
| + | |
− | if topbit:
| + | |
− | register ^= self.Poly
| + | |
− | | + | |
− | for i in range(self.Width):
| + | |
− | topbit = register & self.MSB_Mask
| + | |
− | register = ((register << 1) & self.Mask)
| + | |
− | if topbit:
| + | |
− | register ^= self.Poly
| + | |
− | | + | |
− | return register ^ self.XorOut
| + | |
− | | + | |
− | class BootloaderTarget(TargetTemplate):
| + | |
− | paramListUpdated = Signal(list)
| + | |
− | | + | |
− | def setupParameters(self):
| + | |
− | self.ser = SimpleSerial_ChipWhisperer()
| + | |
− | self.keylength = 16
| + | |
− | self.input = ""
| + | |
− | self.crc = Crc(width=16, poly=0x1021)
| + | |
− | | + | |
− | def setOpenADC(self, oadc):
| + | |
− | try:
| + | |
− | self.ser.setOpenADC(oadc)
| + | |
− | except:
| + | |
− | pass
| + | |
− | | + | |
− | def setKeyLen(self, klen):
| + | |
− | """ Set key length in BITS """
| + | |
− | self.keylength = klen / 8
| + | |
− | | + | |
− | def keyLen(self):
| + | |
− | """ Return key length in BYTES """
| + | |
− | return self.keylength
| + | |
− | | + | |
− | | + | |
− | def paramList(self):
| + | |
− | return []
| + | |
− | | + | |
− | def con(self):
| + | |
− | self.ser.con()
| + | |
− | self.ser.flush()
| + | |
− | | + | |
− | def dis(self):
| + | |
− | self.close()
| + | |
− | | + | |
− | def close(self):
| + | |
− | if self.ser != None:
| + | |
− | self.ser.close()
| + | |
− | self.ser = None
| + | |
− | return
| + | |
− | | + | |
− | def init(self):
| + | |
− | pass
| + | |
− | | + | |
− | def setModeEncrypt(self):
| + | |
− | return
| + | |
− | | + | |
− | def setModeDecrypt(self):
| + | |
− | return
| + | |
− | | + | |
− | def loadEncryptionKey(self, key):
| + | |
− | pass
| + | |
− | | + | |
− | def loadInput(self, inputtext):
| + | |
− | self.input = inputtext
| + | |
− | | + | |
− | def isDone(self):
| + | |
− | return True
| + | |
− | | + | |
− | def readOutput(self):
| + | |
− | #No actual output
| + | |
− | return [0] * 16
| + | |
− | | + | |
− | def go(self):
| + | |
− | # Starting byte is 0x00
| + | |
− | message = [0x00]
| + | |
− | | + | |
− | # Append 16 bytes of data
| + | |
− | message.extend(self.input)
| + | |
− | | + | |
− | # Append 2 bytes of CRC for input only (not including 0x00)
| + | |
− | crcdata = self.crc.bit_by_bit(self.input)
| + | |
− | | + | |
− | message.append(crcdata >> 8)
| + | |
− | message.append(crcdata & 0xff)
| + | |
− | | + | |
− | # Write message
| + | |
− | for i in range(0, 5):
| + | |
− | self.ser.flush()
| + | |
− | self.ser.write(message)
| + | |
− | time.sleep(0.1)
| + | |
− | data = self.ser.read(1)
| + | |
− | | + | |
− | if len(data) > 0:
| + | |
− | resp = ord(data[0])
| + | |
− | | + | |
− | if resp == 0xA4:
| + | |
− | # Encryption run OK
| + | |
− | break
| + | |
− | | + | |
− | if resp != 0xA1:
| + | |
− | raise IOError("Bad Response %x" % resp)
| + | |
− | | + | |
− | if len(data) > 0:
| + | |
− | if resp != 0xA4:
| + | |
− | raise IOError("Failed to communicate, last response: %x" % resp)
| + | |
− | else:
| + | |
− | raise IOError("Failed to communicate, no response")
| + | |
− | | + | |
− | def checkEncryptionKey(self, kin):
| + | |
− | return kin
| + | |
− | | + | |
− | class userScript(QObject):
| + | |
− | | + | |
− | def __init__(self, capture):
| + | |
− | super(userScript, self).__init__()
| + | |
− | self.capture = capture
| + | |
− | | + | |
− | | + | |
− | def run(self):
| + | |
− | cap = self.capture
| + | |
− | | + | |
− | #User commands here
| + | |
− | print "***** Starting User Script *****"
| + | |
− | | + | |
− | tbootloader = BootloaderTarget()
| + | |
− | | + | |
− | cap.setParameter(['Generic Settings', 'Scope Module', 'ChipWhisperer/OpenADC'])
| + | |
− | cap.setParameter(['Generic Settings', 'Trace Format', 'ChipWhisperer/Native'])
| + | |
− | | + | |
− | cap.target.setDriver(tbootloader)
| + | |
− | | + | |
− | #Load FW (must be configured in GUI first)
| + | |
− | cap.FWLoaderGo()
| + | |
− | | + | |
− | #NOTE: You MUST add this call to pe() to process events. This is done automatically
| + | |
− | #for setParameter() calls, but everything else REQUIRES this
| + | |
− | pe()
| + | |
− | | + | |
− | cap.doConDis()
| + | |
− | | + | |
− | pe()
| + | |
− | | + | |
− | #Example of using a list to set parameters. Slightly easier to copy/paste in this format
| + | |
− | lstexample = [['CW Extra', 'CW Extra Settings', 'Trigger Pins', 'Front Panel A', False],
| + | |
− | ['CW Extra', 'CW Extra Settings', 'Trigger Pins', 'Target IO4 (Trigger Line)', True],
| + | |
− | ['CW Extra', 'CW Extra Settings', 'Clock Source', 'Target IO-IN'],
| + | |
− | ['OpenADC', 'Clock Setup', 'ADC Clock', 'Source', 'EXTCLK x4 via DCM'],
| + | |
− | ['OpenADC', 'Trigger Setup', 'Total Samples', 11000],
| + | |
− | ['OpenADC', 'Trigger Setup', 'Offset', 0],
| + | |
− | ['OpenADC', 'Gain Setting', 'Setting', 45],
| + | |
− | ['OpenADC', 'Trigger Setup', 'Mode', 'rising edge'],
| + | |
− | #Final step: make DCMs relock in case they are lost
| + | |
− | ['OpenADC', 'Clock Setup', 'ADC Clock', 'Reset ADC DCM', None],
| + | |
− | | + | |
− | ['Generic Settings', 'Auxilary Module', 'Toggle FPGA-GPIO Pins'],
| + | |
− | ['GPIO Toggle', 'Standby State', 'High'],
| + | |
− | ['GPIO Toggle', 'Post-Toggle Delay', 150],
| + | |
− | ['GPIO Toggle', 'Toggle Length', 100],
| + | |
− | ]
| + | |
− | | + | |
− | # For IV: offset = 70000
| + | |
− | | + | |
− | #Download all hardware setup parameters
| + | |
− | for cmd in lstexample: cap.setParameter(cmd)
| + | |
− | | + | |
− | #Let's only do a few traces
| + | |
− | cap.setParameter(['Generic Settings', 'Acquisition Settings', 'Number of Traces', 50])
| + | |
− | | + | |
− | #Throw away first few
| + | |
− | cap.capture1()
| + | |
− | pe()
| + | |
− | cap.capture1()
| + | |
− | pe()
| + | |
− | | + | |
− | print "***** Ending User Script *****"
| + | |
− | | + | |
− | | + | |
− | if __name__ == '__main__':
| + | |
− | #Make the application
| + | |
− | app = cwc.makeApplication()
| + | |
− | | + | |
− | #If you DO NOT want to overwrite/use settings from the GUI version including
| + | |
− | #the recent files list, uncomment the following:
| + | |
− | #app.setApplicationName("Capture V2 Scripted")
| + | |
− | | + | |
− | #Get main module
| + | |
− | capture = cwc.ChipWhispererCapture()
| + | |
− | | + | |
− | #Show window - even if not used
| + | |
− | capture.show()
| + | |
− | | + | |
− | #NB: Must call processEvents since we aren't using proper event loop
| + | |
− | pe()
| + | |
− | # Call user-specific commands
| + | |
− | usercommands = userScript(capture)
| + | |
− | | + | |
− | usercommands.run()
| + | |
− | | + | |
− | app.exec_()
| + | |
− | | + | |
− | sys.exit()</pre>
| + | |
− | | + | |
− | | + | |
− | = Appendix B: Capture Script =
| + | |
− | <pre>
| + | |
− | #!/usr/bin/python
| + | |
− | # -*- coding: utf-8 -*-
| + | |
− | #
| + | |
− | # Copyright (c) 2013-2016, NewAE Technology Inc
| + | |
− | # All rights reserved.
| + | |
− | #
| + | |
− | # Authors: Colin O'Flynn, Greg d'Eon
| + | |
− | #
| + | |
− | # Find this and more at newae.com - this file is part of the chipwhisperer
| + | |
− | # project, http://www.assembla.com/spaces/chipwhisperer
| + | |
− | #
| + | |
− | # This file is part of chipwhisperer.
| + | |
− | #
| + | |
− | # chipwhisperer is free software: you can redistribute it and/or modify
| + | |
− | # it under the terms of the GNU General Public License as published by
| + | |
− | # the Free Software Foundation, either version 3 of the License, or
| + | |
− | # (at your option) any later version.
| + | |
− | #
| + | |
− | # chipwhisperer is distributed in the hope that it will be useful,
| + | |
− | # but WITHOUT ANY WARRANTY; without even the implied warranty of
| + | |
− | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
| + | |
− | # GNU Lesser General Public License for more details.
| + | |
− | #
| + | |
− | # You should have received a copy of the GNU General Public License
| + | |
− | # along with chipwhisperer. If not, see <http://www.gnu.org/licenses/>.
| + | |
− | #=================================================
| + | |
− | | + | |
− | import sys
| + | |
− | import chipwhisperer.capture.ui.CWCaptureGUI as cwc
| + | |
− | from chipwhisperer.common.api.CWCoreAPI import CWCoreAPI
| + | |
− | from chipwhisperer.common.scripts.base import UserScriptBase
| + | |
− | from chipwhisperer.common.utils.parameter import Parameter
| + | |
− | | + | |
− | # Check for PySide
| + | |
− | try:
| + | |
− | from PySide.QtCore import *
| + | |
− | from PySide.QtGui import *
| + | |
− | except ImportError:
| + | |
− | print "ERROR: PySide is required for this program"
| + | |
− | sys.exit()
| + | |
− | | + | |
− | class UserScript(UserScriptBase):
| + | |
− | def __init__(self, api):
| + | |
− | super(UserScript, self).__init__(api)
| + | |
− | | + | |
− | def run(self):
| + | |
− | #User commands here
| + | |
− | print "***** Starting User Script *****"
| + | |
− |
| + | |
− | # Set up board and target
| + | |
− | self.api.setParameter(['Generic Settings', 'Scope Module', 'ChipWhisperer/OpenADC'])
| + | |
− | self.api.setParameter(['Generic Settings', 'Trace Format', 'ChipWhisperer/Native'])
| + | |
− | self.api.setParameter(['Generic Settings', 'Target Module', 'AES Bootloader'])
| + | |
− | self.api.connect()
| + | |
− | | + | |
− | # Fill in our other settings
| + | |
− | lstexample = [['CW Extra Settings', 'Trigger Pins', 'Target IO4 (Trigger Line)', True],
| + | |
− | ['CW Extra Settings', 'Clock Source', 'Target IO-IN'],
| + | |
− | ['CW Extra Settings', 'Target IOn Pins', 'Target IO2', 'Serial TXD'],
| + | |
− | ['CW Extra Settings', 'Target IOn Pins', 'Target IO1', 'Serial RXD'],
| + | |
− | ['CW Extra Settings', 'Target HS IO-Out', 'CLKGEN'],
| + | |
− | ['OpenADC', 'Clock Setup', 'ADC Clock', 'Source', 'CLKGEN x4 via DCM'],
| + | |
− | ['OpenADC', 'Trigger Setup', 'Total Samples', 11000],
| + | |
− | ['OpenADC', 'Trigger Setup', 'Offset', 0],
| + | |
− | ['OpenADC', 'Gain Setting', 'Setting', 45],
| + | |
− | ['OpenADC', 'Trigger Setup', 'Mode', 'rising edge'],
| + | |
− | ['OpenADC', 'Clock Setup', 'CLKGEN Settings', 'Multiply', 2],
| + | |
− | ['OpenADC', 'Clock Setup', 'CLKGEN Settings', 'Divide', 26],
| + | |
− | ['OpenADC', 'Clock Setup', 'ADC Clock', 'Reset ADC DCM', None],
| + | |
− | ]
| + | |
− | | + | |
− | # NOTE: For IV: offset = 70000
| + | |
− | #Download all hardware setup parameters
| + | |
− | for cmd in lstexample:
| + | |
− | self.api.setParameter(cmd)
| + | |
− | | + | |
− | # Try a couple of captures
| + | |
− | self.api.capture1()
| + | |
− | self.api.capture1()
| + | |
− | | + | |
− | print "***** Ending User Script *****"
| + | |
− | | + | |
− | | + | |
− | if __name__ == '__main__':
| + | |
− | # Run the program
| + | |
− | app = cwc.makeApplication()
| + | |
− | Parameter.usePyQtGraph = True
| + | |
− | api = CWCoreAPI()
| + | |
− | gui = cwc.CWCaptureGUI(api)
| + | |
− | gui.show()
| + | |
− |
| + | |
− | # Run our program and let the GUI take over
| + | |
− | api.runScriptClass(UserScript)
| + | |
− | sys.exit(app.exec_())
| + | |
− | </pre>
| + | |
− | | + | |
− | | + | |
− | = Appendix C: AES-256 14th Round Key Script =
| + | |
− | | + | |
− | '''NB: This script works for 0.10 release or later, see local copy in doc/html directory of chipwhisperer release if you need earlier versions'''
| + | |
− | | + | |
− | Full attack script, copy/paste into a file then add as active attack script:
| + | |
− | | + | |
− | <pre># AES-256 14th Round Key Attack
| + | |
− | from chipwhisperer.common.autoscript import AutoScriptBase
| + | |
− | #Imports from Preprocessing
| + | |
− | import chipwhisperer.analyzer.preprocessing as preprocessing
| + | |
− | #Imports from Capture
| + | |
− | from chipwhisperer.analyzer.attacks.CPA import CPA
| + | |
− | from chipwhisperer.analyzer.attacks.CPAProgressive import CPAProgressive
| + | |
− | import chipwhisperer.analyzer.attacks.models.AES128_8bit
| + | |
− | #Imports from utilList
| + | |
− | | + | |
− | class userScript(AutoScriptBase):
| + | |
− | preProcessingList = []
| + | |
− | def initProject(self):
| + | |
− | pass
| + | |
− | | + | |
− | def initPreprocessing(self):
| + | |
− | self.preProcessingList = []
| + | |
− | return self.preProcessingList
| + | |
− | | + | |
− | def initAnalysis(self):
| + | |
− | self.attack = CPA(self.parent, console=self.console, showScriptParameter=self.showScriptParameter)
| + | |
− | self.attack.setAnalysisAlgorithm(CPAProgressive,chipwhisperer.analyzer.attacks.models.AES128_8bit,chipwhisperer.analyzer.attacks.models.AES128_8bit.LEAK_HW_INVSBOXOUT_FIRSTROUND)
| + | |
− | self.attack.setTraceStart(0)
| + | |
− | self.attack.setTracesPerAttack(99)
| + | |
− | self.attack.setIterations(1)
| + | |
− | self.attack.setReportingInterval(10)
| + | |
− | self.attack.setTargetBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
| + | |
− | self.attack.setTraceManager(self.traceManager())
| + | |
− | self.attack.setProject(self.project())
| + | |
− | self.attack.setPointRange((0,10992))
| + | |
− | return self.attack
| + | |
− | | + | |
− | def initReporting(self, results):
| + | |
− | results.setAttack(self.attack)
| + | |
− | results.setTraceManager(self.traceManager())
| + | |
− | self.results = results
| + | |
− | | + | |
− | def doAnalysis(self):
| + | |
− | self.attack.doAttack()</pre>
| + | |
− | | + | |
− | | + | |
− | | + | |
− | = Appendix D: AES-256 13th Round Key Script =
| + | |
− | | + | |
− | '''NB: This script works for 0.10 release or later, see local copy in doc/html directory of chipwhisperer release if you need earlier versions'''
| + | |
− | | + | |
− | Full attack script, copy/paste into a file then add as active attack script:
| + | |
− | | + | |
− | <pre># AES-256 13th Round Key Script
| + | |
− | from chipwhisperer.common.autoscript import AutoScriptBase
| + | |
− | #Imports from Preprocessing
| + | |
− | import chipwhisperer.analyzer.preprocessing as preprocessing
| + | |
− | #Imports from Capture
| + | |
− | from chipwhisperer.analyzer.attacks.CPA import CPA
| + | |
− | from chipwhisperer.analyzer.attacks.CPAProgressive import CPAProgressive
| + | |
− | import chipwhisperer.analyzer.attacks.models.AES128_8bit
| + | |
− | # Imports from utilList
| + | |
− | | + | |
− | # Imports for AES256 Attack
| + | |
− | from chipwhisperer.analyzer.attacks.models.AES128_8bit import getHW
| + | |
− | from chipwhisperer.analyzer.models.aes.funcs import sbox, inv_sbox, inv_shiftrows, inv_mixcolumns, inv_subbytes
| + | |
− | | + | |
− | class AES256Attack(object):
| + | |
− | numSubKeys = 16
| + | |
− | | + | |
− | @staticmethod
| + | |
− | def leakage(textin, textout, guess, bnum, setting, state):
| + | |
− | if setting == 13:
| + | |
− | knownkey = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]
| + | |
− | xored = [knownkey[i] ^ textin[i] for i in range(0, 16)]
| + | |
− | block = xored
| + | |
− | block = inv_shiftrows(block)
| + | |
− | block = inv_subbytes(block)
| + | |
− | block = inv_mixcolumns(block)
| + | |
− | block = inv_shiftrows(block)
| + | |
− | result = block
| + | |
− | return getHW(inv_sbox((result[bnum] ^ guess)))
| + | |
− | | + | |
− | class userScript(AutoScriptBase):
| + | |
− | preProcessingList = []
| + | |
− | def initProject(self):
| + | |
− | pass
| + | |
− | | + | |
− | def initPreprocessing(self):
| + | |
− | self.preProcessingResyncSAD0 = preprocessing.ResyncSAD.ResyncSAD(self.parent)
| + | |
− | self.preProcessingResyncSAD0.setEnabled(True)
| + | |
− | self.preProcessingResyncSAD0.setReference(rtraceno=0, refpoints=(9063,9177), inputwindow=(9010,9180))
| + | |
− | self.preProcessingList = [self.preProcessingResyncSAD0,]
| + | |
− | return self.preProcessingList
| + | |
− | | + | |
− | def initAnalysis(self):
| + | |
− | self.attack = CPA(self.parent, console=self.console, showScriptParameter=self.showScriptParameter)
| + | |
− | self.attack.setAnalysisAlgorithm(CPAProgressive, AES256Attack, 13)
| + | |
− | self.attack.setTraceStart(0)
| + | |
− | self.attack.setTracesPerAttack(100)
| + | |
− | self.attack.setIterations(1)
| + | |
− | self.attack.setReportingInterval(25)
| + | |
− | self.attack.setTargetBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
| + | |
− | self.attack.setTraceManager(self.traceManager())
| + | |
− | self.attack.setProject(self.project())
| + | |
− | self.attack.setPointRange((8000,10990))
| + | |
− | return self.attack
| + | |
− | | + | |
− | def initReporting(self, results):
| + | |
− | results.setAttack(self.attack)
| + | |
− | results.setTraceManager(self.traceManager())
| + | |
− | self.results = results
| + | |
− | | + | |
− | def doAnalysis(self):
| + | |
− | self.attack.doAttack()</pre>
| + | |
− | | + | |
− | {{Template:Tutorials}}
| + | |
− | [[Category:Tutorials]]
| + | |
This tutorial will introduce you to measuring the power consumption of a device under attack. It will demonstrate how you can view the difference between assembly instructions. In ChipWhisperer 5 Release, the software documentation is now held outside the wiki. See links below.
Running the tutorial uses the referenced Jupyter notebook file.