diff --git a/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference (SIMULATED).ipynb b/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference (SIMULATED).ipynb deleted file mode 100644 index 524526e8..00000000 --- a/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference (SIMULATED).ipynb +++ /dev/null @@ -1,67 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Trace Capture\n", - "\n", - "We'll be attacking our usual implementation of AES in this lab, with one small modification. If we use the EXTRA_OPTS=ADD_JITTER, a for loop that runs between 0 and 15 times through will be inserted:\n", - "\n", - "```C\n", - " #ifdef ADD_JITTER\n", - " for (volatile uint8_t k = 0; k < (*pt & 0x0F); k++);\n", - " #endif\n", - "```\n", - "\n", - "As you can see, this jitter is actually based on the first byte of our plaintext. This in itself is a vulnerability. We won't use this fact, but as an exercise, try hypothocizing some ways to use this to overcome the jitter." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "ename": "OSError", - "evalue": "File C:\\Users\\adewa\\Code\\chipwhisperer\\jupyter\\courses\\sca201\\traces\\Lab_Resync.cwp does not exist or is not a file", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mOSError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mchipwhisperer\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mcw\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mproj\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcw\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mopen_project\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"traces/Lab_Resync.cwp\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32mc:\\users\\adewa\\code\\chipwhisperer\\software\\chipwhisperer\\__init__.py\u001b[0m in \u001b[0;36mopen_project\u001b[1;34m(filename)\u001b[0m\n\u001b[0;32m 75\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 76\u001b[0m \u001b[0mproj\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mproject\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mProject\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 77\u001b[1;33m \u001b[0mproj\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mload\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mfilename\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 78\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mproj\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 79\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32mc:\\users\\adewa\\code\\chipwhisperer\\software\\chipwhisperer\\common\\api\\ProjectFormat.py\u001b[0m in \u001b[0;36mload\u001b[1;34m(self, f)\u001b[0m\n\u001b[0;32m 297\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 298\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mos\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mpath\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0misfile\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfilename\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 299\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mIOError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"File \"\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfilename\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;34m\" does not exist or is not a file\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 300\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 301\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mconfig\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mConfigObjProj\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minfile\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfilename\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mcallback\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mconfigObjChanged\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mOSError\u001b[0m: File C:\\Users\\adewa\\Code\\chipwhisperer\\jupyter\\courses\\sca201\\traces\\Lab_Resync.cwp does not exist or is not a file" - ] - } - ], - "source": [ - "import chipwhisperer as cw\n", - "import numpy as np\n", - "proj = cw.open_project(\"traces/Lab_Resync.cwp\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (SIMULATED).ipynb b/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (SIMULATED).ipynb deleted file mode 100644 index 270c6427..00000000 --- a/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (SIMULATED).ipynb +++ /dev/null @@ -1,46 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Trace Capture\n", - "\n", - "Like with the last lab, we'll be attacking a jittery AES implementation. This time, however, the jitter will be dispersed throughout the operations, making it much harder to resynchronize." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import chipwhisperer as cw\n", - "import numpy as np\n", - "import time\n", - "proj = cw.open_project(\"traces/Lab_Resync_WORSE.cwp\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (SIMULATED).ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (SIMULATED).ipynb deleted file mode 100644 index 3d0a8d6b..00000000 --- a/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (SIMULATED).ipynb +++ /dev/null @@ -1,52 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Part 2, Topic 1: CPA Attack on 32bit AES (SIMULATED)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "NOTE: This lab references some (commercial) training material on [ChipWhisperer.io](https://www.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.\n", - "\n", - "---" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import chipwhisperer as cw\n", - "project = cw.open_project(\"traces/32bit_AES.cwp\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (SIMULATED).ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (SIMULATED).ipynb deleted file mode 100644 index 00198335..00000000 --- a/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (SIMULATED).ipynb +++ /dev/null @@ -1,292 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Topic 2, Part 2 - CPA on Hardware AES Implementation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "NOTE: This lab references some (commercial) training material on [ChipWhisperer.io](https://www.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.\n", - "\n", - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**SUMMARY:** *By now you should have a pretty good understanding of how software implementations of AES are vulnerable to CPA attacks. You might be wondering: are hardware implementations of AES also vulnerable to CPA attacks?*\n", - "\n", - "*In this lab, we'll perform a CPA attack on the hardware AES implementation in the STM32F415. We'll also introduce LASCAR for increased performance when analyzing large datasets.*\n", - "\n", - "**LEARNING OUTCOMES:**\n", - "* Understanding how leakage differs between software AES and hardware AES implementations\n", - "* Using LASCAR for CPA attacks\n", - "* Identifying different leakage points" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Capture traces as normal. We'll need to select the HWAES crypto target instead of TINYAES or MBEDTLS. Also we don't need to capture as many traces - the whole AES block will fit in less than 2000 traces. We'll also boost the gain a little bit - HWAES won't result in as big of power spikes:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "SCOPETYPE = 'OPENADC'\n", - "PLATFORM = 'CW308_STM32F4'\n", - "CRYPTO_TARGET = 'HWAES'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%bash -s \"$PLATFORM\" \"$CRYPTO_TARGET\"\n", - "cd ../../../firmware/mcu/simpleserial-aes\n", - "make PLATFORM=$1 CRYPTO_TARGET=$2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%run \"../../Setup_Scripts/Setup_Generic.ipynb\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fw_path = '../../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex'.format(PLATFORM)\n", - "cw.program_target(scope, prog, fw_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "project = cw.create_project(\"32bit_AES.cwp\", overwrite=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#Capture Traces\n", - "from tqdm.notebook import trange, trange\n", - "import numpy as np\n", - "import time\n", - "\n", - "ktp = cw.ktp.Basic()\n", - "\n", - "traces = []\n", - "N = 15000 # Number of traces\n", - "scope.adc.samples=2000\n", - "\n", - "scope.gain.db = 38\n", - "\n", - "\n", - "for i in trange(N, desc='Capturing traces'):\n", - " key, text = ktp.next() # manual creation of a key, text pair can be substituted here\n", - "\n", - " trace = cw.capture_trace(scope, target, text, key)\n", - " if trace is None:\n", - " continue\n", - " project.traces.append(trace)\n", - "\n", - "print(scope.adc.trig_count)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Introducing LASCAR\n", - "\n", - "With how many traces we're capturing, analyzing our traces will take a lot of time with ChipWhisperer - Analyzer wasn't designed for performance. It is for this reason that we will be using LASCAR, an open source side channel analysis library with a bigger emphasis on speed than ChipWhisperer Analyzer. Normally, it would take a bit of work to massage ChipWhisperer into the LASCAR format; however, ChipWhisperer has recently integrated some basic LASCAR support, making it easy to combine LASCAR and ChipWhisperer projects! Note that this support is a WIP and not offically documented - the interface can change at any time!\n", - "\n", - "Basic setup is as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import chipwhisperer.common.api.lascar as cw_lascar\n", - "from lascar import *\n", - "cw_container = cw_lascar.CWContainer(project, project.textouts, start=None, end=None) #optional start and end args set start and end points for analysis\n", - "guess_range = range(256)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Leakage Model\n", - "\n", - "Thus far, we've been exclusively focusing on software AES. Here, each AES operation (shift rows, add round key, mix columns, etc) is implemented using one basic operation (XOR, reads/writes, multiplies, etc.) per clock cycle. With a hardware implementation, it's often possible to not only combine basic operations into a block that can run in a single clock cycle, but also combine multiple AES operations and run them in a single block! For example, the CW305 FPGA board can run each round of AES in a single clock cycle!\n", - "\n", - "Because of this, running a CPA attack on hardware AES is much trickier than on software AES. In software, we found that it was easy to search for the outputs of the s-boxes because these values would need to be loaded from memory onto a high-capacitance data bus. This is not necessarily true for hardware AES, where the output of the s-boxes may be directly fed into the next stage of the algorithm. In general, we may need some more knowledge of the hardware implementation to successfully complete an attack. That being said, if we take a look at a block diagram of AES:\n", - "\n", - "![](https://wiki.newae.com/images/8/8e/AES_Encryption.png)\n", - "\n", - "the last round jumps out for a few reasons:\n", - "\n", - "* It's not far removed from the ciphertext or the plaintext\n", - "* It's got an AddRoundKey and a SubBytes, meaning we get a nonlinear addition of the key between the ciphertext and the input of the round\n", - "* There's no Mix Columns\n", - "\n", - "Let's make a guess at the implementation and say that it'll do the last round in a single clock cycle and store the input and output in the same memory block. Our reset assumption that allowed us to simply use the Hamming weight instead of the Hamming distance also probably won't be valid here. As such, let's use the Hamming distance between the output and the input of the last round.\n", - "\n", - "ChipWhisperer now includes a few leakage models for use with LASCAR:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "leakage = cw_lascar.lastround_HD_gen" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then, we can actually run the analysis. It should chew through our 15k traces in only a minute or two!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n", - "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's print out our results and plot the correlation of our guesses:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from bokeh.plotting import figure, show\n", - "from bokeh.io import output_notebook\n", - "output_notebook()\n", - "p = figure()\n", - "key_guess = []\n", - "for i in range(16):\n", - " results = cpa_engines[i].finalize()\n", - " xrange = list(range(len(results[0xD0])))\n", - " guess = abs(results).max(1).argmax()\n", - " print(\"Best Guess is {:02X} (Corr = {})\".format(guess, abs(results).max()))\n", - " p.line(xrange, results[guess], color=\"red\")\n", - " key_guess.append(guess)\n", - " \n", - "show(p)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "ChipWhisperer also includes a class to interpret the results of the analysis:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import chipwhisperer.analyzer as cwa\n", - "last_round_key = cwa.aes_funcs.key_schedule_rounds(list(project.keys[0]),0,10)\n", - "disp = cw_lascar.LascarDisplay(cpa_engines, last_round_key)\n", - "disp.show_pge()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Interestingly, you should see that the attack has worked fairly well for most of the bytes. All of them, in fact, except bytes 0, 4, 8, and 12. Looking the correlation plot, you should see two large spikes instead of one like you might expect. Try focusing the attack on either one of these points by adjusting `start=` and `end=` when making the `cw_container` and try answering the following questions:\n", - "\n", - "* Which spike was our expected leakage actually at (last round state diff)?\n", - "* How might you be able to tell that the attack failed for certain bytes at the incorrect leakage point?\n", - "* Why might this other spike be occuring?" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scope.dis()\n", - "target.dis()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/jupyter/lab/2-SCA_lab.ipynb b/jupyter/lab/2-SCA_lab.ipynb new file mode 100644 index 00000000..bc34a718 --- /dev/null +++ b/jupyter/lab/2-SCA_lab.ipynb @@ -0,0 +1,729 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Lab 2\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This lab is based on the SCA course from https://github.com/newaetech/chipwhisperer-jupyter\n", + "\n", + "**SUMMARY:** \n", + "*In this lab, we'll look at how jitter-based countermeasures can be overcome by resynchronizing the traces. More specifically, we'll use the sum of absolute difference (SAD) measure. Afterwards we will look at a device with jitter during the actual encryption operation and how jitter-based countermeasures can be overcome by resyncronizing traces.* \n", + "\n", + "**LEARNING OUTCOMES:**\n", + "* Attempting a CPA attack againt a jittery AES implementation\n", + "* Writing code to resynchronize traces using SAD\n", + "* Using ChipWhisperer Analyzer to resynchronize traces.\n", + "\n", + "* Understanding limitations of SAD resynchronization\n", + "* Using the dynamic time warp preprocessor\n", + "\n", + "\n", + "### Setup:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SCOPETYPE = 'OPENADC'\n", + "PLATFORM = 'CW308_SAM4S'\n", + "CRYPTO_TARGET = 'TINYAES128C'\n", + "SS_VER = 'SS_VER_1_1'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%run \"./Setup.ipynb\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash -s \"$PLATFORM\" \"$CRYPTO_TARGET\" \"$SS_VER\"\n", + "cd ../../firmware/mcu/simpleserial-aes\n", + "make PLATFORM=$1 CRYPTO_TARGET=$2 EXTRA_OPTS=ADD_JITTER SS_VER=$3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cw.program_target(scope, prog, \"../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex\".format(PLATFORM))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm.notebook import trange\n", + "import numpy as np\n", + "import time\n", + "\n", + "ktp = cw.ktp.Basic()\n", + "trace_array = []\n", + "textin_array = []\n", + "\n", + "\n", + "proj = cw.create_project(\"traces/Lab_Resync.cwp\", overwrite=True)\n", + "\n", + "N = 200\n", + "for i in trange(N, desc='Capturing traces'):\n", + " key, text = ktp.next()\n", + " trace = cw.capture_trace(scope, target, text, key)\n", + " if not trace:\n", + " continue\n", + " \n", + " proj.traces.append(trace)\n", + "proj.save()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Trace Capture\n", + "\n", + "We'll be attacking our usual implementation of AES in this lab, with one small modification. If we use the EXTRA_OPTS=ADD_JITTER, a for loop that runs between 0 and 15 times through will be inserted:\n", + "\n", + "```C\n", + " #ifdef ADD_JITTER\n", + " for (volatile uint8_t k = 0; k < (*pt & 0x0F); k++);\n", + " #endif\n", + "```\n", + "\n", + "As you can see, this jitter is actually based on the first byte of our plaintext. This in itself is a vulnerability. We won't use this fact, but as an exercise, try hypothocizing some ways to use this to overcome the jitter.\n", + "\n", + "1. Compile and program the CW with and without jitter and plot the traces below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Perform the capture, resulting in a project of 200 jittery traces. See the notebooks to copy your data into!\n", + "# (1)\n", + "raise NotImplementedError(\"Add your code here, and delete this.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CPA Against Jittery Traces\n", + "\n", + "As a first test, let's try plotting a few traces:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt = cw.plot([])\n", + "for i in range(10):\n", + " plt *= cw.plot(proj.waves[i])\n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "which looks fairly discouraging. Remember that for a CPA attack, we're calculation correlation across our trace set by point, meaning if the SBox output for one trace is in a different spot than the rest, it will decrease the correlation, not increase it. It probably won't end well, but let's try an attack anyway:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.analyzer as cwa\n", + "leak_model = cwa.leakage_models.sbox_output\n", + "attack = cwa.cpa(proj, leak_model)\n", + "cb = cwa.get_jupyter_callback(attack)\n", + "results = attack.run(cb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected, the attack completely failed. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Manual SAD Resync\n", + "\n", + "In concept at least, we have a similar situation to the password bypass, except this time the time difference between the traces is an obstacle instead of a vulnerability we're exploiting. We can actually use a very similar technique to figure out how much the traces have shifted! The only modification to the SAD calculation we need to make is that we'll need to use a subset of our reference trace as the reference instead. This means we won't have to figure out how to deal with the parts of the trace that are shifted outside the capture window.\n", + "\n", + "To find the offset, all we need to do is slide the reference along the trace we're trying to resyncronize, calculating the SAD at each offset. The difference between the offset we picked for our reference and the offset with the lowest SAD will be our timeshift! In practice, we can skip finding all the offsets and just use the first offset that falls below a threshold like we did in the password bypass. If we captured a much longer power trace that had all the rounds of AES, this would also have the advantage of not accidentally matching with a later round.\n", + "\n", + "To start, make a function that figures out the offset between two traces using SAD:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_trace_offset(ref_trace, orig_offset, target_trace, threshold):\n", + " ref_len = len(ref_trace)\n", + " for i in range(0, len(target_trace) - ref_len):\n", + " if SAD_SUM < threshold:\n", + " return i - orig_offset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = np.arange(50)\n", + "b = np.arange(70)[20:]\n", + "assert calculate_trace_offset(a[35:40], 35, b, 1) == -20\n", + "assert calculate_trace_offset(a[35:40], 35, b, 29) == -25" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, let's pick a reference trace. First, let's plot the first trace. Some stuff to keep in mind:\n", + "* We want it to be fairly \"unique\" - we don't want something earlier or later on to also match easily.\n", + "* The maximum offset, and therefore shift, we can get is limited how close the reference is to the edges of the power trace.\n", + "* This isn't really a concern for this lab, but we don't want it to be unique per trace. For example, if your trigger was a bit earlier (think the UART lab), the transition between serial communication and AES is very distinct. It's a bad SAD location, however, since the serial part varies a lot between traces\n", + "\n", + "for example:\n", + "\n", + "![](img/Resync_traces_ref.png)\n", + "\n", + "the reference in green would be a good spot to pick, while the reference in red would be a poor choice\n", + "\n", + "There's a bit of a learning curve to this, so don't be afraid to come back and select a different reference trace if you find it's not working well when trying to resynchronize other traces." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cw.plot(proj.waves[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ref_trace = proj.waves[0][1700:2000]\n", + "cw.plot(proj.waves[0][1700:2000])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One thing we can do is slide our reference along another trace and calculate the SAD at each point. This should be similar to your SAD offset function earlier:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_sad_plot(ref_trace, target_trace):\n", + " ref_len = len(ref_trace)\n", + " sads = []\n", + " for i in range(0, len(target_trace) - ref_len):\n", + " sads.append(np.sum(abs(ref_trace-target_trace[i:i+ref_len])))\n", + " return sads\n", + "\n", + "cw.plot(get_sad_plot(ref_trace, proj.waves[1]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you don't see a single downward spike:\n", + "\n", + "![](img/GoodVBadRef.png)\n", + "\n", + "instead of a few spikes, you'll want to select a different reference. You can also use this plot to get an idea of what sort of threshold to use.\n", + "\n", + "Anyway, try it on some different traces. You should get a varying amount offsets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we can easily get how much our trace is offset by, let's see if the plots actually line up:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "target_offset = calculate_trace_offset(ref_trace, ???, proj.waves[1], 10)\n", + "print(target_offset)\n", + "\n", + "cw.plot(proj.waves[0][:]) * cw.plot(proj.waves[1][target_offset:])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looks good! Try plotting a few different traces, however, and you'll run into a problem: how do we deal with a trace that needs to be shifted forward? There's a related problem for our CPA attack well: we need all the traces to be the same length, but if we move traces backwards, we won't have any data at the end, meaning the traces will be shorter than usual. The easiest way to deal with these problems is just to fill in the rest of the data with zeros:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "target_offset2 = calculate_trace_offset(ref_trace, ???, proj.waves[5], 10)\n", + "print(target_offset2)\n", + "new_trace = np.zeros(len(proj.waves[0]))\n", + "new_trace[-target_offset2:] = proj.waves[5][:target_offset2]\n", + "\n", + "cw.plot(proj.waves[0]) * cw.plot(new_trace)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Anyway, take what we've done and use it to make a function that takes in a trace and returns one that's resynchronized with the reference.\n", + "\n", + "**HINT: You'll need to handle 3 cases: offset > 0, offset < 0, and offset = 0**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def resync_with_SAD(ref, orig_offset, target_trace, threshold):\n", + " target_offset = calculate_trace_offset(ref, orig_offset, target_trace, threshold)\n", + " if target_offset > 0:\n", + " new_trace = np.zeros(len(target_trace))\n", + " raise NotImplementedError(\"Add your code here, and delete this.\")\n", + " return new_trace\n", + " elif target_offset < 0:\n", + " raise NotImplementedError(\"Add your code here, and delete this.\")\n", + " return new_trace\n", + " return target_trace" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test it out on some jittery traces:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "traces = [resync_with_SAD(ref_trace, ???, proj.waves[i], 10) for i in range(10)]\n", + "plt = cw.plot([])\n", + "for i in range(10):\n", + " plt *= cw.plot(traces[i])\n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hopefully all your traces lined up perfectly. If not, you might have to go back and select another reference or adjust your `resync_with_SAD()` function.\n", + "\n", + "Let's make a new project, resynchronize our traces, and insert them into that:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "resync_proj = cw.create_project(\"Lab 1_1_Resync\", overwrite=True)\n", + "for trace in proj.traces:\n", + " resync_wave = resync_with_SAD(ref_trace, ???, trace.wave, 10)\n", + " new_trace = cw.Trace(resync_wave, trace.textin, trace.textout, trace.key)\n", + " resync_proj.traces.append(new_trace)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and let's retry the attack:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.analyzer as cwa\n", + "leak_model = cwa.leakage_models.sbox_output\n", + "attack = cwa.cpa(resync_proj, leak_model)\n", + "cb = cwa.get_jupyter_callback(attack)\n", + "results = attack.run(cb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should see the attack succeed this time!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SAD Resync with ChipWhisperer Analyzer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like with the CPA attack itself, ChipWhisperer Analyzer can make our lives a lot easier:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "resync_traces = cwa.preprocessing.ResyncSAD(proj)\n", + "resync_traces.ref_trace = 0\n", + "resync_traces.target_window = (???, ???)\n", + "resync_traces.max_shift = ???\n", + "resync_analyzer = resync_traces.preprocess()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All these settings should look familiar except `max_shift`, which will cause the ResyncSAD object to discard a trace if it needs to be shifted more than `max_shift`. Why is this useful? Consider the case where there's enough jitter to move the point we're looking at outside of what we captured. It would be much better to simply discard this trace, rather than including it and having it disrupt our data.\n", + "\n", + "Anyway, plotting 10 resychronized traces:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt = cw.plot([])\n", + "for i in range(10):\n", + " plt *= cw.plot(resync_analyzer.waves[i])\n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should find that everything is resychronized, just as it was from our code. Running the attack again:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.analyzer as cwa\n", + "leak_model = cwa.leakage_models.sbox_output\n", + "attack = cwa.cpa(resync_analyzer, leak_model)\n", + "cb = cwa.get_jupyter_callback(attack)\n", + "results = attack.run(cb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "which easily succeeds." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More resynchronization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "**NOTE: DTW is a very slow algorithm. Make sure you have a compiled version of FasterDTW installed, otherwise resynchronization will take a long time**\n", + "\n", + "### Setup: Same as in before" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Perform the capture, resulting in a project of 1000 jittery traces. See the notebooks to copy your data into!\n", + "#\n", + "raise NotImplementedError(\"Add your code here, and delete this.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3 CPA Against Jittery Traces\n", + "\n", + "Again, let's try plotting a few traces:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt = cw.plot([])\n", + "for i in range(10):\n", + " plt *= cw.plot(proj.waves[i])\n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From afar, these probably don't look to bad. The trace shape is a little weird, but that shouldn't be a problem. Zooming in should reveal that the traces are desynchronized. Again, it probably won't work, but let's try our usual CPA attack. We'll use LASCAR, a very fast side channel analysis library, to speed things up:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.common.api.lascar as cw_lascar\n", + "from lascar import *\n", + "cw_container = cw_lascar.CWContainer(proj, proj.textins, start=None, end=None) #optional start and end args set start and end points for analysis\n", + "guess_range = range(256)\n", + "leakage = cw_lascar.sbox_HW_gen\n", + "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n", + "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)\n", + "\n", + "plt = cw.plot([])\n", + "key_guess = []\n", + "for i in range(16):\n", + " results = cpa_engines[i].finalize()\n", + " xrange = range(len(results[0xD0]))\n", + " guess = abs(results).max(1).argmax()\n", + " print(\"Best Guess is {:02X} ({:02X}) (Corr = {})\".format(guess, proj.keys[0][i], abs(results).max()))\n", + " plt *= cw.plot(results[guess]).opts(color=\"red\")\n", + " key_guess.append(guess)\n", + " \n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Nothing! Again, nothing unexpected, but it shows that there's enough jitter here to not just be averaged out...\n", + "\n", + "Last lab, we were able to overcome jitter by resynchronizing the traces with a SAD match. Let's try that technique again:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.analyzer as cwa\n", + "resync_traces = cwa.preprocessing.ResyncSAD(proj)\n", + "resync_traces.ref_trace = 0\n", + "resync_traces.target_window = (???, ???)\n", + "resync_traces.max_shift = ???\n", + "resync_analyzer = resync_traces.preprocess()\n", + "plt = cw.plot([])\n", + "for i in range(10):\n", + " plt *= cw.plot(resync_analyzer.waves[i])\n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The traces probably look a bit better than before during `subbytes`, but if you zoom in you'll again see that there's lots of jitter here. Try the CPA attack again:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.common.api.lascar as cw_lascar\n", + "from lascar import *\n", + "cw_container = cw_lascar.CWContainer(resync_analyzer, proj.textins, start=None, end=None) #optional start and end args set start and end points for analysis\n", + "guess_range = range(256)\n", + "leakage = cw_lascar.sbox_HW_gen\n", + "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n", + "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)\n", + "\n", + "plt = cw.plot([])\n", + "key_guess = []\n", + "for i in range(16):\n", + " results = cpa_engines[i].finalize()\n", + " xrange = range(len(results[0xD0]))\n", + " guess = abs(results).max(1).argmax()\n", + " print(\"Best Guess is {:02X} ({:02X}) (Corr = {})\".format(guess, proj.keys[0][i], abs(results).max()))\n", + " plt *= cw.plot(results[guess]).opts(color=\"red\")\n", + " key_guess.append(guess)\n", + " \n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You'll probably find that you didn't have any luck. That's because this target is inserting multiple delays during the actual encryption itself. This means that shifting traces by a single offset isn't going to be very effective.\n", + "\n", + "Luckily, SAD isn't the only way to resyncrhonize traces. ChipWhisperer Analyzer also has support for a technique called [dynamic time warp](https://en.wikipedia.org/wiki/Dynamic_time_warping). The details are much more complicated than with SAD, so we'll skip implementing it ourselves this time. Instead, skip straight to the preprocessing.\n", + "\n", + "ChipWhisperer uses a faster approximation for dynamic time warp, which is a very slow algorithm. This comes with a parameter to vary: radius. Higher radii will generally give a better synchronization, but at the cost of a higher processing time. 1 is a good value to start with, but you might have to bump it a bit higher to get a good resync." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "import chipwhisperer.analyzer as cwa\n", + "resync_traces = cwa.preprocessing.ResyncDTW(proj)\n", + "resync_traces.ref_trace = 0\n", + "resync_traces.radius = ???\n", + "resync_analyzer = resync_traces.preprocess()\n", + "\n", + "plt = cw.plot([])\n", + "for i in range(10):\n", + " plt *= cw.plot(resync_analyzer.waves[i])\n", + "plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These traces are looking a lot better! This technique isn't perfect, but it works a lot better than SAD in this case!\n", + "\n", + "Let's try a CPA attack now:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer.common.api.lascar as cw_lascar\n", + "from lascar import *\n", + "cw_container = cw_lascar.CWContainer(resync_analyzer, proj.textins, start=None, end=None) #optional start and end args set start and end points for analysis\n", + "guess_range = range(256)\n", + "leakage = cw_lascar.sbox_HW_gen\n", + "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n", + "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)\n", + "\n", + "plt = cw.plot([])\n", + "key_guess = []\n", + "for i in range(16):\n", + " results = cpa_engines[i].finalize()\n", + " xrange = range(len(results[0xD0]))\n", + " guess = abs(results).max(1).argmax()\n", + " print(\"Best Guess is {:02X} ({:02X}) (Corr = {})\".format(guess, proj.keys[0][i], abs(results).max()))\n", + " plt *= cw.plot(results[guess]).opts(color=\"red\")\n", + " key_guess.append(guess)\n", + " \n", + "plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert key_guess == list(proj.keys[0])" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}