PowerAnalysis: Part 2 picoCTF 2023 Solution

Published: April 26, 2023

Description

Full AES-128 key recovery via Correlation Power Analysis (CPA) on noisier power traces, building on PowerAnalysis Part 1. You receive only 100 pre-collected traces (fewer than Part 1 allowed you to gather) and additional signal processing is needed to extract the key from this noisier, limited dataset.

Download and unzip the challenge files.

Install NumPy, SciPy, and optionally tqdm for progress tracking.

bash
wget https://artifacts.picoctf.net/c/502/PowerAnalysis_Part2.zip && unzip PowerAnalysis_Part2.zip
bash
pip3 install numpy scipy tqdm

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Assess the noise level in the traces
    Observation
    I noticed the challenge description warned that Part 2 uses fewer traces (100) and a noisier dataset than Part 1, which suggested that quantifying the signal-to-noise ratio first was essential before running any attack, since the required trace count scales quadratically with noise.
    Plot a few traces and compute their signal-to-noise ratio to understand how much harder this is than Part 1.
    python
    python3 -c "
    import numpy as np
    import matplotlib.pyplot as plt
    traces = np.load('traces.npy')
    plt.plot(traces[0]); plt.title('Sample trace'); plt.show()
    print('SNR estimate:', traces.mean(axis=0).max() / traces.std(axis=0).max())
    "
    What didn't work first

    Tried: Use the same 100-trace corpus from Part 1 without scaling, expecting it to be enough for Part 2.

    Part 2 uses a noisier trace set, so 100 traces fall short of the statistical threshold where the correct key byte's correlation separates from the noise floor. The Pearson correlation converges as 1/sqrt(N), meaning a 3x increase in noise requires roughly 9x more traces. Running CPA on only 100 noisy traces produces every key byte showing similar peak correlations, making it impossible to identify the correct guess.

    Tried: Estimate SNR by dividing the global trace maximum by the global standard deviation rather than comparing per-sample mean to per-sample std.

    Using global (scalar) statistics conflates high-amplitude but non-informative time samples with the actual leakage region. A single high-amplitude noise spike can dominate the numerator and produce a misleadingly optimistic SNR. The correct approach computes the SNR per time sample - mean(axis=0) divided by std(axis=0) - so you see which specific samples actually carry secret-dependent signal.

    Learn more

    Part 2 adds significantly more noise to the simulated power traces, modeling realistic measurement conditions where electromagnetic interference, power supply fluctuations, and jitter obscure the actual computation signal. A low signal-to-noise ratio (SNR) means many more traces are needed to achieve the same confidence in the correlation.

    The relationship between noise and required traces is roughly quadratic: doubling the noise standard deviation requires 4x more traces to achieve the same correlation separation. The reason is that the Pearson correlation of a noisy signal converges as 1/sqrt(N): the standard error of the sample correlation shrinks with the square root of the sample size. To keep the CPA peak detectable above the noise floor when sigma doubles, you need to shrink the standard error by half, which means quadrupling N. If Part 1 needed ~512 traces, Part 2 with 3x the noise needs roughly 9 * 512 = 4608 traces.

  2. Step 2
    Apply trace averaging and pre-processing
    Observation
    I noticed that the SNR assessment revealed a very low signal-to-noise ratio across the 100 traces, which suggested that subtracting the mean trace and windowing around the known point of interest from Part 1 would reduce noise integration and make the correlation attack viable without needing many more traces.
    Average traces with the same plaintext byte value to reduce noise, and optionally apply a bandpass filter around the known POI (point of interest).
    python
    python3 -c "
    import numpy as np
    traces = np.load('traces.npy')
    plaintexts = np.load('plaintexts.npy')
    # Average across all traces to get mean trace
    mean_trace = traces.mean(axis=0)
    # Subtract mean to center data
    centered = traces - mean_trace
    print('Pre-processing complete, shape:', centered.shape)
    "
    What didn't work first

    Tried: Average all 100 traces together into a single mean trace and run CPA on just that single vector.

    Averaging all traces into one eliminates the per-trace plaintext variation that CPA depends on. The attack works by correlating 256 predicted Hamming-weight values (one per trace, derived from the plaintext and a key guess) against the measured power at each time sample. Collapsing all traces into one mean destroys the per-trace dimension, leaving no matrix to correlate against the hypothesis vector, so you either get a dimension mismatch or a flat correlation of zero for every key guess.

    Tried: Skip mean subtraction and go straight to CPA, reasoning that Pearson correlation is already zero-mean by definition.

    While the Pearson formula internally centers each variable, skipping explicit mean removal leaves a large DC offset that inflates sample variance estimates across the full trace array. More importantly, without per-sample centering you miss the windowing step: Part 1's identified point of interest should be used to slice the traces down to just the leakage region, which reduces noise integration by orders of magnitude. Skipping pre-processing means the attack must average out noise over the entire trace length instead of focusing on the informative samples.

    Learn more

    Signal processing techniques can improve CPA effectiveness on noisy data:

    • Per-trace centering (traces - mean_trace) removes the DC bias and any fixed offset that every trace shares. The CPA correlation only cares about the variance that depends on the secret, so subtracting the mean trace strips out everything that is constant across the dataset and tightens the estimate.
    • Windowing (POI selection) reuses Part 1's findings: the time sample where the correct key byte's correlation peaked in Part 1 is the same point of interest now. Take the bandpass window (or just the index) around that sample and ignore the rest of the trace; this drastically reduces the search space for CPA and the noise it has to integrate over.
    • Averaging across multiple traces for the same plaintext byte reduces random noise (assumes the signal is deterministic).
    • Sum of Absolute Differences (SAD) is an alternative to Pearson correlation that can be more robust in certain noise models.
  3. Step 3
    Run full CPA over all 16 key bytes
    Observation
    I noticed that after pre-processing the centered and windowed traces still covered 16 independent key bytes, which suggested running the same Pearson-correlation distinguisher from Part 1 byte-by-byte across the full corpus and monitoring convergence to confirm enough traces are available for each byte.
    Execute the same correlation attack from Part 1 but with the full (larger) trace set and any pre-processing applied.
    python
    python3 cpa_part2.py
    What didn't work first

    Tried: Reuse the Part 1 script unchanged (same trace count, no pre-processing) and expect CPA to converge on the noisier Part 2 dataset.

    The Part 2 traces have a lower SNR, so the same trace count that sufficed in Part 1 leaves the correct key byte's correlation peak indistinguishable from the noise floor. Without the mean subtraction and POI windowing added in the previous step, the correlation values for correct and incorrect guesses cluster together. The output will show several key bytes where 2-3 candidate guesses have nearly identical peak correlations, and the recovered key will fail to decrypt any known ciphertext.

    Tried: Use Sum of Absolute Differences (SAD) as the distinguisher instead of Pearson correlation, then compare results byte by byte without a convergence plot.

    SAD can work for power analysis but its statistical behavior differs from Pearson correlation and it is more sensitive to amplitude scaling between traces. More critically, skipping the convergence plot (running maximum correlation vs. trace count per hypothesis) means you have no way to tell whether the attack is converging or just producing a random winner from noise. A correct CPA run shows one hypothesis line breaking away from the noise floor as N grows; without that diagnostic you might record a wrong key byte from an unconverged run and not realize it until decryption fails.

    Learn more

    The fundamental attack algorithm is identical to Part 1. The improvements are quantitative (more traces) and qualitative (signal pre-processing). With enough traces, the Pearson correlation for the correct key byte will eventually dominate the correlation for all incorrect guesses, regardless of noise level - this is the statistical power of the CPA attack.

    Monitor convergence by plotting the maximum correlation for each of the 256 key guesses as a function of N (the number of traces used so far). The correct key byte's line climbs steadily and plateaus at a clearly elevated peak, while the 255 incorrect guesses fluctuate near the noise floor (~1/sqrt(N)) without ever separating. This convergence plot is the cleanest visual confirmation that you have enough traces and that your model is right; if no line breaks away from the rest after the full corpus, increase N or revisit the POI selection.

    See the AES for CTF guide for the round-by-round structure that the attack model assumes.

  4. Step 4
    Verify and format the recovered key as the flag
    Observation
    I noticed the CPA output yielded 16 candidate key bytes, which suggested decrypting a known ciphertext first to confirm the recovered key is correct before formatting it as a lowercase hex string inside picoCTF{}.
    Decrypt a known ciphertext with the recovered key to verify, then format the hex key as the flag.
    python
    python3 -c "
    from Crypto.Cipher import AES
    key = bytes([<K0>, <K1>, ..., <K15>])
    cipher = AES.new(key, AES.MODE_ECB)
    print(cipher.decrypt(bytes.fromhex('<CT>')))
    print('Flag: picoCTF{' + key.hex() + '}')
    "

    Expected output

    picoCTF{...}
    What didn't work first

    Tried: Format the flag by joining the recovered key bytes as decimal integers separated by underscores rather than as a contiguous hex string.

    The flag format for this challenge is picoCTF{} wrapping the lowercase hex representation of the 16-byte AES key, e.g. picoCTF{0a1b2c...}. Using decimal values or adding separators produces a string that does not match the expected submission format. The key.hex() call on a Python bytes object produces the correct 32-character lowercase hex string with no separators.

    Learn more

    Completing Part 2 demonstrates the full power analysis attack pipeline against a realistically noisy target. In actual hardware security evaluations, researchers use oscilloscopes and current probes to capture real power traces from microcontrollers, then apply the same statistical correlation methodology to recover keys from production devices.

    Countermeasures in certified products include: first-order masking (XOR intermediates with random values to break the Hamming-weight model), higher-order masking (resists DPA of any fixed order), shuffling (random byte processing order), and hardware-level noise injection. These add cost and complexity but are required for EAL5+ and FIPS 140-3 Level 3 certifications.

Flag

Reveal flag

picoCTF{...}

This challenge was not solved during the competition. Follow the steps above to reproduce the solution.

Key takeaway

Correlation Power Analysis exploits the statistical relationship between secret-dependent intermediate values and physical side-channel measurements like power consumption. Because the Pearson correlation between a predicted leakage model and measured traces converges as 1/sqrt(N), noise raises the bar quadratically: doubling the noise requires four times as many traces to separate the correct key hypothesis from wrong ones. The same attack framework applies to any unmasked cryptographic implementation on real hardware, from smart cards to microcontrollers, which is why masking, shuffling, and hardware noise injection are mandatory in certified security products.

Related reading

Want more picoCTF 2023 writeups?

Useful tools for Cryptography

What to try next