From 676305473b2ddf606aa4c133229ef9bb1ea804e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caroline=20Sandsbr=C3=A5ten?= Date: Fri, 12 Sep 2025 10:48:25 +0200 Subject: [PATCH] add ecc task and rename folders again --- .../4_BONUS_RSA_Fault_Lab.ipynb | 567 ------- .../5_BONUS_AES_Loop_Skip.ipynb | 762 ---------- ...Extending AES-128 Attacks to AES-256.ipynb | 0 ...m of Absolute Difference (SIMULATED).ipynb | 0 ...aces with Sum of Absolute Difference.ipynb | 0 ...um of Absolute Difference (HARDWARE).ipynb | 0 ...es with Dynamic Time Warp (HARDWARE).ipynb | 0 ...s with Dynamic Time Warp (SIMULATED).ipynb | 0 ...nizing Traces with Dynamic Time Warp.ipynb | 0 ...ab 2_1 - CPA on 32bit AES (HARDWARE).ipynb | 0 ...b 2_1 - CPA on 32bit AES (SIMULATED).ipynb | 0 .../Lab 2_1 - CPA on 32bit AES.ipynb | 0 ...rdware AES Implementation (HARDWARE).ipynb | 0 ...dware AES Implementation (SIMULATED).ipynb | 0 ...- CPA on Hardware AES Implementation.ipynb | 0 ...ab 2_3 - Attacking Across MixColumns.ipynb | 0 .../Lab 3_1A - AES256 Bootloader Attack.ipynb | 0 ...Engineering on the AES256 Bootloader.ipynb | 0 .../1_SCA_Lab/img/AES_Encryption.png | Bin .../1_SCA_Lab/img/AES_MixCol.png | Bin .../1_SCA_Lab/img/Aes256_cbc.png | Bin .../1_SCA_Lab/img/GoodVBadRef.png | Bin .../1_SCA_Lab/img/Resync_traces_ref.png | Bin .../1_SCA_Lab/img/aes_operations.png | Bin .../1_SCA_Lab/img/stm_run1.png | Bin .../1_Constructing_the_Glitch_Loop.ipynb | 0 .../2_Glitching_Past_a_Password_Check.ipynb | 0 .../3_Glitching_a_Memory_Dump.ipynb | 0 .../4_Glitching_a_Bootloader.ipynb | 0 .../2_Fault_Lab/img/Clock-glitched.png | Bin .../2_Fault_Lab/img/Clock-normal.png | Bin .../2_Fault_Lab/img/Glitchgen-mux.png | Bin .../2_Fault_Lab/img/Glitchgen-phaseshift.png | Bin .../2_Fault_Lab/img/Mcu-unglitched.png | Bin .../3_RSA_Lab/3_RSA_Lab.ipynb | 0 jupyter/lab/4_ECC_Lab/5-ECC_lab.ipynb | 1308 +++++++++++++++++ jupyter/lab/4_ECC_Lab/ECC_lab_setup.ipynb | 943 ++++++++++++ jupyter/{Lab_Tasks => lab}/README.md | 0 .../Tutorial/Tutorial_1.ipynb | 0 .../Tutorial/Tutorial_2.ipynb | 0 .../Tutorial/Tutorial_3.ipynb | 0 .../Tutorial/Tutorial_4.ipynb | 0 .../Tutorial/Tutorial_5.ipynb | 0 .../Tutorial/Tutorial_6.ipynb | 0 .../Tutorial/img/4traces_aes_clkx1.png | Bin .../img/4traces_aes_clkx1_offset60000.png | Bin .../img/4traces_aes_clkx1_presample5000.png | Bin .../4traces_aes_clkx1_presample5000_zoom.png | Bin .../Tutorial/img/4traces_aes_clkx4.png | Bin .../Tutorial/img/4traces_aes_poortrigger.png | Bin .../Tutorial/img/aesinput.png | Bin .../Tutorial/img/dpa-doublepeak.png | Bin .../Tutorial/img/dpa_peakexample.png | Bin .../Tutorial/img/shunt_chipwhisperer.png | Bin .../Tutorial/img/spa_password_diffexample.png | Bin .../img/spa_password_h_vs_0_overview.png | Bin .../img/spa_password_h_vs_0_zoomed.png | Bin .../Tutorial/img/spa_password_list_char1.png | Bin .../Tutorial/img/traces_wrong.png | Bin .../Tutorial/img/uart_triggers.png | Bin 60 files changed, 2251 insertions(+), 1329 deletions(-) delete mode 100644 jupyter/Lab_Tasks/BONUS_NOT_2025/4_BONUS_RSA_Fault_Lab.ipynb delete mode 100644 jupyter/Lab_Tasks/BONUS_NOT_2025/5_BONUS_AES_Loop_Skip.ipynb rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Extending AES-128 Attacks to AES-256.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference (SIMULATED).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 1_1A - Resynchronizing Traces with Sum of Absolute Difference (HARDWARE).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (HARDWARE).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (SIMULATED).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (SIMULATED).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (SIMULATED).ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/Lab 3_1B - Reverse Engineering on the AES256 Bootloader.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/AES_Encryption.png (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/AES_MixCol.png (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/Aes256_cbc.png (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/GoodVBadRef.png (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/Resync_traces_ref.png (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/aes_operations.png (100%) rename jupyter/{Lab_Tasks => lab}/1_SCA_Lab/img/stm_run1.png (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/1_Constructing_the_Glitch_Loop.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/2_Glitching_Past_a_Password_Check.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/3_Glitching_a_Memory_Dump.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/4_Glitching_a_Bootloader.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/img/Clock-glitched.png (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/img/Clock-normal.png (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/img/Glitchgen-mux.png (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/img/Glitchgen-phaseshift.png (100%) rename jupyter/{Lab_Tasks => lab}/2_Fault_Lab/img/Mcu-unglitched.png (100%) rename jupyter/{Lab_Tasks => lab}/3_RSA_Lab/3_RSA_Lab.ipynb (100%) create mode 100644 jupyter/lab/4_ECC_Lab/5-ECC_lab.ipynb create mode 100644 jupyter/lab/4_ECC_Lab/ECC_lab_setup.ipynb rename jupyter/{Lab_Tasks => lab}/README.md (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/Tutorial_1.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/Tutorial_2.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/Tutorial_3.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/Tutorial_4.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/Tutorial_5.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/Tutorial_6.ipynb (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/4traces_aes_clkx1.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/4traces_aes_clkx1_offset60000.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/4traces_aes_clkx1_presample5000.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/4traces_aes_clkx1_presample5000_zoom.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/4traces_aes_clkx4.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/4traces_aes_poortrigger.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/aesinput.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/dpa-doublepeak.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/dpa_peakexample.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/shunt_chipwhisperer.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/spa_password_diffexample.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/spa_password_h_vs_0_overview.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/spa_password_h_vs_0_zoomed.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/spa_password_list_char1.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/traces_wrong.png (100%) rename jupyter/{Lab_Tasks => lab}/Tutorial/img/uart_triggers.png (100%) diff --git a/jupyter/Lab_Tasks/BONUS_NOT_2025/4_BONUS_RSA_Fault_Lab.ipynb b/jupyter/Lab_Tasks/BONUS_NOT_2025/4_BONUS_RSA_Fault_Lab.ipynb deleted file mode 100644 index 0c3f03cf..00000000 --- a/jupyter/Lab_Tasks/BONUS_NOT_2025/4_BONUS_RSA_Fault_Lab.ipynb +++ /dev/null @@ -1,567 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Part 2, Topic 1: Fault Attack on RSA (MAIN)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Acknowledgements\n", - "\n", - "This attack is was originally detailed in a [1997 paper by Boneh, Demillo, and Lipton](https://www.researchgate.net/publication/2409434_On_the_Importance_of_Checking_Computations). This tutorial draws heavily from a blog post by David Wong, which you can find [here](https://www.cryptologie.net/article/371/fault-attacks-on-rsas-signatures/)." - ] - }, - { - "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:** *So far, we've seen how clock and voltage glitches can be used to disrupt a microcontroller or FPGA, potentially changing the execution path. We've also seen that, in the case of AES, if we disrupt the calculation in a certian spot only twice, we can easily recover a quarter of the key bytes.*\n", - "\n", - "*In this lab, we'll explore an attack on RSA, more specifically an RSA optimization, that can recover the entire key with a single fault.*\n", - "\n", - "**LEARNING OUTCOMES:**\n", - "\n", - "* Understanding conditions for the fault\n", - "* Recovering an RSA private key from a faulty signature" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Attack Theory\n", - "\n", - "The theory for this attack is a bit simpler than the one for AES, so we'll take you through it here instead of putting it in another notebook. If you don't know, RSA is an asymmetric cryptographic algorithm, meaning it has a public and private key, with one being used for encryption and the other for decryption. It can be used in both a public encryption/private decryption configuration and a private encryption/public decryption situation. We'll be attacking the latter case. Let's assume that the target is signing a message and is sending it to us so we can verify the identity of the device.\n", - "\n", - "For RSA, the public key is made up of two numbers, $n$ and $e$. The private key is made up of $n$ and $d$. These numbers have some special properties:\n", - "\n", - "1. $n$ is constructed as the product of 2 prime numbers, $p$ and $q$.\n", - "1. $d$ is derived from $p$, $q$, and $e$.\n", - "\n", - "As a result of these properties, a few things are evident:\n", - "\n", - "1. If we learn $p$ or $q$, we can derive the other prime number since $n$ is public information.\n", - "1. If we learn $p$ and $q$, we can derive $d$ since $e$ is public information.\n", - "\n", - "The target can encrypt/sign a message with the following equation ($s$ is the signature and $m$ is the message):\n", - "\n", - "$$s = m^d({mod}\\space n)$$\n", - "\n", - "And we can decrypt/verify the message with the following equation:\n", - "\n", - "$$s^e = m(mod\\space n)$$\n", - "\n", - "One issue with RSA is that it's very slow - these equations are simple, but they use massive numbers; if the numbers used are too small, it's trivial for a computer to factor $n$ into $p$ and $q$. $n$ for the firmware we're attacking is 1024 bits long, which is on the smaller size of secure keys at this point. This encryption/signing operation is especially slow.\n", - "\n", - "An important property of the signature verification equation is that the following is also true:\n", - "\n", - "$$(m - s^e)(mod\\space n) = 0$$\n", - "\n", - "aka $(m - s^e)$ is divisible by $n$. It follows that $(m - s^e)$ is divisible by $p$ and $q$ as well.\n", - "\n", - "## The Chinese Remainder Theorem (CRT)\n", - "\n", - "To help speed up the encryption, an optimization known as the Chinese Remainder Theorem (CRT) can be used. This theorem allows us derive two new exponents, $d_p$ and $d_q$, which are much smaller than $d$. We can then replace\n", - "\n", - "$$s = m^d({mod}\\space n)$$\n", - "\n", - "with two equations\n", - "\n", - "$$s_1 = m^{d_p}(mod\\ p) \\\\\n", - "s_2 = m^{d_q}(mod\\ q)$$\n", - "\n", - "$s$ is then:\n", - "\n", - "$$i_q = q^{-1}mod\\ p \\\\\n", - "s = s_2 + q(i_q(s_1 - s_2)mod\\ p)\n", - "$$\n", - "\n", - "$s_1$ and $s_2$ can be combined into $s$ via CRT. Even though there's two modular exponentiations, this is still much faster since $d_p$ and $d_q$ are much smaller than $d$ and $p$ and $q$ are much smaller than $n$. This is the optimization that our target, which is using a slightly modified version of MBEDTLS (more on that in a bit), makes.\n", - "\n", - "## Inserting a Fault\n", - "\n", - "Suppose that instead of everything going smoothly as above, a fault happens during the calculation of $s_2$, turning it into $s'_2$. $s_1$ and $s'_2$ will then be combined into $s'$. If that happens, a couple of things will be true:\n", - "\n", - "1. When we go to verify the signature, it won't work: $s'^e \\neq m (mod\\ n)$. Concequently, $(m - s'^e)(mod\\space n) \\neq 0$, so $(m - s'^e)$ is not divisible by $n$\n", - "1. Since $(m - s'^e)$ is not divisible by $n$, it cannot be divisible by both $p$ and $q$ anymore. Due to how the CRT works, it will still be divisible by $p$, but not by $q$. This can be expressed as $(m - s'^e) = pk$ for some integer $k$. \n", - "1. We now have $(m - s'^e) = pk$ and $n = pq$. This means that we can find $p$ by computing $gcd(m-s'^e, n)$\n", - " \n", - "Once we have $p$, we can compute $q = \\frac{n}{p}$ and $d$ via the rest of the RSA key generation algorithm. \n", - "\n", - "The math is similar in the case that $s_1$ is faulted, with the only difference being that we recover $q$ at the end instead of $p$.\n", - "\n", - "## Communicating with the Target\n", - "\n", - "With the math out of the way, we can get to attacking the target:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "SCOPETYPE = 'OPENADC'\n", - "PLATFORM = 'CW308_SAM4S'\n", - "SS_VER='SS_VER_1_1'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%run \"../../Setup_Scripts/Setup_Generic.ipynb\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%bash -s \"$PLATFORM\" \"$SS_VER\"\n", - "cd ../../../firmware/mcu/simpleserial-rsa\n", - "make PLATFORM=$1 CRYPTO_TARGET=MBEDTLS CRYPTO_OPTIONS=RSA OPT=2 SS_VER=$2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cw.target_logger.setLevel(cw.logging.WARNING)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "fw_path = \"../../../firmware/mcu/simpleserial-rsa/simpleserial-rsa-{}.hex\".format(PLATFORM)\n", - "cw.program_target(scope, prog, fw_path)\n", - "time.sleep(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This target is using the simpleserial protocol, but the full signature is too big to read back in a single command. This means we instead read back the signature in 3 commands:\n", - "\n", - "1. `'t'` will do the signature calculation and respond with the first 48 bytes of the signature\n", - "1. `'1'` will return the next 48 bytes of the signature\n", - "1. `'2'` will return the final 32 bytes of the signature" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "scope.clock.adc_src = \"clkgen_x1\"\n", - "reset_target(scope)\n", - "target.flush()\n", - "scope.arm()\n", - "target.simpleserial_write(\"t\", bytearray([]))\n", - " \n", - "ret = scope.capture()\n", - "if ret:\n", - " print('Timeout happened during acquisition')\n", - " \n", - "time.sleep(2)\n", - "if SS_VER=='SS_VER_2_1':\n", - " output = target.simpleserial_read_witherrors('r', 128, timeout=1)\n", - "else:\n", - " output = target.simpleserial_read_witherrors('r', 48, timeout=1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sig = None\n", - "if output['valid']:\n", - " print(output['payload'])\n", - " sig = output['payload']\n", - " \n", - "print(scope.adc.trig_count)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That took a long time, probably more than 10M cycles! Let's read back the rest of the message and append it to our signature:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if SS_VER!='SS_VER_2_1':\n", - " target.simpleserial_write(\"1\", bytearray())\n", - " time.sleep(0.2)\n", - " output = target.simpleserial_read_witherrors('r', 48, timeout=10)\n", - " if output['valid']:\n", - " sig.extend(output['payload'])\n", - "\n", - " target.simpleserial_write(\"2\", bytearray())\n", - " time.sleep(0.2)\n", - " output = target.simpleserial_read_witherrors('r', 32, timeout=10)\n", - " if output['valid']:\n", - " sig.extend(output['payload'])\n", - "\n", - " print(sig)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if PLATFORM == \"CWLITEXMEGA\":\n", - " def reboot_flush(): \n", - " scope.io.pdic = False\n", - " time.sleep(0.1)\n", - " scope.io.pdic = \"high_z\"\n", - " time.sleep(0.1)\n", - " #Flush garbage too\n", - " target.flush()\n", - "else:\n", - " def reboot_flush(): \n", - " scope.io.nrst = False\n", - " time.sleep(0.05)\n", - " scope.io.nrst = \"high_z\"\n", - " time.sleep(0.05)\n", - " #Flush garbage too\n", - " target.flush()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's verify that our signature is correct and that we can verify it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from Crypto.PublicKey import RSA\n", - "from Crypto.Signature import PKCS1_v1_5 \n", - "\n", - "from Crypto.Hash import SHA256\n", - "\n", - "e = 0x10001\n", - "n = 0x9292758453063D803DD603D5E777D7888ED1D5BF35786190FA2F23EBC0848AEADDA92CA6C3D80B32C4D109BE0F36D6AE7130B9CED7ACDF54CFC7555AC14EEBAB93A89813FBF3C4F8066D2D800F7C38A81AE31942917403FF4946B0A83D3D3E05EE57C6F5F5606FB5D4BC6CD34EE0801A5E94BB77B07507233A0BC7BAC8F90F79\n", - "m = b\"Hello World!\"\n", - "\n", - "hash_object = SHA256.new(data=m)\n", - "pub_key = RSA.construct((n, e))\n", - "signer = PKCS1_v1_5.new(pub_key) \n", - "sig_check = signer.verify(hash_object, sig)\n", - "print(sig_check)\n", - "\n", - "assert sig_check, \"Failed to verify signature on device. Got: {}\".format(newout)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's setup the glitch module. Use settings here that worked for you before. Ideally, you'll have a single group of settings here - RSA is very slow (as you've seen), so trying to scan settings here would take forever!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if scope._is_husky:\n", - " scope.glitch.enabled = True\n", - "scope.glitch.clk_src = \"clkgen\"\n", - "scope.glitch.output = \"clock_xor\"\n", - "scope.glitch.trigger_src = \"ext_single\"\n", - "scope.glitch.repeat = 1\n", - "# These width/offset numbers are for CW-Lite/Pro; for CW-Husky, convert as per Fault 1_1:\n", - "scope.glitch.width = -9\n", - "scope.glitch.offset = -38.3\n", - "scope.io.hs2 = \"glitch\"\n", - "print(scope.glitch)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from tqdm.notebook import trange\n", - "import time\n", - "for i in trange(7000000, 7100000): #look for something kind of near the end\n", - " scope.glitch.ext_offset = i\n", - " scope.adc.timeout = 3\n", - " target.flush()\n", - " scope.arm()\n", - " target.simpleserial_write(\"t\", bytearray([]))\n", - "\n", - " ret = scope.capture()\n", - " if ret:\n", - " print('Timeout happened during acquisition')\n", - "\n", - " time.sleep(2)\n", - " if SS_VER=='SS_VER_2_0':\n", - " output = target.simpleserial_read_witherrors('r', 128, timeout=100, glitch_timeout=1)\n", - " else: \n", - " output = target.simpleserial_read_witherrors('r', 48, timeout=100, glitch_timeout=1)\n", - " if invalid_output: # replace with invalid output detection\n", - " print(\"crash\") #we can't really do anything here - we need the full signature back\n", - " else:\n", - " if glitched_output: #detect if the calculation was messed up\n", - " # call the faulty signature whatever you want\n", - " # but we'll assume it's called sig for the rest of the lab\n", - " sig = \n", - " else:\n", - " pass # normal operation, nothing special" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Padding the Message\n", - "\n", - "We've got our glitched signature, but we've still got a little work to do. The $m$ isn't actually the message by itself. Instead, it's a PKCS#1 v1.5 padded hash of it. Luckily, this standard's fairly simple. PKCS#1 v1.5 padding looks like:\n", - "\n", - "\\|00\\|01\\|ff...\\|00\\|hash_prefix\\|message_hash\\|\n", - "\n", - "Here, the ff... part is a string of ff bytes long enough to make the size of the padded message the same as N, while hash_prefix is an identifier number for the hash algorithm used on message_hash. Our message was hashed using SHA256, which has the hash prefix `3031300d060960864801650304020105000420`.\n", - "\n", - "We can get our hashed message and $m$ with the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from Crypto.Hash import SHA256\n", - "from binascii import hexlify, unhexlify\n", - "\n", - "def build_message(m, n):\n", - " sha_id = \"3031300d060960864801650304020105000420\"\n", - " N_len = (len(bin(n)) - 2 + 7) // 8\n", - " pad_len = (len(hex(n)) - 2) // 2 - 3 - len(m)//2 - len(sha_id)//2\n", - " padded_m = \"0001\" + \"ff\" * pad_len + \"00\" + sha_id + m\n", - " return padded_m\n", - "\n", - "hashed_m = hexlify(hash_object.digest()).decode()\n", - "padded_m = build_message(hashed_m, n)\n", - "print(hashed_m)\n", - "print(padded_m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Recovering the Private Key\n", - "\n", - "Now we can recover the private values by plugging them into the equations from earlier. If you can, install the gmpy2 Python library, which has much better support for big integer math like this. Otherwise, run the next block and wait for a few minutes for the calculation to finish.\n", - "\n", - "You can get gmpy2 on Windows from the following link: https://pypi.org/project/gmpy2/#files. On Linux, you can install it via your package manager (python3-gmpy2 on Apt, for example)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from math import gcd\n", - "n = 0x9292758453063D803DD603D5E777D7888ED1D5BF35786190FA2F23EBC0848AEADDA92CA6C3D80B32C4D109BE0F36D6AE7130B9CED7ACDF54CFC7555AC14EEBAB93A89813FBF3C4F8066D2D800F7C38A81AE31942917403FF4946B0A83D3D3E05EE57C6F5F5606FB5D4BC6CD34EE0801A5E94BB77B07507233A0BC7BAC8F90F79\n", - "e = 0x10001\n", - "try:\n", - " import gmpy2\n", - " from gmpy2 import mpz\n", - " sig_int = mpz(int.from_bytes(sig, \"big\"))\n", - " m_int = mpz(int.from_bytes(unhexlify(padded_m), \"big\"))\n", - " p_test = gmpy2.gcd(???)\n", - "except (ImportError, ModuleNotFoundError) as error:\n", - " print(\"gmpy2 not found, falling back to Python\")\n", - " sig_int = int.from_bytes(sig, \"big\")\n", - " padded_m_int = int.from_bytes(unhexlify(padded_m), \"big\")\n", - " p_test = gcd(???)\n", - " \n", - "print(hex(p_test))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We should now have either $p$ or $q$! We can get the other prime by simply dividing $n$ by the one we have." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "q_test = n//p_test\n", - "print(hex(q_test))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The $d$ calculation is a bit more complicated. Here's some code to derive it from $q$, $p$, and $e$:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "phi = (q_test - 1)*(p_test - 1)\n", - "def egcd(a, b):\n", - " x,y, u,v = 0,1, 1,0\n", - " while a != 0:\n", - " q, r = b//a, b%a\n", - " m, n = x-u*q, y-v*q\n", - " b,a, x,y, u,v = a,r, u,v, m,n\n", - " gcd = b\n", - " return gcd, x, y\n", - "\n", - "gcd, d, b = egcd(e, phi)\n", - "\n", - "print(hex(d))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's sign the original message and see if we can verify it with our original verifier:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from Crypto.PublicKey import RSA\n", - "from Crypto.Signature import PKCS1_v1_5 \n", - "\n", - "from Crypto.Hash import SHA256\n", - "\n", - "private_key = RSA.construct((n, e, int(d), int(p_test), int(q_test)))\n", - "private_signer = PKCS1_v1_5.new(private_key) \n", - "new_sig = private_signer.sign(hash_object)\n", - "print(sig)\n", - "\n", - "new_sig_check = signer.verify(hash_object, new_sig)\n", - "print(new_sig_check)\n", - "\n", - "assert new_sig_check, \"Failed to verify signature on device. Got: {}\".format(newout)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that you've seen the attack work, you might want to try doing the falut in the other half of the RSA calculation to see if you can get the other prime number back." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Caveats to the Attack\n", - "\n", - "The crypto implementation we're attack isn't actually vulnerable to this attack without some additional glitching. This is because it verifies the signature is valid before sending it off. With a more complicated glitch setup, we could try glitching past it, but this is outside the scope of this lab. Some ideas include modifying the ChipWhisperer FPGA code to generate a second glitch, using a second ChipWhisperer (easiest if you were voltage glitching), and using a second trigger and trying to rearm between the two signature encryptions. You might even be able to increase the repeat and glitch near the end of the second encryption algorithm to glitch past both.\n", - "\n", - "Instead though, we copied some of the functions and commented out the following signature check:\n", - "\n", - "```C\n", - " /* Compare in constant time just in case */\n", - " for( diff = 0, i = 0; i < ctx->len; i++ )\n", - " diff |= verif[i] ^ sig[i];\n", - " diff_no_optimize = diff;\n", - "\n", - " if( diff_no_optimize != 0 )\n", - " {\n", - " ret = MBEDTLS_ERR_RSA_PRIVATE_FAILED;\n", - " goto cleanup;\n", - " }\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusions & Next Steps\n", - "\n", - "You've seen in previous labs that there are very powerful fault attacks on symmetric cryptographic algorithms that can be used to recover an AES key with a few faults. From this one, you've seen an even more powerful attack is possible on the asymmetric cryptographic algorithm RSA-CRT that can recover the plaintext in a single fault. " - ] - } - ], - "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.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/jupyter/Lab_Tasks/BONUS_NOT_2025/5_BONUS_AES_Loop_Skip.ipynb b/jupyter/Lab_Tasks/BONUS_NOT_2025/5_BONUS_AES_Loop_Skip.ipynb deleted file mode 100644 index e9a708f6..00000000 --- a/jupyter/Lab_Tasks/BONUS_NOT_2025/5_BONUS_AES_Loop_Skip.ipynb +++ /dev/null @@ -1,762 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Part 1, Topic 1, Lab B: AES Loop Skip Fault Attack in Practice" - ] - }, - { - "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:** *In the last lab, we showed recovering an AES becomes trivial if we're able to skip the repeated rounds of AES. In this lab, we'll look at applying this attack to a real AES implementation.*\n", - "\n", - "**This lab is only supported with TINYAES128C firmware**\n", - "\n", - "**LEARNING OUTCOMES:**\n", - "* Understanding how C code can be modified by a compiler\n", - "* Using a theoretical fault model to mount a real attack\n", - "\n", - "## Can we skip the loop of AES\n", - "\n", - "For this particular fault, the answer is that it depends! Let's take a look at the source code for TINYAES:\n", - "\n", - "```C\n", - "// Cipher is the main function that encrypts the PlainText.\n", - "static void Cipher(void)\n", - "{\n", - " uint8_t round = 0;\n", - "\n", - " // Add the First round key to the state before starting the rounds.\n", - " AddRoundKey(0); \n", - " \n", - " // There will be Nr rounds.\n", - " // The first Nr-1 rounds are identical.\n", - " // These Nr-1 rounds are executed in the loop below.\n", - " for(round = 1; round < Nr; ++round)\n", - " {\n", - " SubBytes();\n", - " ShiftRows();\n", - " MixColumns();\n", - " AddRoundKey(round);\n", - " }\n", - " \n", - " // The last round is given below.\n", - " // The MixColumns function is not here in the last round.\n", - " SubBytes();\n", - " ShiftRows();\n", - " AddRoundKey(Nr);\n", - "}\n", - "```\n", - "\n", - "We learned in Fault101 that fault injection can be used to bypass the final check and stay in a loop longer. Similarly, glitching can also be used to break out of a loop early. This is likely one of the effects that we saw when glitching past a password check in that course as well. That being said, it's up to the compiler how this loop gets translated to assembly. For example, `Nr` is constant here, so the compiler might decided to completely unroll the loop, preventing us from ever breaking free! For our glitch to work we really have three requirements:\n", - "\n", - "1. There actually needs to be a loop for us to break free from. The compiler can't unroll the loop.\n", - "1. The registers need to line up properly such that, if we skip the loop, the last round still completes properly.\n", - "1. The compiler can't skip the first `round < Nr` check. If the compiler doesn't know that `round` always starts off less than `Nr`, it needs to do a check at the start of the loop.\n", - "\n", - "unfortunately for us, while our default build of TINYAES128C will fulfill the first two requirements, if we look at the generated assembly from `simpleserial-aes-PLATFORM.lss` (CWLITEARM in this case):\n", - "\n", - "```C\n", - "// Cipher is the main function that encrypts the PlainText.\n", - "static void Cipher(void)\n", - "{\n", - " 8001378:\te92d 4ff8 \tstmdb\tsp!, {r3, r4, r5, r6, r7, r8, r9, sl, fp, lr}\n", - " uint8_t round = 0;\n", - "\n", - " // Add the First round key to the state before starting the rounds.\n", - " AddRoundKey(0); \n", - " 800137c:\t2000 \tmovs\tr0, #0\n", - " 800137e:\tf7ff ffa1 \tbl\t80012c4 \n", - " \n", - " // There will be Nr rounds.\n", - " // The first Nr-1 rounds are identical.\n", - " // These Nr-1 rounds are executed in the loop below.\n", - " for(round = 1; round < Nr; ++round)\n", - " 8001382:\t2401 \tmovs\tr4, #1\n", - " {\n", - " SubBytes();\n", - " 8001384:\tf7ff ffb8 \tbl\t80012f8 \n", - " ShiftRows();\n", - " 8001388:\tf7ff ffce \tbl\t8001328 \n", - " for(i = 0; i < 4; ++i)\n", - " 800138c:\t4b1f \tldr\tr3, [pc, #124]\t; (800140c )\n", - " 800138e:\tf8d3 10b4 \tldr.w\tr1, [r3, #180]\t; 0xb4\n", - " 8001392:\tf101 0b10 \tadd.w\tfp, r1, #16\n", - " t = (*state)[i][0];\n", - " 8001396:\tf891 a000 \tldrb.w\tsl, [r1]\n", - " Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ;\n", - " 800139a:\t784e \tldrb\tr6, [r1, #1]\n", - " 800139c:\t788d \tldrb\tr5, [r1, #2]\n", - " 800139e:\tf891 9003 \tldrb.w\tr9, [r1, #3]\n", - " 80013a2:\tea8a 0006 \teor.w\tr0, sl, r6\n", - " 80013a6:\tea85 0809 \teor.w\tr8, r5, r9\n", - " 80013aa:\tea88 0700 \teor.w\tr7, r8, r0\n", - " Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ;\n", - " 80013ae:\tf7ff ffd9 \tbl\t8001364 \n", - " 80013b2:\tea8a 0000 \teor.w\tr0, sl, r0\n", - " 80013b6:\t4078 \teors\tr0, r7\n", - " 80013b8:\t7008 \tstrb\tr0, [r1, #0]\n", - " Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ;\n", - " 80013ba:\tea86 0005 \teor.w\tr0, r6, r5\n", - " 80013be:\tf7ff ffd1 \tbl\t8001364 \n", - " 80013c2:\t4046 \teors\tr6, r0\n", - " 80013c4:\t407e \teors\tr6, r7\n", - " 80013c6:\t704e \tstrb\tr6, [r1, #1]\n", - " Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ;\n", - " 80013c8:\t4640 \tmov\tr0, r8\n", - " 80013ca:\tf7ff ffcb \tbl\t8001364 \n", - " 80013ce:\t4045 \teors\tr5, r0\n", - " 80013d0:\t407d \teors\tr5, r7\n", - " 80013d2:\t708d \tstrb\tr5, [r1, #2]\n", - " Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ;\n", - " 80013d4:\tea8a 0009 \teor.w\tr0, sl, r9\n", - " 80013d8:\tf7ff ffc4 \tbl\t8001364 \n", - " 80013dc:\tea89 0900 \teor.w\tr9, r9, r0\n", - " 80013e0:\tea87 0709 \teor.w\tr7, r7, r9\n", - " 80013e4:\t70cf \tstrb\tr7, [r1, #3]\n", - " for(i = 0; i < 4; ++i)\n", - " 80013e6:\t3104 \tadds\tr1, #4\n", - " 80013e8:\t4559 \tcmp\tr1, fp\n", - " 80013ea:\td1d4 \tbne.n\t8001396 \n", - " MixColumns();\n", - " AddRoundKey(round);\n", - " 80013ec:\t4620 \tmov\tr0, r4\n", - " for(round = 1; round < Nr; ++round)\n", - " 80013ee:\t3401 \tadds\tr4, #1\n", - " 80013f0:\tb2e4 \tuxtb\tr4, r4\n", - " AddRoundKey(round);\n", - " 80013f2:\tf7ff ff67 \tbl\t80012c4 \n", - " for(round = 1; round < Nr; ++round)\n", - " 80013f6:\t2c0a \tcmp\tr4, #10 ; <-- round != Nr check\n", - " 80013f8:\td1c4 \tbne.n\t8001384 ; <--\n", - " }\n", - " \n", - " // The last round is given below.\n", - " // The MixColumns function is not here in the last round.\n", - " SubBytes();\n", - " 80013fa:\tf7ff ff7d \tbl\t80012f8 \n", - " ShiftRows();\n", - " 80013fe:\tf7ff ff93 \tbl\t8001328 \n", - " AddRoundKey(Nr);\n", - " 8001402:\t4620 \tmov\tr0, r4\n", - "}\n", - " 8001404:\te8bd 4ff8 \tldmia.w\tsp!, {r3, r4, r5, r6, r7, r8, r9, sl, fp, lr}\n", - "\n", - "```\n", - "it won't fulfill the last one. This means that the device will always go through one round of AES before we have the opportunity to break out of the loop. An attack is still possible on this single round of AES (with only 2 faults to boot), but the math behind it is much more complicated. We'll look at this attack in the next lab, but it would still be good to see our simpler attack working on real hardware. Luckily, it's actually pretty easy to get the firmware to fulfill the last requirement! All we need to do is make `round` a volatile variable:\n", - "\n", - "```C\n", - "// Cipher is the main function that encrypts the PlainText.\n", - "static void Cipher(void)\n", - "{\n", - " volatile uint8_t round = 0;\n", - "\n", - " // Add the First round key to the state before starting the rounds.\n", - " AddRoundKey(0); \n", - " \n", - " // There will be Nr rounds.\n", - " // The first Nr-1 rounds are identical.\n", - " // These Nr-1 rounds are executed in the loop below.\n", - " for(round = 1; round < Nr; ++round)\n", - " {\n", - " SubBytes();\n", - " ShiftRows();\n", - " MixColumns();\n", - " AddRoundKey(round);\n", - " }\n", - " \n", - " // The last round is given below.\n", - " // The MixColumns function is not here in the last round.\n", - " SubBytes();\n", - " ShiftRows();\n", - " AddRoundKey(Nr);\n", - "}\n", - "```\n", - "\n", - "This will result in the following assembly (again on CWLITEARM):\n", - "\n", - "```C\n", - "// Cipher is the main function that encrypts the PlainText.\n", - "static void Cipher(void)\n", - "{\n", - " 8001378:\te92d 4ff7 \tstmdb\tsp!, {r0, r1, r2, r4, r5, r6, r7, r8, r9, sl, fp, lr}\n", - " volatile uint8_t round = 0;\n", - " 800137c:\t2000 \tmovs\tr0, #0\n", - " 800137e:\tf88d 0007 \tstrb.w\tr0, [sp, #7]\n", - " t = (*state)[i][0];\n", - " 8001382:\t4f29 \tldr\tr7, [pc, #164]\t; (8001428 )\n", - "\n", - " // Add the First round key to the state before starting the rounds.\n", - " AddRoundKey(0); \n", - " 8001384:\tf7ff ff9e \tbl\t80012c4 \n", - " \n", - " // There will be Nr rounds.\n", - " // The first Nr-1 rounds are identical.\n", - " // These Nr-1 rounds are executed in the loop below.\n", - " for(round = 1; round < Nr; ++round)\n", - " 8001388:\t2301 \tmovs\tr3, #1\n", - " 800138a:\tf88d 3007 \tstrb.w\tr3, [sp, #7]\n", - " 800138e:\tf89d 3007 \tldrb.w\tr3, [sp, #7]\n", - " 8001392:\t2b09 \tcmp\tr3, #9 ; <-- round < Nr check\n", - " 8001394:\td909 \tbls.n\t80013aa ; <--\n", - " AddRoundKey(round);\n", - " }\n", - " \n", - " // The last round is given below.\n", - " // The MixColumns function is not here in the last round.\n", - " SubBytes();\n", - " 8001396:\tf7ff ffaf \tbl\t80012f8 \n", - " ShiftRows();\n", - " 800139a:\tf7ff ffc5 \tbl\t8001328 \n", - " AddRoundKey(Nr);\n", - " 800139e:\t200a \tmovs\tr0, #10\n", - "}\n", - " 80013a0:\tb003 \tadd\tsp, #12\n", - " 80013a2:\te8bd 4ff0 \tldmia.w\tsp!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}\n", - " AddRoundKey(Nr);\n", - " 80013a6:\tf7ff bf8d \tb.w\t80012c4 \n", - " SubBytes();\n", - " 80013aa:\tf7ff ffa5 \tbl\t80012f8 \n", - " ShiftRows();\n", - " 80013ae:\tf7ff ffbb \tbl\t8001328 \n", - " for(i = 0; i < 4; ++i)\n", - " 80013b2:\tf8d7 10b4 \tldr.w\tr1, [r7, #180]\t; 0xb4\n", - " 80013b6:\tf101 0a10 \tadd.w\tsl, r1, #16\n", - " t = (*state)[i][0];\n", - " 80013ba:\tf891 9000 \tldrb.w\tr9, [r1]\n", - " Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ;\n", - " 80013be:\t784d \tldrb\tr5, [r1, #1]\n", - " 80013c0:\t788c \tldrb\tr4, [r1, #2]\n", - " 80013c2:\tf891 8003 \tldrb.w\tr8, [r1, #3]\n", - " 80013c6:\tea89 0005 \teor.w\tr0, r9, r5\n", - " 80013ca:\tea84 0b08 \teor.w\tfp, r4, r8\n", - " 80013ce:\tea8b 0600 \teor.w\tr6, fp, r0\n", - " Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ;\n", - " 80013d2:\tf7ff ffc7 \tbl\t8001364 \n", - " 80013d6:\tea89 0000 \teor.w\tr0, r9, r0\n", - " 80013da:\t4070 \teors\tr0, r6\n", - " 80013dc:\t7008 \tstrb\tr0, [r1, #0]\n", - " Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ;\n", - " 80013de:\tea85 0004 \teor.w\tr0, r5, r4\n", - " 80013e2:\tf7ff ffbf \tbl\t8001364 \n", - " 80013e6:\t4045 \teors\tr5, r0\n", - " 80013e8:\t4075 \teors\tr5, r6\n", - " 80013ea:\t704d \tstrb\tr5, [r1, #1]\n", - " Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ;\n", - " 80013ec:\t4658 \tmov\tr0, fp\n", - " 80013ee:\tf7ff ffb9 \tbl\t8001364 \n", - " 80013f2:\t4044 \teors\tr4, r0\n", - " 80013f4:\t4074 \teors\tr4, r6\n", - " 80013f6:\t708c \tstrb\tr4, [r1, #2]\n", - " Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ;\n", - " 80013f8:\tea89 0008 \teor.w\tr0, r9, r8\n", - " 80013fc:\tf7ff ffb2 \tbl\t8001364 \n", - " 8001400:\tea88 0800 \teor.w\tr8, r8, r0\n", - " 8001404:\tea86 0608 \teor.w\tr6, r6, r8\n", - " 8001408:\t70ce \tstrb\tr6, [r1, #3]\n", - " for(i = 0; i < 4; ++i)\n", - " 800140a:\t3104 \tadds\tr1, #4\n", - " 800140c:\t4551 \tcmp\tr1, sl\n", - " 800140e:\td1d4 \tbne.n\t80013ba \n", - " AddRoundKey(round);\n", - " 8001410:\tf89d 0007 \tldrb.w\tr0, [sp, #7]\n", - " 8001414:\tf7ff ff56 \tbl\t80012c4 \n", - " for(round = 1; round < Nr; ++round)\n", - " 8001418:\tf89d 3007 \tldrb.w\tr3, [sp, #7]\n", - " 800141c:\t3301 \tadds\tr3, #1\n", - " 800141e:\tb2db \tuxtb\tr3, r3\n", - " 8001420:\tf88d 3007 \tstrb.w\tr3, [sp, #7]\n", - " 8001424:\te7b3 \tb.n\t800138e \n", - " 8001426:\tbf00 \tnop\n", - " 8001428:\t200006dc \t.word\t0x200006dc\n", - "```\n", - "\n", - "This time, we can see the check is being done at the beginning of the loop instead of at the end at address 0x8001392. If you look closely, you can see if we skip this check, we go directly into the last round! Make this change in your own `firmware/mcu/crypto/TINYAES128C/aes.c` and we can start the lab!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "SCOPETYPE = 'OPENADC'\n", - "PLATFORM = 'CWLITEARM'\n", - "CRYPTO_TARGET = 'TINYAES128C'\n", - "SS_VER='SS_VER_2_1'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%run \"../../Setup_Scripts/Setup_Generic.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 SS_VER=$3" - ] - }, - { - "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": [ - "if PLATFORM == \"CWLITEXMEGA\":\n", - " def reboot_flush(): \n", - " scope.io.pdic = False\n", - " time.sleep(0.1)\n", - " scope.io.pdic = \"high_z\"\n", - " time.sleep(0.1)\n", - " #Flush garbage too\n", - " target.flush()\n", - "else:\n", - " def reboot_flush(): \n", - " scope.io.nrst = False\n", - " time.sleep(0.05)\n", - " scope.io.nrst = \"high_z\"\n", - " time.sleep(0.05)\n", - " #Flush garbage too\n", - " target.flush()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Getting a Fault\n", - "\n", - "There's two problems that we're going to face in getting a fault out of this code:\n", - "\n", - "1. Where do we insert the fault? Obviously, it'll be somewhere near the beginning, but it would be nice to narrow down the location\n", - "1. How do we know we've gotten the specific fault we're looking for? Inserting faults nearby to the correct location will generate faults in the output, but it's hard to tell just from looking at the output if we've broken out of the loop.\n", - "\n", - "We can pretty easily solve both these problems with power analysis! Since the different operations of AES are pretty distinct in AES, we can visually inspect an unfaulted power trace to know where to insert the fault. For the second issue, we can again visually inspect the power trace. Breaking out of the loop will look very different to completing the rest of AES.\n", - "\n", - "Let's get the target to encrypt something and capture a power trace. We'll also capture a copy of the ciphertext, which we can use to detect glitches in general:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "scope.clock.adc_src = \"clkgen_x1\"\n", - "reboot_flush()\n", - "scope.arm()\n", - "target.simpleserial_write('p', bytearray([0]*16))\n", - "ret = scope.capture()\n", - "if ret:\n", - " print(\"No trigger!\")\n", - "\n", - "wave = scope.get_last_trace()\n", - "\n", - "output = target.simpleserial_read_witherrors('r', 16)\n", - "gold_ct = output['payload']\n", - "\n", - "print(gold_ct)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cw.plot(wave[:2000])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now select a range of values of where to glitch. Remember the add round key operation is done at the beginning, then at the end of very loop through an AES round. You should be glitching between the last little bit of this operation and the beginning of the next one." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "glitch_loc = range(300, 340)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By this point, you should have some pretty reliable settings for glitching the target, so the provided glitch loop won't even use the GlitchController, but feel free to add it in if you're not super confident in using only one pair of glitch settings." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if scope._is_husky:\n", - " scope.glitch.enabled = True\n", - "scope.glitch.clk_src = \"clkgen\"\n", - "scope.glitch.output = \"clock_xor\"\n", - "scope.glitch.trigger_src = \"ext_single\"\n", - "scope.glitch.repeat = 1\n", - "scope.io.hs2 = \"glitch\"\n", - "# These width/offset numbers are for CW-Lite/Pro; for CW-Husky, convert as per Fault 1_1:\n", - "scope.glitch.width = 3\n", - "scope.glitch.offset = -12.8\n", - "print(scope.glitch)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You could use a SAD comparison to detect when the glitch looks substantially different, but it'll be a bit easier to loop until we glitch, then check what the waveform looks like. If you get a glitch, but it doesn't result in the correct effect, you can pretty safely skip that glitch spot." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from tqdm.notebook import tqdm, trange\n", - "wave = None\n", - "import logging\n", - "ktp = cw.ktp.Basic()\n", - "logging.getLogger().setLevel(logging.ERROR)\n", - "reboot_flush()\n", - "for i in trange(min(glitch_loc), max(glitch_loc) + 1):\n", - " scope.adc.timeout = 0.2\n", - " scope.glitch.ext_offset = i\n", - " ack = None\n", - " while ack is None:\n", - " target.simpleserial_write('k', ktp.next()[0])\n", - " ack = target.simpleserial_wait_ack()\n", - " if ack is None:\n", - " reboot_flush()\n", - " time.sleep(0.1)\n", - " \n", - " scope.arm()\n", - " \n", - " pt = bytearray([0]*16)\n", - " target.simpleserial_write('p', pt)\n", - " ret = scope.capture()\n", - " if ret:\n", - " reboot_flush() #bad if we accidentally didn't have this work\n", - " time.sleep(0.1)\n", - " print(\"timed out!\")\n", - " continue\n", - " output = target.simpleserial_read_witherrors('r', 16, glitch_timeout = 1)\n", - " if output['valid']:\n", - " if output['payload'] != gold_ct:\n", - " print(\"Glitched at {}\".format(i))\n", - " wave = scope.get_last_trace()\n", - " break\n", - " else:\n", - " reboot_flush()\n", - " \n", - "cw.plot(wave)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's collect our glitched ciphertext:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "glitched_ct0 = bytearray(output['payload'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "glitched_ct0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pt0 = bytearray(pt)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can repeat this twice more with different plaintexts to get all the faults needed:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from tqdm.notebook import tqdm, trange\n", - "wave = None\n", - "import logging\n", - "ktp = cw.ktp.Basic()\n", - "logging.getLogger().setLevel(logging.ERROR)\n", - "reboot_flush()\n", - "while True:\n", - " scope.adc.timeout = 0.2\n", - " scope.glitch.ext_offset = i #should still be in the right spot from the last glitch\n", - " ack = None\n", - " while ack is None:\n", - " target.simpleserial_write('k', ktp.next()[0])\n", - " ack = target.simpleserial_wait_ack()\n", - " if ack is None:\n", - " reboot_flush()\n", - " time.sleep(0.1)\n", - " \n", - " scope.arm()\n", - " \n", - " pt = bytearray([1]*16)\n", - " target.simpleserial_write('p', pt)\n", - " ret = scope.capture()\n", - " if ret:\n", - " reboot_flush() #bad if we accidentally didn't have this work\n", - " time.sleep(0.1)\n", - " print(\"timed out!\")\n", - " continue\n", - " output = target.simpleserial_read_witherrors('r', 16, glitch_timeout = 1)\n", - " if output['valid']:\n", - " if output['payload'] != gold_ct:\n", - " print(\"Glitched at {}\".format(i))\n", - " wave = scope.get_last_trace()\n", - " break\n", - " else:\n", - " reboot_flush()\n", - " \n", - "cw.plot(wave)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "glitched_ct1 = bytearray(output['payload'])\n", - "pt1 = bytearray(pt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from tqdm.notebook import tqdm, trange\n", - "wave = None\n", - "import logging\n", - "ktp = cw.ktp.Basic()\n", - "logging.getLogger().setLevel(logging.ERROR)\n", - "reboot_flush()\n", - "while True:\n", - " scope.adc.timeout = 0.2\n", - " scope.glitch.ext_offset = i #should still be in the right spot from the last glitch\n", - " ack = None\n", - " while ack is None:\n", - " target.simpleserial_write('k', ktp.next()[0])\n", - " ack = target.simpleserial_wait_ack()\n", - " if ack is None:\n", - " reboot_flush()\n", - " time.sleep(0.1)\n", - " \n", - " scope.arm()\n", - " \n", - " pt = bytearray([2]*16)\n", - " target.simpleserial_write('p', pt)\n", - " ret = scope.capture()\n", - " if ret:\n", - " reboot_flush() #bad if we accidentally didn't have this work\n", - " time.sleep(0.1)\n", - " print(\"timed out!\")\n", - " continue\n", - " output = target.simpleserial_read_witherrors('r', 16, glitch_timeout = 1)\n", - " if output['valid']:\n", - " if output['payload'] != gold_ct:\n", - " print(\"Glitched at {}\".format(i))\n", - " wave = scope.get_last_trace()\n", - " break\n", - " else:\n", - " reboot_flush()\n", - " \n", - "cw.plot(wave)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "glitched_ct2 = bytearray(output['payload'])\n", - "pt2 = bytearray(pt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sbox = [\n", - " # 0 1 2 3 4 5 6 7 8 9 a b c d e f \n", - " 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, # 0\n", - " 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, # 1\n", - " 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, # 2\n", - " 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, # 3\n", - " 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, # 4\n", - " 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, # 5\n", - " 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, # 6\n", - " 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, # 7\n", - " 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, # 8\n", - " 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, # 9\n", - " 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, # a\n", - " 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, # b\n", - " 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, # c\n", - " 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, # d\n", - " 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, # e\n", - " 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 # f\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "key_guess = []\n", - "for kbyte in range(16):\n", - " for i in range(255):\n", - " if (sbox[i ^ pt0[kbyte]] ^ sbox[i ^ pt1[kbyte]]) == (glitched_ct0[kbyte] ^ glitched_ct1[kbyte]):\n", - " if (sbox[i ^ pt0[kbyte]] ^ sbox[i ^ pt2[kbyte]]) == (glitched_ct0[kbyte] ^ glitched_ct2[kbyte]):\n", - " print(i)\n", - " key_guess.append(i)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bytearray(key_guess)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You'll see that you've got the correct key bytes, but they're out of order! This is because we didn't account for the ShiftRows operation (this doesn't matter for the analysis since we were using repeated bytes for the plaintext. If this wasn't the case, we'd have to match the plaintext up with the ciphertext instead of just leaving it to the end). If we swap the bytes of the key around based on ShiftRows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "SR = [0, 13, 10, 7, 4, 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3]\n", - "for i in range(16):\n", - " print(hex(key_guess[SR[i]]))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can print the key in the correct order.\n", - "\n", - "## Conclusions & Next Steps\n", - "\n", - "Though we didn't glitch an unmodified version of TINYAES128C, we did see that, given the right implementation of AES, we can use glitching to skip the repeated rounds of AES, thus bypassing its security and allowing us to recover the key. In the next lab, we'll look at mounting an attack against an unmodified TINYAES." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\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.\n", - "\n", - "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).\n", - "\n", - "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." - ] - } - ], - "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_Tasks/1_SCA_Lab/Extending AES-128 Attacks to AES-256.ipynb b/jupyter/lab/1_SCA_Lab/Extending AES-128 Attacks to AES-256.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Extending AES-128 Attacks to AES-256.ipynb rename to jupyter/lab/1_SCA_Lab/Extending AES-128 Attacks to AES-256.ipynb diff --git a/jupyter/Lab_Tasks/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 similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference (SIMULATED).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference (SIMULATED).ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference.ipynb b/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1A - Resynchronizing Traces with Sum of Absolute Difference (HARDWARE).ipynb b/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resynchronizing Traces with Sum of Absolute Difference (HARDWARE).ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1A - Resynchronizing Traces with Sum of Absolute Difference (HARDWARE).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 1_1A - Resynchronizing Traces with Sum of Absolute Difference (HARDWARE).ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (HARDWARE).ipynb b/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (HARDWARE).ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (HARDWARE).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (HARDWARE).ipynb diff --git a/jupyter/Lab_Tasks/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 similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (SIMULATED).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (SIMULATED).ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp.ipynb b/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb diff --git a/jupyter/Lab_Tasks/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 similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (SIMULATED).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (SIMULATED).ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES.ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb diff --git a/jupyter/Lab_Tasks/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 similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (SIMULATED).ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (SIMULATED).ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation.ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb b/jupyter/lab/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/Lab 3_1B - Reverse Engineering on the AES256 Bootloader.ipynb b/jupyter/lab/1_SCA_Lab/Lab 3_1B - Reverse Engineering on the AES256 Bootloader.ipynb similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/Lab 3_1B - Reverse Engineering on the AES256 Bootloader.ipynb rename to jupyter/lab/1_SCA_Lab/Lab 3_1B - Reverse Engineering on the AES256 Bootloader.ipynb diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/AES_Encryption.png b/jupyter/lab/1_SCA_Lab/img/AES_Encryption.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/AES_Encryption.png rename to jupyter/lab/1_SCA_Lab/img/AES_Encryption.png diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/AES_MixCol.png b/jupyter/lab/1_SCA_Lab/img/AES_MixCol.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/AES_MixCol.png rename to jupyter/lab/1_SCA_Lab/img/AES_MixCol.png diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/Aes256_cbc.png b/jupyter/lab/1_SCA_Lab/img/Aes256_cbc.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/Aes256_cbc.png rename to jupyter/lab/1_SCA_Lab/img/Aes256_cbc.png diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/GoodVBadRef.png b/jupyter/lab/1_SCA_Lab/img/GoodVBadRef.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/GoodVBadRef.png rename to jupyter/lab/1_SCA_Lab/img/GoodVBadRef.png diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/Resync_traces_ref.png b/jupyter/lab/1_SCA_Lab/img/Resync_traces_ref.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/Resync_traces_ref.png rename to jupyter/lab/1_SCA_Lab/img/Resync_traces_ref.png diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/aes_operations.png b/jupyter/lab/1_SCA_Lab/img/aes_operations.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/aes_operations.png rename to jupyter/lab/1_SCA_Lab/img/aes_operations.png diff --git a/jupyter/Lab_Tasks/1_SCA_Lab/img/stm_run1.png b/jupyter/lab/1_SCA_Lab/img/stm_run1.png similarity index 100% rename from jupyter/Lab_Tasks/1_SCA_Lab/img/stm_run1.png rename to jupyter/lab/1_SCA_Lab/img/stm_run1.png diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/1_Constructing_the_Glitch_Loop.ipynb b/jupyter/lab/2_Fault_Lab/1_Constructing_the_Glitch_Loop.ipynb similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/1_Constructing_the_Glitch_Loop.ipynb rename to jupyter/lab/2_Fault_Lab/1_Constructing_the_Glitch_Loop.ipynb diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/2_Glitching_Past_a_Password_Check.ipynb b/jupyter/lab/2_Fault_Lab/2_Glitching_Past_a_Password_Check.ipynb similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/2_Glitching_Past_a_Password_Check.ipynb rename to jupyter/lab/2_Fault_Lab/2_Glitching_Past_a_Password_Check.ipynb diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/3_Glitching_a_Memory_Dump.ipynb b/jupyter/lab/2_Fault_Lab/3_Glitching_a_Memory_Dump.ipynb similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/3_Glitching_a_Memory_Dump.ipynb rename to jupyter/lab/2_Fault_Lab/3_Glitching_a_Memory_Dump.ipynb diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/4_Glitching_a_Bootloader.ipynb b/jupyter/lab/2_Fault_Lab/4_Glitching_a_Bootloader.ipynb similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/4_Glitching_a_Bootloader.ipynb rename to jupyter/lab/2_Fault_Lab/4_Glitching_a_Bootloader.ipynb diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/img/Clock-glitched.png b/jupyter/lab/2_Fault_Lab/img/Clock-glitched.png similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/img/Clock-glitched.png rename to jupyter/lab/2_Fault_Lab/img/Clock-glitched.png diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/img/Clock-normal.png b/jupyter/lab/2_Fault_Lab/img/Clock-normal.png similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/img/Clock-normal.png rename to jupyter/lab/2_Fault_Lab/img/Clock-normal.png diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/img/Glitchgen-mux.png b/jupyter/lab/2_Fault_Lab/img/Glitchgen-mux.png similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/img/Glitchgen-mux.png rename to jupyter/lab/2_Fault_Lab/img/Glitchgen-mux.png diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/img/Glitchgen-phaseshift.png b/jupyter/lab/2_Fault_Lab/img/Glitchgen-phaseshift.png similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/img/Glitchgen-phaseshift.png rename to jupyter/lab/2_Fault_Lab/img/Glitchgen-phaseshift.png diff --git a/jupyter/Lab_Tasks/2_Fault_Lab/img/Mcu-unglitched.png b/jupyter/lab/2_Fault_Lab/img/Mcu-unglitched.png similarity index 100% rename from jupyter/Lab_Tasks/2_Fault_Lab/img/Mcu-unglitched.png rename to jupyter/lab/2_Fault_Lab/img/Mcu-unglitched.png diff --git a/jupyter/Lab_Tasks/3_RSA_Lab/3_RSA_Lab.ipynb b/jupyter/lab/3_RSA_Lab/3_RSA_Lab.ipynb similarity index 100% rename from jupyter/Lab_Tasks/3_RSA_Lab/3_RSA_Lab.ipynb rename to jupyter/lab/3_RSA_Lab/3_RSA_Lab.ipynb diff --git a/jupyter/lab/4_ECC_Lab/5-ECC_lab.ipynb b/jupyter/lab/4_ECC_Lab/5-ECC_lab.ipynb new file mode 100644 index 00000000..e8b644ff --- /dev/null +++ b/jupyter/lab/4_ECC_Lab/5-ECC_lab.ipynb @@ -0,0 +1,1308 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Breaking Hardware ECC on CW305 FPGA" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Background\n", + "To get the most out of this tutorial, some basic knowledge of elliptic curves, and in particular of point multiplication on elliptic curves, is required. A good overview is available here: https://cryptojedi.org/peter/data/eccss-20130911b.pdf.\n", + "\n", + "The side-channel attack presented here targets the scalar multiplier (\"k\") in the elliptic curve point multiplication. Point multiplication is the most expensive operation in many (if not all?) cryptographic uses of elliptic curves. The secret scalar is not the private key, but learning the scalar used in an ECDSA signature (for example) allows the secret key to be trivially calculated.\n", + "\n", + "This attack is quite different from the AES side-channel attacks in our other tutorials. In most ECC point multiplication implementations (including the target used here), the secret scalar k is consumed one bit at a time. At a high level, the attack is very simple:\n", + "1. Identify when each bit of k is processed on the power trace.\n", + "2. Find how processing a '1' is different from processing a '0'.\n", + "3. Assemble the secret k, one bit at a time.\n", + "\n", + "Since we are attacking k one bit at a time, its size has no impact on the difficulty of the attack. The curve used in this attack is the NIST P-256 curve; the same approach would work just as well with a larger curve.\n", + "\n", + "Our attack requires multiple traces to be collected. The secret k remains constant for each trace, but a different point must be used for each trace. However, we require no knowledge whatsoever of what the points actually are. Furthermore, if the attacker is limited to collecting a single trace for a given value of k, we will show in the end that we can correctly guess most of k.\n", + "\n", + "The target for this attack is the point multiplication submodule of the [Cryptech ecdsa256 core](https://wiki.cryptech.is/log/user/shatov/ecdsa256).\n", + "Refer to the README in your ChipWhisperer repository (`hardware/victims/cw305_artixtarget/fpga/cryptosrc/cryptech/ecdsa256-v1/README.md`) for details on the target and the modifications that were made to it for this attack." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Series Overview\n", + "\n", + "This tutorial is the first part of a 5-part series. In this tutorial, we develop a basic attack and demonstrate its viability.\n", + "\n", + "Part 2 improves the attack and introduces new ways to measure its performance.\n", + "\n", + "In part 3, we switch from the attacker chair to the defender chair: we propose and evaluate several countermeasures to resist our attack.\n", + "\n", + "In part 4, we study one more countermeasure which yields new insights on the target's leakage.\n", + "\n", + "Part 5 concludes by looking at what TVLA can tell us about the target's leakage, with the benefit of all that we learned in the previous sections.\n", + "\n", + "If you only do part 1, you'll learn quite a bit about hardware ECC and how it may be attacked, and you could choose to stop there. Hopefully you'll find this sufficiently interesting to cover the remaining sections and learn even more!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Capture Notes\n", + "\n", + "Most of the capture settings used below are similar to the standard ChipWhisperer scope settings. Some important points to note:\n", + "\n", + "- The full ECC operation takes over one million clock cycles, so it is best done with a ChipWhisperer-Pro or Husky.\n", + "- With a ChipWhisperer-Lite, every trace needs to be captured in several steps, using the sample offset feature (47 steps to be precise!), so trace acquisition is much slower: around 5 seconds/trace, versus 4 traces/second with the CW-pro. Be patient! Luckily, the attack doesn't require a large number of traces.\n", + "- It's possible that better results would be obtained with x4 sampling, but that would make trace acquisition with the CW-lite *very* slow.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Supported Setups\n", + "\n", + "This tutorial requires a CW305 or CW312T-A35 target, and either a CW-Lite (two-part), CW-Pro or CW-Husky.\n", + "\n", + "The tutorial was developed with a CW-Pro and the CW305 100t FPGA; the observations made in the attack's development will be accurate if you're using the same, but other combinations of capture and target hardware can diverge and may require some modifications to the attack code.\n", + "\n", + "The same holds if you re-build the FPGA bitfile.\n", + "\n", + "#### CW312T_A35 notes:\n", + "- More traces may be required for the attacks to succeed.\n", + "- Some of the observations with respect to the efficacy of countermeasures (in parts 3 onwards) won't line up exactly with what you observe.\n", + "- The CW312T_A35-specific settings that are used assume the use of the inductive shunt; if you're using a different shunt, some tweaks will be required.\n", + "\n", + "#### CW-Lite notes:\n", + "- Your results may not correspond exactly to the notebook's comments, especially in the first attempt when using the power measured on specific clock cycles. This is likely due to the 5x higher clock frequency being used, which is done to keep the trace acquisition time reasonable.\n", + "- In the end, the final attack works well with the CW-lite, although it tends to require a few more traces.\n", + "\n", + "If you don't have hardware, you can still follow along using the provided pre-recorded traces (set `TRACES` to `\"SIMULATED\"`). Because this is ECC and traces are so large (the full target operation takes 1.2 million cycles), most saved traces are not full traces of the ECC operation; they are segmented traces (chopped up subsets of full traces, keeping only the parts needed for the attack). Additionally, many of the results shown in this series of notebooks are also presented in the [Ark of the ECC eprint paper](https://eprint.iacr.org/2021/1520.pdf).\n", + "\n", + "Pre-recorded traces were obtained with `CWHUSKY` and `CW305_100t`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "#PLATFORM = 'CWLITE'\n", + "#PLATFORM = 'CWPRO'\n", + "PLATFORM = 'CWHUSKY'" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "TARGET_PLATFORM = 'CW305_100t'\n", + "#TARGET_PLATFORM = 'CW305_35t'\n", + "#TARGET_PLATFORM = 'CW312T_A35'" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "#TRACES = 'HARDWARE' # if you have the required capture+target hardware: capture actual traces\n", + "TRACES = 'SIMULATED' # if you don't have capture+target hardware: use pre-captured traces (these traces were obtained using CW-Husky with a CW305_100t)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import chipwhisperer as cw" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Trace Capture\n", + "Below is the capture loop. The main body of the loop loads some new multiplication parameters, arms the scope, then finally records and appends our new trace to the `traces[]` list.\n", + "\n", + "Note that the multiplication result is read from the target and compared to the expected results, as a sanity check." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First let's pick a scalar for which we can very easily distinguish ones from zeros. Remember that k is the secret that we want to be able to retrieve with our side-channel attack." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "k = 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook contains a bunch of methods that we'll use frequently, including a trace capture method:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "%run \"ECC_lab_setup.ipynb\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We just need a single trace to start with. The `full=True` argument specifies that the full power trace should be captured:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "traces = get_traces(1, k, 'part1_1', full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Buidling the Attack: Trace Analysis\n", + "\n", + "In the following, we build up the attack from scratch. In this way, while we are developing an attack which is very specific to our target, we show the methods you would use to build an attack for a different target.\n", + "\n", + "Let's start by looking at a single trace. Let's start with the first 20k cycles only (you can plot the full trace but that will be very slow because it's a long trace!)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from bokeh.plotting import figure, show\n", + "from bokeh.resources import INLINE\n", + "from bokeh.io import output_notebook\n", + "\n", + "output_notebook(INLINE)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "p = figure(width=2000)\n", + "\n", + "samples = 20000\n", + "#samples = len(traces[0].wave)\n", + "xrange = list(range(samples))\n", + "p.line(xrange, traces[0].wave[:samples], line_color=\"red\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(p)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Simulation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There seems to be a very strong periodicity to the trace. We can confirm this by simulating the target core and looking at what it's actually doing.\n", + "\n", + "If you want to go through the whole process, install the [iverilog simulator](http://iverilog.icarus.com/) (Ubuntu: `apt-get install iverilog`) and follow along below; otherwise skip ahead to the next section, **\"Finding Ones and Zeros\"**.\n", + "\n", + "The next step runs the simulation and takes several minutes; you can see that it's still alive by looking at the `make.log` file. Once it's done, you'll see its output here." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "cd ../../../firmware/fpgas/ecc/sim/\n", + "make DUMP=1 WAVEFORMAT=vcd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This produces a simulation waveform `../../../firmware/fpgas/ecc/sim/results/tb.fst` which you can look at with gtkwave.\n", + "\n", + "What we're going to do is record at what times the multiplication core's internal `bit_counter` changes, which tells us when the core is processing which bit of the secret k scalar.\n", + "\n", + "We can automatically extract these event times with the vcdvcd package (https://github.com/cirosantilli/vcdvcd). Unfortunately this step needs to ingest the full 2.7G file all at once, so it's also very slow (again if you're impatient you can skip ahead to the next section)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from vcdvcd import VCDVCD\n", + "vcd = VCDVCD('../../../firmware/fpgas/ecc/sim//results/tb.fst')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we've ingested the simulation waveform, extracting event times from it is almost instantaneous:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "kbittimes = vcd['tb.U_dut.U_curve_mul_256.bit_counter[7:0]']\n", + "cyclecounts = vcd['tb.cycle_count[31:0]']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With a bit of Python magic we build up the `cycles` array, which contains the clock cycle number for when each bit of k is processed, relative to the start of the point multiplication operation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cycles = []\n", + "deltas = []\n", + "for i in range(1,257):\n", + " cycles.append(int(cyclecounts[kbittimes.tv[i][0]],2))\n", + " if (i > 1):\n", + " deltas.append(cycles[-1] - cycles[-2])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One thing we can see right away is that each bit takes *exactly* 4204 cycles to process. So, no timing attacks here: the operation is rock-solid time-constant.\n", + "\n", + "Go ahead and try with different values of k and P if you want; don't bother with the lengthy waveform generation and extraction, just look at the `trace.textout['cycles']` attribute to see how many clock cycles the job took (as measured by the scope looking at the target's trigger signal)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "min(deltas), max(deltas)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "traces[0].textout['cycles']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We only need to do this lengthy step once, so let's save the results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import os\n", + "\n", + "cycles_file = 'data/ecc_cycles.npy'\n", + "# avoid overwriting:\n", + "if not os.path.exists(cycles_file):\n", + " numpy.save(cycles_file, cycles)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Finding Ones and Zeros:\n", + "\n", + "The previously saved `cycles.npy` tells us at which clock cycle the target core is processing each bit of k. If you skipped over the previous section, carry on from here.\n", + "\n", + "We begin by loading the array which tells us on which clock cycle processing begins for every bit of k:" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "cycles = np.load('data/ecc_cycles.npy')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's overlay the power trace from a few differents bits of k, including both ones and zeros:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "r = figure(width=2000)\n", + "\n", + "samples = 4204\n", + "xrange = list(range(samples))\n", + "for i, color in zip([10, 20, 30, 200, 210, 220], ['red', 'green', 'blue', 'orange', 'purple', 'brown']):\n", + " r.line(xrange, traces[0].wave[cycles[i]:cycles[i]+samples], line_color=color)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(r)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The peaks line up *perfectly*, and the different bits appear indistinguishable.\n", + "\n", + "Of course, side-channel attacks work by picking up the smallest of differences, so we're not done yet...\n", + "\n", + "Our next step is to average the power trace for all k=1 bits and all k=0 bits **from a single multiplication trace** to see if we can spot any differences:" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "# pick any trace here:\n", + "trace = traces[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "avg_trace = np.zeros(samples)\n", + "\n", + "for start in cycles[1:]:\n", + " avg_trace += trace.wave[start:start+samples]\n", + "\n", + "avg_trace /= len(cycles[1:])" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "avg_ones = np.zeros(samples)\n", + "\n", + "for start in cycles[1:128]:\n", + " avg_ones += trace.wave[start:start+samples]\n", + "\n", + "avg_ones /= 128" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "avg_zeros = np.zeros(samples)\n", + "\n", + "for start in cycles[128:256]:\n", + " avg_zeros += trace.wave[start:start+samples]\n", + "\n", + "avg_zeros /= 128" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s = figure(width=2000)\n", + "\n", + "xrange = list(range(len(avg_trace)))\n", + "#s.line(xrange, avg_ones, line_color=\"red\")\n", + "#s.line(xrange, avg_zeros, line_color=\"blue\")\n", + "s.line(xrange, avg_ones - avg_zeros, line_color=\"orange\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(s)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Bingo!** We see substantial differences at the very start and very end of the bit processing. Zoom in around cycle 4202 to quantify the difference; it's not big, but it's there.\n", + "\n", + "Now, remember that the difference we've found here is from the average of 128 measurements.\n", + "\n", + "The question is: is the difference seen in the average consistently present for *individual* bits of k. Let's look at that with some interactive plotting.\n", + "\n", + "First let's define a helper function to sum the power samples." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "def get_sums(no_traces):\n", + " sums = []\n", + " # in case samples were recorded as ints, translate result to make it as though they were floats\n", + " if 'int' in str(type(traces[0].wave[0])):\n", + " shift = True\n", + " if PLATFORM != 'CWHUSKY':\n", + " center = 2**9\n", + " div = 2**10\n", + " # infer whether trace was collected with 8 or 12 bits per sample:\n", + " elif max(abs(traces[0].wave)) > 255:\n", + " center = 2**11\n", + " div = 2**12\n", + " else:\n", + " center = 2**7\n", + " div = 2**8\n", + " else:\n", + " shift = False\n", + "\n", + " if len(traces[0].wave) == 1130000:\n", + " # full captures\n", + " for c in cycles:\n", + " sum = 0\n", + " for trace in traces[:no_traces]:\n", + " for i in poi:\n", + " power = trace.wave[c+abs(i)]\n", + " if shift:\n", + " power = (power - center)/div\n", + " if i < 0:\n", + " sum -= power\n", + " else:\n", + " sum += power\n", + " sums.append(sum)\n", + " else:\n", + " # segmented captures (used for pre-captured traces, to save space)\n", + " segment_size = len(traces[0].wave) // SEGMENTS\n", + " for c in range(256):\n", + " sum = 0\n", + " for trace in traces[:no_traces]:\n", + " for i in poi:\n", + " # complicated mapping to deal with the segmented traces\n", + " if abs(i) > segment_size:\n", + " absi = segment_size - (SEGMENT_CYCLES - abs(i))\n", + " else:\n", + " absi = abs(i)\n", + " if i < 0:\n", + " i = -absi\n", + " else:\n", + " i = absi\n", + " index = c*segment_size+abs(i) + cycles[0]\n", + " power = trace.wave[index]\n", + " if shift:\n", + " power = (power - center)/div\n", + " if i < 0:\n", + " sum -= power\n", + " else:\n", + " sum += power\n", + " sums.append(sum)\n", + " return sums" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `poi` array contains the clock cycles for which we sum the power measurement.\n", + "\n", + "For positive numbers, `get_sums()` adds the power measurement at that clock cycle; for negative numbers, `get_sums()` substracts the power measurement at abs(clock cycle).\n", + "\n", + "The `poi` defined here was chosen for the CW-Pro + CW305-100T combination; **some other combinations may require different indices to obtain good results**!\n", + "\n", + "The earlier plot which shows the difference between average one's and average zero's should guide you to choose the proper points for your setup." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "if TARGET_PLATFORM == 'CW312T_A35':\n", + " poi = [4202, -4203, 7, -8]\n", + "else:\n", + " poi = [4202, -6, 7]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we acquire more traces. Here we specify `full=False` in order to capture only the segments of the power trace that we need for our analysis and attack.\n", + "\n", + "This is only supported by CW-Husky; if you're using CW-Pro or CW-Lite, you'll have to change this argument to `False`. The analysis code will automatically how to index into the power trace for both cases, so you need not worry about that." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "traces = get_traces(30, k, 'part1_2', full=False, samples_per_segment=64, as_int=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you get a number of warnings stating that the operation too more clock cycles than expected (typically 1 more clock cycle), you may get slightly different results from what's described in this notebook (due to some samples being off by 1).\n", + "You should be able to resolve the issue by resetting the ADC DCM (`scope.clock.reset_adc()`) or restarting the notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's set up an interactive plot which lets us see whether we can distinguish k bits that are ones from k bits that are zeros, and how many traces might be required to do so reliably:" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "def update_plot(no_traces):\n", + " SS.data_source.data['y'] = get_sums(no_traces)\n", + " push_notebook()" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import interact, Layout\n", + "from bokeh.io import push_notebook\n", + "\n", + "no_traces = 15\n", + "S = figure(width=2000)\n", + "\n", + "xrange = list(range(len(cycles)))\n", + "sums = get_sums(no_traces)\n", + "SS = S.line(xrange, sums)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(S, notebook_handle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "interact(update_plot, no_traces=(1, len(traces)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The x-axis of this plot is the index of the k bit being processed by the target; the y-axis is the metric which we hope to use to distinguish ones from zeros. The metric we used here is the sum of the power measurements at cycles 6, 7 and 4202.\n", + "\n", + "Recall that our secret scalar k was set to {128 ones, 128 zeros}. So if our distinguishing metric is good, we expect the first half of the plot to be distinguishable from the second half.\n", + "\n", + "With a single trace, the results aren't great: the two halves are statistically different, but an attacker wouldn't be able to correctly guess all k bits.\n", + "\n", + "But by the time the slider hits about 8 traces, the two halves no longer overlap. With over 15 traces, the two halves are very distinct. We may have a successful side-channel attack!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sanity check\n", + "\n", + "Before we declare victory, let's check whether our 0/1 distinguisher still works when k is **not** made of very long strings of 0's and 1's:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "k = 0xffffffffffffffff0000000000000000aaaa0000cccc00001111000033330000\n", + "traces = get_traces(50, k, 'part1_3', full=False, samples_per_segment=64)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [], + "source": [ + "from bokeh.models import Label\n", + "S2 = figure(width=2000)\n", + "#poi = [4202, -6, 7]\n", + "xrange = list(range(len(cycles)))\n", + "sums = get_sums(len(traces))\n", + "SS = S2.line(xrange, sums)\n", + "\n", + "if PLATFORM == 'CWPRO':\n", + " shift = 6\n", + "else:\n", + " shift = 0\n", + "\n", + "mytext_AAAA = Label(x=130, y=6+shift, text='0xAAAA')\n", + "mytext_CCCC = Label(x=160, y=7+shift, text='0xCCCC')\n", + "mytext_1111 = Label(x=195, y=6+shift, text='0x1111')\n", + "mytext_3333 = Label(x=230, y=7+shift, text='0x3333')\n", + "\n", + "S2.add_layout(mytext_AAAA)\n", + "S2.add_layout(mytext_CCCC)\n", + "S2.add_layout(mytext_1111)\n", + "S2.add_layout(mytext_3333)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(S2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Uh-oh**: when the k bits alternate between 0 and 1 every bit (e.g. bits 128-143), it looks like we get a constant metric that's about halfway between what we get for long strings of ones and long strings of zeros (e.g. bit 0-63 and 64-127).\n", + "\n", + "Let's plot some components of our metric separately:" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "S3 = figure(width=2000)\n", + "\n", + "no_traces = len(traces)\n", + "\n", + "if TARGET_PLATFORM == 'CW312T_A35':\n", + " pois = [-4203, 7]\n", + "else:\n", + " pois = [4202, 7]\n", + "\n", + "poi = [pois[0]]\n", + "sums = np.asarray(get_sums(no_traces)) + 4 # just a vertical shift for easier visualization \n", + "S4202 = S3.line(xrange, sums, line_color='red')\n", + "\n", + "poi = [pois[1]]\n", + "sums = np.asarray(get_sums(no_traces)) - 2\n", + "S7 = S3.line(xrange, sums, line_color='blue')\n", + "\n", + "mytext_AAAA = Label(x=130, y=4, text='0xAAAA')\n", + "mytext_CCCC = Label(x=160, y=4, text='0xCCCC')\n", + "mytext_1111 = Label(x=195, y=4, text='0x1111')\n", + "mytext_3333 = Label(x=230, y=4, text='0x3333')\n", + "\n", + "S3.add_layout(mytext_AAAA)\n", + "S3.add_layout(mytext_CCCC)\n", + "S3.add_layout(mytext_1111)\n", + "S3.add_layout(mytext_3333)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(S3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Ah-ha!** At bits 128-143, we see that the red curve is offset from the blue curve by one cycle, so when k changes every bit, the changes tend to cancel each other out.\n", + "\n", + "This plot also shows that the red curve appears to have a better signal-to-noise ratio. It also has a more regular behaviour at the very beginning.\n", + "\n", + "Maybe we can proceed with this attack by using only the power measurement at cycle 4202." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### One more thing...\n", + "\n", + "But what about those peaks seen in the first few cycles?\n", + "\n", + "Let's do another sanity check, this time with k not starting with a long string of ones:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "k = 0x0000ffffffffffff0000000000000000aaaa0000cccc00001111000033330000\n", + "traces = get_traces(30, k, 'part1_4', full=False, samples_per_segment=530) # using more samples here because we'll need them for the correlation attack later" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [], + "source": [ + "S4 = figure(width=2000)\n", + "\n", + "no_traces = len(traces)\n", + "\n", + "poi = [pois[0]]\n", + "sums = np.asarray(get_sums(no_traces)) + 2 # just a vertical shift for easier visualization \n", + "S4202 = S4.line(xrange, sums, line_color='red')\n", + "\n", + "poi = [pois[1]]\n", + "sums = np.asarray(get_sums(no_traces)) - 2\n", + "S7 = S4.line(xrange, sums, line_color='blue')\n", + "\n", + "leading_zeros = Label(x=0, y=2.75, text='leading zeros')\n", + "first_one = Label(x=17, y=4.25, text='first one')\n", + "ones = Label(x=20, y=2, text='0xFFF...')\n", + "zeros = Label(x=70, y=1, text='0x000...')\n", + "\n", + "\n", + "S4.add_layout(leading_zeros)\n", + "S4.add_layout(first_one)\n", + "S4.add_layout(ones)\n", + "S4.add_layout(zeros)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(S4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Normally, zeros get the lowest score, but when they are at the beginning of k, they get the highest score with cycle 4202; the behaviour with cycles 6 and 7 is stranger still.\n", + "\n", + "This is getting a bit messy: having to distinguish between 3 levels will require a higher SNR. Also, if you repeat the above test with `k=0x1000...`, `k=0x3000...` and similar values, you'll see that it harder still to properly identify those first few bits.\n", + "\n", + "We're **really** close to a working attack. In fact we could pretty much stop here: we can identify most bits of k, we just have some trouble with the first few; if we omit the unlikely cases where k starts with a very long string of zeros, we could simply and quickly brute force those first few bits.\n", + "\n", + "But let's try something else (promise, this one's going to work): a slightly different approach which will give a cleaner attack, and which will also give us some insight into **why** the leakage is happening.\n", + "\n", + "If you're a hardware designer, then what follows may be the most instructive part of this tutorial." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ...To the Verilog!\n", + "\n", + "If you're not too scared of a little Verilog, run the Verilog simulation as shown earlier in the notebook and open the simulation waveform in gtkwave. (If you are scared, just skip over to the **\"Correlation Attack\"** section.)\n", + "\n", + "Then bring up `hardware/victims/cw305_artixtarget/fpga/cryptosrc/cryptech/ecdsa256-v1/rtl/curve/curve_mul_256.v` in a text editor.\n", + "\n", + "Follow the `k_din` input. This is our secret k that we wish to retrieve with the side-channel attack.\n", + "\n", + "`k_din` gets loaded into `k_din_reg`, and its most significant bit is assigned to `move_inhibit`, which in turns goes to `copy_t2r_int`. This last signal is used to enable the writing of intermediate results to the `bram_1rw_1ro_readfirst` memory instances. There are 3 such memories; one for each of the x, y, and z point coordinates." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cryptograpy Detour\n", + "\n", + "To progress from here, a little bit of elliptic curve knowledge is required.\n", + "\n", + "You may have noticed that the point that the target core is tasked to multiply is given with $(x,y)$ coordinates. Why is there a $z$ coordinate now in the source code? Without fully reversing the implementation, it's safe to assume that the target takes the given point from its *affine* $(x,y)$ coordinates and transforms it into *projective* $(x,y,z)$ coordinates. Many (most?) ECC implementations do this because point multiplication is faster in projective coordinates (https://www.nayuki.io/page/elliptic-curve-point-addition-in-projective-coordinates gives a good overview of this).\n", + "\n", + "Now let's load that simulation waveform and look at the write timing on those `bram_1rw_1ro_readfirst` instances by probing `bram_rx_wr_en`, `bram_ry_wr_en` and `bram_rz_wr_en`. If you stare at it for a few minutes you should recognize that the write timings are *identical* for every bit of k, except for the last set of writes which are blocked whenever `move_inhibit` is high.\n", + "\n", + "We can now make a pretty safe guess that the multiplication algorithm used is **double and always add**. Point multiplication in general consists of repeated doublings and adds. In this implementation, for each bit of k, the intermediate result goes through a point doubling and a point add; the result of the point add is discarded if the addition is not required, which is dependent on the value of the k bit being processed. `move_inhibit` is the logic which controls this discarding. This multiplication algorithm is a simple way to achieve time-constant execution, and, depending on implementation details, make it harder for side-channel attacks to identify whether the secret bit being processed is a 1 or a 0.\n", + "\n", + "We now have a decent (and hopefully correct!) understanding of the implementation. For the purpose of side-channel attacks, we are now reasonably certain that the target does *exactly* the same thing independent of k, *except* for the storage operation which is masked via `move_inhibit`, depending on k.\n", + "\n", + "The set of 8 writes that are blocked by `move_inhibit` occur on clock cycles 4195 to 4203 (relative to the processing of each bit of k). *Hmm,* 4195-4203... do these numbers sound familiar? Recall that 4202 is the clock cycle where we noted a statistical difference between processing a 1 versus a 0!\n", + "\n", + "We know from our first attempt that looking at only the last set of writes doesn't lead to a clean attack. But now that we understand what's happening at those clock cycles, we can try something else to leverage the leakage that we've found. On the simulation waveform, we can look at the next time that the `bram_1rw_1ro_readfirst` memory instances are read (after the possibly blocked memory write). Here's the idea: if `move_inhibit` was not set, then the next memory read will return what was written at cycles 4195-4203; otherwise, it will return something else. The correlation between the power samples at those two points in time might be able to tell us whether `move_inhibit` was set or not." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Correlation Attack\n", + "\n", + "First let's define the cycle offsets where the memory read and writes that we're interested in are occurring.\n", + "\n", + "The x/y/z writes happen simultaneously (at cycle `rupdate_offset`), but the three coordinates are read at three different times (`r[x|y|z]read_offset`).\n", + "\n", + "The correlation is computed over `rupdate_cycles = 8` clock cycles, because that's how many clock cycles it takes to read or write an intermediate $R_x$, $R_y$ or $R_z$ value (256-bit values into 32-bit wide memories)." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "rupdate_offset = 4195\n", + "rupdate_cycles = 8\n", + "rxread_offset = 205\n", + "ryread_offset = 473\n", + "rzread_offset = 17\n", + "\n", + "if TARGET_PLATFORM == 'CW312T_A35':\n", + " rupdate_cycles += 8\n", + " rupdate_offset -= 4\n", + " rxread_offset -= 4\n", + " ryread_offset -= 4\n", + " rzread_offset -= 4" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we compute the correlations:" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "t = len(traces)\n", + "corrsxonly = []\n", + "corrsyonly = []\n", + "corrszonly = []\n", + "\n", + "segment_size = len(traces[0].wave) // SEGMENTS\n", + "\n", + "for i in range (0, len(cycles)-1):\n", + " corrx = 0\n", + " corry = 0\n", + " corrz = 0\n", + "\n", + " if len(traces[0].wave) == 1130000:\n", + " start1 = cycles[i] + rupdate_offset\n", + " stop1 = cycles[i] + rupdate_offset + rupdate_cycles\n", + "\n", + " start2x = cycles[i+1] + rxread_offset\n", + " start2y = cycles[i+1] + ryread_offset\n", + " start2z = cycles[i+1] + rzread_offset\n", + "\n", + " stop2x = cycles[i+1] + rxread_offset + rupdate_cycles\n", + " stop2y = cycles[i+1] + ryread_offset + rupdate_cycles\n", + " stop2z = cycles[i+1] + rzread_offset + rupdate_cycles\n", + "\n", + " else:\n", + " start1 = cycles[0] + (i+1)*segment_size - (SEGMENT_CYCLES - rupdate_offset)\n", + " stop1 = cycles[0] + (i+1)*segment_size - (SEGMENT_CYCLES - rupdate_offset) + rupdate_cycles\n", + "\n", + " start2x = cycles[0] + (i+1)*segment_size + rxread_offset\n", + " start2y = cycles[0] + (i+1)*segment_size + ryread_offset\n", + " start2z = cycles[0] + (i+1)*segment_size + rzread_offset\n", + "\n", + " stop2x = cycles[0] + (i+1)*segment_size + rxread_offset + rupdate_cycles\n", + " stop2y = cycles[0] + (i+1)*segment_size + ryread_offset + rupdate_cycles\n", + " stop2z = cycles[0] + (i+1)*segment_size + rzread_offset + rupdate_cycles\n", + "\n", + "\n", + " for trace in traces[:t]:\n", + " corrx += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2x:stop2x])[0][1]\n", + " corry += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2y:stop2y])[0][1]\n", + " corrz += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2z:stop2z])[0][1]\n", + "\n", + " corrsxonly.append(corrx/t)\n", + " corrsyonly.append(corry/t)\n", + " corrszonly.append(corrz/t)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "C = figure(width=2000)\n", + "\n", + "xrange = list(range(len(corrsyonly)))\n", + "\n", + "#C.line(xrange, corrsxonly, line_color=\"orange\")\n", + "C.line(xrange, corrsyonly, line_color=\"purple\", line_width=3)\n", + "#C.line(xrange, corrszonly, line_color=\"brown\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "show(C)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We appear to have good results with 30 traces! The correlation with the y-coordinate read is very good; the x-coordinate read has a strange peak at the start that we don't want to deal with, as well as a lower SNR, and the z-coordinate read appears to have no correlation whatsoever.\n", + "\n", + "For the attack, we'll use correlation from the y-coordinate read only.\n", + "\n", + "This image below illustrates illustrates how to define the thresholds that we'll use to identify ones and zeros. When $k$ starts with a leading zero, the correlations scores behave differently until the first one is encountered, so we use two thresholds to decide whether each bit is a one or a zero.\n", + "\n", + "![Thresholds](img/ECC_threshold.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5.2 The Attack\n", + "\n", + "Finally: here is the attack in full. We start by repeating the trace acquisition, this time with a non-trivial value for k." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "k = 0x70a12c2db16845ed56ff68cfc21a472b3f04d7d6851bf6349f2d7d5b3452b38a\n", + "#k = random_k()\n", + "traces = get_traces(40, k, 'part1_5', full=False, samples_per_segment=530)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll repeat some previous cells that are needed here, so that you don't have to run through the whole notebook in order for this to work:" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "from tqdm.notebook import trange\n", + "import numpy as np\n", + "import time\n", + "cycles = np.load('data/ecc_cycles.npy')\n", + "rupdate_offset = 4195\n", + "rupdate_cycles = 8\n", + "ryread_offset = 473\n", + "\n", + "if TARGET_PLATFORM == 'CW312T_A35':\n", + " rupdate_cycles += 8\n", + " rupdate_offset -= 4\n", + " ryread_offset -= 4" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define the decision thresholds:\n", + "\n", + "If these parameters don't work for you, go back up to the last plot before this section and pick appropriate thresholds based on what you see on your own plot." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "if PLATFORM == 'CWPRO':\n", + " initial_threshold = -0.02\n", + " regular_threshold = 0\n", + "elif PLATFORM == 'CWLITE':\n", + " initial_threshold = -0.38\n", + " regular_threshold = -0.23\n", + "elif PLATFORM == 'CWHUSKY':\n", + " if TARGET_PLATFORM == 'CW312T_A35':\n", + " initial_threshold = -0.15\n", + " regular_threshold = -0.2\n", + " else:\n", + " initial_threshold = -0.35\n", + " regular_threshold = -0.40" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compute the correlations:" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "corrs = []\n", + "attack_traces = len(traces)\n", + "#attack_traces = 5\n", + "\n", + "for i in range (0, len(cycles)-1):\n", + " corr = 0\n", + " if len(traces[0].wave) == 1130000:\n", + " start1 = cycles[i] + rupdate_offset\n", + " stop1 = cycles[i] + rupdate_offset + rupdate_cycles\n", + " start2y = cycles[i+1] + ryread_offset\n", + " stop2y = cycles[i+1] + ryread_offset + rupdate_cycles\n", + " else:\n", + " start1 = cycles[0] + (i+1)*segment_size - (SEGMENT_CYCLES - rupdate_offset)\n", + " stop1 = cycles[0] + (i+1)*segment_size - (SEGMENT_CYCLES - rupdate_offset) + rupdate_cycles\n", + " start2y = cycles[0] + (i+1)*segment_size + ryread_offset\n", + " stop2y = cycles[0] + (i+1)*segment_size + ryread_offset + rupdate_cycles\n", + " for trace in traces[:attack_traces]:\n", + " corr += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2y:stop2y])[0][1]\n", + " \n", + " corr /= attack_traces # normalize so that decisions thresholds remain constant if we change no_traces\n", + " corrs.append(corr)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Guess k one bit at a time:" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "threshold = initial_threshold\n", + "guess = ''\n", + "for kbit in range(255):\n", + " raise NotImplementedError(\"guess 0 or 1 bit\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since our decision metric for bit $i$ calculates correlation with events in processing bit $i+1$, we cannot use it to guess the last bit of $k$.\n", + "\n", + "But since there are only two possibilities, we simply check which of the two is correct:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "guesses = [int(guess + '0', 2), int(guess + '1', 2)]\n", + "\n", + "if k in guesses:\n", + " print('Guessed right!')\n", + "else:\n", + " print('Attack failed.')\n", + " print('Guesses: %s' % hex(guesses[0]))\n", + " print(' %s' % hex(guesses[1]))\n", + " print('Correct: %s' % hex(k))\n", + " wrong_bits = []\n", + " for kbit in range(255):\n", + " if int(guess[kbit]) != ((k >> (255-kbit)) & 1):\n", + " wrong_bits.append(255-kbit)\n", + " print('%d wrong bits: %s' % (len(wrong_bits), wrong_bits))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Go back and reduce the number of traces used to see how many are required (`attack_traces` variable in the correlation calculation cell).\n", + "\n", + "You may see the attack succeed with as few as 8 traces. With the recorded traces, 40 traces are required. What's perhaps surprising is that with just a single trace, a large percentage of the bits are guessed correctly (even with the recorded traces). There are attacks which allow the full k to be recovered from partial knowledge of k (see for example https://link.springer.com/article/10.1023/A:1025436905711), but that's a lot more math-heavy and beyond the scope of this tutorial.\n", + "\n", + "Go ahead and repeat the attack for different values of k. Note that k must be nonzero and must be less than the curve order; here is a function to generate a random valid k:" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [], + "source": [ + "def random_k(bits=256, tries=100):\n", + " import random\n", + " for i in range(tries):\n", + " k = random.getranplot_dbits(bits)\n", + " if k < target.curve.order and k > 0:\n", + " return k\n", + " raise ValueError(\"Failed to generate a valid random k after %d tries!\" % self.tries)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Combining with a lattice attack" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Step 0: Set 3 256-bit secrets\n", + "k1 = \n", + "k2 = \n", + "k3 = \n", + "\n", + "# Step 1: guess 128 bits\n", + "threshold = initial_threshold\n", + "guess = ''\n", + "for kbit in range(128):\n", + " raise NotImplementedError(\"guess 0 or 1 bit\")\n", + "\n", + "# Step 2: " + ] + } + ], + "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.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/jupyter/lab/4_ECC_Lab/ECC_lab_setup.ipynb b/jupyter/lab/4_ECC_Lab/ECC_lab_setup.ipynb new file mode 100644 index 00000000..8bc36442 --- /dev/null +++ b/jupyter/lab/4_ECC_Lab/ECC_lab_setup.ipynb @@ -0,0 +1,943 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Common setup and functions used by the CW305 ECC demos." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "CURRENT_BITFILE = 'original'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if TRACES != 'SIMULATED':\n", + " # Basic initialization:\n", + " scope.adc.offset = 0\n", + " scope.adc.basic_mode = \"rising_edge\"\n", + " scope.trigger.triggers = \"tio4\"\n", + " scope.io.tio1 = \"serial_rx\"\n", + " scope.io.tio2 = \"serial_tx\"\n", + " scope.io.hs2 = \"disabled\"\n", + "\n", + " if PLATFORM == 'CWPRO':\n", + " scope.adc.stream_mode = True\n", + " scope.adc.samples = 1200000\n", + " target.pll.pll_outfreq_set(10E6, 1)\n", + " target._clksleeptime = 150\n", + " scope.gain.db = 30\n", + " elif PLATFORM == 'CWHUSKY':\n", + " scope.adc.stream_mode = True\n", + " scope.adc.samples = 1200000\n", + " target.pll.pll_outfreq_set(15E6, 1)\n", + " target._clksleeptime = 100\n", + " scope.gain.db = 20\n", + " elif PLATFORM == 'CWLITE':\n", + " scope.adc.samples = 24400\n", + " target.pll.pll_outfreq_set(50E6, 1)\n", + " target._clksleeptime = 30\n", + " scope.gain.db = 30\n", + "\n", + "\n", + " if TARGET_PLATFORM == 'CW312T_A35':\n", + " scope.clock.clkgen_freq = 7.37e6\n", + " scope.io.hs2 = 'clkgen'\n", + " scope.gain.db = 31\n", + " if PLATFORM == 'CWHUSKY':\n", + " scope.clock.clkgen_src = 'system'\n", + " scope.clock.adc_mul = 1\n", + " scope.clock.reset_dcms()\n", + " else:\n", + " scope.clock.adc_src = \"clkgen_x1\"\n", + " import time\n", + " time.sleep(0.1)\n", + " target._ss2_test_echo()\n", + "\n", + " else:\n", + " if PLATFORM == 'CWHUSKY':\n", + " scope.clock.clkgen_freq = 15e6\n", + " scope.clock.clkgen_src = 'extclk'\n", + " scope.clock.adc_mul = 1\n", + " else:\n", + " scope.clock.adc_src = \"extclk_x1\"\n", + "\n", + " if PLATFORM == 'CWHUSKY':\n", + " scope.adc.offset = 3\n", + " else:\n", + " scope.adc.offset = 0\n", + "\n", + " if 'CW305' in TARGET_PLATFORM:\n", + " target.vccint_set(1.0)\n", + " # we only need PLL1:\n", + " target.pll.pll_enable_set(True)\n", + " target.pll.pll_outenable_set(False, 0)\n", + " target.pll.pll_outenable_set(True, 1)\n", + " target.pll.pll_outenable_set(False, 2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "cycles = np.load('data/ecc_cycles.npy')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def set_adc(samples):\n", + " if PLATFORM == 'CWPRO':\n", + " scope.adc.stream_mode = True\n", + " scope.adc.samples = samples\n", + " scope.adc.offset = 0\n", + " elif PLATFORM == 'CWHUSKY':\n", + " scope.adc.stream_mode = True\n", + " scope.adc.samples = samples\n", + " scope.adc.offset = 3\n", + " scope.adc.segments = 1\n", + " elif PLATFORM == 'CWLITE':\n", + " scope.adc.samples = 24400\n", + " scope.adc.offset = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def random_k(bits=256, tries=100):\n", + " import random\n", + " if TRACES == 'SIMULATED':\n", + " return None\n", + " for i in range(tries):\n", + " k = random.getrandbits(bits)\n", + " if k < target.curve.order and k > 0:\n", + " return k\n", + " raise ValueError(\"Failed to generate a valid random k after %d tries!\" % self.tries)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from chipwhisperer.common.traces import Trace\n", + "from tqdm.notebook import trange\n", + "import numpy as np\n", + "import time\n", + "import math \n", + "\n", + "SEGMENTS = 257 # +1 so we can grab the trailing POIs\n", + "SEGMENT_CYCLES = 4204\n", + "\n", + "def get_traces(N=50, k=0, step='part1_1', randomize_k=False, full=False, samples_per_segment=256, as_int=True):\n", + " samples = 1130000\n", + " traces = []\n", + " \n", + " if TRACES == 'SIMULATED':\n", + " # eh maybe not optimal but it works\n", + " raws = np.load('data/%s.npz' % step, allow_pickle=True)\n", + " for t in raws['arr_0']:\n", + " traces.append(Trace(t[0], t[1], t[2], None))\n", + " raws.close()\n", + " print('Pre-recorded traces loaded ✅.')\n", + "\n", + " else:\n", + " attempt4 = get_bitfile_version() == 'attempt4'\n", + " if PLATFORM == 'CWHUSKY':\n", + " scope.adc.bits_per_sample = 8 # for smaller recorded traces; doesn't appear to impact attack success rates\n", + " else:\n", + " full = True # force full capture on non-Husky because not supported\n", + " if PLATFORM == 'CWLITE':\n", + " set_adc(samples)\n", + " else:\n", + " if not full:\n", + " scope.adc.segments = SEGMENTS\n", + " scope.adc.segment_cycles = SEGMENT_CYCLES\n", + " scope.adc.segment_cycle_counter_en = True\n", + " scope.adc.samples = samples_per_segment\n", + " scope.adc.stream_mode = True\n", + " scope.adc.offset = 3\n", + " else:\n", + " set_adc(samples)\n", + " if PLATFORM == 'CWHUSKY':\n", + " scope.adc.segments = 1\n", + " scope.adc.segment_cycles = 0\n", + " scope.adc.segment_cycle_counter_en = False\n", + "\n", + " for i in trange(N, desc='Capturing traces'):\n", + " P = target.new_point() # every trace uses a different point\n", + " if randomize_k:\n", + " k = random_k()\n", + " assert k != 0\n", + " if attempt4:\n", + " kb = 0x10000000000000000000000000000000000000000000000000000000000000000 - k\n", + " target.fpga_write(target.REG_KB, list(int.to_bytes(kb, length=32, byteorder='little')))\n", + "\n", + " if PLATFORM == 'CWPRO' or PLATFORM == 'CWHUSKY':\n", + " ret = target.capture_trace(scope, Px=P.x, Py=P.y, k=k, check=True, as_int=as_int)\n", + " if not ret:\n", + " print(\"Failed capture\")\n", + " continue\n", + " traces.append(ret)\n", + "\n", + " elif PLATFORM == 'CWLITE':\n", + " # assumes 'full'\n", + " segments = math.ceil(target.pmul_cycles / scope.adc.samples)\n", + " for j in range(segments):\n", + " ret = target.capture_trace(scope, Px=P.x, Py=P.y, k=k)\n", + " if not ret:\n", + " print(\"Failed capture\")\n", + " continue\n", + " wave = np.append(wave, ret.wave)\n", + " scope.adc.offset += scope.adc.samples\n", + " traces.append(Trace(wave[1:], ret.textin, ret.textout, None))\n", + "\n", + " if TRACES == 'COLLECT':\n", + " np.savez_compressed('data/%s.npz' % step, np.asarray(traces, dtype=object))\n", + " \n", + " return traces\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_sums(traces, poi):\n", + " sums = []\n", + " # in case samples were recorded as ints, translate result to make it as though they were floats\n", + " if 'int' in str(type(traces[0].wave[0])):\n", + " shift = True\n", + " if PLATFORM != 'CWHUSKY':\n", + " center = 2**9\n", + " div = 2**10\n", + " # infer whether trace was collected with 8 or 12 bits per sample:\n", + " elif max(abs(traces[0].wave)) > 255:\n", + " center = 2**11\n", + " div = 2**12\n", + " else:\n", + " center = 2**7\n", + " div = 2**8\n", + " else:\n", + " shift = False\n", + "\n", + " if len(traces[0].wave) == 1130000:\n", + " # full captures\n", + " for c in cycles:\n", + " sum = 0\n", + " for trace in traces:\n", + " for i in poi:\n", + " power = trace.wave[c+abs(i)]\n", + " if shift:\n", + " power = (power-center)/div\n", + " if i < 0:\n", + " sum -= power\n", + " else:\n", + " sum += power\n", + " sums.append(sum/len(traces))\n", + " else:\n", + " # segmented captures (used for pre-captured traces, to save space)\n", + " segment_size = len(traces[0].wave) // SEGMENTS\n", + " for c in range(256):\n", + " sum = 0\n", + " for trace in traces:\n", + " for i in poi:\n", + " # complicated mapping to deal with the segmented traces\n", + " if abs(i) > segment_size:\n", + " absi = segment_size - (SEGMENT_CYCLES - abs(i))\n", + " else:\n", + " absi = abs(i)\n", + " if i < 0:\n", + " i = -absi\n", + " else:\n", + " i = absi\n", + " index = c*segment_size+abs(i) + cycles[0]\n", + " power = trace.wave[index]\n", + " if shift:\n", + " power = (power-center)/div\n", + " if i < 0:\n", + " sum -= power\n", + " else:\n", + " sum += power\n", + " sums.append(sum/len(traces))\n", + "\n", + " return sums" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_corrected_sums(traces, poi):\n", + " sums = []\n", + " # in case samples were recorded as ints, translate result to make it as though they were floats\n", + " if 'int' in str(type(traces[0].wave[0])):\n", + " shift = True\n", + " if PLATFORM != 'CWHUSKY':\n", + " center = 2**9\n", + " div = 2**10\n", + " # infer whether trace was collected with 8 or 12 bits per sample:\n", + " elif max(abs(traces[0].wave)) > 255:\n", + " center = 2**11\n", + " div = 2**12\n", + " else:\n", + " center = 2**7\n", + " div = 2**8\n", + " else:\n", + " shift = False\n", + " \n", + " if len(traces[0].wave) == 1130000:\n", + " # full captures\n", + " for c in range(len(cycles)-1):\n", + " sum = 0\n", + " for trace in traces:\n", + " for p in poi:\n", + " # shortcut: use the ~halfway point to determine whether the leakage influences the current bit or not\n", + " if abs(p) > 2000:\n", + " power = trace.wave[cycles[c]+abs(p)]\n", + " else:\n", + " power = trace.wave[cycles[c+1]+abs(p)]\n", + " if shift:\n", + " power = (power-center)/div\n", + " if p < 0:\n", + " sum -= power\n", + " else:\n", + " sum += power\n", + " sums.append(sum/len(traces))\n", + " else:\n", + " # segmented captures (used for pre-captured traces, to save space)\n", + " segment_size = len(traces[0].wave) // SEGMENTS\n", + " for c in range(len(cycles)-1):\n", + " sum = 0\n", + " for trace in traces:\n", + " for p in poi:\n", + " # complicated mapping to deal with the segmented traces; also we (mis-)use segment_size to determine whether the leakage influences the current bit or not\n", + " if abs(p) > segment_size:\n", + " absp = segment_size - (SEGMENT_CYCLES - abs(p))\n", + " d = c\n", + " else:\n", + " absp = abs(p)\n", + " d = c + 1\n", + " if p < 0:\n", + " p = -absp\n", + " else:\n", + " p = absp\n", + " index = d*segment_size+abs(p) + cycles[0]\n", + " power = trace.wave[index]\n", + " if shift:\n", + " power = (power-center)/div\n", + " if p < 0:\n", + " sum -= power\n", + " else:\n", + " sum += power\n", + " sums.append(sum/len(traces))\n", + "\n", + " return sums" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_corrs(traces):\n", + " corrsxonly = []\n", + " corrsyonly = []\n", + " corrszonly = []\n", + " corrsall = []\n", + "\n", + " segment_size = len(traces[0].wave) // SEGMENTS\n", + "\n", + " for i in range (0, len(cycles)-1):\n", + " corrx = 0\n", + " corry = 0\n", + " corrz = 0\n", + "\n", + " if len(traces[0].wave) == 1130000:\n", + " start1 = cycles[i] + rupdate_offset\n", + " stop1 = cycles[i] + rupdate_offset + rupdate_cycles\n", + "\n", + " start2x = cycles[i+1] + rxread_offset\n", + " start2y = cycles[i+1] + ryread_offset\n", + " start2z = cycles[i+1] + rzread_offset\n", + "\n", + " stop2x = cycles[i+1] + rxread_offset + rupdate_cycles\n", + " stop2y = cycles[i+1] + ryread_offset + rupdate_cycles\n", + " stop2z = cycles[i+1] + rzread_offset + rupdate_cycles\n", + "\n", + " else:\n", + " start1 = cycles[0] + (i+1)*segment_size - (SEGMENT_CYCLES - rupdate_offset)\n", + " stop1 = cycles[0] + (i+1)*segment_size - (SEGMENT_CYCLES - rupdate_offset) + rupdate_cycles\n", + "\n", + " start2x = cycles[0] + (i+1)*segment_size + rxread_offset\n", + " start2y = cycles[0] + (i+1)*segment_size + ryread_offset\n", + " start2z = cycles[0] + (i+1)*segment_size + rzread_offset\n", + "\n", + " stop2x = cycles[0] + (i+1)*segment_size + rxread_offset + rupdate_cycles\n", + " stop2y = cycles[0] + (i+1)*segment_size + ryread_offset + rupdate_cycles\n", + " stop2z = cycles[0] + (i+1)*segment_size + rzread_offset + rupdate_cycles\n", + "\n", + "\n", + " for trace in traces:\n", + " corrx += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2x:stop2x])[0][1]\n", + " corry += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2y:stop2y])[0][1]\n", + " corrz += np.corrcoef(trace.wave[start1:stop1], trace.wave[start2z:stop2z])[0][1]\n", + " \n", + " #corrsall.append((corrx+corry+corrz)/len(traces))\n", + " # consider only Y component for attack; uncomment above to study effect of other X/Z components:\n", + " corrsall.append(corry/len(traces))\n", + " #corrsall.append((corry+corrx)/len(traces))\n", + " return corrsall\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def poi_guess(metric, thresholds):\n", + " poi_init_threshold, poi_reg_threshold = thresholds\n", + " guess = ''\n", + " if get_bitfile_version() == 'attempt4':\n", + " start = 1\n", + " initial = False\n", + " else:\n", + " start = 0\n", + " initial = True\n", + " for kbit in range(start,255):\n", + " if initial:\n", + " if metric[kbit] < poi_init_threshold:\n", + " guess += '0'\n", + " else:\n", + " guess += '1'\n", + " initial = False\n", + " else:\n", + " if metric[kbit] < poi_reg_threshold:\n", + " guess += '0'\n", + " else:\n", + " guess += '1'\n", + " \n", + " return guess" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def corr_guess(metric):\n", + " initial = True\n", + " guess = ''\n", + " for kbit in range(0,255):\n", + " if initial:\n", + " if metric[kbit] > corr_init_threshold:\n", + " guess += '0'\n", + " else:\n", + " guess += '1'\n", + " initial = False\n", + " else:\n", + " if metric[kbit] > corr_reg_threshold:\n", + " guess += '0'\n", + " else:\n", + " guess += '1'\n", + "\n", + " return guess" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def check_guess(guess, k, verbose=False):\n", + " guesses = []\n", + " if get_bitfile_version() == 'attempt4':\n", + " top = 254\n", + " for a in (['0', '1']):\n", + " for b in (['0', '1']):\n", + " guesses.append(int(a + guess + b, 2))\n", + " else:\n", + " top = 255\n", + " guesses = [int(guess + '0', 2), int(guess + '1', 2)]\n", + "\n", + " if k in guesses:\n", + " return ('Guessed right!', 0, 0)\n", + " else:\n", + " wrong_bits = []\n", + " for kbit in range(top):\n", + " if int(guess[kbit]) != ((k >> (top-kbit)) & 1):\n", + " wrong_bits.append(top-kbit)\n", + " if verbose:\n", + " print('Attack failed.')\n", + " print('Guesses: %s' % hex(guesses[0]))\n", + " for guess in guesses[1:]:\n", + " print(' %s' % hex(guess))\n", + " print('Correct: %s' % hex(k))\n", + " print('Wrong bits: %s' % wrong_bits)\n", + " return ('Failed: %3d wrong bits' % len(wrong_bits), len(wrong_bits), wrong_bits)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def poi_guess_threshold(metric, distance_threshold, thresholds):\n", + " poi_init_threshold, poi_reg_threshold = thresholds\n", + " guess = ''\n", + " guessed_bits = []\n", + " distances = []\n", + " \n", + " if get_bitfile_version() == 'attempt4':\n", + " start = 1\n", + " initial = False\n", + " else:\n", + " start = 0\n", + " initial = True\n", + " \n", + " if distance_threshold <= 0:\n", + " raise ValueEror(\"Threshold must be greater than 0\")\n", + " \n", + " #1. Calculate distances from decision thresholds:\n", + " for kbit in range(0,255):\n", + " if initial:\n", + " distances.append(abs(metric[kbit]- poi_init_threshold))\n", + " else:\n", + " distances.append(abs(metric[kbit]- poi_reg_threshold))\n", + "\n", + " #2. Calculate the mininum distance from decision threshold for which we'll enter a guess:\n", + " avg = np.average(distances)\n", + " top = max(distances)\n", + " base = top-avg\n", + " distance_threshold = distance_threshold * base\n", + "\n", + " #3. \n", + " if get_bitfile_version() == 'attempt4':\n", + " initial = False\n", + " else:\n", + " initial = True\n", + " for kbit in range(start,255):\n", + " if initial:\n", + " if abs(metric[kbit] - poi_init_threshold) > distance_threshold:\n", + " guessed_bits.append(kbit)\n", + " else:\n", + " pass\n", + " if metric[kbit] > poi_init_threshold:\n", + " guess += '0'\n", + " else:\n", + " guess += '1'\n", + " initial = False\n", + " else:\n", + " if abs(metric[kbit] - poi_reg_threshold) > distance_threshold:\n", + " guessed_bits.append(kbit)\n", + " else:\n", + " pass\n", + " if metric[kbit] < poi_reg_threshold:\n", + " guess += '0'\n", + " else:\n", + " guess += '1'\n", + " \n", + " return guess, guessed_bits" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_trace_segments(N=50, poi=[-6, 7, 4202, -4203], randomize_k=False, k=0, husky_timed_segments=True, step='partXXX', as_int=True):\n", + " trace_segments = []\n", + " if TRACES == 'SIMULATED':\n", + " # eh maybe not optimal but it works\n", + " raws = np.load('data/%s.npz' % step, allow_pickle=True)\n", + " for t in raws['arr_0']:\n", + " trace_segments.append(Trace(t[0], t[1], t[2], None))\n", + " raws.close()\n", + " print('Pre-recorded traces loaded.')\n", + " return trace_segments\n", + "\n", + " attempt4 = get_bitfile_version() == 'attempt4'\n", + " if PLATFORM == 'CWPRO' or (PLATFORM == 'CWHUSKY' and not husky_timed_segments): # note this approach can be used for Husky as well, but the segmented capture is faster!:\n", + " if PLATFORM == 'CWHUSKY':\n", + " scope.adc.segments = 1\n", + " scope.adc.segment_cycles = 0\n", + " scope.adc.offset = 3\n", + " else:\n", + " scope.adc.offset = 0\n", + " scope.adc.stream_mode = True\n", + " scope.adc.samples = 1120000\n", + " \n", + " for i in trange(N, desc='Capturing traces'):\n", + " P = target.new_point() # every trace uses a different point\n", + " \n", + " if randomize_k:\n", + " k = random_k()\n", + " assert k != 0\n", + " if attempt4:\n", + " kb = 0x10000000000000000000000000000000000000000000000000000000000000000 - k\n", + " target.fpga_write(target.REG_KB, list(int.to_bytes(kb, length=32, byteorder='little')))\n", + "\n", + " ret = target.capture_trace(scope, Px=P.x, Py=P.y, k=k, as_int=as_int)\n", + " if not ret:\n", + " print(\"Failed capture\")\n", + " continue\n", + " trace_segment = []\n", + " for c in cycles:\n", + " for p in poi:\n", + " trace_segment.append(ret.wave[c+abs(p)])\n", + " trace_segments.append(Trace(trace_segment, ret.textin, ret.textout, None))\n", + " \n", + " elif PLATFORM == 'CWHUSKY':\n", + " scope.adc.stream_mode = False\n", + " scope.adc.segments = 256\n", + " scope.adc.segment_cycles = 4204\n", + " scope.adc.segment_cycle_counter_en = True\n", + " scope.adc.samples = 11\n", + " scope.adc.offset = int(cycles[0] + 4201 + 3)\n", + " if poi == [-6, 7, 4202, -4203]:\n", + " indices = [1, 2, 9, 10]\n", + " elif poi == [-6, 7, 4201, -4202]:\n", + " indices = [0, 1, 9, 10]\n", + " elif poi == [-6, 7]:\n", + " indices = [9, 10]\n", + " else:\n", + " raise ValueError(\"Sorry, Husky timed segments only work for a specific set of markers; either set husky_timed_segments=False, or write your own segmented capture function\" % poi)\n", + "\n", + " for i in trange(N, desc='Capturing traces'):\n", + " P = target.new_point() # every trace uses a different point\n", + " \n", + " if randomize_k:\n", + " k = random_k()\n", + " assert k != 0\n", + " if attempt4:\n", + " kb = 0x10000000000000000000000000000000000000000000000000000000000000000 - k\n", + " target.fpga_write(target.REG_KB, list(int.to_bytes(kb, length=32, byteorder='little')))\n", + " \n", + " ret = target.capture_trace(scope, Px=P.x, Py=P.y, k=k, as_int=as_int)\n", + " if not ret:\n", + " print(\"Failed capture\")\n", + " continue\n", + " trace_segment = [0, 0] # first two samples are missed but that's inconsequential since they provide no useful side channel leakage\n", + " for j,c in enumerate(cycles):\n", + " base = scope.adc.samples*j\n", + " for i,p in enumerate(poi):\n", + " trace_segment.append(ret.wave[base+indices[i]])\n", + " trace_segments.append(Trace(trace_segment, ret.textin, ret.textout, None))\n", + "\n", + " elif PLATFORM == 'CWLITE':\n", + " raise ValueError('Not implemented for CW-lite')\n", + "\n", + " if TRACES == 'COLLECT':\n", + " np.savez_compressed('data/%s.npz' % step, np.asarray(trace_segments, dtype=object))\n", + "\n", + " return trace_segments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def gget_segment_sums(trace_segments, poi):\n", + " # Note: crucial that poi be identical to that used for get_trace_segments! (including order of elements)\n", + " sums = []\n", + " npois = len(poi)\n", + " for c in range(len(cycles)-1):\n", + " sum = 0\n", + " for segment in trace_segments:\n", + " for i,p in enumerate(poi):\n", + " # shortcut: use the ~halfway point to determine whether the leakage influences the current bit or not\n", + " if abs(p) > 2000:\n", + " base = c*npois\n", + " else:\n", + " base = (c+1)*npois\n", + " if p > 0:\n", + " sum += segment.wave[base+i]\n", + " else:\n", + " sum -= segment.wave[base+i]\n", + " sums.append(sum/len(trace_segments))\n", + " return sums" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_segment_sums(trace_segments, poi):\n", + " # Note: crucial that poi be identical to that used for get_trace_segments! (including order of elements)\n", + " sums = []\n", + " # in case samples were recorded as ints, translate result to make it as though they were floats\n", + " if 'int' in str(type(traces[0].wave[0])):\n", + " shift = True\n", + " if PLATFORM != 'CWHUSKY':\n", + " center = 2**9\n", + " div = 2**10\n", + " # infer whether trace was collected with 8 or 12 bits per sample:\n", + " elif max(abs(traces[0].wave)) > 255:\n", + " center = 2**11\n", + " div = 2**12\n", + " else:\n", + " center = 2**7\n", + " div = 2**8\n", + " else:\n", + " shift = False\n", + " \n", + " npois = len(poi)\n", + " for c in range(len(cycles)-1):\n", + " sum = 0\n", + " for segment in trace_segments:\n", + " for i,p in enumerate(poi):\n", + " # shortcut: use the ~halfway point to determine whether the leakage influences the current bit or not\n", + " if abs(p) > 2000:\n", + " base = c*npois\n", + " else:\n", + " base = (c+1)*npois\n", + " power = segment.wave[base+i]\n", + " if shift:\n", + " power = (power-center)/div\n", + " if p > 0:\n", + " sum += power\n", + " else:\n", + " sum -= power\n", + " sums.append(sum/len(trace_segments))\n", + " return sums" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def change_bitfile(VERSION):\n", + " if TRACES == 'SIMULATED':\n", + " global CURRENT_BITFILE\n", + " CURRENT_BITFILE = VERSION\n", + " else:\n", + " global target\n", + " if VERSION == 'original':\n", + " rev = 0\n", + " elif VERSION == 'attempt1':\n", + " rev = 1\n", + " elif VERSION == 'attempt2':\n", + " rev = 2\n", + " elif VERSION == 'attempt3':\n", + " rev = 3\n", + " elif VERSION == 'attempt4':\n", + " rev = 4\n", + " else:\n", + " raise ValueError(\"Unsupported version %s\" % VERSION)\n", + " if target._fpga_id in ['cw312t_a35', '35t'] and rev == 3:\n", + " raise ValueError(\"attempt3 is not supported on this platform (the FPGA is not large enough for it)\")\n", + " target.dis()\n", + " target = cw.target(scope, cw.targets.CW305_ECC, force=True, fpga_id=target._fpga_id, platform=target.platform, version=rev)\n", + " assert get_bitfile_version() == VERSION\n", + "\n", + " if PLATFORM == 'CWHUSKY':\n", + " # on Husky, reloading the FPGA will cause Husky's external clock frequency monitor to flag an error:\n", + " import time\n", + " time.sleep(0.5)\n", + " scope.errors.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_bitfile_version():\n", + " if TRACES == 'SIMULATED':\n", + " return CURRENT_BITFILE\n", + " else:\n", + " rev = target.fpga_read(target.REG_CRYPT_REV, 1)[0]\n", + " if rev == 0:\n", + " return \"original\"\n", + " elif rev == 1:\n", + " return \"attempt1\"\n", + " elif rev == 2:\n", + " return \"attempt2\"\n", + " elif rev == 3:\n", + " return \"attempt3\"\n", + " elif rev == 4:\n", + " return \"attempt4\"\n", + " else:\n", + " raise ValueError(\"Warning: unrecognized version % d.\" % rev)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def consecutives(trace_segments, poi, distance_threshold, thresholds):\n", + "\n", + " wrong_bits = []\n", + " solid_guessed_bits = []\n", + " total_wrong_bits = 0\n", + " total_solid_guessed_bits = 0\n", + " total_right_solid_guesses = 0\n", + " total_wrong_solid_guesses = 0\n", + " correct_solid_guesses = []\n", + " all_wrong_bits = []\n", + "\n", + " print('Computing averages...')\n", + " for trace_segment in trace_segments:\n", + " sums = get_segment_sums([trace_segment], poi)\n", + "\n", + " guess, tguessed_bits = poi_guess_threshold(sums, distance_threshold, thresholds)\n", + " (status, num_wrong_bits, twrong_bits) = check_guess(guess, trace_segment.textin['k'])\n", + "\n", + " total_wrong_bits += num_wrong_bits\n", + " all_wrong_bits.append(num_wrong_bits)\n", + " total_solid_guessed_bits += len(tguessed_bits)\n", + "\n", + " wrong_solid_guesses = len(set(twrong_bits) & set(tguessed_bits))\n", + " right_solid_guesses = len(tguessed_bits) - wrong_solid_guesses\n", + "\n", + " total_wrong_solid_guesses += wrong_solid_guesses\n", + " total_right_solid_guesses += right_solid_guesses\n", + "\n", + " wrong_bits.append(twrong_bits)\n", + " solid_guessed_bits.append(tguessed_bits)\n", + "\n", + " correct_solid_guesses.append(list(set(tguessed_bits) - set(twrong_bits)))\n", + "\n", + " print('All results are per-trace averages:')\n", + " print('Average number of wrong bits (all guesses): %5.1f' % (total_wrong_bits/len(trace_segments)))\n", + " print('Average number of solid guessed bits: %5.1f' % (total_solid_guessed_bits/len(trace_segments)))\n", + " print('Average number of correct solid guessed bits: %5.1f' % (total_right_solid_guesses/len(trace_segments)))\n", + " print('Average number of incorrect solid guessed bits: %5.1f' % (total_wrong_solid_guesses/len(trace_segments)))\n", + "\n", + " print('Computing number of good traces...')\n", + " # stats when taking only what we think are good guesses\n", + " min_c_len = 3 # we only care about at least this many correct consecutive guesses\n", + " total_good_consecutives = 0\n", + " total_bad_consecutives = 0\n", + " all_run_counts = np.zeros(255, np.int16)\n", + " good_traces = 0\n", + " bad_good_traces = 0\n", + " good_trace_ids = []\n", + " for t in range(len(trace_segments)):\n", + " run_counts = np.zeros(255, np.int16)\n", + " good_trace = False\n", + " bad_good_trace = False\n", + "\n", + " # now we look for consecutive guesses, among the list of good *and* bad guesses - then we'll flag whether any bad guesses snuck in there\n", + " guesses = np.sort(solid_guessed_bits[t])\n", + " consecutives = np.split(guesses, np.where(np.diff(guesses) != 1)[0]+1)\n", + " good_consecutives = 0\n", + " bad_consecutives = 0\n", + " for i,c in enumerate(consecutives):\n", + " if len(c) >= min_c_len:\n", + " if any(x in consecutives[i] for x in wrong_bits[t]):\n", + " bad_consecutives += 1\n", + " bad_good_trace = True\n", + " else:\n", + " good_consecutives += 1\n", + " run_counts[len(c)] += 1\n", + " if len(c) >= 5:\n", + " good_trace = True\n", + " total_good_consecutives += good_consecutives\n", + " total_bad_consecutives += bad_consecutives\n", + " all_run_counts += run_counts\n", + " if run_counts[3] >= 3 or run_counts[4] >= 2:\n", + " good_trace = True\n", + " if good_trace:\n", + " good_traces += 1\n", + " good_trace_ids.append(t)\n", + " if bad_good_trace:\n", + " bad_good_traces += 1\n", + "\n", + " print(\"Total good consecutives: %3d (%5.2f per traces)\" % (total_good_consecutives, float(total_good_consecutives/len(trace_segments))))\n", + " print(\"Total bad consecutives: %3d (%5.2f per traces)\" % (total_bad_consecutives, float(total_bad_consecutives/len(trace_segments))))\n", + " print('Number of good traces: %d' % good_traces)\n", + " print('Number of BAD good traces: %d' % bad_good_traces)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def find0to1trans(data):\n", + " pattern = [0,1]\n", + " return [i for i in range(0,len(data)) if list(data[i:i+len(pattern)])==pattern]\n", + "\n", + "def check_adc_clock_phase():\n", + " #scope.LA.enabled = True\n", + " #scope.LA.clk_source = 'target'\n", + " #scope.LA.oversampling_factor = 20\n", + " #scope.LA.capture_group = 'CW 20-pin'\n", + " #scope.LA.capture_depth = 50\n", + "\n", + " scope.LA.arm()\n", + " scope.LA.trigger_now()\n", + "\n", + " raw = scope.LA.read_capture_data()\n", + " adcclock = scope.LA.extract(raw, 8)\n", + " hs1clock = scope.LA.extract(raw, 4)\n", + "\n", + " hs1_edge = find0to1trans(hs1clock)[0]\n", + " adc_hs1_delta = find0to1trans(adcclock[hs1_edge:])[0]\n", + " assert adc_hs1_delta == 13, 'Got unexpected delta: %d' % adc_hs1_delta" + ] + } + ], + "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_Tasks/README.md b/jupyter/lab/README.md similarity index 100% rename from jupyter/Lab_Tasks/README.md rename to jupyter/lab/README.md diff --git a/jupyter/Lab_Tasks/Tutorial/Tutorial_1.ipynb b/jupyter/lab/Tutorial/Tutorial_1.ipynb similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/Tutorial_1.ipynb rename to jupyter/lab/Tutorial/Tutorial_1.ipynb diff --git a/jupyter/Lab_Tasks/Tutorial/Tutorial_2.ipynb b/jupyter/lab/Tutorial/Tutorial_2.ipynb similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/Tutorial_2.ipynb rename to jupyter/lab/Tutorial/Tutorial_2.ipynb diff --git a/jupyter/Lab_Tasks/Tutorial/Tutorial_3.ipynb b/jupyter/lab/Tutorial/Tutorial_3.ipynb similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/Tutorial_3.ipynb rename to jupyter/lab/Tutorial/Tutorial_3.ipynb diff --git a/jupyter/Lab_Tasks/Tutorial/Tutorial_4.ipynb b/jupyter/lab/Tutorial/Tutorial_4.ipynb similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/Tutorial_4.ipynb rename to jupyter/lab/Tutorial/Tutorial_4.ipynb diff --git a/jupyter/Lab_Tasks/Tutorial/Tutorial_5.ipynb b/jupyter/lab/Tutorial/Tutorial_5.ipynb similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/Tutorial_5.ipynb rename to jupyter/lab/Tutorial/Tutorial_5.ipynb diff --git a/jupyter/Lab_Tasks/Tutorial/Tutorial_6.ipynb b/jupyter/lab/Tutorial/Tutorial_6.ipynb similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/Tutorial_6.ipynb rename to jupyter/lab/Tutorial/Tutorial_6.ipynb diff --git a/jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1.png b/jupyter/lab/Tutorial/img/4traces_aes_clkx1.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1.png rename to jupyter/lab/Tutorial/img/4traces_aes_clkx1.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1_offset60000.png b/jupyter/lab/Tutorial/img/4traces_aes_clkx1_offset60000.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1_offset60000.png rename to jupyter/lab/Tutorial/img/4traces_aes_clkx1_offset60000.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1_presample5000.png b/jupyter/lab/Tutorial/img/4traces_aes_clkx1_presample5000.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1_presample5000.png rename to jupyter/lab/Tutorial/img/4traces_aes_clkx1_presample5000.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1_presample5000_zoom.png b/jupyter/lab/Tutorial/img/4traces_aes_clkx1_presample5000_zoom.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx1_presample5000_zoom.png rename to jupyter/lab/Tutorial/img/4traces_aes_clkx1_presample5000_zoom.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx4.png b/jupyter/lab/Tutorial/img/4traces_aes_clkx4.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/4traces_aes_clkx4.png rename to jupyter/lab/Tutorial/img/4traces_aes_clkx4.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/4traces_aes_poortrigger.png b/jupyter/lab/Tutorial/img/4traces_aes_poortrigger.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/4traces_aes_poortrigger.png rename to jupyter/lab/Tutorial/img/4traces_aes_poortrigger.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/aesinput.png b/jupyter/lab/Tutorial/img/aesinput.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/aesinput.png rename to jupyter/lab/Tutorial/img/aesinput.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/dpa-doublepeak.png b/jupyter/lab/Tutorial/img/dpa-doublepeak.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/dpa-doublepeak.png rename to jupyter/lab/Tutorial/img/dpa-doublepeak.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/dpa_peakexample.png b/jupyter/lab/Tutorial/img/dpa_peakexample.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/dpa_peakexample.png rename to jupyter/lab/Tutorial/img/dpa_peakexample.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/shunt_chipwhisperer.png b/jupyter/lab/Tutorial/img/shunt_chipwhisperer.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/shunt_chipwhisperer.png rename to jupyter/lab/Tutorial/img/shunt_chipwhisperer.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/spa_password_diffexample.png b/jupyter/lab/Tutorial/img/spa_password_diffexample.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/spa_password_diffexample.png rename to jupyter/lab/Tutorial/img/spa_password_diffexample.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/spa_password_h_vs_0_overview.png b/jupyter/lab/Tutorial/img/spa_password_h_vs_0_overview.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/spa_password_h_vs_0_overview.png rename to jupyter/lab/Tutorial/img/spa_password_h_vs_0_overview.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/spa_password_h_vs_0_zoomed.png b/jupyter/lab/Tutorial/img/spa_password_h_vs_0_zoomed.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/spa_password_h_vs_0_zoomed.png rename to jupyter/lab/Tutorial/img/spa_password_h_vs_0_zoomed.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/spa_password_list_char1.png b/jupyter/lab/Tutorial/img/spa_password_list_char1.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/spa_password_list_char1.png rename to jupyter/lab/Tutorial/img/spa_password_list_char1.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/traces_wrong.png b/jupyter/lab/Tutorial/img/traces_wrong.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/traces_wrong.png rename to jupyter/lab/Tutorial/img/traces_wrong.png diff --git a/jupyter/Lab_Tasks/Tutorial/img/uart_triggers.png b/jupyter/lab/Tutorial/img/uart_triggers.png similarity index 100% rename from jupyter/Lab_Tasks/Tutorial/img/uart_triggers.png rename to jupyter/lab/Tutorial/img/uart_triggers.png