Skip to main content

PowerAnalysis: Part 2 picoCTF 2023 Solution

Average the noisy traces, then run correlation power analysis over all sixteen key bytes; the AES key is the flag.

Published: April 26, 2023Updated: August 25, 2026

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. Get the files from the challenge page on CyLab Security Academy (formerly play.picoctf.org).

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

bash
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 1Assess the noise level in the traces
    Observation
    The description warns that Part 2 gives fewer traces, 100, and a noisier dataset than Part 1. Quantify the signal-to-noise ratio before running anything, because the trace count you need 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) / traces.std(axis=0)).max())
    "
    What didn't work first

    Tried: Point the unmodified Part 1 attack at the 100 supplied traces and expect it to converge the same way.

    Part 2's traces are noisier and there are far fewer of them, so 100 raw traces fall short of the threshold where the correct byte's correlation clears the noise floor. Pearson correlation converges with the square root of the trace count, so tripling the noise costs roughly nine times as many traces. At 100, every key byte shows a similar peak and none is identifiable, which is why the pre-processing step exists.

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

    A global scalar statistic lumps high-amplitude but uninformative samples in with the actual leakage region, and one noise spike can dominate the numerator into a misleadingly good number. Compute the ratio per time sample instead, mean over standard deviation across traces, and you can see which samples genuinely 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, a naive attack on Part 2 with 3x the noise would need roughly 9 * 512 = 4608 traces. You are handed 100 and cannot collect more, which is why the next step buys back signal with pre-processing instead of with data.

  2. Step 2Apply trace averaging and pre-processing
    Observation
    The assessment shows a very low signal-to-noise ratio across those 100 traces. Subtracting the mean trace and windowing around the point of interest identified in Part 1 cuts the noise integrated into each correlation, which makes the attack viable without 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 every trace into one destroys the per-trace plaintext variation the attack depends on. CPA correlates a predicted Hamming weight per trace, derived from that trace's plaintext and a key guess, against the measured power at each sample. Collapse the traces and there is no per-trace dimension left to correlate, so you get a dimension mismatch or a flat zero for every guess.

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

    The Pearson formula does centre each variable internally, but skipping explicit mean removal leaves a large DC offset that inflates the variance estimates across the array. More to the point, you also skip the windowing: Part 1's point of interest should slice the traces down to the leakage region, which cuts integrated noise by orders of magnitude. Without that, the attack averages noise over the whole trace instead of 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 3Run full CPA over all 16 key bytes
    Observation
    After pre-processing, the centred and windowed traces still cover all 16 independent key bytes. Run the same Pearson distinguisher from Part 1 byte by byte across the corpus, watching convergence to confirm each byte has enough traces behind it.
    Execute the same correlation attack from Part 1, but over the centred, POI-windowed traces produced in the previous step.
    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.

    These traces carry a lower signal-to-noise ratio, so the count that sufficed in Part 1 leaves the correct peak indistinguishable from the floor. Without the mean subtraction and windowing from the previous step, correct and incorrect guesses cluster together: several key positions show two or three candidates with near-identical peaks, and the recovered key decrypts nothing.

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

    Sum of absolute differences can work for power analysis, though it behaves differently from Pearson correlation and is more sensitive to amplitude scaling between traces. Worse, skipping the convergence plot, maximum correlation against trace count per hypothesis, leaves no way to tell a converging attack from a random winner pulled out of noise. A correct run shows one hypothesis breaking away from the floor as traces accumulate; without that you may record a wrong byte and only find out when decryption fails.

    Learn more

    The fundamental attack algorithm is identical to Part 1. What changes is that the corpus is fixed at 100 traces, so every gain has to come from signal pre-processing: centring and POI windowing raise the effective signal-to-noise ratio of the samples the correlation actually sees. Given enough effective signal, the Pearson correlation for the correct key byte dominates the correlation for all incorrect guesses, and that separation 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, tighten the POI window or revisit the pre-processing, since the trace count here is fixed.

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

  4. Step 4Verify and format the recovered key as the flag
    Observation
    The attack yields 16 candidate key bytes. Decrypt a known ciphertext with them first to confirm the key before formatting it as lowercase hex 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 wraps the lowercase hex form of the 16-byte AES key. Decimal values, or separators between bytes, produce a string the checker will not accept. Calling .hex() on the Python bytes object gives the right 32-character string with nothing between.

    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.

Interactive tools
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Frequency AnalysisAnalyze letter frequencies in a substitution cipher and interactively build the decryption mapping with auto-filled guesses.

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 link between secret-dependent intermediate values and a physical measurement like power draw. Because the correlation between predicted leakage and measured traces converges with the square root of the trace count, noise raises the bar quadratically: double the noise and you need four times the traces to separate the right hypothesis from the wrong ones. The same framework applies to any unmasked implementation on real hardware, smart cards through microcontrollers, which is why masking, shuffling, and hardware noise injection are mandatory in certified products.

Related reading

Useful tools for Cryptography

Where to go next