diff --git a/jupyter/lab/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
deleted file mode 100644
index 4a3e61d5..00000000
--- a/jupyter/lab/1_SCA_Lab/Extending AES-128 Attacks to AES-256.ipynb
+++ /dev/null
@@ -1,124 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Extending AES-128 Attacks to AES-256\n",
- "\n",
- "Many of the labs discuss attacks on AES-128 encryption. It turns out that its big brother, AES-256, can be attacked by extending the same attacks. This page discusses AES-256 and how to reuse an AES-128 attack to obtain the key. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## AES-128 and AES-256\n",
- "\n",
- "In AES-128, we used the following steps to encrypt 16 bytes of plaintext: \n",
- "\n",
- "1. Use a 16 byte key to generate a key schedule, which is 176 bytes long (11 words, 16 bytes per round)\n",
- "1. Put the 16 bytes of plaintext into a 4x4 state matrix\n",
- "1. Combine the 1st round key and the state matrix\n",
- "1. Apply 10 rounds of transformations to the state, involving the key schedule\n",
- "1. Retrieve 16 bytes of ciphertext from the state matrix\n",
- "\n",
- "This looks like:\n",
- "\n",
- "\n",
- "\n",
- "AES-256 is very similar. It looks like (differences with AES-128 in **bold**):\n",
- "\n",
- "1. **Use a 32 byte key to generate a key schedule, which is 240 bytes long (15 words, 16 bytes per round)**\n",
- "1. Put the 16 bytes of plaintext into a 4x4 state matrix\n",
- "1. Combine the 1st round key and the state matrix\n",
- "1. **Apply 14 rounds of transformations to the state, involving the key schedule**\n",
- "1. Retrieve 16 bytes of ciphertext from the state matrix\n",
- "\n",
- "Most of this algoritm is the same. In fact, we can simply repeat our AES-128 attack to get the first half of the key!\n",
- "\n",
- "### Getting the Second Half of the Key\n",
- "\n",
- "Getting the second half of the key is similarly simple: all we need to do is use our newly found first round key to calculate the state matrix at the output of that first `MixColumns` operation. From there, we're basically in the same spot as before: we've got an `AddRoundKey` operation with an unknown key and a `SubBytes` lookup. Repeat the attack, using the calculated state matrix instead of the plaintext (and make sure your traces also have this next `AddRoundKey`/`SubBytes` in it as well) and you'll recover the second half of the key!\n",
- "\n",
- "## AES-256 Decryption\n",
- "\n",
- "Not all devices perform an AES encryption - some devices perform a decryption instead. All of those AES operations have an inverse. The last three operations of AES are (there's no `MixColumns` in the last round:\n",
- "\n",
- "1. `SubBytes`\n",
- "1. `ShiftRows`\n",
- "1. `AddRoundKey`\n",
- "\n",
- "So the start of decryption looks like:\n",
- "\n",
- "1. `AddRoundKey`\n",
- "1. `InvShiftRows`\n",
- "1. `InvSubBytes`\n",
- "\n",
- "Ignore the `InvShiftRows` (it only changes which byte of ciphertext goes with which byte of key, it doesn't actually affect the attack), and we've basically got the same situation as with encryption: an `AddRoundKey` followed by an `InvSubBytes` lookup. Replace the plaintext with the ciphertext, and the `SBox` with `InvSBox` in your Hamming weight calculation, and you'll recover the first 16 bytes of the decryption key.\n",
- "\n",
- "### Getting the Second Half of the Key\n",
- "\n",
- "Getting the second half of the key appears not to be as simple as with encryption. After that `InvSubBytes`, we've got:\n",
- "\n",
- "1. `AddRoundKey`\n",
- "1. `InvMixColumns`\n",
- "1. `InvShiftRows`\n",
- "1. `InvSBox`\n",
- "\n",
- "This time there's an `InvMixColumns` in there, which combines 4 bytes of state together. Suddenly, we're not attacking a single byte at a time (256 values), we're attacking 4 bytes (4 294 967 296 values)! To solve this, let's try writing the last step as an equation:\n",
- "\n",
- "$$X_{13}= \\text{SubBytes}^{-1}\\left(\\text {MixColumns }^{-1}\\left(\\text {ShiftRows }^{-1}\\left(X_{14} \\oplus K_{13}\\right)\\right)\\right)$$\n",
- "\n",
- "where $X_{14}$ is the output of round 14, $K_{13}$ is the 16 byte round key for round 13, and $X_{13}$ is the output of round 13 (our attack point). `MixColumns` is actually a linear function; that is:\n",
- "\n",
- "$$\\text{MixColumns(A+B)} = \\text{MixColumns(A)} + \\text{MixColumns(B)}$$\n",
- "\n",
- "Using this property, we can write down the hypothetical key for round 13, which is:\n",
- "\n",
- "$$K_{13}' = \\text{MixColumns}^{-1}(\\text{ShiftRows}^{-1}(K_{13}))$$\n",
- "\n",
- "and we can use this hypothetical key to calculate the output as:\n",
- "\n",
- "$$\\left.X_{13}=\\text { SubBytes }^{-1}(\\text {MixColumns }^{-1}\\left(\\text {ShiftRows }^{-1}\\left(X_{14}\\right)\\right) \\oplus K_{13}^{\\prime}\\right)$$\n",
- "\n",
- "We know all of $X_{14}$ and can therefore do the `InvMixColumns` and `InvShiftRows`, meaning we're once again at an `AddRoundKey` followed by an `InvSubBytes`! Then, we can recover the actual round key by calculating $$K_{13} = \\text{MixColumns}(\\text{ShiftRows}(K_{13}'))$$\n",
- "\n",
- "Once you've got the 14th and 13th round keys, you can calculate the rest of the key schedule, including the 1st and 2nd round keys.\n",
- "\n",
- "\n",
- "#### Alternative Decryption\n",
- "\n",
- "You might have noticed an oddity about the decryption process in the AES block diagram; the `InvMixColumns` and `AddRoundKey` operations are switched! Since `MixColumns` is linear, you can apply `MixColumns` to the round key and swap the operations. AES decryption is sometimes written in this way to make it look more like the encryption process."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/jupyter/lab/1_SCA_Lab/Lab 1_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
deleted file mode 100644
index 45092e9e..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resychronizing Traces with Sum of Absolute Difference.ipynb
+++ /dev/null
@@ -1,499 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 1, Topic 1, Lab A: Resynchronizing Traces with Sum of Absolute Difference"
- ]
- },
- {
- "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:** *At the end of SCA101, you saw how the communication lines of a microcontroller can be used as a trigger in a side channel attack. This attack was made much more difficult by the presence of jitter; the traces didn't all line up with each other. One thing you might have asked after that lab was if jitter can be used as a countermeasure to a CPA attack. If our target introduces enough jitter, will our CPA attack become impractical?*\n",
- "\n",
- "*In this lab, we'll look at how jitter-based countermeasures can be overcome by resynchronizing the traces. More specifically, we'll use the sum of absolute difference (SAD) measure that we last looked at early on in SCA101.* \n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "* Attempting a CPA attack againt a jittery AES implementation\n",
- "* Writing code to resynchronize traces using SAD\n",
- "* Using ChipWhisperer Analyzer to resynchronize traces."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Prerequisites\n",
- "\n",
- "This lab will build upon the material in SCA101. In particular, you may want to have a quick look over the following labs:\n",
- "\n",
- "* Lab 2_1 (We used SAD to crack the password here)\n",
- "* Lab 5_1 (You saw how jittery traces can disrupt a CPA attack)\n",
- "* Lab 4_3 (Intro to ChipWhisperer Analyzer)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Trace Capture\n",
- "\n",
- "We'll be attacking our usual implementation of AES in this lab, with one small modification. If we use the EXTRA_OPTS=ADD_JITTER, a for loop that runs between 0 and 15 times through will be inserted:\n",
- "\n",
- "```C\n",
- " #ifdef ADD_JITTER\n",
- " for (volatile uint8_t k = 0; k < (*pt & 0x0F); k++);\n",
- " #endif\n",
- "```\n",
- "\n",
- "As you can see, this jitter is actually based on the first byte of our plaintext. This in itself is a vulnerability. We won't use this fact, but as an exercise, try hypothocizing some ways to use this to overcome the jitter."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#\n",
- "# Perform the capture, resulting in a project of 200 jittery traces. See the notebooks to copy your data into!\n",
- "#\n",
- "raise NotImplementedError(\"Add your code here, and delete this.\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## CPA Against Jittery Traces\n",
- "\n",
- "As a first test, let's try plotting a few traces:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(proj.waves[i])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "which looks fairly discouraging. Remember that for a CPA attack, we're calculation correlation across our trace set by point, meaning if the SBox output for one trace is in a different spot than the rest, it will decrease the correlation, not increase it. It probably won't end well, but let's try an attack anyway:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.analyzer as cwa\n",
- "leak_model = cwa.leakage_models.sbox_output\n",
- "attack = cwa.cpa(proj, leak_model)\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As expected, the attack completely failed. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Manual SAD Resync\n",
- "\n",
- "In concept at least, we have a similar situation to the password bypass, except this time the time difference between the traces is an obstacle instead of a vulnerability we're exploiting. We can actually use a very similar technique to figure out how much the traces have shifted! The only modification to the SAD calculation we need to make is that we'll need to use a subset of our reference trace as the reference instead. This means we won't have to figure out how to deal with the parts of the trace that are shifted outside the capture window.\n",
- "\n",
- "To find the offset, all we need to do is slide the reference along the trace we're trying to resyncronize, calculating the SAD at each offset. The difference between the offset we picked for our reference and the offset with the lowest SAD will be our timeshift! In practice, we can skip finding all the offsets and just use the first offset that falls below a threshold like we did in the password bypass. If we captured a much longer power trace that had all the rounds of AES, this would also have the advantage of not accidentally matching with a later round.\n",
- "\n",
- "To start, make a function that figures out the offset between two traces using SAD:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def calculate_trace_offset(ref_trace, orig_offset, target_trace, threshold):\n",
- " ref_len = len(ref_trace)\n",
- " for i in range(0, len(target_trace) - ref_len):\n",
- " if SAD_SUM < threshold:\n",
- " return i - orig_offset"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "a = np.arange(50)\n",
- "b = np.arange(70)[20:]\n",
- "assert calculate_trace_offset(a[35:40], 35, b, 1) == -20\n",
- "assert calculate_trace_offset(a[35:40], 35, b, 29) == -25"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, let's pick a reference trace. First, let's plot the first trace. Some stuff to keep in mind:\n",
- "* We want it to be fairly \"unique\" - we don't want something earlier or later on to also match easily.\n",
- "* The maximum offset, and therefore shift, we can get is limited how close the reference is to the edges of the power trace.\n",
- "* This isn't really a concern for this lab, but we don't want it to be unique per trace. For example, if your trigger was a bit earlier (think the UART lab), the transition between serial communication and AES is very distinct. It's a bad SAD location, however, since the serial part varies a lot between traces\n",
- "\n",
- "for example:\n",
- "\n",
- "\n",
- "\n",
- "the reference in green would be a good spot to pick, while the reference in red would be a poor choice\n",
- "\n",
- "There's a bit of a learning curve to this, so don't be afraid to come back and select a different reference trace if you find it's not working well when trying to resynchronize other traces."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.plot(proj.waves[0])"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "ref_trace = proj.waves[0][???:???]\n",
- "cw.plot(proj.waves[0][1700:2000])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "One thing we can do is slide our reference along another trace and calculate the SAD at each point. This should be similar to your SAD offset function earlier:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def get_sad_plot(ref_trace, target_trace):\n",
- " ref_len = len(ref_trace)\n",
- " sads = []\n",
- " for i in range(0, len(target_trace) - ref_len):\n",
- " sads.append(np.sum(abs(ref_trace-target_trace[i:i+ref_len])))\n",
- " return sads\n",
- "\n",
- "cw.plot(get_sad_plot(ref_trace, proj.waves[1]))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If you don't see a single downward spike:\n",
- "\n",
- "\n",
- "\n",
- "instead of a few spikes, you'll want to select a different reference. You can also use this plot to get an idea of what sort of threshold to use.\n",
- "\n",
- "Anyway, try it on some different traces. You should get a varying amount offsets."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now that we can easily get how much our trace is offset by, let's see if the plots actually line up:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "target_offset = calculate_trace_offset(ref_trace, ???, proj.waves[1], 10)\n",
- "print(target_offset)\n",
- "\n",
- "cw.plot(proj.waves[0][:]) * cw.plot(proj.waves[1][target_offset:])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Looks good! Try plotting a few different traces, however, and you'll run into a problem: how do we deal with a trace that needs to be shifted forward? There's a related problem for our CPA attack well: we need all the traces to be the same length, but if we move traces backwards, we won't have any data at the end, meaning the traces will be shorter than usual. The easiest way to deal with these problems is just to fill in the rest of the data with zeros:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "target_offset2 = calculate_trace_offset(ref_trace, ???, proj.waves[5], 10)\n",
- "print(target_offset2)\n",
- "new_trace = np.zeros(len(proj.waves[0]))\n",
- "new_trace[-target_offset2:] = proj.waves[5][:target_offset2]\n",
- "\n",
- "cw.plot(proj.waves[0]) * cw.plot(new_trace)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Anyway, take what we've done and use it to make a function that takes in a trace and returns one that's resynchronized with the reference.\n",
- "\n",
- "**HINT: You'll need to handle 3 cases: offset > 0, offset < 0, and offset = 0**"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def resync_with_SAD(ref, orig_offset, target_trace, threshold):\n",
- " target_offset = calculate_trace_offset(ref, orig_offset, target_trace, threshold)\n",
- " if target_offset > 0:\n",
- " new_trace = np.zeros(len(target_trace))\n",
- " # uncomment the correct calculation\n",
- " # new_trace[:-target_offset] = target_trace[target_offset:]\n",
- " # new_trace[-target_offset:] = target_trace[:target_offset]\n",
- " return new_trace\n",
- " elif target_offset < 0:\n",
- " # uncomment the correct calculation\n",
- " # new_trace[:-target_offset] = target_trace[target_offset:]\n",
- " # new_trace[-target_offset:] = target_trace[:target_offset]\n",
- " return new_trace\n",
- " return target_trace"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's test it out on some jittery traces:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "traces = [resync_with_SAD(ref_trace, ???, proj.waves[i], 10) for i in range(10)]\n",
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(traces[i])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Hopefully all your traces lined up perfectly. If not, you might have to go back and select another reference or adjust your `resync_with_SAD()` function.\n",
- "\n",
- "Let's make a new project, resynchronize our traces, and insert them into that:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "resync_proj = cw.create_project(\"Lab 1_1_Resync\", overwrite=True)\n",
- "for trace in proj.traces:\n",
- " resync_wave = resync_with_SAD(ref_trace, ???, trace.wave, 10)\n",
- " new_trace = cw.Trace(resync_wave, trace.textin, trace.textout, trace.key)\n",
- " resync_proj.traces.append(new_trace)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "and let's retry the attack:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.analyzer as cwa\n",
- "leak_model = cwa.leakage_models.sbox_output\n",
- "attack = cwa.cpa(resync_proj, leak_model)\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You should see the attack succeed this time!"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## SAD Resync with ChipWhisperer Analyzer"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Like with the CPA attack itself, ChipWhisperer Analyzer can make our lives a lot easier:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "resync_traces = cwa.preprocessing.ResyncSAD(proj)\n",
- "resync_traces.ref_trace = 0\n",
- "resync_traces.target_window = (???, ???)\n",
- "resync_traces.max_shift = ???\n",
- "resync_analyzer = resync_traces.preprocess()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "All these settings should look familiar except `max_shift`, which will cause the ResyncSAD object to discard a trace if it needs to be shifted more than `max_shift`. Why is this useful? Consider the case where there's enough jitter to move the point we're looking at outside of what we captured. It would be much better to simply discard this trace, rather than including it and having it disrupt our data.\n",
- "\n",
- "Anyway, plotting 10 resychronized traces:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(resync_analyzer.waves[i])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You should find that everything is resychronized, just as it was from our code. Running the attack again:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.analyzer as cwa\n",
- "leak_model = cwa.leakage_models.sbox_output\n",
- "attack = cwa.cpa(resync_analyzer, leak_model)\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "which easily succeeds."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Conclusions & Next Steps\n",
- "\n",
- "In this lab, you saw how we can resychronize traces using sum of absolute difference, allowing us to overcome jittery traces. With two uses of SAD under your belt, you should start to see how it is a useful metric for comparing two power traces, as it is both simple and effective. If you're looking for another usecase for SAD in the ChipWhisperer project, we also have it available as a trigger for the CW1200. This allows you to trigger based on the shape of a wave. In the case of our simple attack, it would completely remove the need for a trigger pin!"
- ]
- },
- {
- "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",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/jupyter/lab/1_SCA_Lab/Lab 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
deleted file mode 100644
index 96e492f7..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 1_1A - Resynchronizing Traces with Sum of Absolute Difference (HARDWARE).ipynb
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Trace Capture\n",
- "\n",
- "We'll be attacking our usual implementation of AES in this lab, with one small modification. If we use the EXTRA_OPTS=ADD_JITTER, a for loop that runs between 0 and 15 times through will be inserted:\n",
- "\n",
- "```C\n",
- " #ifdef ADD_JITTER\n",
- " for (volatile uint8_t k = 0; k < (*pt & 0x0F); k++);\n",
- " #endif\n",
- "```\n",
- "\n",
- "As you can see, this jitter is actually based on the first byte of our plaintext. This in itself is a vulnerability. We won't use this fact, but as an exercise, try hypothocizing some ways to use this to overcome the jitter."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#SCOPETYPE = 'OPENADC'\n",
- "#PLATFORM = 'CWLITEARM'\n",
- "#CRYPTO_TARGET = 'TINYAES128C'\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\" \"$CRYPTO_TARGET\" \"$SS_VER\"\n",
- "cd ../../../firmware/mcu/simpleserial-aes\n",
- "make PLATFORM=$1 CRYPTO_TARGET=$2 EXTRA_OPTS=ADD_JITTER SS_VER=$3"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.program_target(scope, prog, \"../../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex\".format(PLATFORM))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import trange\n",
- "import numpy as np\n",
- "import time\n",
- "\n",
- "ktp = cw.ktp.Basic()\n",
- "trace_array = []\n",
- "textin_array = []\n",
- "\n",
- "\n",
- "proj = cw.create_project(\"traces/Lab_Resync.cwp\", overwrite=True)\n",
- "\n",
- "N = 200\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " key, text = ktp.next()\n",
- " trace = cw.capture_trace(scope, target, text, key)\n",
- " if not trace:\n",
- " continue\n",
- " \n",
- " proj.traces.append(trace)\n",
- "proj.save()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.dis()\n",
- "target.dis()"
- ]
- }
- ],
- "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/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
deleted file mode 100644
index 26e3aaf5..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp (HARDWARE).ipynb
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Trace Capture\n",
- "\n",
- "Like with the last lab, we'll be attacking a jittery AES implementation. This time, however, the jitter will be dispersed throughout the operations, making it much harder to resynchronize."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#SCOPETYPE = 'OPENADC'\n",
- "#PLATFORM = 'CWLITEARM'\n",
- "#CRYPTO_TARGET = 'TINYAES128C'\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\" \"$CRYPTO_TARGET\" \"$SS_VER\"\n",
- "cd ../../../firmware/mcu/simpleserial-aes\n",
- "make PLATFORM=$1 CRYPTO_TARGET=$2 EXTRA_OPTS=JITTER_2 SS_VER=$3"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.program_target(scope, prog, \"../../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex\".format(PLATFORM))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import trange\n",
- "import numpy as np\n",
- "import time\n",
- "\n",
- "ktp = cw.ktp.Basic()\n",
- "trace_array = []\n",
- "textin_array = []\n",
- "\n",
- "\n",
- "proj = cw.create_project(\"traces/Lab_Resync_WORSE.cwp\", overwrite=True)\n",
- "scope.adc.samples = 5000\n",
- "N = 1000\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " key, text = ktp.next()\n",
- " trace = cw.capture_trace(scope, target, text, key)\n",
- " if not trace:\n",
- " continue\n",
- " \n",
- " proj.traces.append(trace)\n",
- "proj.save()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.dis()\n",
- "target.dis()"
- ]
- }
- ],
- "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/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
deleted file mode 100644
index 1cf5c668..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 1_1B - Resychronizing Traces with Dynamic Time Warp.ipynb
+++ /dev/null
@@ -1,290 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 1, Topic 1, Lab B: Resynchronizing Traces with Dynamic Time Warp"
- ]
- },
- {
- "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 looked at a simple method for resynchronizing traces: the Sum of Absolute Difference. This technique was enough to defeat the simple jitter between the device triggering and it performing an encryption. In this lab, we'll look at a device with jitter during the actual encryption operation.*\n",
- "\n",
- "*In this lab, we'll look at how jitter-based countermeasures can be overcome by resynchronizing the traces. More specifically, we'll use the sum of absolute difference (SAD) measure that we last looked at early on in SCA101.* \n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "* Understanding limitations of SAD resynchronization\n",
- "* Using the dynamic time warp preprocessor\n",
- "\n",
- "**NOTE: DTW is a very slow algorithm. Make sure you have a compiled version of FasterDTW installed, otherwise resynchronization will take a long time**"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Prerequisites\n",
- "\n",
- "This lab will build upon the last lab: Lab 1_1A. It's recommended that you complete that lab before trying this one."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#\n",
- "# Perform the capture, resulting in a project of 1000 jittery traces. See the notebooks to copy your data into!\n",
- "#\n",
- "raise NotImplementedError(\"Add your code here, and delete this.\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## CPA Against Jittery Traces\n",
- "\n",
- "Again, let's try plotting a few traces:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(proj.waves[i])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "From afar, these probably don't look to bad. The trace shape is a little weird, but that shouldn't be a problem. Zooming in should reveal that the traces are desynchronized. Again, it probably won't work, but let's try our usual CPA attack. We'll use LASCAR, a very fast side channel analysis library, to speed things up:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.common.api.lascar as cw_lascar\n",
- "from lascar import *\n",
- "cw_container = cw_lascar.CWContainer(proj, proj.textins, start=None, end=None) #optional start and end args set start and end points for analysis\n",
- "guess_range = range(256)\n",
- "leakage = cw_lascar.sbox_HW_gen\n",
- "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n",
- "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)\n",
- "\n",
- "plt = cw.plot([])\n",
- "key_guess = []\n",
- "for i in range(16):\n",
- " results = cpa_engines[i].finalize()\n",
- " xrange = range(len(results[0xD0]))\n",
- " guess = abs(results).max(1).argmax()\n",
- " print(\"Best Guess is {:02X} ({:02X}) (Corr = {})\".format(guess, proj.keys[0][i], abs(results).max()))\n",
- " plt *= cw.plot(results[guess]).opts(color=\"red\")\n",
- " key_guess.append(guess)\n",
- " \n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Nothing! Again, nothing unexpected, but it shows that there's enough jitter here to not just be averaged out...\n",
- "\n",
- "Last lab, we were able to overcome jitter by resynchronizing the traces with a SAD match. Let's try that technique again:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.analyzer as cwa\n",
- "resync_traces = cwa.preprocessing.ResyncSAD(proj)\n",
- "resync_traces.ref_trace = 0\n",
- "resync_traces.target_window = (???, ???)\n",
- "resync_traces.max_shift = ???\n",
- "resync_analyzer = resync_traces.preprocess()\n",
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(resync_analyzer.waves[i])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The traces probably look a bit better than before during `subbytes`, but if you zoom in you'll again see that there's lots of jitter here. Try the CPA attack again:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.common.api.lascar as cw_lascar\n",
- "from lascar import *\n",
- "cw_container = cw_lascar.CWContainer(resync_analyzer, proj.textins, start=None, end=None) #optional start and end args set start and end points for analysis\n",
- "guess_range = range(256)\n",
- "leakage = cw_lascar.sbox_HW_gen\n",
- "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n",
- "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)\n",
- "\n",
- "plt = cw.plot([])\n",
- "key_guess = []\n",
- "for i in range(16):\n",
- " results = cpa_engines[i].finalize()\n",
- " xrange = range(len(results[0xD0]))\n",
- " guess = abs(results).max(1).argmax()\n",
- " print(\"Best Guess is {:02X} ({:02X}) (Corr = {})\".format(guess, proj.keys[0][i], abs(results).max()))\n",
- " plt *= cw.plot(results[guess]).opts(color=\"red\")\n",
- " key_guess.append(guess)\n",
- " \n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You'll probably find that you didn't have any luck. That's because this target is inserting multiple delays during the actual encryption itself. This means that shifting traces by a single offset isn't going to be very effective.\n",
- "\n",
- "Luckily, SAD isn't the only way to resyncrhonize traces. ChipWhisperer Analyzer also has support for a technique called [dynamic time warp](https://en.wikipedia.org/wiki/Dynamic_time_warping). The details are much more complicated than with SAD, so we'll skip implementing it ourselves this time. Instead, skip straight to the preprocessing.\n",
- "\n",
- "ChipWhisperer uses a faster approximation for dynamic time warp, which is a very slow algorithm. This comes with a parameter to vary: radius. Higher radii will generally give a better synchronization, but at the cost of a higher processing time. 1 is a good value to start with, but you might have to bump it a bit higher to get a good resync."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%time\n",
- "import chipwhisperer.analyzer as cwa\n",
- "resync_traces = cwa.preprocessing.ResyncDTW(proj)\n",
- "resync_traces.ref_trace = 0\n",
- "resync_traces.radius = ???\n",
- "resync_analyzer = resync_traces.preprocess()\n",
- "\n",
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(resync_analyzer.waves[i])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "These traces are looking a lot better! This technique isn't perfect, but it works a lot better than SAD in this case!\n",
- "\n",
- "Let's try a CPA attack now:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.common.api.lascar as cw_lascar\n",
- "from lascar import *\n",
- "cw_container = cw_lascar.CWContainer(resync_analyzer, proj.textins, start=None, end=None) #optional start and end args set start and end points for analysis\n",
- "guess_range = range(256)\n",
- "leakage = cw_lascar.sbox_HW_gen\n",
- "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n",
- "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)\n",
- "\n",
- "plt = cw.plot([])\n",
- "key_guess = []\n",
- "for i in range(16):\n",
- " results = cpa_engines[i].finalize()\n",
- " xrange = range(len(results[0xD0]))\n",
- " guess = abs(results).max(1).argmax()\n",
- " print(\"Best Guess is {:02X} ({:02X}) (Corr = {})\".format(guess, proj.keys[0][i], abs(results).max()))\n",
- " plt *= cw.plot(results[guess]).opts(color=\"red\")\n",
- " key_guess.append(guess)\n",
- " \n",
- "plt"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "assert key_guess == list(proj.keys[0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Conclusions & Next Steps\n",
- "\n",
- "In this lab, you saw how more complicated jitter can defeat the SAD resynchronization technique we used in the last lab. In response, we used the dynamic time warp preprocessor to perform a better resynchronization and defeat the jitter."
- ]
- },
- {
- "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",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb
deleted file mode 100644
index 73cd0d5d..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES (HARDWARE).ipynb
+++ /dev/null
@@ -1,129 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 2, Topic 1: CPA Attack on 32bit AES (HARDWARE)"
- ]
- },
- {
- "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": [
- "Usual capture, just using MBEDTLS instead of TINYAES128"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "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": [
- "#Capture Traces\n",
- "from tqdm.notebook import trange, trange\n",
- "import numpy as np\n",
- "import time\n",
- "\n",
- "ktp = cw.ktp.Basic()\n",
- "project = cw.project(\"32bit_aes\", overwrite=True)\n",
- "\n",
- "traces = []\n",
- "N = 100 # Number of traces\n",
- "project = cw.create_project(\"traces/32bit_AES.cwp\", overwrite=True)\n",
- "\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " key, text = ktp.next() # manual creation of a key, text pair can be substituted here\n",
- "\n",
- " trace = cw.capture_trace(scope, target, text, key)\n",
- " if trace is None:\n",
- " continue\n",
- " project.traces.append(trace)\n",
- "\n",
- "try:\n",
- " print(scope.adc.trig_count) # print if this exists\n",
- "except:\n",
- " pass\n",
- "project.save()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.dis()\n",
- "target.dis()"
- ]
- }
- ],
- "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.12.0"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/jupyter/lab/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
deleted file mode 100644
index ad8ec0b7..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 2_1 - CPA on 32bit AES.ipynb
+++ /dev/null
@@ -1,220 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 2, Topic 1: CPA Attack on 32bit AES (MAIN)"
- ]
- },
- {
- "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 been focusing on a single implementation of AES, TINYAES128C (or AVRCRYPTOLIB, if you're on XMEGA). TINYAES128C, which is designed to run on a variety of microcontrollers, doesn't make any implementation specific optimizations. In this lab, we'll look at how we can break a 32-bit optimized version of AES using a CPA attack.*\n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "\n",
- "* Understanding how AES can be optimized on 32-bit platforms.\n",
- "* Attacking an optimized version of AES using CPA"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Optimizing AES\n",
- "\n",
- "A 32-bit machine can operate on 32-bit words, so it seems wasteful to use the same 8-bit operations. For example, if we look at the SBox operation:\n",
- "\n",
- "$\n",
- "b = sbox(state) = sbox(\\left[ \\begin{array}\n",
- "& S0 & S4 & S8 & S12 \\\\\n",
- "S1 & S5 & S9 & S13 \\\\\n",
- "S2 & S6 & S10 & S14 \\\\\n",
- "S3 & S7 & S11 & S15\n",
- "\\end{array} \\right]) = \\left[ \\begin{array}\n",
- "& S0 & S4 & S8 & S12 \\\\\n",
- "S5 & S9 & S13 & S1 \\\\\n",
- "S10 & S14 & S2 & S6 \\\\\n",
- "S15 & S3 & S7 & S11\n",
- "\\end{array} \\right]\n",
- "$\n",
- "\n",
- "we could consider each row as a 32-bit number and do three bitwise rotates instead of moving a bunch of stuff around in memory. Even better, we can speed up AES considerably by generating 32-bit lookup tables, called T-Tables, as was described in the book [The Design of Rijndael](http://www.springer.com/gp/book/9783540425809) which was published by the authors of AES.\n",
- "\n",
- "In order to take full advantage of our 32 bit machine, we can examine a typical round of AES. With the exception of the final round, each round looks like:\n",
- "\n",
- "$\\text{a = Round Input}$\n",
- "\n",
- "$\\text{b = SubBytes(a)}$\n",
- "\n",
- "$\\text{c = ShiftRows(b)}$\n",
- "\n",
- "$\\text{d = MixColumns(c)}$\n",
- "\n",
- "$\\text{a' = AddRoundKey(d) = Round Output}$\n",
- "\n",
- "We'll leave AddRoundKey the way it is. The other operations are:\n",
- "\n",
- "$b_{i,j} = \\text{sbox}[a_{i,j}]$\n",
- "\n",
- "$\\left[ \\begin{array} { c } { c _ { 0 , j } } \\\\ { c _ { 1 , j } } \\\\ { c _ { 2 , j } } \\\\ { c _ { 3 , j } } \\end{array} \\right] = \\left[ \\begin{array} { l } { b _ { 0 , j + 0 } } \\\\ { b _ { 1 , j + 1 } } \\\\ { b _ { 2 , j + 2 } } \\\\ { b _ { 3 , j + 3 } } \\end{array} \\right]$\n",
- "\n",
- "$\\left[ \\begin{array} { l } { d _ { 0 , j } } \\\\ { d _ { 1 , j } } \\\\ { d _ { 2 , j } } \\\\ { d _ { 3 , j } } \\end{array} \\right] = \\left[ \\begin{array} { l l l l } { 02 } & { 03 } & { 01 } & { 01 } \\\\ { 01 } & { 02 } & { 03 } & { 01 } \\\\ { 01 } & { 01 } & { 02 } & { 03 } \\\\ { 03 } & { 01 } & { 01 } & { 02 } \\end{array} \\right] \\times \\left[ \\begin{array} { c } { c _ { 0 , j } } \\\\ { c _ { 1 , j } } \\\\ { c _ { 2 , j } } \\\\ { c _ { 3 , j } } \\end{array} \\right]$\n",
- "\n",
- "Note that the ShiftRows operation $b_{i, j+c}$ is a cyclic shift and the matrix multiplcation in MixColumns denotes the xtime operation in GF($2^8$).\n",
- "\n",
- "It's possible to combine all three of these operations into a single line. We can write 4 bytes of $d$ as the linear combination of four different 4 byte vectors:\n",
- "\n",
- "$\\left[ \\begin{array} { l } { d _ { 0 , j } } \\\\ { d _ { 1 , j } } \\\\ { d _ { 2 , j } } \\\\ { d _ { 3 , j } } \\end{array} \\right] = \\left[ \\begin{array} { l } { 02 } \\\\ { 01 } \\\\ { 01 } \\\\ { 03 } \\end{array} \\right] \\operatorname { sbox } \\left[ a _ { 0 , j + 0 } \\right] \\oplus \\left[ \\begin{array} { l } { 03 } \\\\ { 02 } \\\\ { 01 } \\\\ { 01 } \\end{array} \\right] \\operatorname { sbox } \\left[ a _ { 1 , j + 1 } \\right] \\oplus \\left[ \\begin{array} { c } { 01 } \\\\ { 03 } \\\\ { 02 } \\\\ { 01 } \\end{array} \\right] \\operatorname { sbox } \\left[ a _ { 2 , j + 2 } \\right] \\oplus \\left[ \\begin{array} { c } { 01 } \\\\ { 01 } \\\\ { 03 } \\\\ { 02 } \\end{array} \\right] \\operatorname { sbox } \\left[ a _ { 3 , j + 3 } \\right]$\n",
- "\n",
- "Now, for each of these four components, we can tabulate the outputs for every possible 8-bit input:\n",
- "\n",
- "$T _ { 0 } [ a ] = \\left[ \\begin{array} { l l } { 02 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 03 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "$T _ { 1 } [ a ] = \\left[ \\begin{array} { l } { 03 \\times \\operatorname { sbox } [ a ] } \\\\ { 02 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "$T _ { 2 } [ a ] = \\left[ \\begin{array} { l l } { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 03 \\times \\operatorname { sbox } [ a ] } \\\\ { 02 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "$T _ { 3 } [ a ] = \\left[ \\begin{array} { l l } { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 03 \\times \\operatorname { sbox } [ a ] } \\\\ { 02 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "These tables have 2^8 different 32-bit entries, so together the tables take up 4 kB. Finally, we can quickly compute one round of AES by calculating\n",
- "\n",
- "$\\left[ \\begin{array} { l } { d _ { 0 , j } } \\\\ { d _ { 1 , j } } \\\\ { d _ { 2 , j } } \\\\ { d _ { 3 , j } } \\end{array} \\right] = T _ { 0 } \\left[ a _ { 0 } , j + 0 \\right] \\oplus T _ { 1 } \\left[ a _ { 1 } , j + 1 \\right] \\oplus T _ { 2 } \\left[ a _ { 2 } , j + 2 \\right] \\oplus T _ { 3 } \\left[ a _ { 3 } , j + 3 \\right]$\n",
- "\n",
- "All together, with AddRoundKey at the end, a single round now takes 16 table lookups and 16 32-bit XOR operations. This arrangement is much more efficient than the traditional 8-bit implementation. There are a few more tradeoffs that can be made: for instance, the tables only differ by 8-bit shifts, so it's also possible to store only 1 kB of lookup tables at the expense of a few rotate operations.\n",
- "\n",
- "While the TINYAES128C library we've been using doesn't make this optimization, another library included with ChipWhisperer called MBEDTLS does."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#\n",
- "# Perform the capture, resulting in a project of 100 MBEDTLS traces. See the notebooks to copy your data into!\n",
- "#\n",
- "raise NotImplementedError(\"Add your code here, and delete this.\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If we plot the AES power trace:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.plot(project.waves[0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You probably can't even pick out the different AES rounds anymore (whereas it was pretty obvious on TINYAES128C). MBED is also way faster - we only got part way into round 2 with 5000 samples of TINYAES, but with MBED we can finish the entire encryption in less than 5000 samples! Two questions we need to answer now are:\n",
- "\n",
- "1. Is it possible for us to break this AES implementation?\n",
- "1. If so, what sort of leakage model do we need?\n",
- "\n",
- "As it turns out, the answers are:\n",
- "\n",
- "1. Yes!\n",
- "1. We can continue to use the same leakage model - the SBox output\n",
- "\n",
- "This might come as a surprise, but it's true! Two of the t_table lookups are just the sbox[key^plaintext] that we used before. Try the analysis for yourself now and verify that this is correct:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Improving the Model\n",
- "\n",
- "While this model works alright for mbedtls, you probably wouldn't be surprised if it wasn't the best model to attack with. Instead, we can attack the full T-Tables. Returning again to the T-Tables:\n",
- "\n",
- "$T _ { 0 } [ a ] = \\left[ \\begin{array} { l l } { 02 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 03 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "$T _ { 1 } [ a ] = \\left[ \\begin{array} { l } { 03 \\times \\operatorname { sbox } [ a ] } \\\\ { 02 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "$T _ { 2 } [ a ] = \\left[ \\begin{array} { l l } { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 03 \\times \\operatorname { sbox } [ a ] } \\\\ { 02 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "$T _ { 3 } [ a ] = \\left[ \\begin{array} { l l } { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 01 \\times \\operatorname { sbox } [ a ] } \\\\ { 03 \\times \\operatorname { sbox } [ a ] } \\\\ { 02 \\times \\operatorname { sbox } [ a ] } \\end{array} \\right]$\n",
- "\n",
- "we can see that for each T-Table lookup, the following is accessed:\n",
- "\n",
- "$\\operatorname {sbox}[a]$, $\\operatorname {sbox}[a]$, $2 \\times \\operatorname {sbox}[a]$, $3 \\times \\operatorname {sbox}[a]$\n",
- "\n",
- "so instead of just taking the Hamming weight of the SBox, we can instead take the Hamming weight of this whole access:\n",
- "\n",
- "$h = \\operatorname {hw}[\\operatorname {sbox}[a]] + \\operatorname {hw}[\\operatorname {sbox}[a]] + \\operatorname {hw}[2 \\times \\operatorname {sbox}[a]] + \\operatorname {hw}[3 \\times \\operatorname {sbox}[a]]$\n",
- "\n",
- "Again, ChipWhisperer already has this model built in, which you can access with `cwa.leakage_models.t_table`. Retry your CPA attack with this new leakage model:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Did this attack work better than the previous one?\n",
- "\n",
- "## T-Tables for Decryption:\n",
- "\n",
- "Recall that the last round of AES is different than the rest of the rounds. Instead of it applying `subbytes`, `shiftrows`, `mixcolumns`, and `addroundkey`, it leaves out `mixcolumns`. You might expect that this means that decryption doesn't use a reverse T-Table in the first decryption round, but this isn't necessarily the case! Since `mixcolumns` is a linear operation, $\\operatorname{mixcolumns}( \\operatorname{key} + \\operatorname{state})$ is equal to $\\operatorname{mixcolumns}(\\operatorname{key}) + \\operatorname{mixcolumns}(\\operatorname{state})$. Again, this is the approach that MBEDTLS takes, so we would be able to use the reverse T-Table to attack decryption."
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.7.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb
deleted file mode 100644
index b11bda33..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation (HARDWARE).ipynb
+++ /dev/null
@@ -1,141 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Topic 2, Part 2 - CPA on Hardware AES Implementation"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "---\n",
- "NOTE: This lab references some (commercial) training material on [ChipWhisperer.io](https://www.ChipWhisperer.io). You can freely execute and use the lab per the open-source license (including using it in your own courses if you distribute similarly), but you must maintain notice about this source location. Consider joining our training course to enjoy the full experience.\n",
- "\n",
- "---"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Capture traces as normal. We'll need to select the HWAES crypto target instead of TINYAES or MBEDTLS. Also we don't need to capture as many traces - the whole AES block will fit in less than 2000 traces. We'll also boost the gain a little bit - HWAES won't result in as big of power spikes:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#SCOPETYPE = 'OPENADC'\n",
- "#PLATFORM = 'CW308_STM32F4'\n",
- "#CRYPTO_TARGET = 'HWAES'"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%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": [
- "project = cw.create_project(\"traces/STM32F4_HW_AES.cwp\", overwrite=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#Capture Traces\n",
- "from tqdm.notebook import trange, trange\n",
- "import numpy as np\n",
- "import time\n",
- "\n",
- "ktp = cw.ktp.Basic()\n",
- "\n",
- "traces = []\n",
- "N = 20000 # Number of traces\n",
- "scope.adc.samples=2000\n",
- "\n",
- "scope.gain.db = 32\n",
- "scope.glitch.arm_timing = \"no_glitch\"\n",
- "\n",
- "\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " key, text = ktp.next() # manual creation of a key, text pair can be substituted here\n",
- "\n",
- " trace = cw.capture_trace(scope, target, text, key)\n",
- " if trace is None:\n",
- " continue\n",
- " project.traces.append(trace)\n",
- "\n",
- "print(scope.adc.trig_count)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.dis()\n",
- "target.dis()"
- ]
- }
- ],
- "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": 2
-}
diff --git a/jupyter/lab/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
deleted file mode 100644
index dbfd6f20..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 2_2 - CPA on Hardware AES Implementation.ipynb
+++ /dev/null
@@ -1,292 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Topic 2, Part 2 - CPA on Hardware AES Implementation"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "---\n",
- "NOTE: This lab references some (commercial) training material on [ChipWhisperer.io](https://www.ChipWhisperer.io). You can freely execute and use the lab per the open-source license (including using it in your own courses if you distribute similarly), but you must maintain notice about this source location. Consider joining our training course to enjoy the full experience.\n",
- "\n",
- "---"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "**SUMMARY:** *By now you should have a pretty good understanding of how software implementations of AES are vulnerable to CPA attacks. You might be wondering: are hardware implementations of AES also vulnerable to CPA attacks?*\n",
- "\n",
- "*In this lab, we'll perform a CPA attack on the hardware AES implementation in the STM32F415. We'll also introduce LASCAR for increased performance when analyzing large datasets.*\n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "* Understanding how leakage differs between software AES and hardware AES implementations\n",
- "* Using LASCAR for CPA attacks\n",
- "* Identifying different leakage points"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Capture traces as normal. We'll need to select the HWAES crypto target instead of TINYAES or MBEDTLS. Also we don't need to capture as many traces - the whole AES block will fit in less than 2000 traces. We'll also boost the gain a little bit - HWAES won't result in as big of power spikes:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "SCOPETYPE = 'OPENADC'\n",
- "PLATFORM = 'CW308_STM32F4'\n",
- "CRYPTO_TARGET = 'HWAES'"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%bash -s \"$PLATFORM\" \"$CRYPTO_TARGET\"\n",
- "cd ../../../firmware/mcu/simpleserial-aes\n",
- "make PLATFORM=$1 CRYPTO_TARGET=$2"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%run \"../../Setup_Scripts/Setup_Generic.ipynb\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "fw_path = '../../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex'.format(PLATFORM)\n",
- "cw.program_target(scope, prog, fw_path)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "project = cw.create_project(\"32bit_AES.cwp\", overwrite=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#Capture Traces\n",
- "from tqdm.notebook import trange, trange\n",
- "import numpy as np\n",
- "import time\n",
- "\n",
- "ktp = cw.ktp.Basic()\n",
- "\n",
- "traces = []\n",
- "N = 15000 # Number of traces\n",
- "scope.adc.samples=2000\n",
- "\n",
- "scope.gain.db = 38\n",
- "\n",
- "\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " key, text = ktp.next() # manual creation of a key, text pair can be substituted here\n",
- "\n",
- " trace = cw.capture_trace(scope, target, text, key)\n",
- " if trace is None:\n",
- " continue\n",
- " project.traces.append(trace)\n",
- "\n",
- "print(scope.adc.trig_count)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Introducing LASCAR\n",
- "\n",
- "With how many traces we're capturing, analyzing our traces will take a lot of time with ChipWhisperer - Analyzer wasn't designed for performance. It is for this reason that we will be using LASCAR, an open source side channel analysis library with a bigger emphasis on speed than ChipWhisperer Analyzer. Normally, it would take a bit of work to massage ChipWhisperer into the LASCAR format; however, ChipWhisperer has recently integrated some basic LASCAR support, making it easy to combine LASCAR and ChipWhisperer projects! Note that this support is a WIP and not offically documented - the interface can change at any time!\n",
- "\n",
- "Basic setup is as follows:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.common.api.lascar as cw_lascar\n",
- "from lascar import *\n",
- "cw_container = cw_lascar.CWContainer(project, project.textouts, start=None, end=None) #optional start and end args set start and end points for analysis\n",
- "guess_range = range(256)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Leakage Model\n",
- "\n",
- "Thus far, we've been exclusively focusing on software AES. Here, each AES operation (shift rows, add round key, mix columns, etc) is implemented using one basic operation (XOR, reads/writes, multiplies, etc.) per clock cycle. With a hardware implementation, it's often possible to not only combine basic operations into a block that can run in a single clock cycle, but also combine multiple AES operations and run them in a single block! For example, the CW305 FPGA board can run each round of AES in a single clock cycle!\n",
- "\n",
- "Because of this, running a CPA attack on hardware AES is much trickier than on software AES. In software, we found that it was easy to search for the outputs of the s-boxes because these values would need to be loaded from memory onto a high-capacitance data bus. This is not necessarily true for hardware AES, where the output of the s-boxes may be directly fed into the next stage of the algorithm. In general, we may need some more knowledge of the hardware implementation to successfully complete an attack. That being said, if we take a look at a block diagram of AES:\n",
- "\n",
- "\n",
- "\n",
- "the last round jumps out for a few reasons:\n",
- "\n",
- "* It's not far removed from the ciphertext or the plaintext\n",
- "* It's got an AddRoundKey and a SubBytes, meaning we get a nonlinear addition of the key between the ciphertext and the input of the round\n",
- "* There's no Mix Columns\n",
- "\n",
- "Let's make a guess at the implementation and say that it'll do the last round in a single clock cycle and store the input and output in the same memory block. Our reset assumption that allowed us to simply use the Hamming weight instead of the Hamming distance also probably won't be valid here. As such, let's use the Hamming distance between the output and the input of the last round.\n",
- "\n",
- "ChipWhisperer now includes a few leakage models for use with LASCAR:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "leakage = cw_lascar.lastround_HD_gen"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Then, we can actually run the analysis. It should chew through our 15k traces in only a minute or two!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cpa_engines = [CpaEngine(\"cpa_%02d\" % i, leakage(i), guess_range) for i in range(16)]\n",
- "session = Session(cw_container, engines=cpa_engines).run(batch_size=50)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's print out our results and plot the correlation of our guesses:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from bokeh.plotting import figure, show\n",
- "from bokeh.io import output_notebook\n",
- "output_notebook()\n",
- "p = figure()\n",
- "key_guess = []\n",
- "for i in range(16):\n",
- " results = cpa_engines[i].finalize()\n",
- " xrange = list(range(len(results[0xD0])))\n",
- " guess = abs(results).max(1).argmax()\n",
- " print(\"Best Guess is {:02X} (Corr = {})\".format(guess, abs(results).max()))\n",
- " p.line(xrange, results[guess], color=\"red\")\n",
- " key_guess.append(guess)\n",
- " \n",
- "show(p)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "ChipWhisperer also includes a class to interpret the results of the analysis:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer.analyzer as cwa\n",
- "last_round_key = cwa.aes_funcs.key_schedule_rounds(list(project.keys[0]),0,10)\n",
- "disp = cw_lascar.LascarDisplay(cpa_engines, last_round_key)\n",
- "disp.show_pge()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Interestingly, you should see that the attack has worked fairly well for most of the bytes. All of them, in fact, except bytes 0, 4, 8, and 12. Looking the correlation plot, you should see two large spikes instead of one like you might expect. Try focusing the attack on either one of these points by adjusting `start=` and `end=` when making the `cw_container` and try answering the following questions:\n",
- "\n",
- "* Which spike was our expected leakage actually at (last round state diff)?\n",
- "* How might you be able to tell that the attack failed for certain bytes at the incorrect leakage point?\n",
- "* Why might this other spike be occuring?"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.dis()\n",
- "target.dis()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/jupyter/lab/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb b/jupyter/lab/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb
deleted file mode 100644
index 6d58f769..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 2_3 - Attacking Across MixColumns.ipynb
+++ /dev/null
@@ -1,700 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 2, Topic 3: Attacking Across MixColumns"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "**SUMMARY:** *In Part 2, Topic 2, you saw how to apply CPA attacks to a hardware implementaiton of AES. As it turns out, this attack will not work against many hardware implementations of AES. This is thanks to the fact that the implementation doesn't need to store the output ciphertext in the same register as the state. In this case, the nearest distance for the state register will usually be between the output of the 9th round and the output of the 8th round. Thanks to MixColumns, our leakage model will no longer depend on a single byte at a time, making our CPA attack completely infeasible.*\n",
- "\n",
- "*In this lab, we'll examine and run a modified CPA attack, allowing our attack to succeed even with a MixColumns operation.*\n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "* Understanding how the modified CPA attack allows attacks across MixColumns\n",
- "* Using ChipWhisperer/SCARED to analyze traces\n",
- "\n",
- "**NOTE:** This lab requires [SCARED](https://github.com/eshard/scared), which may be difficult to install on some systems (noteably Windows). If you're unable to install SCARED, you'll need to skip this lab.\n",
- "\n",
- "Targets known to work with this leakage model:\n",
- "\n",
- "* STM32F4\n",
- "* STM32L4\n",
- "* STM32L5\n",
- "* K82F MMCAU\n",
- "* CW305 Default AES Implementation\n",
- "* MBEDTLS and TINYAES128C\n",
- "\n",
- "A target with hardware AES is recommended as they often require this attack instead of a simpler attack; however, software AES is also vulnerable to this attack (though no more than a normal CPA attack)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "To start with, let's take a look at an example AES implementation that the last round state diff we used in Lab 2_2 won't work for:\n",
- "\n",
- "\n",
- "\n",
- "AES operations are in orange, while registers are in purple. The mux before AddRoundKey allows the plaintext to be loaded at the beginning, as well as MixColumns to be skipped during the last round. As we can see, the demux after AddRoundKey stops the ciphertext from being put into the state register. Instead, if we work backwards from the ciphertext, we can see the two closest states will be the output of the 9th round and the output of the 8th round. The big issue that we run into is that MixColumns, as the name suggests, combines the 4 bytes an AES state column together. Up to that point, each byte was completely independent of one another, meaning we could also evaluate each byte independently, turning a $2^{128}$ search into 16 $2^8$ searches. Now, with MixColumns, we need to evaluate 4 bytes at a time, giving us 4 $2^{32}$ searches. \n",
- "\n",
- "Attacking from the plaintext side is no better - the MixColumns is still there. Does that mean if we cross a MixColumns in our leakage model, our attack is thwarted? Luckily, the answer to this is no! Instead of doing a full random plaintext like our usual attack, let's instead make all the bytes except for a column or row constant and we'll walk through the start of AES:\n",
- "\n",
- "$\n",
- "\\left[\\begin{array}{llll}\n",
- "v_0 & v_1 & v_2 & v_3 \\\\\n",
- "c_0 & c_3 & c_6 & c_9 \\\\\n",
- "c_1 & c_4 & c_7 & c_{10} \\\\\n",
- "c_2 & c_5 & c_8 & c_{11}\n",
- "\\end{array}\\right]$\n",
- "\n",
- "Thanks to shift rows, that will give us 3 constant bytes and 1 variable byte in each column (everything is $'$ here because of SubBytes):\n",
- "\n",
- "$\n",
- "\\left[\\begin{array}{llll}\n",
- "v'_0 & v'_1 & v'_2 & v'_3 \\\\\n",
- "c'_3 & c'_6 & c'_9 & c'_0 \\\\\n",
- "c'_7 & c'_{10} & c'_1 & c'_{4} \\\\\n",
- "c'_{11} & c'_2 & c'_5 & c'_{8}\n",
- "\\end{array}\\right]$\n",
- "\n",
- "MixColumns (on a single column) is the following operation ($v$ is the variable byte, $c_n$ are the constants). Note that the input of MixColumns is really just the output of SubBytes, our usual attack point, since ShiftRows doesn't modify the values, it just moves them:\n",
- "\n",
- "$\\left[\\begin{array}{l}\n",
- "d_{0} \\\\\n",
- "d_{1} \\\\\n",
- "d_{2} \\\\\n",
- "d_{3}\n",
- "\\end{array}\\right]=\\left[\\begin{array}{llll}\n",
- "2 & 3 & 1 & 1 \\\\\n",
- "1 & 2 & 3 & 1 \\\\\n",
- "1 & 1 & 2 & 3 \\\\\n",
- "3 & 1 & 1 & 2\n",
- "\\end{array}\\right]\\left[\\begin{array}{l}\n",
- "v'_0 \\\\\n",
- "c'_{3} \\\\\n",
- "c'_{7} \\\\\n",
- "c'_{11}\n",
- "\\end{array}\\right]$\n",
- "\n",
- "$d_0$, for example, will be:\n",
- "\n",
- "$d_0 = 2v'_0 + 3c'_3 + c'_7 + c'_{11}$\n",
- "\n",
- "We can combine the three constants into a single constant:\n",
- "\n",
- "$d_0 = 2v'_0 + c_a$\n",
- "\n",
- "It might not be immediately obvious, but this has actually taken our $2^{32}$ attack down to $2^{16}$! If $c_a$ wasn't there, this would pretty much just be our regular CPA attack, so doing a CPA attack for each possible value of $c_a$ should allow us to to recover the key! This constant we've combined everything into is actually extremely powerful: any constants XOR'd ($+$) with with the SBox output will completely disappear, meaning the AddRoundKey following MixColumns is also eliminated! It is due to this fact that this attack will work against many different target implementations, including software AES, STM32F4 style implementations (which was vulnerable to last round state diff), and many models resistant to last round state diff.\n",
- "\n",
- "While this seems very promising, it does suffer from a few issues:\n",
- "\n",
- "1. $2^{16}$ is still a fairly large search space, meaning our attack will take a very long time\n",
- "1. $c$ is only being XOR'd here, which means we'll run into ghost peaks\n",
- "1. This XOR also means the correct and incorrect values for $c$ will have very similar correlations.\n",
- "\n",
- "Instead, we can attack $v'$ one bit at a time. This is very similar to a single bit DPA attack, except we're still using correlation instead of the difference. Now the constant will only have the effect of inverting the correlation (if that constant bit is 1) or not inverting it (if that constant bit is 0). This means we can completely remove the effect of the constant by taking the absolute value of the correlation, taking us back to a $2^8$ attack! We also don't have to worry about the ghost peaks or the similar correlations for $c_a$.\n",
- "\n",
- "As you might expect, on its own this attack works very poorly - 1 bit is not enough information for a CPA attack. Much better is to perform the attack on each bit of $2v$ and sum the absolute correlations. This isn't the only spot $v$ shows up though! We also have the other $d$ values:\n",
- "\n",
- "$d_1 = v'_0 + 2c'_3 + 3c'_7 + c'_{11} = v'_0 + c_b$\n",
- "\n",
- "$d_2 = v'_0 + c'_3 + 2c'_7 + 3c'_{11} = v'_0 + c_c$\n",
- "\n",
- "$d_3 = 3v'_0 + c'_3 + c'_7 + 2c'_{11} = 3v'_0 + c_d$\n",
- "\n",
- "Adding in these brings us up to 32 CPA attacks. If we only needed Hamming weight, this would be pretty simple, with our leakage model being:\n",
- "\n",
- "`h = ((2*sbox(pt0 + key0)>>bit) & 0x01`\n",
- "\n",
- "Things get a bit trickier with the Hamming distance. The key can be incorporated into $c$ since it's constant. However, we need to line up the MixColumns output with the correct byte in the input. This is still simple with the top byte of each column ($d_0$ lines up with $p_0$, $d_1$ lines up with $p_1$, and so on), but later bytes get shifted by ShiftRows. For example, attacking $k_1$, $d_0$ will line up with $p_{12}$, $d_1$ will line up with $p_{13}$ and so on. They also have a different order in MixColumns - $p_0$ goes 2, 1, 1, 3, but $p_1$ goes 3, 2, 1, 1.\n",
- "\n",
- "Aside from being more complicated than our usual CPA attack, this attack also has the disadvantage of only targeting 4 key bytes per capture campaign, since the other bytes in the AES column have to be constant. It's also a fairly lengthy analysis since we need to do 256 CPA attacks to recover all 16 key bytes.\n",
- "\n",
- "As an aside, if we didn't need to go through ARK1, we could make a big optimization on the attack. We can actually calculate the constant values by using the sign of each correlation. These constants can then be used to calculate the corresponding key from a system of equations. This version of the attack is detailed in https://eprint.iacr.org/2019/343.pdf. This paper also details an optimization that would allow us to bring the attack down to a single capture campaign, as well as extensions to AES-192 and AES-256, though we won't take a look at either of these extensions in this lab.\n",
- "\n",
- "This attack was originally detailed in https://eprint.iacr.org/2016/249.pdf by Amir Moradi and Tobias Schneider. Many thanks to them for the theory behind this tutorial!"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Setup"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer as cw\n",
- "scope = cw.scope()\n",
- "target = cw.target(scope)\n",
- "scope.default_setup()\n",
- "\n",
- "## target specific setup after here..."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "SCOPETYPE = 'OPENADC'\n",
- "PLATFORM = 'CWLITEARM'\n",
- "CRYPTO_TARGET = 'MBEDTLS'\n",
- "VERSION='HARDWARE'\n",
- "SS_VER='SS_VER_2_0'"
- ]
- },
- {
- "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": "markdown",
- "metadata": {},
- "source": [
- "## Capture\n",
- "\n",
- "For capture, we need to do four separate capture campaigns to recover the key, since we're only targeting 4 bytes at a time. ChipWhisperer includes a special key-text pair to change out which parts of the plaintext are constant and which are variable."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import trange\n",
- "import cwtvla\n",
- "import numpy as np\n",
- "vec_type = 'row'\n",
- "ktp = cw.ktp.VarVec(vec_type=vec_type)\n",
- "key, pt = ktp.next()\n",
- "scope.adc.samples = 10000\n",
- "N = 500\n",
- "projects = []\n",
- "\n",
- "target.simpleserial_write('k', key)\n",
- "target.simpleserial_wait_ack()\n",
- "\n",
- "for cmpgn in trange(4):\n",
- " project = cw.create_project(f\"Var_Vec_{cmpgn}\", overwrite=True)\n",
- " projects.append(project)\n",
- " for i in trange(N, leave=False):\n",
- " ktp.var_vec = cmpgn\n",
- " key, text = ktp.next()\n",
- " trace = cw.capture_trace(scope, target, text, key)\n",
- " if trace is None:\n",
- " continue\n",
- " project.traces.append(trace)\n",
- " project.save()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%matplotlib notebook\n",
- "import matplotlib.pyplot as plt\n",
- "plt.figure()\n",
- "for i in range(5):\n",
- " plt.plot(projects[0].waves[i])\n",
- " \n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Analysis\n",
- "\n",
- "If you closed the project, reopen it now:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer as cw\n",
- "projects = []\n",
- "for i in range(4):\n",
- " project = cw.open_project(f\"Var_Vec_{i}\")\n",
- " projects.append(project)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "ChipWhisperer/SCARED will take care all of the analysis for us here. You can adjust the number of traces you use with `n_traces`. Windowing can also be very important with this attack. You can adjust the window you analyzer with `trace_slice`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from chipwhisperer.analyzer.attacks.attack_mix_columns import AttackMixColumns\n",
- "attack = AttackMixColumns(projects, vec_type=vec_type, hd=False)\n",
- "results = attack.run(n_traces=100, trace_slice=slice(200, 1200))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(bytearray(results[\"guess\"]))\n",
- "print(bytearray(projects[0].keys[0]))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's plot the correlations of the best guess/key or second best guess/key:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%matplotlib notebook\n",
- "import matplotlib.pyplot as plt\n",
- "import numpy as np\n",
- "plt.figure()\n",
- "for i in range(16):\n",
- " c = results[\"corr\"][i]\n",
- " maxes = np.max(c, axis=1)\n",
- " guess = np.argsort(maxes)[-1]\n",
- " guess2 = np.argsort(maxes)[-2]\n",
- " actual = projects[0].keys[0][i]\n",
- " x = np.argmax(c[actual])\n",
- " if guess != actual:\n",
- " plt.plot(c[guess], \"g-\")\n",
- " else:\n",
- " plt.plot(c[guess2], \"g-\")\n",
- " plt.plot(c[actual], \"r--\")\n",
- " plt.plot(x, c[actual][x], \"ro\")\n",
- " print(f\"Best guess {hex(guess)} (corr={maxes[guess]}), next best = {maxes[guess2]}, real = {maxes[actual]}\")\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Original Implementations\n",
- "\n",
- "Provided for reference."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%time\n",
- "import scared\n",
- "import numpy as np\n",
- "b = None\n",
- "gal2=np.array((\n",
- "0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,\n",
- "0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,\n",
- "0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,\n",
- "0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,\n",
- "0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,\n",
- "0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,\n",
- "0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,\n",
- "0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,\n",
- "0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,\n",
- "0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,\n",
- "0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,\n",
- "0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,\n",
- "0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,\n",
- "0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,\n",
- "0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,\n",
- "0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5), dtype='uint8')\n",
- "\n",
- "gal3=np.array((\n",
- "0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,\n",
- "0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,\n",
- "0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,\n",
- "0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,\n",
- "0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,\n",
- "0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,\n",
- "0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,\n",
- "0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,\n",
- "0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,\n",
- "0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,\n",
- "0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,\n",
- "0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,\n",
- "0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,\n",
- "0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,\n",
- "0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,\n",
- "0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a), dtype='uint8')\n",
- "\n",
- "\n",
- "\n",
- "w=0x00\n",
- "lut_input = [[0, 1, 2, 3],\n",
- " [4, 5, 6, 7],\n",
- " [8, 9, 10, 11],\n",
- " [12, 13, 14, 15]]\n",
- "\n",
- "def round_gen_0(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[0, 13, 10, 7],\n",
- " [4, 1, 14, 11],\n",
- " [8, 5, 2, 15],\n",
- " [12, 9, 6, 3]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(0, gal2[scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess))])\n",
- " return res\n",
- "\n",
- "def round_gen_1(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[1, 14, 11, 4],\n",
- " [5, 2, 15, 8],\n",
- " [9, 6, 3, 12],\n",
- " [13, 10, 7, 0]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(0, scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess)))\n",
- " return res\n",
- "\n",
- "def round_gen_2(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[2, 15, 8, 5],\n",
- " [6, 3, 12, 9],\n",
- " [10, 7, 0, 13],\n",
- " [14, 1, 4, 1]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(0, scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess)))\n",
- " return res\n",
- "\n",
- "def round_gen_3(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[3, 12, 9, 6],\n",
- " [7, 0, 13, 10],\n",
- " [11, 4, 14, 1],\n",
- " [15, 8, 5, 2]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(0, gal3[scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess))])\n",
- " return res\n",
- "\n",
- "leakage_cmpgns = []\n",
- "for campaign in range(4):\n",
- " leakage_cmpgns.append([scared.attack_selection_function(lambda plaintext, guesses: round_gen_0(plaintext, guesses, campaign)),\n",
- " scared.attack_selection_function(lambda plaintext, guesses: round_gen_1(plaintext, guesses, campaign)),\n",
- " scared.attack_selection_function(lambda plaintext, guesses: round_gen_2(plaintext, guesses, campaign)),\n",
- " scared.attack_selection_function(lambda plaintext, guesses: round_gen_3(plaintext, guesses, campaign))])"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import tqdm.autonotebook"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import estraces, scared\n",
- "import numpy as np\n",
- "from tqdm.notebook import trange\n",
- "#del cw_traces\n",
- "campaign = 0\n",
- "n_traces = 10000\n",
- "key_guess = []\n",
- "%matplotlib notebook\n",
- "import matplotlib.pyplot as plt\n",
- "plt.figure()\n",
- " \n",
- "\n",
- "\n",
- "for campaign in trange(0,4):\n",
- " b = None\n",
- " cw_traces = estraces.read_ths_from_ram(np.array(projects[campaign].waves)[:n_traces,:], \n",
- " plaintext=np.array([textin for textin in projects[campaign].textins], dtype='uint8')[:n_traces])\n",
- " #cw_traces = estraces.read_ths_from_ram(np.array(projects[campaign].waves)[:n_traces,550:900], \n",
- " # plaintext=np.array([textin for textin in projects[campaign].textins], dtype='uint8')[:n_traces])\n",
- " for t in trange(4, leave=False):\n",
- " for i in trange(8, leave=False):\n",
- " container = scared.Container(cw_traces)\n",
- " a = scared.CPAAttack(selection_function=leakage_cmpgns[campaign][t],\n",
- " model=scared.Monobit(i),\n",
- " discriminant=scared.maxabs)\n",
- "\n",
- "\n",
- " a.run(container)\n",
- " if b is None:\n",
- " b = abs(a.results)\n",
- " else:\n",
- " b += abs(a.results)\n",
- " for i in range(0+4*campaign, 4+4*campaign):\n",
- " c = np.nan_to_num(b[:,i,:])\n",
- " maxes = np.max(c, axis=1)\n",
- " guess = np.argsort(maxes)[-1]\n",
- " guess2 = np.argsort(maxes)[-2]\n",
- " actual = projects[0].keys[0][i]\n",
- " x = np.argmax(c[actual])\n",
- " if guess != actual:\n",
- " plt.plot(c[guess], \"g-\")\n",
- " else:\n",
- " plt.plot(c[guess2], \"g-\")\n",
- " plt.plot(c[actual], \"r--\")\n",
- " plt.plot(x, c[actual][x], \"ro\")\n",
- " print(f\"Best guess {hex(guess)} (corr={maxes[guess]}), next best = {maxes[guess2]}, real = {maxes[actual]}\")\n",
- " key_guess.append(guess)\n",
- "\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(bytearray(key_guess))\n",
- "print(bytearray(projects[0].keys[0]))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%matplotlib notebook\n",
- "import matplotlib.pyplot as plt\n",
- "plt.figure()\n",
- "plt.plot(c[0x16])\n",
- "plt.plot(c[0x7f])\n",
- " \n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%matplotlib notebook\n",
- "import matplotlib.pyplot as plt\n",
- "plt.figure()\n",
- "for i in range(256):\n",
- " plt.plot(c[i])\n",
- " \n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%time\n",
- "import scared\n",
- "import numpy as np\n",
- "gal2=np.array((\n",
- "0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,\n",
- "0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,\n",
- "0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,\n",
- "0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,\n",
- "0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,\n",
- "0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,\n",
- "0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,\n",
- "0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,\n",
- "0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,\n",
- "0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,\n",
- "0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,\n",
- "0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,\n",
- "0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,\n",
- "0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,\n",
- "0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,\n",
- "0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5), dtype='uint8')\n",
- "\n",
- "gal3=np.array((\n",
- "0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,\n",
- "0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,\n",
- "0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,\n",
- "0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,\n",
- "0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,\n",
- "0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,\n",
- "0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,\n",
- "0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,\n",
- "0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,\n",
- "0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,\n",
- "0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,\n",
- "0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,\n",
- "0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,\n",
- "0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,\n",
- "0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,\n",
- "0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a), dtype='uint8')\n",
- "\n",
- "\n",
- "\n",
- "w=0x00\n",
- "lut_input = [[0, 1, 2, 3],\n",
- " [4, 5, 6, 7],\n",
- " [8, 9, 10, 11],\n",
- " [12, 13, 14, 15]]\n",
- "\n",
- "def round_gen_0(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[0, 4, 8, 12],\n",
- " [13, 1, 5, 9],\n",
- " [10, 14, 2, 6],\n",
- " [7, 11, 15, 3]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(new_pt, gal2[scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess))])\n",
- " return res\n",
- "\n",
- "def round_gen_1(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[1, 5, 9, 13],\n",
- " [14, 2, 6, 10],\n",
- " [11, 15, 3, 7],\n",
- " [4, 8, 12, 0]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(new_pt, scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess)))\n",
- " return res\n",
- "\n",
- "def round_gen_2(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[2, 6, 10, 14],\n",
- " [15, 3, 7, 11],\n",
- " [8, 12, 0, 4],\n",
- " [5, 9, 13, 1]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(new_pt, scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess)))\n",
- " return res\n",
- "\n",
- "def round_gen_3(plaintext, guesses, cmpgn):\n",
- " lut_mix_column = [[3, 7, 11, 15],\n",
- " [12, 0, 4, 8],\n",
- " [9, 13, 1, 5],\n",
- " [6, 10, 14, 2]] # lut to find which pt to xor with mixcolumn output\n",
- " res = np.empty((plaintext.shape[0], len(guesses), plaintext.shape[1]), dtype='uint8')\n",
- " new_pt = np.repeat(plaintext[:,lut_mix_column[cmpgn][0]][:, np.newaxis], 16, axis=1)\n",
- " new_pt[:,lut_input[cmpgn][1]] = plaintext[:,lut_mix_column[cmpgn][1]]\n",
- " new_pt[:,lut_input[cmpgn][2]] = plaintext[:,lut_mix_column[cmpgn][2]]\n",
- " new_pt[:,lut_input[cmpgn][3]] = plaintext[:,lut_mix_column[cmpgn][3]]\n",
- " for i, guess in enumerate(guesses):\n",
- " res[:,i,:] = np.bitwise_xor(new_pt, gal3[scared.aes.sub_bytes(np.bitwise_xor(plaintext, guess))])\n",
- " return res\n",
- "\n",
- "leakage_cmpgns = []\n",
- "for campaign in range(4):\n",
- " leakage_cmpgns.append([scared.attack_selection_function(lambda plaintext, guesses: round_gen_0(plaintext, guesses, campaign)),\n",
- " scared.attack_selection_function(lambda plaintext, guesses: round_gen_1(plaintext, guesses, campaign)),\n",
- " scared.attack_selection_function(lambda plaintext, guesses: round_gen_2(plaintext, guesses, campaign)),\n",
- " scared.attack_selection_function(lambda plaintext, guesses: round_gen_3(plaintext, guesses, campaign))])"
- ]
- }
- ],
- "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/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb b/jupyter/lab/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb
deleted file mode 100644
index 145fe97b..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 3_1A - AES256 Bootloader Attack.ipynb
+++ /dev/null
@@ -1,1843 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 3, Topic 1, Lab A: AES256 Bootloader Attack (MAIN)"
- ]
- },
- {
- "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:** *Through the previous labs, we've gained a lot of tools to attack unknown embedded devices: SPA, DPA, CPA, trace resynchronization, and more. In this lab, we'll be using some of those techniques to break a more realistic target: a bootloader. Note that there are two versions of this lab. In this one (Lab A), we'll proceed with a lot of knowledge about how the bootloader is working. In a real scenario, we may or may not be able to get that information from a datasheet. In Lab B, we'll do some reverse engineering work to figure this stuff out on our own. It's up to you whether you want to run this lab or Lab B first!*\n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "\n",
- "* Extending AES128 attacks to AES256\n",
- "* Using DPA to recover an initialization vector\n",
- "* Using a timing attack (via power analysis) to recover a signature"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This tutorial will take you through a complete attack on an encrypted bootloader using AES-256. This demonstrates how to use side-channel power analysis on practical systems, along with discussing how to perform analysis with different Analyzer models."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "SCOPETYPE = 'OPENADC'\n",
- "PLATFORM = 'CWLITEARM'"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Background"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In the world of microcontrollers, a bootloader is a special piece of firmware that is made to let the user upload new programs into memory. This is especially useful for devices with complex code that may need to be patched or otherwise updated in the future - a bootloader makes it possible for the user to upload a patched version of the firmware onto the micro. The bootloader receives information from a communication line (a USB port, serial port, ethernet port, WiFi connection, etc...) and stores this data into program memory. Once the full firmware has been received, the micro can happily run its updated code.\n",
- "\n",
- "There is one big security issue to worry about with bootloaders. A company may want to stop their customers from writing their own firmware and uploading it onto the micro. For example, this might be for protection reasons - hackers might be able to access parts of the device that weren't meant to be accessed. One way of stopping this is to add encryption. The company can add their own secret signature to the firmware code and encrypt it with a secret key. Then, the bootloader can decrypt the incoming firmware and confirm that the incoming firmware is correctly signed. Users will not know the secret key or the signature tied to the firmware, so they won't be able to \"fake\" their own.\n",
- "\n",
- "This tutorial will work with a simple AES-256 bootloader. The victim will receive data through a serial connection, decrypt the command, and confirm that the included signature is correct. Then, it will only save the code into memory if the signature check succeeded. To make this system more robust against attacks, the bootloader will use cipher-block chaining (CBC mode). Our goal is to find the secret key and the CBC initialization vector so that we could successfully fake our own firmware."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Bootloader Communications Protocol"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The bootloader's communications protocol operates over a serial port at 38400 baud rate. The bootloader is always waiting for new data to be sent in this example; in real life one would typically force the bootloader to enter through a command sequence.\n",
- "\n",
- "Commands sent to the bootloader look as follows:\n",
- "\n",
- "```\n",
- " |<-------- Encrypted block (16 bytes) ---------->|\n",
- " | |\n",
- "+------+------+------+------+------+------+ .... +------+------+------+\n",
- "| 0x00 | Signature (4 Bytes) | Data (12 Bytes) | CRC-16 |\n",
- "+------+------+------+------+------+------+ .... +------+------+------+\n",
- "```\n",
- "\n",
- "This frame has four parts:\n",
- "\n",
- "* `0x00`: 1 byte of fixed header\n",
- "* Signature: A secret 4 byte constant. The bootloader will confirm that this signature is correct after decrypting the frame.\n",
- "* Data: 12 bytes of the incoming firmware. This system forces us to send the code 12 bytes at a time; more complete bootloaders may allow longer variable-length frames.\n",
- "* CRC-16: A 16-bit checksum using the CRC-CCITT polynomial (0x1021). The LSB of the CRC is sent first, followed by the MSB. The bootloader will reply over the serial port, describing whether or not this CRC check was valid.\n",
- "\n",
- "As described in the diagram, the 16 byte block is not sent as plaintext. Instead, it is encrypted using AES-256 in CBC mode. This encryption method will be described in the next section.\n",
- "\n",
- "The bootloader responds to each command with a single byte indicating if the CRC-16 was OK or not:\n",
- "\n",
- "```\n",
- " +------+\n",
- "CRC-OK: | 0xA1 |\n",
- " +------+\n",
- "\n",
- " +------+\n",
- "CRC Failed: | 0xA4 |\n",
- " +------+\n",
- "```\n",
- "Then, after replying to the command, the bootloader veries that the signature is correct. If it matches the expected manufacturer's signature, the 12 bytes of data will be written to flash memory. Otherwise, the data is discarded."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Details of AES-256 CBC"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The system uses the AES algorithm in Cipher Block Chaining (CBC) mode. In general one avoids using encryption 'as-is' (i.e. Electronic Code Book), since it means any piece of plaintext always maps to the same piece of ciphertext. Cipher Block Chaining ensures that if you encrypted the same thing a bunch of times it would always encrypt to a new piece of ciphertext.\n",
- "\n",
- "You can see another reference on the design of the encryption side; we'll be only talking about the decryption side here. In this case AES-256 CBC mode is used as follows, where the details of the AES-256 Decryption block will be discussed in detail later:\n",
- "\n",
- "\n",
- "\n",
- "This diagram shows that the output of the decryption is no longer used directly as the plaintext. Instead, the output is XORed with a 16 byte mask, which is usually taken from the previous ciphertext. Also, the first decryption block has no previous ciphertext to use, so a secret initialization vector (IV) is used instead. If we are going to decrypt the entire ciphertext (including block 0) or correctly generate our own ciphertext, we'll need to find this IV along with the AES key."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Attacking AES-256"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The system in this tutorial uses AES-256 encryption, which has a 256 bit (32 byte) key - twice as large as the 16 byte key we've attacked in previous tutorials. This means that our regular AES-128 CPA attacks won't quite work. However, extending these attacks to AES-256 is fairly straightforward: the theory is explained in detail in [Extending AES-128 Attacks to AES-256](Extending%20AES-128%20Attacks%20to%20AES-256.ipynb).\n",
- "\n",
- "As the theory page explains, our AES-256 attack will have 4 steps:\n",
- "\n",
- "1. Perform a standard attack (as in AES-128 decryption) to determine the first 16 bytes of the key, corresponding to the 14th round encryption key.\n",
- "1. Using the known 14th round key, calculate the hypothetical outputs of each S-Box from the 13th round using the ciphertext processed by the 14th round, and determine the 16 bytes of the 13th round key manipulated by inverse MixColumns.\n",
- "1. Perform the MixColumns and ShiftRows operation on the hypothetical key determined above, recovering the 13th round key.\n",
- "1. Using the AES-256 key schedule, reverse the 13th and 14th round keys to determine the original AES-256 encryption key."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Firmware"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For this tutorial, we'll be using the `bootloader-aes256` project, which we'll build as usual:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%bash -s \"$PLATFORM\" \n",
- "cd ../../../firmware/mcu/bootloader-aes256\n",
- "make PLATFORM=$1 CRYPTO_TARGET=NONE"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Capturing Traces"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Setup"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "To start, we'll proceed with setup as usual:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%run \"../../Setup_Scripts/Setup_Generic.ipynb\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "fw_path = \"../../../firmware/mcu/bootloader-aes256/bootloader-aes256-{}.hex\".format(PLATFORM)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.program_target(scope, prog, fw_path)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Calculating the CRC"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The next step we'll need to take in attacking this target is to communicate with it. Most of the transmission is fairly straight forward, but the CRC is a little tricky. Luckily, there's a lot of open source out there for calculating CRCs. In this case, we'll pull some code from pycrc:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Class Crc\n",
- "#############################################################\n",
- "# These CRC routines are copy-pasted from pycrc, which are:\n",
- "# Copyright (c) 2006-2013 Thomas Pircher \n",
- "#\n",
- "class Crc(object):\n",
- " \"\"\"\n",
- " A base class for CRC routines.\n",
- " \"\"\"\n",
- "\n",
- " def __init__(self, width, poly):\n",
- " \"\"\"The Crc constructor.\n",
- "\n",
- " The parameters are as follows:\n",
- " width\n",
- " poly\n",
- " reflect_in\n",
- " xor_in\n",
- " reflect_out\n",
- " xor_out\n",
- " \"\"\"\n",
- " self.Width = width\n",
- " self.Poly = poly\n",
- "\n",
- "\n",
- " self.MSB_Mask = 0x1 << (self.Width - 1)\n",
- " self.Mask = ((self.MSB_Mask - 1) << 1) | 1\n",
- "\n",
- " self.XorIn = 0x0000\n",
- " self.XorOut = 0x0000\n",
- "\n",
- " self.DirectInit = self.XorIn\n",
- " self.NonDirectInit = self.__get_nondirect_init(self.XorIn)\n",
- " if self.Width < 8:\n",
- " self.CrcShift = 8 - self.Width\n",
- " else:\n",
- " self.CrcShift = 0\n",
- "\n",
- " def __get_nondirect_init(self, init):\n",
- " \"\"\"\n",
- " return the non-direct init if the direct algorithm has been selected.\n",
- " \"\"\"\n",
- " crc = init\n",
- " for i in range(self.Width):\n",
- " bit = crc & 0x01\n",
- " if bit:\n",
- " crc ^= self.Poly\n",
- " crc >>= 1\n",
- " if bit:\n",
- " crc |= self.MSB_Mask\n",
- " return crc & self.Mask\n",
- "\n",
- "\n",
- " def bit_by_bit(self, in_data):\n",
- " \"\"\"\n",
- " Classic simple and slow CRC implementation. This function iterates bit\n",
- " by bit over the augmented input message and returns the calculated CRC\n",
- " value at the end.\n",
- " \"\"\"\n",
- " # If the input data is a string, convert to bytes.\n",
- " if isinstance(in_data, str):\n",
- " in_data = [ord(c) for c in in_data]\n",
- "\n",
- " register = self.NonDirectInit\n",
- " for octet in in_data:\n",
- " for i in range(8):\n",
- " topbit = register & self.MSB_Mask\n",
- " register = ((register << 1) & self.Mask) | ((octet >> (7 - i)) & 0x01)\n",
- " if topbit:\n",
- " register ^= self.Poly\n",
- "\n",
- " for i in range(self.Width):\n",
- " topbit = register & self.MSB_Mask\n",
- " register = ((register << 1) & self.Mask)\n",
- " if topbit:\n",
- " register ^= self.Poly\n",
- "\n",
- " return register ^ self.XorOut\n",
- " \n",
- "bl_crc = Crc(width = 16, poly=0x1021)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now we can easily get the CRC for our message by calling `bl_crc.bit_by_bit(message)`. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Communicating with the Bootloader"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "With that done, we can start communicating with the bootloader. The following block will ensure that the target has finished booting:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import time\n",
- "okay = 0\n",
- "reset_target(scope)\n",
- "\n",
- "while not okay:\n",
- " target.write('\\0xxxxxxxxxxxxxxxxxx')\n",
- " time.sleep(0.05)\n",
- " response = target.read()\n",
- " if response:\n",
- " print(response)\n",
- " if ord(response[0]) == 0xA1:\n",
- " okay = 1"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Recall that the bootloader expects:\n",
- "\n",
- "* To start with `0x00`\n",
- "* A 16 byte encrypted message (4 bytes signature + 12 bytes data)\n",
- "* CRC16\n",
- "\n",
- "We don't really care what the 16 byte message is (just that each is different so that we get a variety of hamming weights), so we'll use the same text/key module from earlier attacks.\n",
- "\n",
- "We can now run the following block, and we should get `0xA4` back. You may need to run this block a few times to get the right response back."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import time\n",
- "message = [0x00]\n",
- "ktp = cw.ktp.Basic()\n",
- "\n",
- "# clear serial buffer\n",
- "print(target.read())\n",
- "\n",
- "key, text = ktp.next() #don't care about key here\n",
- "message.extend(text)\n",
- "\n",
- "crc = bl_crc.bit_by_bit(text)\n",
- "\n",
- "message.append(crc >> 8)\n",
- "message.append(crc & 0xFF)\n",
- "\n",
- "target.write(message)\n",
- "time.sleep(0.1)\n",
- "\n",
- "response = target.read()\n",
- "print(\"Response: {:02X}\".format(ord(response[0])))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Capturing Traces"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "With that out of the way, we can proceed to capturing our traces. The normal 5000 traces we capture isn't long enough to get the rounds we care about, so we'll need to increase it (15000 should be fine):"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.adc.samples = 15000"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We'll be working with Analyzer, so we'll need to use a ChipWhisperer project to store our traces and text:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "project = cw.create_project(\"projects/Tutorial_A5\", overwrite=True)\n",
- "ktp = cw.ktp.Basic()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Below you'll find our capture loop. This will be pretty similar to Tutorial B5, but we've added our communication code. We also check the response and just skip the data we get if it isn't correct."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#Capture Traces\n",
- "from tqdm.notebook import trange\n",
- "import numpy as np\n",
- "import time\n",
- "N = 200 # Number of traces\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " message = [0x00]\n",
- " target.read()\n",
- " \n",
- " key, text = ktp.next()\n",
- " message.extend(text)\n",
- " \n",
- " crc = bl_crc.bit_by_bit(text)\n",
- " message.append(crc >> 8)\n",
- " message.append(crc & 0xFF)\n",
- "\n",
- " scope.arm()\n",
- "\n",
- " target.write(message)\n",
- " \n",
- " ret = scope.capture()\n",
- " if ret:\n",
- " print('Timeout happened during acquisition')\n",
- " response = target.read()\n",
- " if ord(response[0]) != 0xA4:\n",
- " # Bad response, just skip\n",
- " print(\"Bad response: {:02X}\".format(ord(response[0])))\n",
- " continue\n",
- " \n",
- " project.traces.append(cw.Trace(scope.get_last_trace(), text, \"\", key))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Analysis"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now that we have our traces, we can go ahead and perform the attack. As described in the background theory, we'll have to do two attacks - one to get the 14th round key, and another (using the first result) to get the 13th round key. Then, we'll do some post-processing to finally get the 256 bit encryption key."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### 14th Round Key"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can attack the 14th round key with a standard, no-frills CPA attack (using the inverse sbox, since it's a decryption that we're breaking):"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer as cw\n",
- "import chipwhisperer.analyzer as cwa\n",
- "\n",
- "leak_model = cwa.leakage_models.inverse_sbox_output\n",
- "\n",
- "attack = cwa.cpa(project, leak_model)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "With the setup done, we can actually preform the attack. 11000 samples is a rather large amount to chew through, so if you want a faster attack you can use a smaller range in `attack.point_range`. `(2900, 4200)` will work for XMEGA, while `(1400, 2600)` will work for the STM32F3 (CWLite ARM)."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "key = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]\n",
- "\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "if PLATFORM == \"CWLITEARM\" or PLATFORM == \"CW308_STM32F3\":\n",
- " attack.point_range = [1400, 2600]\n",
- "elif PLATFORM == \"CWLITEXMEGA\" or PLATFORM == \"CW303\":\n",
- " pass\n",
- "attack_results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rec_key = []\n",
- "for bnum in attack_results.find_maximums():\n",
- " rec_key.append(bnum[0][0])\n",
- " print(\"Best Guess = 0x{:02X}, Corr = {}\".format(bnum[0][0], bnum[0][2]))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### 13th Round Key"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Analyzer doesn't have a leakage model for the 13th round key built in, so we'll need to create our own. Let's work through this now. To make a new model, we start off by inheriting the `AESLeakageHelper` class. We need to make a `leakage()` method that calculates the Hamming weight we use in the CPA attack. To get you started:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "class AES256_Round13_Model(cwa.AESLeakageHelper):\n",
- " def leakage(self, pt, ct, guess, bnum):\n",
- " #You must put YOUR recovered 14th round key here - this example may not be accurate!\n",
- " calc_round_key = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]\n",
- " state = reverse_round_14(self, pt, calc_round_key)\n",
- " state = reverse_round_13(self, state) #reverse state just before inv_subbytes\n",
- " return self.inv_sbox(state[bnum] ^ guess[bnum])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now we just need to make the `reverse_round_14()` and `reverse_round_13()` functions. By passing the class in, we get access to `self.inv_shiftrows()`, `self.inv_subbytes()`, and `self.inv_mixcolumns()`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def reverse_round_14(self, pt, key):\n",
- " state = [pt[i] ^ key[i] for i in range(16)] #AddRoundKey\n",
- " state = self.inv_shiftrows(state)\n",
- " state = self.inv_subbytes(state)\n",
- " return state # we're now at the end of decryption round 1\n",
- "\n",
- "def reverse_round_13(self, state):\n",
- " state = self.inv_mixcolumns(state)\n",
- " state = self.inv_shiftrows(state)\n",
- " return state"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can now make a new leakage model, and set our attack to use it instead of the other one:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "leak_model = cwa.leakage_models.new_model(AES256_Round13_Model)\n",
- "attack.leak_model = leak_model"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### Resyncing Traces (XMEGA Only)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The traces for the XMEGA version of the firmware become desynced around sample 7000. This is due to a non-constant AES implementation: the code does not always take the same amount of time to run for every input. (It's actually possible to do a timing attack on this AES implementation! We'll stick with our CPA attack for now.)\n",
- "\n",
- "While this does open up a timing attack, it actually makes our AES attack a little harder, since we'll have to resync the traces. Luckily, this can be done pretty easily by using the ResyncSAD preprocessing module:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "if PLATFORM == \"CWLITEXMEGA\" or PLATFORM == \"CW303\":\n",
- " resync_traces = cwa.preprocessing.ResyncSAD(project)\n",
- " resync_traces.enabled = True\n",
- " resync_traces.ref_trace = 0\n",
- " resync_traces.target_window = (9100, 9300)\n",
- " resync_traces.max_shift = 200\n",
- " attack.change_project(resync_traces.preprocess())"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### Running the Attack"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Like in the 14th round attack, we can use a smaller range of points to make the attack faster. `(8000,10990)` works well for the XMEGA, while `(6500, 8500)` works well for the STM32F3."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "if PLATFORM == \"CWLITEARM\" or PLATFORM == \"CW308_STM32F3\":\n",
- " attack.point_range = [6500,8500]\n",
- "elif PLATFORM == \"CWLITEXMEGA\" or PLATFORM == \"CW303\":\n",
- " attack.point_range = [8000,10990]\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "attack_results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You can run the block below and the correct key should be printed out:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rec_key2 = []\n",
- "for bnum in attack_results.find_maximums():\n",
- " print(\"Best Guess = 0x{:02X}, Corr = {}\".format(bnum[0][0], bnum[0][2]))\n",
- " rec_key2.append(bnum[0][0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This, however, isn't actually the 13th round key. To get the real 13th round key, we'll need to run what we've recovered through a `shiftrows()` and `mixcolumns()` operation:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "real_key2 = cwa.aes_funcs.shiftrows(rec_key2)\n",
- "real_key2 = cwa.aes_funcs.mixcolumns(real_key2)\n",
- "\n",
- "print(\"Recovered:\", end=\"\")\n",
- "for subkey in real_key2:\n",
- " print(\" {:02X}\".format(subkey), end=\"\")\n",
- "print(\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We now have everything we need to recover the full key! We'll start by combining the 13th and 14th round keys:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rec_key_comb = real_key2.copy()\n",
- "rec_key_comb.extend(rec_key)\n",
- "\n",
- "print(\"Key:\", end=\"\")\n",
- "for subkey in rec_key_comb:\n",
- " print(\" {:02X}\".format(subkey), end=\"\")\n",
- "print(\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "and then we can use the `AES128_8bit` leakage model to recover the first two rounds:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "btldr_key = leak_model.key_schedule_rounds(rec_key_comb, 13, 0)\n",
- "btldr_key.extend(leak_model.key_schedule_rounds(rec_key_comb, 13, 1))\n",
- "print(\"Key:\", end=\"\")\n",
- "for subkey in btldr_key:\n",
- " print(\" {:02X}\".format(subkey), end=\"\")\n",
- "print(\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You should see a 32 byte key printed out. Open `supersecret.h`, confirm that we have the right key, and celebrate! "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Recovering the IV"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now that we have the encryption key, we can proceed onto an attack of the next secret value: the IV.\n",
- "\n",
- "Here, we have the luxury of seeing the source code of the bootloader. This is generally not something we would have access to in the real world, so we'll try not to use it to cheat. (peeking at `supersecret.h` counts as cheating). Instead, we'll use the source to help us identify important parts of the power traces."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Bootloader Source Code"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Inside the bootloader's main loop, it does three tasks that we're interested in:\n",
- "\n",
- "* it decrypts the incoming ciphertext;\n",
- "* it applies the IV to the decryption's result; and\n",
- "* it checks for the signature in the resulting plaintext.\n",
- "\n",
- "This snippet from `bootloader.c` shows all three of the tasks:\n",
- "\n",
- "```C\n",
- "// Continue with decryption\n",
- "trigger_high(); \n",
- "aes256_decrypt_ecb(&ctx, tmp32);\n",
- "trigger_low();\n",
- " \n",
- "// Apply IV (first 16 bytes)\n",
- "for (i = 0; i < 16; i++){\n",
- " tmp32[i] ^= iv[i];\n",
- "}\n",
- "\n",
- "//Save IV for next time from original ciphertext \n",
- "for (i = 0; i < 16; i++){\n",
- " iv[i] = tmp32[i+16];\n",
- "}\n",
- "\n",
- "// Tell the user that the CRC check was okay\n",
- "putch(COMM_OK);\n",
- "putch(COMM_OK);\n",
- "\n",
- "//Check the signature\n",
- "if ((tmp32[0] == SIGNATURE1) &&\n",
- " (tmp32[1] == SIGNATURE2) &&\n",
- " (tmp32[2] == SIGNATURE3) &&\n",
- " (tmp32[3] == SIGNATURE4)){\n",
- " \n",
- " // Delay to emulate a write to flash memory\n",
- " _delay_ms(1);\n",
- "} \n",
- "```\n",
- "\n",
- "This gives us a pretty good idea of how the microcontroller is going to do its job, but if you'd like to go further, you can open the `.lss` file for the binary that was built. This is called a listing file and it lets you see the assembly that the C was compiled and linked to."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Power Traces"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you can see from both files, after the decryption process, the bootloader executes a few distinct pieces of code:\n",
- "\n",
- "* To apply the IV, it uses an XOR operation;\n",
- "* To store the new IV, it copies the previous ciphertext into the IV array;\n",
- "* It sends two bytes on the serial port;\n",
- "* It checks the bytes of the signature one by one.\n",
- "\n",
- "We should be able to recognize these four parts of the code in the power traces. Let's modify our capture routine to find them:\n",
- "\n",
- "1. We're looking for the original IV, but it's overwritten after each successful decryption. This means we'll have to reset the target before each trace we capture\n",
- "1. We'd like to skip over all of the decryption process. Recall that the trigger pin is set low after the decryption finishes. This means we can skip over the AES-256 function by triggering on a falling edge instead\n",
- "1. Depending on the target, we may have to flush the target's serial lines by sending it a bunch of invalid data and looking for a bad CRC return. This slows down the capture process by a lot, so you may want to try without doing this first.\n",
- "1. We won't need as many samples, so we can reduce how many we capture. 3000 should be sufficient for most targets.\n",
- "\n",
- "Let's start by reducing our samples and making a function to reset our target (depending on your target, you may need to change the reset pin):"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import time\n",
- "scope.adc.samples = 3000"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can trigger on a falling edge by changing `scope.adc.basic_mode` to `\"falling_edge\"`:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.adc.basic_mode = \"falling_edge\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can flush the serial line by sending an invalid message, then checking for a bad CRC return value (`0xA1`). Let's make sure our changes work by getting a trace:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from bokeh.plotting import figure, show\n",
- "from bokeh.io import output_notebook\n",
- "reset_target(scope)\n",
- "message = [0x00]\n",
- "\n",
- "target.read()\n",
- "\n",
- "key, text = ktp.new_pair() # manual creation of a key, text pair can be substituted here\n",
- "\n",
- "message.extend(text)\n",
- "\n",
- "crc = bl_crc.bit_by_bit(text)\n",
- "message.append(crc >> 8)\n",
- "message.append(crc & 0xFF)\n",
- "\n",
- "scope.arm()\n",
- "\n",
- "okay = 0\n",
- "while not okay:\n",
- " target.write(message)\n",
- " time.sleep(0.005)\n",
- " response = target.read()\n",
- " if response:\n",
- " if ord(response[0]) == 0xA4:\n",
- " okay = 1\n",
- "ret = scope.capture()\n",
- "\n",
- "trace = scope.get_last_trace()\n",
- "\n",
- "output_notebook()\n",
- "p = figure()\n",
- "\n",
- "xrange = list(range(len(trace)))\n",
- "p.line(xrange, trace, line_color=\"red\")\n",
- "show(p)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You should see 5 different sections:\n",
- "\n",
- "* 16 XORs\n",
- "* 16 register loads (this is the new IV being copied over)\n",
- "* Some serial communication\n",
- "* The signature check\n",
- "* The serial line going idle\n",
- "\n",
- "Different targets have different power traces (for example, on Arm the XORs and register loads are almost identical), but hopefully you can pick out where each section is. For example, on XMEGA:\n",
- "\n",
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "With all of these things clearly visible, we have a pretty good idea of how to attack the IV and the signature. We should be able to look at each of the XOR spikes to find each of the IV bytes - each byte is processed on its own. Then, the signature check uses a short-circuiting comparison: as soon as it finds a byte in error, it stops checking the remaining bytes. This type of check is susceptible to a timing attack."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "With those things done, we can move onto our capture loop. It's pretty similar to our last one. We're done with Analyzer, so we can store our traces in Python lists (we'll convert to numpy arrays later for easy analysis)."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import trange\n",
- "import numpy as np\n",
- "import time\n",
- "traces = []\n",
- "keys = []\n",
- "plaintexts = []\n",
- "\n",
- "if PLATFORM == \"CWLITEARM\" or PLATFORM == \"CW308_STM32F3\":\n",
- " N = 500 # Number of traces\n",
- "elif PLATFORM == \"CWLITEXMEGA\" or PLATFORM == \"CW303\":\n",
- " N=5000 #oof DPA attacks on XMEGA\n",
- "for i in trange(N, desc='Capturing traces'):\n",
- " reset_target(scope)\n",
- " message = [0x00]\n",
- " \n",
- " target.read()\n",
- " \n",
- " key, text = ktp.new_pair() # manual creation of a key, text pair can be substituted here\n",
- " keys.append(key)\n",
- " plaintexts.append(text)\n",
- " \n",
- " message.extend(text)\n",
- " \n",
- " crc = bl_crc.bit_by_bit(text)\n",
- " message.append(crc >> 8)\n",
- " message.append(crc & 0xFF)\n",
- " scope.arm()\n",
- " \n",
- " \n",
- " okay = 0\n",
- " passes = 0\n",
- " while not okay:\n",
- " target.write(message)\n",
- " time.sleep(0.01)\n",
- " response = target.read()\n",
- " passes += 1\n",
- " if response:\n",
- " if ord(response[0]) == 0xA4:\n",
- " okay = 1\n",
- " elif passes >= 100:\n",
- " break\n",
- " ret = scope.capture()\n",
- " if ret:\n",
- " print('Timeout happened during acquisition')\n",
- " continue\n",
- " \n",
- " traces.append(scope.get_last_trace())"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Analysis"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### Attack Theory"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The bootloader applies the IV to the AES decryption result (`DR`) by calculating\n",
- "\n",
- "\n",
- "$\\text{PT} = \\text{DR} \\oplus \\text{IV}$\n",
- "\n",
- "where DR is the decrypted ciphertext, IV is the secret vector, and PT is the plaintext that the bootloader will use later. We only have access to one of these: since we know the AES-256 key, we can calculate DR. This exclusive or will be visible in the power traces.\n",
- "\n",
- "This is enough information for us to attack a single bit of the IV. Suppose we only wanted to get the first bit (bit 0) of the first byte (byte 0) of the IV. We could do the following:\n",
- "\n",
- "* Split all of the traces into two groups: those with `(DR[0] & 0x01) = 0`, and those with `(DR[0] & 0x01) = 1`. \n",
- "* Calculate the average trace for both groups.\n",
- "* Find the difference between the two averages. Provided we've got sufficient data, we should see a spike where the xor is occuring.\n",
- "* Look at the direction of the spike to decide if the IV bit is 0 `(PT[0] = DR[0])` or if the IV bit is 1 `(PT[0] = ~DR[0])`.\n",
- "\n",
- "This is effectively a DPA attack on a single bit of the IV. We can repeat this attack across the whole IV by instead separating by `(DR[byte] & (1 << bit) = 0` and `(DR[byte] & (1 << bit) = bit`."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### Finding the XOR"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Recall that we're looking for the xor operation between the last decrypted block (`DR`), so we'll need to decrypt it up to that point. PyCryptoDome includes an AES decryption routine, so we'll be using that. We'll start by importing the necessary modules and converting our traces/plaintext to numpy arrays:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from Crypto.Cipher import AES\n",
- "import numpy as np\n",
- "\n",
- "trace_array = np.asarray(traces) # if you prefer to work with numpy array for number crunching\n",
- "textin_array = np.asarray(plaintexts)\n",
- "\n",
- "numTraces = len(trace_array)\n",
- "traceLen = len(trace_array[0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next we'll do the AES256 decryption. If you got a different key in the earlier part, you'll need to change `knownkey`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "knownkey = [0x94, 0x28, 0x5D, 0x4D, 0x6D, 0xCF, 0xEC, 0x08, 0xD8, 0xAC, 0xDD, 0xF6, 0xBE, 0x25, 0xA4, 0x99,\n",
- " 0xC4, 0xD9, 0xD0, 0x1E, 0xC3, 0x40, 0x7E, 0xD7, 0xD5, 0x28, 0xD4, 0x09, 0xE9, 0xF0, 0x88, 0xA1]\n",
- "\n",
- "knownkey = bytes(knownkey)\n",
- "dr = []\n",
- "aes = AES.new(knownkey, AES.MODE_ECB)\n",
- "for i in range(numTraces):\n",
- " ct = bytes(textin_array[i])\n",
- " pt = aes.decrypt(ct)\n",
- " d = [bytearray(pt)[i] for i in range(16)]\n",
- " dr.append(d)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now that we have `DR`, we can move on to the next step of our attack plan by separating our traces. We'll do this for each bit of the first byte, since it'll make it easier to identify the xor operation when the time comes:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "grouped_byte_traces = []\n",
- "byte = 0\n",
- "\n",
- "for bit in range(8):\n",
- " grouped_bit_traces = [], []\n",
- " for i in range(numTraces):\n",
- " if (dr[i][byte] & (1 << bit)):\n",
- " grouped_bit_traces[0].append(trace_array[i])\n",
- " else:\n",
- " grouped_bit_traces[1].append(trace_array[i])\n",
- " grouped_byte_traces.append(grouped_bit_traces)\n",
- " print(len(grouped_bit_traces[0]))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If you have 1000 traces, you should expect this to print a number around 500 - roughly half of the traces should fit into each group. Now, NumPy's average function lets us easily calculate the average at each point:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Find averages and differences\n",
- "diffs = []\n",
- "for i in range(8):\n",
- " means = np.average(grouped_byte_traces[i][0], axis=0), np.average(grouped_byte_traces[i][1], axis=0)\n",
- " diffs.append(means[1] - means[0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Finally, we'll need to locate the XOR operation for the first IV byte. Since each bit is being XORed in the same operation, each should have a spike in the same spot. Therefore, if we plot all the bits, there should be a single spot where every bit has a spike:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Split traces into 2 groups\n",
- "from bokeh.plotting import figure, show\n",
- "from bokeh.io import output_notebook\n",
- "\n",
- "output_notebook()\n",
- "p = figure()\n",
- "\n",
- "xrange = list(range(len(diffs[0])))\n",
- "xrange2 = list(range(len(traces[0])))\n",
- "colours = [\"red\", \"blue\", \"green\", \"black\"]\n",
- "for i in range(8):\n",
- " p.line(xrange, diffs[i], line_color=colours[i%4])\n",
- "show(p)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If you zoom into the beginning of the plot, you should be able to locate a few potential locations for the XOR. We could manually look through the plot to find potential XOR locations, but we can make our lives a little easier by selecting potential locations by each diff having a spike in that location. We'll select a location based on the value of the largest spike divided by 3. This isn't a rigourous selection: if you get an overwhelming number of points, try increasing the threshold. If you don't get any, reduce the threshold."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def find_potential_xors(diffs):\n",
- " threshold = np.max(abs(np.array(diffs))) / 3\n",
- " print(threshold)\n",
- " interesting_pts = []\n",
- " for pt in range(len(diffs[0][:1400])):\n",
- " interesting = True\n",
- " for bit in range(8):\n",
- " if abs(diffs[bit][pt]) < threshold:\n",
- " interesting = False\n",
- " if interesting:\n",
- " interesting_pts.append(pt)\n",
- " return interesting_pts"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "find_potential_xors(diffs)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We're almost there! To narrow the selection further, we can find interesting points of a few other bytes as well. Since the same operation is being performed on each byte, there should be a constant offset between the spikes of different bytes. Let's repeat the process on a few different bytes and see what we get:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def get_diffs(byte):\n",
- " grouped_byte_traces = []\n",
- "\n",
- " for bit in range(8):\n",
- " grouped_bit_traces = [], []\n",
- " for i in range(numTraces):\n",
- " if (dr[i][byte] & (1 << bit)):\n",
- " grouped_bit_traces[0].append(trace_array[i])\n",
- " else:\n",
- " grouped_bit_traces[1].append(trace_array[i])\n",
- " grouped_byte_traces.append(grouped_bit_traces)\n",
- " \n",
- " # Find averages and differences\n",
- " diffs = []\n",
- " for i in range(8):\n",
- " means = np.average(grouped_byte_traces[i][0], axis=0), np.average(grouped_byte_traces[i][1], axis=0)\n",
- " diffs.append(means[1] - means[0])\n",
- " return diffs\n",
- "\n",
- "for i in range(5):\n",
- " diffs = get_diffs(i)\n",
- " print(find_potential_xors(diffs))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Again, you might have to adjust the threshold based on how many values you get. Try to get a few bytes that only have a few potential values.\n",
- "\n",
- "Now that you have some peak data, you'll want to use this to find the time shift between XORs. This time shift should be constant between samples and needs to work for all samples (each run through the loop is the same, so it makes sense that the time shift should be constant). We'd also expect that the difference between the peaks would be a multiple of whole clock cycles. We're sampling at x4 the devices clock, so the offset should be divisible by 4. For example, you might have:\n",
- "\n",
- "```\n",
- "0th byte @ 41, 42\n",
- "1st byte @ 77, 81\n",
- "2nd byte @ 117, 121\n",
- "3rd byte @ 157, 158, 159, 161, 162\n",
- "4th byte @ 197, 198, 201, 202\n",
- "```\n",
- "\n",
- "Starting off, we have 41 and 42 as potential locations. The next byte only has odd locations, so our first XOR likely occured at sample 41. 41 could lead to either 77 (offset of 36) or 81 (offset of 40). 77 doesn't lead to any interesting points in byte 2 with an offset of 36, but 81 does lead onto 121, which will lead onto 161, then onto 201. Therefore, our offset is likely 40.\n",
- "\n",
- "As an exercise, try writing some python code to automatically find the starting location and offset."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#your code here..."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### The Other 127"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The best way to attack the IV would be to repeat the 1-bit conceptual attack for each of the bits. Try to do this yourself! (Really!) If you're stuck, here are a few hints to get you going:\n",
- "\n",
- "One easy way of looping through the bits is by using two nested loops, like this:\n",
- "\n",
- "```python\n",
- "for byte in range(16):\n",
- " for bit in range(8):\n",
- " # Attack bit number (byte*8 + bit)\n",
- "```\n",
- "\n",
- "The sample that you'll want to look at will depend on which byte you're attacking. We had success when we used `location = 51 + byte*60`, but your mileage will vary.\n",
- "\n",
- "The bitshift operator and the bitwise-AND operator are useful for getting at a single bit:\n",
- "\n",
- "```python\n",
- "# This will either result in a 0 or a 1\n",
- "checkIfBitSet = (byteToCheck >> bit) & 0x01\n",
- "```"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "btldr_IV = [0] * 16 #181\n",
- "for byte in range(16):\n",
- " if PLATFORM == \"CWLITEARM\" or PLATFORM == \"CW308_STM32F3\":\n",
- " location = 53 + byte * 36\n",
- " elif PLATFORM == \"CWLITEXMEGA\" or PLATFORM == \"CW303\":\n",
- " location = 55 + byte * 60\n",
- " iv = 0\n",
- " for bit in range(8):\n",
- " pt_bits = [((dr[i][byte] >> (7-bit)) & 0x01) for i in range(numTraces)]\n",
- "\n",
- " # Split traces into 2 groups\n",
- " groupedPoints = [[] for _ in range(2)]\n",
- " for i in range(numTraces):\n",
- " groupedPoints[pt_bits[i]].append(trace_array[i][location])\n",
- " \n",
- " means = []\n",
- " for i in range(2):\n",
- " means.append(np.average(groupedPoints[i]))\n",
- " diff = means[1] - means[0]\n",
- " \n",
- " iv_bit = 1 if diff > 0 else 0\n",
- " iv = (iv << 1) | iv_bit\n",
- " \n",
- " print(iv_bit, end = \" \")\n",
- " \n",
- " print(\"{:02X}\".format(iv))\n",
- " btldr_IV[byte] = iv\n",
- " \n",
- "print(btldr_IV)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Attacking the Signature"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The last thing we can do with this bootloader is attack the signature. This final section will show how one byte of the signature could be recovered. If you want more of this kind of analysis, a more complete timing attack is shown in Tutorial B3-1 Timing Analysis with Power for Password Bypass."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Attack Theory"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Recall from earlier that the signature check in C looks like:\n",
- "\n",
- "```C\n",
- "if ((tmp32[0] == SIGNATURE1) &&\n",
- " (tmp32[1] == SIGNATURE2) &&\n",
- " (tmp32[2] == SIGNATURE3) &&\n",
- " (tmp32[3] == SIGNATURE4)){\n",
- "```\n",
- "\n",
- "In C, boolean expressions support short-circuiting. When checking multiple conditions, the program will stop evaluating these booleans as soon as it can tell what the final value will be. In this case, unless all four of the equality checks are true, the result will be false. Thus, as soon as the program finds a single false condition, it's done.\n",
- "\n",
- "Open the listing file for your binary (`.lss`), find the signature check, and confirm that this is happening. For example, on the STM32F3, the assembly looks like this:\n",
- "\n",
- "```\n",
- " //Check the signature\n",
- " if ((tmp32[0] == SIGNATURE1) &&\n",
- " 8000338:\tf89d 3018 \tldrb.w\tr3, [sp, #24]\n",
- " 800033c:\t2b00 \tcmp\tr3, #0\n",
- " 800033e:\td1c2 \tbne.n\t80002c6 \n",
- " 8000340:\tf89d 2019 \tldrb.w\tr2, [sp, #25]\n",
- " 8000344:\t2aeb \tcmp\tr2, #235\t; 0xeb\n",
- " 8000346:\td1be \tbne.n\t80002c6 \n",
- " (tmp32[1] == SIGNATURE2) &&\n",
- " 8000348:\tf89d 201a \tldrb.w\tr2, [sp, #26]\n",
- " 800034c:\t2a02 \tcmp\tr2, #2\n",
- " 800034e:\td1ba \tbne.n\t80002c6 \n",
- " (tmp32[2] == SIGNATURE3) &&\n",
- " 8000350:\tf89d 201b \tldrb.w\tr2, [sp, #27]\n",
- " 8000354:\t2a1d \tcmp\tr2, #29\n",
- " 8000356:\td1b6 \tbne.n\t80002c6 \n",
- " (tmp32[3] == SIGNATURE4)){\n",
- "```\n",
- "\n",
- "This assembly code confirms the short-circuiting operation. Each of the four assembly blocks include a comparison and a conditional branch. All four of the conditional branches (`bne.n`) return the program to the same location (the start of the `while(1)` loop). All four branches must fail to get into the body of the if block.\n",
- "\n",
- "The short-circuiting conditions are perfect for us. We can use our power traces to watch how long it takes for the signature check to fail. If the check takes longer than usual, then we know that the first byte of our signature was right."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Power Traces"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Our capture loop will be pretty similar to the one we used to break the IV, but now that we know the secret values of the encryption process we can make some improvements by encrypting the text that we send. This has two important advantages:\n",
- "\n",
- "1. We can control the signature. We could reuse the traces we took during the IV attack, but this way ensures that we hit each possible value once. It also simplifies the analysis, since we don't have to worry about decrypting the text we sent.\n",
- "1. We no longer have to reset after each attempt, since we know what the next IV is going to be (we do need to reset at the beginning to make sure we're on the same starting IV as the target). This speeds up the capture process considerably. \n",
- "\n",
- "To perform the AES256 CBC encryption, there's a few steps we need to take:\n",
- "\n",
- "1. XOR the IV with the text we want to send\n",
- "1. Encrypt this new text\n",
- "1. Set this cipher text as the new IV\n",
- "\n",
- "We can use PyCrypto again to make the encryption process easy and the other two steps are simple operations. We'll run our loop 256 times (one for each possible byte value) and assign that value to the byte we want to check. We're not quite sure where the check is happening, so we'll be safe and capture 24000 traces. Everthing else should look familiar from earlier parts of the tutorial:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import tqdm\n",
- "import numpy as np\n",
- "from Crypto.Cipher import AES\n",
- "import time\n",
- "\n",
- "traces = []\n",
- "keys = []\n",
- "plaintexts = []\n",
- "\n",
- "iv = [0xC1, 0x25, 0x68, 0xDF, 0xE7, 0xD3, 0x19, 0xDA, 0x10, 0xE2, 0x41, 0x71, 0x33, 0xB0, 0xEB, 0x3C]\n",
- "\n",
- "knownkey = [0x94, 0x28, 0x5D, 0x4D, 0x6D, 0xCF, 0xEC, 0x08, 0xD8, 0xAC, 0xDD, 0xF6, 0xBE, 0x25, 0xA4, 0x99,\n",
- " 0xC4, 0xD9, 0xD0, 0x1E, 0xC3, 0x40, 0x7E, 0xD7, 0xD5, 0x28, 0xD4, 0x09, 0xE9, 0xF0, 0x88, 0xA1]\n",
- "\n",
- "knownkey = bytes(knownkey)\n",
- "aes = AES.new(knownkey, AES.MODE_ECB)\n",
- "N = 256 # Number of traces\n",
- "\n",
- "reset_target(scope)\n",
- "okay=0\n",
- "scope.adc.basic_mode = \"falling_edge\"\n",
- "while not okay:\n",
- " target.write(\"\\0xxxxxxxxxxxxxxxxxx\")\n",
- " time.sleep(0.005)\n",
- " response = target.read()\n",
- " if response:\n",
- " if ord(response[0]) == 0xA1:\n",
- " okay = 1\n",
- "\n",
- "scope.adc.samples = 24000\n",
- "scope.adc.offset = 0\n",
- "for byte in trange(N, desc='Attacking Signature Byte'):\n",
- " message = [0x00]\n",
- " text = [0] * 16\n",
- " \n",
- " # the 4 signature bytes\n",
- " text[0] = byte\n",
- " text[1] = 0\n",
- " text[2] = 0\n",
- " text[3] = 0\n",
- " \n",
- " target.read()\n",
- " \n",
- " textcpy = [0] * 16\n",
- " textcpy[:] = text[:]\n",
- " plaintexts.append(textcpy)\n",
- " \n",
- " # Apply IV\n",
- " for i in range(len(iv)):\n",
- " text[i] ^= iv[i]\n",
- " \n",
- " # Encrypt text\n",
- " ct = aes.encrypt(bytes(text))\n",
- " \n",
- " message.extend(ct)\n",
- " \n",
- " # Use ct as new IV\n",
- " iv[:] = ct[:]\n",
- " \n",
- " crc = bl_crc.bit_by_bit(ct)\n",
- " message.append(crc >> 8)\n",
- " message.append(crc & 0xFF)\n",
- " \n",
- " scope.arm()\n",
- "\n",
- " target.write(message)\n",
- " timeout = 50\n",
- " \n",
- " ret = scope.capture()\n",
- " if ret:\n",
- " print('Timeout happened during acquisition')\n",
- " continue\n",
- " \n",
- " response = target.read()\n",
- " if ord(response[0]) != 0xA4:\n",
- " # Bad response, just skip\n",
- " print(\"Bad response: {:02X}\".format(ord(response[0])))\n",
- " continue\n",
- " \n",
- " traces.append(scope.get_last_trace())"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Analysis"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now that we've captured our traces, the actual analysis is pretty simple. We're looking for a single trace that looks very different from the rest. A simple way to find this is to compare all the traces to a reference trace. We'll use the average of all the traces as our reference:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "mean = np.average(traces, axis=0)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "That leaves us with comparing the traces. Let's start by plotting the difference between some of the traces and the mean:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from bokeh.plotting import figure, show\n",
- "from bokeh.io import output_notebook\n",
- "\n",
- "output_notebook()\n",
- "p = figure()\n",
- "colors = [\"red\", \"blue\", \"green\", \"yellow\"]\n",
- "for i in range(0,10):\n",
- " p.line(range(len(traces[i])), traces[i]-mean, line_color=colors[i%4])\n",
- " \n",
- "show(p)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Depending on your target, you might have seen something like this:\n",
- "\n",
- "\n",
- "\n",
- "Looks like we've found our trace! However, let's clean this up with some statistics. We can use the correlation coefficient to see which bytes are the furthest away from the average. We only want to take the correlation across where the plots differ, chose a subset of the plot where there's a large difference. In the case of the above picture, the difference starts at around 18k, and continues until the end. A range of 18000 to 20000 should work nicely:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "corr = []\n",
- "for i in range(256):\n",
- " corr.append(np.corrcoef(mean[18000:20000], traces[i][18000:20000])[0, 1])\n",
- "print(np.sort(corr))\n",
- "print(np.argsort(corr))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This output tells us two things:\n",
- "\n",
- "* The first list says that almost every trace looks very similar to the overall mean (98% correlated or higher). However, there's one trace that is totally different with much lower correlation. This is probably our correct guess.\n",
- "* The second list gives the signature guess that matches each of the above correlations. The first number in the list is 0x00, which is the correct signature!\n",
- "\n",
- "To finish this attack, change the capture loop to keep the first byte fixed and vary the second byte instead. Repeat this with the rest of the bytes and you should have the signature."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import tqdm\n",
- "import numpy as np\n",
- "from Crypto.Cipher import AES\n",
- "import time\n",
- "\n",
- "traces = []\n",
- "keys = []\n",
- "plaintexts = []\n",
- "btldr_sig = [0] * 4\n",
- "\n",
- "iv = [0xC1, 0x25, 0x68, 0xDF, 0xE7, 0xD3, 0x19, 0xDA, 0x10, 0xE2, 0x41, 0x71, 0x33, 0xB0, 0xEB, 0x3C]\n",
- "\n",
- "knownkey = [0x94, 0x28, 0x5D, 0x4D, 0x6D, 0xCF, 0xEC, 0x08, 0xD8, 0xAC, 0xDD, 0xF6, 0xBE, 0x25, 0xA4, 0x99,\n",
- " 0xC4, 0xD9, 0xD0, 0x1E, 0xC3, 0x40, 0x7E, 0xD7, 0xD5, 0x28, 0xD4, 0x09, 0xE9, 0xF0, 0x88, 0xA1]\n",
- "\n",
- "knownkey = bytes(knownkey)\n",
- "aes = AES.new(knownkey, AES.MODE_ECB)\n",
- "N = 256 # Number of traces\n",
- "\n",
- "reset_target(scope)\n",
- "okay=0\n",
- "scope.adc.basic_mode = \"falling_edge\"\n",
- "while not okay:\n",
- " target.write(\"\\0xxxxxxxxxxxxxxxxxx\")\n",
- " time.sleep(0.005)\n",
- " response = target.read()\n",
- " if response:\n",
- " if ord(response[0]) == 0xA1:\n",
- " okay = 1\n",
- " \n",
- "scope.adc.samples = 24000\n",
- "scope.adc.offset = 0\n",
- "for bnum in range(4):\n",
- " traces = []\n",
- " for byte in trange(N, desc='Attacking Signature Byte {}'.format(bnum)):\n",
- " message = [0x00]\n",
- " text = [0] * 16\n",
- "\n",
- " # the 4 signature bytes\n",
- " for j in range(bnum):\n",
- " text[j] = btldr_sig[j]\n",
- " text[bnum] = byte\n",
- " \n",
- " target.read()\n",
- "\n",
- " textcpy = [0] * 16\n",
- " textcpy[:] = text[:]\n",
- " plaintexts.append(textcpy)\n",
- "\n",
- " # Apply IV\n",
- " for i in range(len(iv)):\n",
- " text[i] ^= iv[i]\n",
- "\n",
- " # Encrypt text\n",
- " ct = aes.encrypt(bytes(text))\n",
- "\n",
- " message.extend(ct)\n",
- "\n",
- " # Use ct as new IV\n",
- " iv[:] = ct[:]\n",
- "\n",
- " crc = bl_crc.bit_by_bit(ct)\n",
- " message.append(crc >> 8)\n",
- " message.append(crc & 0xFF)\n",
- "\n",
- " scope.arm()\n",
- " target.write(message)\n",
- " ret = scope.capture()\n",
- " if ret:\n",
- " print('Timeout happened during acquisition')\n",
- " continue\n",
- "\n",
- " # run aux stuff that should happen after trace here\n",
- " response = target.read()\n",
- " if ord(response[0]) != 0xA4:\n",
- " # Bad response, just skip\n",
- " print(\"Bad response: {:02X}\".format(ord(response[0])))\n",
- " continue\n",
- "\n",
- " traces.append(scope.get_last_trace())\n",
- " \n",
- " mean = np.average(traces, axis=0)\n",
- " corr = []\n",
- " for i in range(256):\n",
- " corr.append(np.corrcoef(mean[18000:20000], traces[i][18000:20000])[0, 1])\n",
- " btldr_sig[bnum] = np.argsort(corr)[0]\n",
- " \n",
- "print(btldr_sig)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.dis()\n",
- "target.dis()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Conclusion"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We've now successfully recovered all of the secrets of the bootloader!"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Tests"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "real_btldr_key = [0x94, 0x28, 0x5D, 0x4D, 0x6D, 0xCF, 0xEC, 0x08, 0xD8, 0xAC, 0xDD, 0xF6, 0xBE, 0x25, 0xA4, 0x99, \\\n",
- " 0xC4, 0xD9, 0xD0, 0x1E, 0xC3, 0x40, 0x7E, 0xD7, 0xD5, 0x28, 0xD4, 0x09, 0xE9, 0xF0, 0x88, 0xA1]\n",
- "\n",
- "real_btldr_IV = [0xC1, 0x25, 0x68, 0xDF, 0xE7, 0xD3, 0x19, 0xDA, 0x10, 0xE2, 0x41, 0x71, 0x33, 0xB0, 0xEB, 0x3C]\n",
- "\n",
- "real_btldr_sig = [0x00, 0xEB, 0x02, 0x1D]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "assert (btldr_key == list(real_btldr_key)), \"Attack on encryption key failed!\\nGot: {}\\nExp: {}\".format(btldr_key, real_btldr_key)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "assert (btldr_IV == real_btldr_IV), \"Attack on IV failed!\\nGot: {}\\nExpected: {}\".format(btldr_IV, real_btldr_IV)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "assert (btldr_sig == real_btldr_sig), \"Attack on signature failed!\\nGot: {}\\nExpected: {}\".format(btldr_sig, real_btldr_sig)"
- ]
- }
- ],
- "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": 2
-}
diff --git a/jupyter/lab/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
deleted file mode 100644
index 0eab0b32..00000000
--- a/jupyter/lab/1_SCA_Lab/Lab 3_1B - Reverse Engineering on the AES256 Bootloader.ipynb
+++ /dev/null
@@ -1,1085 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Part 3, Topic 1, Lab B: AES256 Bootloader Attack (MAIN)"
- ]
- },
- {
- "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:** *Through the previous labs, we've gained a lot of tools to attack unknown embedded devices: SPA, DPA, CPA, trace resynchronization, and more. In this lab, we'll be using some of those techniques to break a more realistic target: a bootloader. Note that there are two versions of this lab. In this one (Lab B), we'll start with no information that couldn't be revealed by watching code be sent to the bootloader. Everything else, we'll need to figure out for ourselves, such as what encryption algorithm the target is using, how it's using it, etc. In lab A, this information will be given and you'll just focus on the attack. It's up to you whether you want to run this lab or Lab A!*\n",
- "\n",
- "**LEARNING OUTCOMES:**\n",
- "\n",
- "* Observing power traces to figure out what encryption operation it's running\n",
- "* Applying CPA and DPA to break different parts of the bootloader\n",
- "* Understanding different operating modes for block ciphers"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Background\n",
- "\n",
- "In the world of microcontrollers, a bootloader is a special piece of firmware that is made to let the user upload new programs into memory. This is especially useful for devices with complex code that may need to be patched or otherwise updated in the future - a bootloader makes it possible for the user to upload a patched version of the firmware onto the micro. The bootloader receives information from a communication line (a USB port, serial port, ethernet port, WiFi connection, etc...) and stores this data into program memory. Once the full firmware has been received, the micro can happily run its updated code.\n",
- "\n",
- "There is one big security issue to worry about with bootloaders. A company may want to stop their customers from writing their own firmware and uploading it onto the micro. For example, this might be for protection reasons - hackers might be able to access parts of the device that weren't meant to be accessed. One way of stopping this is to add encryption. The company can add their own secret signature to the firmware code and encrypt it with a secret key. Then, the bootloader can decrypt the incoming firmware and confirm that the incoming firmware is correctly signed. Users will not know the secret key or the signature tied to the firmware, so they won't be able to \"fake\" their own."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "SCOPETYPE = 'OPENADC'\n",
- "PLATFORM = 'CWLITEARM'"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%bash -s \"$PLATFORM\" \n",
- "cd ../../../firmware/mcu/bootloader-aes256\n",
- "make PLATFORM=$1 CRYPTO_TARGET=NONE"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%run \"../../Setup_Scripts/Setup_Generic.ipynb\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "fw_path = \"../../../firmware/mcu/bootloader-aes256/bootloader-aes256-{}.hex\".format(PLATFORM)\n",
- "cw.program_target(scope, prog, fw_path)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## The Situation\n",
- "\n",
- "Simply put, we've got a target device running an encrypted bootloader (a program used to upload new code onto a device) and we want to see if we can get our own code running on the device. We've done a bit of sniffing on the serial lines when the device's firmware is being updated and we've learned the following:\n",
- "\n",
- "* The device communicates over serial at 38400bps\n",
- "* When writing memory, the first byte is always zero (probably a command byte)\n",
- "* There's a 16 byte block of random looking memory (aka it doesn't look like firmware). This part is probably encrypted\n",
- "* There's a 2 byte CRC at the end of each message\n",
- "* There's no repetition in the ciphertext.\n",
- "\n",
- "All together this looks like:\n",
- "\n",
- "```\n",
- " |<-------- Encrypted block (16 bytes) ---------->|\n",
- " | |\n",
- "+------+------+------+------+------+------+ .... +------+------+------+\n",
- "| 0x00 | Random looking data | CRC-16 |\n",
- "+------+------+------+------+------+------+ .... +------+------+------+\n",
- "```\n",
- "\n",
- "After sending data to the bootloader it responds with either `0xA4` or `0xA1`. The former only happened when we sent a bad CRC.\n",
- "\n",
- "This time, we won't be triggering off of our trigger pins (you can remove them from the code if you'd like).\n",
- "\n",
- "From our initial sniffing of the communication lines, we've got the first few messages that were sent:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import pickle\n",
- "with open(\"./firmware.pickle\", \"rb\") as f:\n",
- " encrypted_firmware = pickle.load(f)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Doing Recon\n",
- "\n",
- "Our first step will be to see if we can learning anything about the bootloader from looking at its power traces. Let's start with the boot sequence:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.trigger.triggers = \"nrst\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.adc.samples = 24400"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.arm()\n",
- "reset_target(scope)\n",
- "scope.capture()\n",
- "wave = scope.get_last_trace()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.plot(wave)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The device does appear to be doing something, but it's clearly nothing major - no encryptions or anything. Every microcontroller has boot code and operations that run when it's reset. The device may even have its own bootloader running in ROM! Let's move onto the messages themselves. We do know that there's a CRC for data integrety. We can use the following code to calculate the CRC for us:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Class Crc\n",
- "#############################################################\n",
- "# These CRC routines are copy-pasted from pycrc, which are:\n",
- "# Copyright (c) 2006-2013 Thomas Pircher \n",
- "#\n",
- "class Crc(object):\n",
- " \"\"\"\n",
- " A base class for CRC routines.\n",
- " \"\"\"\n",
- "\n",
- " def __init__(self, width, poly):\n",
- " \"\"\"The Crc constructor.\n",
- "\n",
- " The parameters are as follows:\n",
- " width\n",
- " poly\n",
- " reflect_in\n",
- " xor_in\n",
- " reflect_out\n",
- " xor_out\n",
- " \"\"\"\n",
- " self.Width = width\n",
- " self.Poly = poly\n",
- "\n",
- "\n",
- " self.MSB_Mask = 0x1 << (self.Width - 1)\n",
- " self.Mask = ((self.MSB_Mask - 1) << 1) | 1\n",
- "\n",
- " self.XorIn = 0x0000\n",
- " self.XorOut = 0x0000\n",
- "\n",
- " self.DirectInit = self.XorIn\n",
- " self.NonDirectInit = self.__get_nondirect_init(self.XorIn)\n",
- " if self.Width < 8:\n",
- " self.CrcShift = 8 - self.Width\n",
- " else:\n",
- " self.CrcShift = 0\n",
- "\n",
- " def __get_nondirect_init(self, init):\n",
- " \"\"\"\n",
- " return the non-direct init if the direct algorithm has been selected.\n",
- " \"\"\"\n",
- " crc = init\n",
- " for i in range(self.Width):\n",
- " bit = crc & 0x01\n",
- " if bit:\n",
- " crc ^= self.Poly\n",
- " crc >>= 1\n",
- " if bit:\n",
- " crc |= self.MSB_Mask\n",
- " return crc & self.Mask\n",
- "\n",
- "\n",
- " def bit_by_bit(self, in_data):\n",
- " \"\"\"\n",
- " Classic simple and slow CRC implementation. This function iterates bit\n",
- " by bit over the augmented input message and returns the calculated CRC\n",
- " value at the end.\n",
- " \"\"\"\n",
- " # If the input data is a string, convert to bytes.\n",
- " if isinstance(in_data, str):\n",
- " in_data = [ord(c) for c in in_data]\n",
- "\n",
- " register = self.NonDirectInit\n",
- " for octet in in_data:\n",
- " for i in range(8):\n",
- " topbit = register & self.MSB_Mask\n",
- " register = ((register << 1) & self.Mask) | ((octet >> (7 - i)) & 0x01)\n",
- " if topbit:\n",
- " register ^= self.Poly\n",
- "\n",
- " for i in range(self.Width):\n",
- " topbit = register & self.MSB_Mask\n",
- " register = ((register << 1) & self.Mask)\n",
- " if topbit:\n",
- " register ^= self.Poly\n",
- "\n",
- " return register ^ self.XorOut\n",
- " \n",
- "bl_crc = Crc(width = 16, poly=0x1021)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's definte a function to do the communication and capture a trace for us. We can try triggering off of our message. There's not much memory on the target, so it's probably decryption on the fly instead of reading in a whole bunch of memory, then doing the decryption."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "scope.trigger.triggers = \"tio2\"\n",
- "scope.adc.samples = 24400\n",
- "scope.adc.decimate = ??? # try to get the full encryption in a single trace, then set back to 1\n",
- "scope.adc.offset = 0\n",
- "def cap_trace(enc_block):\n",
- " message = [0x00]\n",
- " target.read()\n",
- "\n",
- " key, text = ktp.next()\n",
- " message.extend(enc_block)\n",
- "\n",
- " crc = bl_crc.bit_by_bit(enc_block)\n",
- " message.append(crc >> 8)\n",
- " message.append(crc & 0xFF)\n",
- "\n",
- " \n",
- "\n",
- " target.write(message[:-1])\n",
- " time.sleep(0.01)\n",
- " scope.arm()\n",
- " target.write([crc&0xFF])\n",
- " ret = scope.capture()\n",
- " if ret:\n",
- " print('Timeout happened during acquisition')\n",
- " response = target.read()\n",
- " if response:\n",
- " if ord(response[0]) != 0xA4:\n",
- " # Bad response, just skip\n",
- " #print(\"Bad response: {:02X}\".format(ord(response[0])))\n",
- " return None\n",
- "\n",
- " return scope.get_last_trace()\n",
- " \n",
- "ktp = cw.ktp.Basic()\n",
- "text, key = ktp.next()\n",
- "wave = cap_trace(text)\n",
- "wave2 = cap_trace(text)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.plot(wave) * cw.plot(wave2)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We've found the decryption. Some things to notice from this:\n",
- "\n",
- "1. The target immediately goes from reading the ciphertext to running a decryption - there's no preprocessing done at all. This means the ciphertext is likely being immediately fed into the decryption algorithm.\n",
- "1. We can see operations in the encryption being repeated. This looks a lot like AES - we can see a long distinct operation that's probably MixColumns in there and the overall structure looks a lot like what we saw when looking at TINYAES128C encryptions. Let's make a weak assumption that this is AES128 - we can adjust this as we learn more about the bootloader.\n",
- "1. We can't see the full encryption\n",
- "1. There's some jitter here. We'll probably have to resync the traces if we run a CPA attack\n",
- "\n",
- "Since we can't see the full encryption, decimate the ADC (we don't care too much about the fine details here) and take another look...\n",
- "\n",
- "\n",
- "## The full encryption\n",
- "\n",
- "You should see that instead of 9 repititions of MixColumns (or what we assume is MixColumns), there's actually 13. This rules out AES128, but AES256 actually has 14 rounds! We can still attack AES256 without much issue: we basically just have to run two CPA attacks, one for the first half of the key and another for the second half. Again, we're not 100% about this, but it's a good starting point. \n",
- "\n",
- "As we mentioned in the debriefing about the bootloader, the ciphertext never seems to repeat. It's pretty unlikely that this device is using straight AES256 (if it even is using AES256), since firmware usually has blocks that repeat. More likely is that AES is being used as a stream cipher: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation. This could pose an immediate problem for our attack efforts: we need to know either what's going into or coming out of the AES block to perform a CPA attack. However, some of the modes listed on that page (if it's even using one on that page) feed an IV or a counter into that block instead of the plaintext or the ciphertext. We didn't see any encryption operations happening on startup (which the device could've done if it was using one of these IV/counter modes), so we'll probably be okay with a normal CPA attack. Let's try it and see if we can get anything out of it:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from tqdm.notebook import trange\n",
- "project = cw.create_project(\"projects/Tutorial_A5\", overwrite=True)\n",
- "scope.adc.offset = 0\n",
- "scope.adc.decimate = 1\n",
- "for i in trange(100):\n",
- " ktp = cw.ktp.Basic()\n",
- " key, text = ktp.next()\n",
- " wave = cap_trace(text)\n",
- " trace = cw.Trace(wave, text, bytearray([0]*16), bytearray([0]*16))\n",
- " project.traces.append(trace)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cw.plot(project.waves[0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can eliminate a lot of this jitter by using the resync SAD module:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer as cw\n",
- "import chipwhisperer.analyzer as cwa\n",
- "\n",
- "leak_model = cwa.leakage_models.inverse_sbox_output\n",
- "resync = cwa.preprocessing.ResyncSAD(project)\n",
- "resync.enabled=True\n",
- "resync.ref_trace = 0\n",
- "resync.target_window = (???, ???)\n",
- "resync.max_shift = 7000\n",
- "new_proj = resync.preprocess()\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Check to make sure your traces are resynced:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(new_proj.waves[i])\n",
- "\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "All that's left is to actually run the attack. We don't know the correct key, so it won't be highlighted in red."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "attack = cwa.cpa(new_proj, leak_model)\n",
- "#attack.pont_range = [???, ???]\n",
- "\n",
- "#key = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]\n",
- "\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "attack_results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The difference in correlation between the best key guess and the next best one makes this look very promising! We now know we're correct about two things:\n",
- "\n",
- "1. The bootloader is actually decrypting the ciphertext\n",
- "1. The device is using AES"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "calc_round_key = attack_results.key_guess()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "With that done, we now need to get the second half of the key. Pop over to [Extending AES-128 Attacks to AES-256](Extending%20AES-128%20Attacks%20to%20AES-256.ipynb), since that page explains how to do that...\n",
- "\n",
- "Back? Let's go through and see if that theory actually holds up.\n",
- "\n",
- "To make a new model, we start off by inheriting the `AESLeakageHelper` class. We need to make a `leakage()` method that calculates the Hamming weight we use in the CPA attack. To get you started:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer as cw\n",
- "class AES256_Round13_Model(cwa.AESLeakageHelper):\n",
- " def leakage(self, pt, ct, guess, bnum):\n",
- " #You must put YOUR recovered 14th round key here - this example may not be accurate!\n",
- " calc_round_key = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]\n",
- " state = reverse_round_14(self, pt, calc_round_key)\n",
- " state = reverse_round_13(self, state) #reverse state just before inv_subbytes\n",
- " return self.inv_sbox(state[bnum] ^ guess[bnum])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now we just need to make the `reverse_round_14()` and `reverse_round_13()` functions. By passing the class in, we get access to `self.inv_shiftrows()`, `self.inv_subbytes()`, and `self.inv_mixcolumns()`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def reverse_round_14(self, pt, key):\n",
- " state = [pt[i] ^ key[i] for i in range(16)] #AddRoundKey\n",
- " state = ???\n",
- " state = ???\n",
- " return state # we're now at the end of decryption round 1\n",
- "\n",
- "def reverse_round_13(self, state):\n",
- " state = ???\n",
- " state = ???\n",
- " return state"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "From there, just update the leakage model and rerun the attack:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "leak_model = cwa.leakage_models.new_model(AES256_Round13_Model)\n",
- "attack.leak_model = leak_model\n",
- "cb = cwa.get_jupyter_callback(attack)\n",
- "attack_results = attack.run(cb)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If you built your model correctly, you should again see a pretty likely guess for a key. All that's left now is to combine the 14th and 13th round keys, then use that to figure out the 0th and 1st round keys.\n",
- "\n",
- "We'll start by getting the transformed 13th round key out of the attack results:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#calc_round_key = [0xea, 0x79, 0x79, 0x20, 0xc8, 0x71, 0x44, 0x7d, 0x46, 0x62, 0x5f, 0x51, 0x85, 0xc1, 0x3b, 0xcb]\n",
- "rec_key = calc_round_key"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rec_key2 = []\n",
- "for bnum in attack_results.find_maximums():\n",
- " print(\"Best Guess = 0x{:02X}, Corr = {}\".format(bnum[0][0], bnum[0][2]))\n",
- " rec_key2.append(bnum[0][0])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now we need to transform that key into the actual 13th round key by running it through ShiftRows and MixColumns:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "real_key2 = cwa.aes_funcs.shiftrows(rec_key2)\n",
- "real_key2 = cwa.aes_funcs.mixcolumns(real_key2)\n",
- "\n",
- "print(\"Recovered:\", end=\"\")\n",
- "for subkey in real_key2:\n",
- " print(\" {:02X}\".format(subkey), end=\"\")\n",
- "print(\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "then we can combine the keys:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "rec_key_comb = real_key2.copy()\n",
- "rec_key_comb.extend(rec_key)\n",
- "\n",
- "print(\"Key:\", end=\"\")\n",
- "for subkey in rec_key_comb:\n",
- " print(\" {:02X}\".format(subkey), end=\"\")\n",
- "print(\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "and use ChipWhisperer's built in key scheduler to reverse them to the 0th and 1st round keys:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "btldr_key = leak_model.key_schedule_rounds(rec_key_comb, 13, 0)\n",
- "btldr_key.extend(leak_model.key_schedule_rounds(rec_key_comb, 13, 1))\n",
- "print(\"Key:\", end=\"\")\n",
- "for subkey in btldr_key:\n",
- " print(\" {:02X}\".format(subkey), end=\"\")\n",
- "print(\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "So we were clearly right about the bootloader running AES256! However, if we try decrypting some of our firmware with the encryption key:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from Crypto.Cipher import AES\n",
- "cipher = AES.new(bytes(btldr_key), AES.MODE_ECB)\n",
- "print(bytearray(cipher.decrypt(encrypted_firmware[:16])))\n",
- "print(bytearray(cipher.decrypt(encrypted_firmware[16:32])))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As we expected, we still get gibberish out of this - the target is definitely using AES as a stream cipher. The question now is, which block cipher mode is it using? Well, we know the ciphertext is being decrypted. We can narrow it down a bit by looking at the end of the encryption. Increase the offset until you reach the end of the encryption."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "#scope.adc.offset = 50000\n",
- "#wave = cap_trace(text)\n",
- "#key, text = ktp.next()\n",
- "wave = cap_trace(text)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%matplotlib notebook\n",
- "import matplotlib.pyplot as plt\n",
- "plt.figure()\n",
- "plt.plot(wave)\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "It might be hard to pick out, but you should be able to find 16 XOR short operations just after the end of the last round of AES. It might be using CBC mode, which means it'll be using the ciphertext as part of the encryption and decryption of subsequent blocks. Let's do a quick check:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cipher = AES.new(bytes(btldr_key), AES.MODE_ECB)\n",
- "dec_fw = cipher.decrypt(encrypted_firmware[16:32])\n",
- "fw = [dec_fw[i] ^ encrypted_firmware[i] for i in range(16)]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(fw)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "It looks like we're finally getting some valid output. Let's try the next block:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cipher = AES.new(bytes(btldr_key), AES.MODE_ECB)\n",
- "dec_fw = cipher.decrypt(encrypted_firmware[32:48])\n",
- "fw = [dec_fw[i] ^ encrypted_firmware[i+16] for i in range(16)]\n",
- "print(fw)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This is very promising! This firmware is repeated twice. The all `FFs` is probably just empty flash memory. The beginning is more curious though:\n",
- "\n",
- "* It's probably not a flash constant - otherwise it wouldn't be in two blocks in a row\n",
- "* That's only enough room for a few instructions at most. Again, it's a little strange that it would be repeated like this\n",
- "\n",
- "It might be some sort of signature. After all, the device doesn't want to write anything to memory unless the ciphertext has properly been decrypted.\n",
- "\n",
- "There's still the issue of the first block of memory though. There's another secret value called an initialization vector that we need to decrypt that block. To be able to recover that, we'll need to revisit the DPA attack:\n",
- "\n",
- "### Attack Theory\n",
- "\n",
- "The bootloader applies the IV to the AES decryption result (`DR`) by calculating\n",
- "\n",
- "\n",
- "$\\text{PT} = \\text{DR} \\oplus \\text{IV}$\n",
- "\n",
- "where DR is the decrypted ciphertext, IV is the secret vector, and PT is the plaintext that the bootloader will use later. We only have access to one of these: since we know the AES-256 key, we can calculate DR. This exclusive or will be visible in the power traces.\n",
- "\n",
- "This is enough information for us to attack a single bit of the IV. Suppose we only wanted to get the first bit (bit 0) of the first byte (byte 0) of the IV. We could do the following:\n",
- "\n",
- "* Split all of the traces into two groups: those with `(DR[0] & 0x01) = 0`, and those with `(DR[0] & 0x01) = 1`. \n",
- "* Calculate the average trace for both groups.\n",
- "* Find the difference between the two averages. Provided we've got sufficient data, we should see a spike where the xor is occuring.\n",
- "* Look at the direction of the spike to decide if the IV bit is 0 `(PT[0] = DR[0])` or if the IV bit is 1 `(PT[0] = ~DR[0])`.\n",
- "\n",
- "This is effectively a DPA attack on a single bit of the IV. We can repeat this attack across the whole IV by instead separating by `(DR[byte] & (1 << bit) = 0` and `(DR[byte] & (1 << bit) = bit`.\n",
- "\n",
- "We'll need to reset the device every encryption since it only uses the IV in the first encryption. This leads to a slightly modified capture loop. You'll need to adjust your offset since we're now triggering near the beginning of the UART transmit instead of nera the end. Run the loop, then interrupt it to get a wave. Then plot and adjust your offset. Repeat until you're near the end of the encryption again:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from Crypto.Cipher import AES\n",
- "import numpy as np\n",
- "\n",
- "from tqdm.notebook import trange\n",
- "import numpy as np\n",
- "import time\n",
- "traces = []\n",
- "keys = []\n",
- "plaintexts = []\n",
- "\n",
- "from tqdm.notebook import trange\n",
- "project = cw.create_project(\"projects/Tutorial_A5_IV\", overwrite=True)\n",
- "scope.adc.decimate = 1\n",
- "scope.adc.offset = 51000\n",
- "scope.adc.timeout = 1\n",
- "scope.trigger.triggers = \"tio2\"\n",
- "for i in trange(1000):\n",
- " scope.io.nrst = 0\n",
- " time.sleep(0.02)\n",
- " scope.io.nrst = \"high_z\"\n",
- " time.sleep(0.01)\n",
- " okay = 0\n",
- " while not okay:\n",
- " target.write('\\0xxxxxxxxxxxxxxxxxx')\n",
- " time.sleep(0.005)\n",
- " response = target.read()\n",
- " i += 1\n",
- " if response:\n",
- " if ord(response[0]) == 0xA1:\n",
- " okay = 1\n",
- " message = [0x00]\n",
- " \n",
- " target.flush()\n",
- " \n",
- " key, text = ktp.new_pair() # manual creation of a key, text pair can be substituted here\n",
- " \n",
- " wave = cap_trace(text)\n",
- " if wave is None:\n",
- " continue\n",
- " \n",
- " \n",
- " #wave = scope.get_last_trace()\n",
- " trace = cw.Trace(wave, text, bytearray([0]*16), bytearray([0]*16))\n",
- " project.traces.append(trace)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you can see, we'll again need to resync to get rid of the jitter:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "plt = cw.plot([])\n",
- "for i in range(2):\n",
- " plt *= cw.plot(project.waves[i])\n",
- " \n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Adjust the target window here as necessary:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import chipwhisperer as cw\n",
- "import chipwhisperer.analyzer as cwa\n",
- "\n",
- "leak_model = cwa.leakage_models.inverse_sbox_output\n",
- "resync = cwa.preprocessing.ResyncSAD(project)\n",
- "resync.enabled=True\n",
- "resync.ref_trace = 0\n",
- "resync.target_window = (???, ???)\n",
- "resync.max_shift = 6000\n",
- "new_proj = resync.preprocess()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "plt = cw.plot([])\n",
- "for i in range(10):\n",
- " plt *= cw.plot(new_proj.waves[i])\n",
- " \n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Some numpy functions will be useful here, so we'll convert our ChipWhisperer project to numpy arrays:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "trace_array = np.array([new_proj.waves[i] for i in range(len(new_proj.traces))])\n",
- "textin_array = np.array([new_proj.textins[i] for i in range(len(new_proj.traces))])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We need to decrypt what we sent to the device to get one half of the input to the XOR:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "knownkey = [0x94, 0x28, 0x5D, 0x4D, 0x6D, 0xCF, 0xEC, 0x08, 0xD8, 0xAC, 0xDD, 0xF6, 0xBE, 0x25, 0xA4, 0x99,\n",
- " 0xC4, 0xD9, 0xD0, 0x1E, 0xC3, 0x40, 0x7E, 0xD7, 0xD5, 0x28, 0xD4, 0x09, 0xE9, 0xF0, 0x88, 0xA1]\n",
- "\n",
- "knownkey = bytes(knownkey)\n",
- "dr = []\n",
- "aes = AES.new(knownkey, AES.MODE_ECB)\n",
- "for i in range(len(new_proj.traces)):\n",
- " ct = bytes(textin_array[i])\n",
- " pt = aes.decrypt(ct)\n",
- " d = [bytearray(pt)[i] for i in range(16)]\n",
- " dr.append(d)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The basic idea is the same as the DPA attack: guess a bit and group traces based on that. We'll do the first byte here as an example. For each of the bits, you should see roughly half the traces fall into each group:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "ename": "SyntaxError",
- "evalue": "invalid syntax (, line 2)",
- "output_type": "error",
- "traceback": [
- "\u001b[1;36m File \u001b[1;32m\"\"\u001b[1;36m, line \u001b[1;32m2\u001b[0m\n\u001b[1;33m byte =\u001b[0m\n\u001b[1;37m ^\u001b[0m\n\u001b[1;31mSyntaxError\u001b[0m\u001b[1;31m:\u001b[0m invalid syntax\n"
- ]
- }
- ],
- "source": [
- "grouped_byte_traces = []\n",
- "byte = ??? # start with zero, then come back later and change\n",
- "\n",
- "for bit in range(8):\n",
- " grouped_bit_traces = [], []\n",
- " for i in range(len(new_proj.traces)):\n",
- " if (dr[i][byte] & (1 << bit)):\n",
- " grouped_bit_traces[0].append(trace_array[i])\n",
- " else:\n",
- " grouped_bit_traces[1].append(trace_array[i])\n",
- " grouped_byte_traces.append(grouped_bit_traces)\n",
- " print(len(grouped_bit_traces[0]))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now we need to figure out where the XOR operation is happening. You'll need to use the shape of the plot and the following plot of the difference of means for each bit. Each colour represents a different bit. For the correct part on the plot, you should see a distinct separation, with some bits peaking above zero, and others peaking below zero. If you see some colours in between peaks, it is probably not the right location. Repeat this for a few bytes - the location should change. Note down this change, as you'll have to use it to adjust the analysis location later:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Find averages and differences\n",
- "diffs = []\n",
- "for i in range(8):\n",
- " means = np.average(grouped_byte_traces[i][0], axis=0), np.average(grouped_byte_traces[i][1], axis=0)\n",
- " diffs.append(means[1] - means[0])"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Split traces into 2 groups\n",
- "from bokeh.plotting import figure, show\n",
- "from bokeh.io import output_notebook\n",
- "\n",
- "output_notebook()\n",
- "p = figure()\n",
- "\n",
- "xrange = range(len(diffs[0]))\n",
- "xrange2 = range(len(trace_array[0]))\n",
- "colours = [\"red\", \"blue\", \"green\", \"black\"]\n",
- "plt = cw.plot([])\n",
- "for i in range(8):\n",
- " plt *= cw.plot(diffs[i]).opts(color=colours[i%4])\n",
- "plt"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Fill in the position of the XOR, as well as how much it changes for each byte. All we're doing here is going byte by byte and bit by bit, and seeing if the difference in means is greater than or less than zero:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "btldr_IV = [0] * 16 #181\n",
- "for byte in range(16):\n",
- " location = ??? + byte * ???\n",
- " iv = 0\n",
- " for bit in range(8):\n",
- " pt_bits = [((dr[i][byte] >> (7-bit)) & 0x01) for i in range(len(new_proj.traces))]\n",
- "\n",
- " # Split traces into 2 groups\n",
- " groupedPoints = [[] for _ in range(2)]\n",
- " for i in range(len(new_proj.traces)):\n",
- " groupedPoints[pt_bits[i]].append(trace_array[i][location])\n",
- " \n",
- " means = []\n",
- " for i in range(2):\n",
- " means.append(np.average(groupedPoints[i]))\n",
- " diff = means[1] - means[0]\n",
- " \n",
- " iv_bit = 1 if diff > 0 else 0\n",
- " iv = (iv << 1) | iv_bit\n",
- " \n",
- " print(iv_bit, end = \" \")\n",
- " \n",
- " print(\"{:02X}\".format(iv))\n",
- " btldr_IV[byte] = iv\n",
- " \n",
- "print(btldr_IV)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Finally, we can do the full decryption!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "cipher = AES.new(bytes(btldr_key), AES.MODE_ECB)\n",
- "first_pt = cipher.decrypt(encrypted_firmware[:16])\n",
- "first_pt = [first_pt[i] ^ btldr_IV[i] for i in range(16)]\n",
- "print(bytearray(first_pt))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "As you can see, the first line of firmware is `deadbeefaabbccddeeff0011`"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.7"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/jupyter/lab/2-SCA_lab.ipynb b/jupyter/lab/1_SCA_Lab/SCA_lab.ipynb
similarity index 100%
rename from jupyter/lab/2-SCA_lab.ipynb
rename to jupyter/lab/1_SCA_Lab/SCA_lab.ipynb
diff --git a/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1.png b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1.png
new file mode 100644
index 00000000..e41f4796
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_offset60000.png b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_offset60000.png
new file mode 100644
index 00000000..7dce8364
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_offset60000.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_presample5000.png b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_presample5000.png
new file mode 100644
index 00000000..c5c03d56
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_presample5000.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_presample5000_zoom.png b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_presample5000_zoom.png
new file mode 100644
index 00000000..22ddd0cd
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx1_presample5000_zoom.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx4.png b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx4.png
new file mode 100644
index 00000000..972f2242
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/4traces_aes_clkx4.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/4traces_aes_poortrigger.png b/jupyter/lab/1_SCA_Lab/img/4traces_aes_poortrigger.png
new file mode 100644
index 00000000..66691a2b
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/4traces_aes_poortrigger.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/Clock-glitched.png b/jupyter/lab/1_SCA_Lab/img/Clock-glitched.png
new file mode 100644
index 00000000..193247a1
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/Clock-glitched.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/Clock-normal.png b/jupyter/lab/1_SCA_Lab/img/Clock-normal.png
new file mode 100644
index 00000000..1be7e98a
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/Clock-normal.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/ECC_threshold.png b/jupyter/lab/1_SCA_Lab/img/ECC_threshold.png
new file mode 100644
index 00000000..69405dde
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/ECC_threshold.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/Glitchgen-mux.png b/jupyter/lab/1_SCA_Lab/img/Glitchgen-mux.png
new file mode 100644
index 00000000..c65907e7
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/Glitchgen-mux.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/Glitchgen-phaseshift.png b/jupyter/lab/1_SCA_Lab/img/Glitchgen-phaseshift.png
new file mode 100644
index 00000000..e4a63860
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/Glitchgen-phaseshift.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/Mcu-unglitched.png b/jupyter/lab/1_SCA_Lab/img/Mcu-unglitched.png
new file mode 100644
index 00000000..e7f8460f
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/Mcu-unglitched.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/aesinput.png b/jupyter/lab/1_SCA_Lab/img/aesinput.png
new file mode 100644
index 00000000..608eede9
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/aesinput.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/attack_plot.png b/jupyter/lab/1_SCA_Lab/img/attack_plot.png
new file mode 100644
index 00000000..ed883d53
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/attack_plot.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/bit_train.png b/jupyter/lab/1_SCA_Lab/img/bit_train.png
new file mode 100644
index 00000000..23b2c480
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/bit_train.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/clock_glitches.png b/jupyter/lab/1_SCA_Lab/img/clock_glitches.png
new file mode 100644
index 00000000..9a3a041a
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/clock_glitches.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/dpa-doublepeak.png b/jupyter/lab/1_SCA_Lab/img/dpa-doublepeak.png
new file mode 100644
index 00000000..7dc40bda
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/dpa-doublepeak.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/dpa_peakexample.png b/jupyter/lab/1_SCA_Lab/img/dpa_peakexample.png
new file mode 100644
index 00000000..ac6cc9ec
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/dpa_peakexample.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/shunt_chipwhisperer.png b/jupyter/lab/1_SCA_Lab/img/shunt_chipwhisperer.png
new file mode 100644
index 00000000..ec4ca754
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/shunt_chipwhisperer.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/spa_password_diffexample.png b/jupyter/lab/1_SCA_Lab/img/spa_password_diffexample.png
new file mode 100644
index 00000000..cb9c7188
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/spa_password_diffexample.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/spa_password_h_vs_0_overview.png b/jupyter/lab/1_SCA_Lab/img/spa_password_h_vs_0_overview.png
new file mode 100644
index 00000000..fb8181b7
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/spa_password_h_vs_0_overview.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/spa_password_h_vs_0_zoomed.png b/jupyter/lab/1_SCA_Lab/img/spa_password_h_vs_0_zoomed.png
new file mode 100644
index 00000000..ec8a6135
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/spa_password_h_vs_0_zoomed.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/spa_password_list_char1.png b/jupyter/lab/1_SCA_Lab/img/spa_password_list_char1.png
new file mode 100644
index 00000000..034c1440
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/spa_password_list_char1.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/traces_wrong.png b/jupyter/lab/1_SCA_Lab/img/traces_wrong.png
new file mode 100644
index 00000000..ac456ae4
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/traces_wrong.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/uart_triggers.png b/jupyter/lab/1_SCA_Lab/img/uart_triggers.png
new file mode 100644
index 00000000..04de15cd
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/uart_triggers.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/uecc_0xaaaa.png b/jupyter/lab/1_SCA_Lab/img/uecc_0xaaaa.png
new file mode 100644
index 00000000..3e694b34
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/uecc_0xaaaa.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/uecc_7narrow_peaks.png b/jupyter/lab/1_SCA_Lab/img/uecc_7narrow_peaks.png
new file mode 100644
index 00000000..d9207233
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/uecc_7narrow_peaks.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/uecc_jumpers.png b/jupyter/lab/1_SCA_Lab/img/uecc_jumpers.png
new file mode 100644
index 00000000..3262999b
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/uecc_jumpers.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/uecc_poi11.png b/jupyter/lab/1_SCA_Lab/img/uecc_poi11.png
new file mode 100644
index 00000000..63685fd4
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/uecc_poi11.png differ
diff --git a/jupyter/lab/1_SCA_Lab/img/uecc_seqtrig_leakage.png b/jupyter/lab/1_SCA_Lab/img/uecc_seqtrig_leakage.png
new file mode 100644
index 00000000..46a18955
Binary files /dev/null and b/jupyter/lab/1_SCA_Lab/img/uecc_seqtrig_leakage.png differ
diff --git a/jupyter/lab/3_RSA_Lab/3_RSA_Lab.ipynb b/jupyter/lab/3_RSA_Lab/3_RSA_Lab.ipynb
index b8886d40..6634a3cf 100644
--- a/jupyter/lab/3_RSA_Lab/3_RSA_Lab.ipynb
+++ b/jupyter/lab/3_RSA_Lab/3_RSA_Lab.ipynb
@@ -167,137 +167,6 @@
"trace = capture_RSA_trace(scope, target, key)"
]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Tunable variables\n",
- "start = 3600\n",
- "window_size = 520\n",
- "SAD_thr = 40\n",
- "min_sep = 320 # Collapse distances that are closer to eachother than this\n",
- "\n",
- "def get_ref_trace_slice(trace, start, length):\n",
- " return trace[start:start+length]\n",
- "\n",
- "def sad_scan(trace, ref_trace):\n",
- " # Compute SAD vs the reference trace at every valid offset\n",
- " L = len(ref_trace)\n",
- " N = len(trace) - L\n",
- " diffs = []\n",
- " for i in range(N):\n",
- " diff = trace[i:i+L] - ref_trace\n",
- " diffs.append(np.sum(np.abs(diff)))\n",
- " return np.array(diffs)\n",
- "\n",
- "def hits(diffs, thr):\n",
- " # Return indices with SAD less than thr (threshold)\n",
- " return np.where(diffs < thr)[0]\n",
- "\n",
- "def collapse(times, min_sep):\n",
- " # Collapse dense clusters into one timestamp per step\n",
- " if len(times) == 0:\n",
- " return []\n",
- " collapsed = [int(times[0])]\n",
- " for t in times[1:]:\n",
- " if t - collapsed[-1] > min_sep:\n",
- " collapsed.append(int(t))\n",
- " return collapsed\n",
- "\n",
- "def choose_bit_threshold_from_deltas(deltas):\n",
- " if len(deltas) < 2:\n",
- " return float(np.median(deltas)) if len(deltas) else 0.0\n",
- " sd = np.sort(deltas)\n",
- " jumps = np.diff(sd)[:-1]\n",
- " if len(jumps) == 0:\n",
- " return float(np.median(sd))\n",
- " k = int(np.argmax(jumps))\n",
- " return 0.5 * (sd[k] + sd[k+1])\n",
- "\n",
- "def decode_bits_from_deltas(deltas, bit_thr):\n",
- " bits = [\"1\" if d > bit_thr else \"0\" for d in deltas]\n",
- " return \"\".join(bits)\n",
- "\n",
- "def bits_to_hex(bitstr):\n",
- " if not bitstr:\n",
- " return \"0x0\"\n",
- " val = int(bitstr, 2)\n",
- " nhex = (len(bitstr) + 3) // 4\n",
- " return f\"0x{val:0{nhex}X}\"\n",
- "\n",
- "def expect_windows_from_key_bytes(key_bytes):\n",
- " bits = \"\".join(f\"{b:08b}\" for b in key_bytes) # MSB→LSB within each byte\n",
- " first1 = bits.find(\"1\")\n",
- " return 0 if first1 == -1 else (len(bits) - first1)\n",
- "\n",
- "#plt.plot(trace, 'r')\n",
- "#plt.title(\"Full trace\")\n",
- "#plt.show()\n",
- "\n",
- "ref_trace = get_ref_trace_slice(trace, start, window_size)\n",
- "\n",
- "#plt.plot(ref_trace, 'b')\n",
- "#plt.title(f\"Reference (start={start}, len={window_size})\")\n",
- "#plt.show()\n",
- "\n",
- "diffs = sad_scan(trace, ref_trace)\n",
- "\n",
- "#plt.figure()\n",
- "#plt.plot(diffs)\n",
- "#plt.title('SAD Match for RSA')\n",
- "#plt.ylabel('SAD Difference')\n",
- "#plt.xlabel('Offset')\n",
- "#plt.grid(True, alpha=0.3)\n",
- "#plt.show()\n",
- "\n",
- "# 4) Hits and collapse (this is the big fix)\n",
- "times_raw = hits(diffs, SAD_thr)\n",
- "times = collapse(times_raw, min_sep=min_sep)\n",
- "\n",
- "print(f\"Raw hits: {len(times_raw)} Collapsed: {len(times)} (expect ~16 for your 0x8AB0 tail)\")\n",
- "\n",
- "# 5) Deltas (your deltalist), visualize like you did\n",
- "deltalist = [ times[i+1] - times[i] for i in range(len(times)-1) ]\n",
- "print(\"deltalist length (should be #steps-1):\", len(deltalist))\n",
- "\n",
- "plt.figure()\n",
- "plt.plot(deltalist, range(0, len(deltalist)), 'or')\n",
- "plt.grid(True)\n",
- "plt.title('Delta scatter (collapsed)')\n",
- "plt.ylabel('Processing Bit Number')\n",
- "plt.xlabel('Time Delta (samples)')\n",
- "plt.show()\n",
- "\n",
- "# 6) Choose bit threshold from the delta distribution (no magic 700)\n",
- "bit_thr = choose_bit_threshold_from_deltas(np.array(deltalist))\n",
- "print(f\"Chosen bit threshold (auto): ~{bit_thr:.1f}\")\n",
- "\n",
- "# Optional: quick histogram to see the 2 clusters and the threshold\n",
- "#plt.figure()\n",
- "#plt.hist(deltalist, bins=30)\n",
- "#plt.axvline(bit_thr, ls='--', color='k', label=f\"bit_thr≈{bit_thr:.1f}\")\n",
- "#plt.title(\"Delta histogram\")\n",
- "#plt.xlabel(\"Delta (samples)\")\n",
- "#plt.ylabel(\"Count\")\n",
- "#plt.legend()\n",
- "#plt.grid(True, alpha=0.3)\n",
- "#plt.show()\n",
- "\n",
- "# Decode bits\n",
- "bitstr_msb = decode_bits_from_deltas(deltalist, bit_thr)\n",
- "\n",
- "\n",
- "print(\"MSB→LSB bits:\", bitstr_msb)\n",
- "print(\"MSB→LSB hex:\", bits_to_hex(bitstr_msb))\n",
- "\n",
- "# 8) Sanity check against firmware behavior (optional but useful)\n",
- "exp_windows = expect_windows_from_key_bytes(key)\n",
- "print(f\"Expected windows after first '1': {exp_windows}\")\n",
- "print(f\"Observed windows (collapsed hits): {len(times)}\") "
- ]
- },
{
"cell_type": "markdown",
"metadata": {},