Skip to main content

PowerAnalysis: Part 1 picoCTF 2023 Solution

Use statistical analysis of power traces from an AES implementation to recover secret key bytes.

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

Description

A live remote system encrypts plaintext you send and returns the CPU power trace for each encryption. Use the scared side-channel analysis library to collect traces, build a chosen-plaintext CPA attack on the first AES round S-Box, and recover the full 16-byte key.

Connect to the challenge server to see the trace format. Send 32 hex characters and receive a power trace array.

Install pwntools, numpy, and the scared library.

bash
nc saturn.picoctf.net <PORT_FROM_INSTANCE>
bash
pip3 install pwntools numpy scared

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Understand the server protocol
    Observation
    The server encrypts chosen plaintexts with AES and returns a power trace. Probe it by hand first to establish the exact input format and the shape of the trace output, before writing any attack code.
    The server prompts for 16 bytes of plaintext (32 hex characters). It encrypts them with a fixed AES key and returns a power trace as a bracketed array of numbers. Each number correlates with the Hamming weight of bits processed during that AES clock cycle.
    bash
    nc saturn.picoctf.net <PORT_FROM_INSTANCE>
    bash
    # Server says: '16 bytes of plaintext (hex):'
    bash
    # Send: 00000000000000000000000000000000
    bash
    # Receive: [0.123, 0.456, ...]
    What didn't work first

    Tried: Send ASCII text instead of hex to the server to see if it accepts plaintext directly.

    The server wants exactly 32 hex characters, representing 16 bytes. Send raw ASCII, or fewer characters, and it rejects the input and closes the connection. The prompt asks for 16 bytes in hex, two digits each, so a 16-character ASCII string is only 8 bytes and produces no trace.

    Tried: Try to replay the same plaintext multiple times to see if the traces are identical and skip random sampling.

    The key is fixed, so identical plaintexts return identical traces and zero variance in the plaintext bytes, which leaves the correlation matrix undefined at those positions. CPA needs varying plaintexts so the Hamming weight of the S-Box output moves across traces. Without variance there is nothing to correlate against.

    Learn more

    Correlation Power Analysis (CPA) works by measuring the correlation between predicted power consumption and actual measured power. For AES, the first-round S-Box lookup SBox[plaintext XOR key] depends on one byte of plaintext and one byte of the key. By choosing different plaintexts and measuring the resulting power traces, you can correlate predicted Hamming weights against actual measurements to identify the correct key byte.

    The challenge leaks power information correlated with Hamming weight of processed values. This is the standard side-channel leakage model for software AES on a microcontroller.

  2. Step 2Collect traces using pwntools and the scared library
    Observation
    Correlation power analysis needs many traces against varying plaintexts before the statistics mean anything. Automate collection with pwntools, gather around 512 random-plaintext samples, and save them for offline analysis.
    Write a script that connects to the server, sends random plaintexts, and captures the returned power traces. Build a ScaRed trace set from the collected data. About 512 traces is enough for a clean attack.
    python
    python3 - <<'PY'
    import numpy as np
    from pwn import remote
    import re, random
    
    HOST = "saturn.picoctf.net"
    PORT = 0  # replace with your port
    
    def get_trace(r, plaintext_bytes):
        r.recvuntil(b":")
        r.sendline(plaintext_bytes.hex().encode())
        response = r.recvline().decode()
        # Parse the bracketed array
        nums = re.findall(r"[-\d.]+(?:e[-+]?\d+)?", response)
        return np.array([float(x) for x in nums])
    
    r = remote(HOST, PORT)
    
    N = 512
    plaintexts = np.zeros((N, 16), dtype=np.uint8)
    traces_list = []
    
    for i in range(N):
        pt = bytes(random.randrange(256) for _ in range(16))
        plaintexts[i] = list(pt)
        trace = get_trace(r, pt)
        traces_list.append(trace)
        if i % 50 == 0:
            print(f"Collected {i}/{N} traces")
    
    traces = np.array(traces_list)
    np.save("plaintexts.npy", plaintexts)
    np.save("traces.npy", traces)
    print("Saved plaintexts.npy and traces.npy")
    PY
    What didn't work first

    Tried: Collect only 32 or 64 traces to save time and then run the attack.

    With too few traces the correlation is dominated by noise and most key byte guesses score alike, so the attack returns wrong bytes or a flat matrix with no clear winner. Around 512 traces gives enough statistical power to separate the correct byte from the 255 others, which matters most when the trace is long and the signal is spread thin.

    Tried: Parse the trace response with a simple split() instead of a regex, assuming a clean space-separated format.

    The response wraps its numbers in square brackets and may use scientific notation. Splitting on spaces leaves bracket characters stuck to the first and last values, so float() raises a ValueError. A regex matching both plain and scientific-notation floats handles them whatever punctuation surrounds them.

    Learn more

    The scared library (from eShard) provides a high-level API for side-channel analysis. It handles the correlation math and returns the most likely key byte per position. The CPAAttack class runs over a trace header set built from your plaintext and trace arrays, against a selection function that names the AES intermediate you are targeting (here the first-round SubBytes).

  3. Step 3Run the CPA attack with scared
    Observation
    The scared library ships a CPA attack with a first-round SubBytes selection function. That round's S-Box output depends on exactly one plaintext byte and one key byte, so the 16-byte key comes back one byte at a time, each from the guess with the highest correlation.
    Build a scared TraceHeaderSet from the collected data, set up a chosen-plaintext CPA attack targeting the first AES SubBytes, run it, and extract the key bytes with the highest correlation scores.
    python
    python3 - <<'PY'
    import numpy as np
    import scared
    
    plaintexts = np.load("plaintexts.npy")
    traces = np.load("traces.npy")
    
    # Build trace header set
    ths = scared.traces.formats.read_ths_from_ram(
        samples=traces,
        plaintext=plaintexts,
    )
    
    # Attack: first SubBytes, Hamming weight leakage model
    attack = scared.CPAAttack(
        selection_function=scared.aes.selection_functions.encrypt.FirstSubBytes(),
        model=scared.HammingWeight(),
        discriminant=scared.maxabs,
    )
    
    attack.run(ths)
    
    # Extract the best key guess per byte position.
    # attack.scores is the discriminated (256 guesses, 16 positions) array;
    # attack.results still carries the time axis, so use scores, not results.
    key = np.argmax(attack.scores, axis=0).astype(np.uint8).tobytes()
    print("Recovered key:", key.hex())
    print("Flag: picoCTF{" + key.hex() + "}")
    PY
    What didn't work first

    Tried: Use scared.LastSubBytes() as the selection function instead of FirstSubBytes() to target the final AES round.

    A last-round CPA is a real technique, but it predicts from the ciphertext (inverse S-Box of ciphertext byte XOR last-round-key byte), and this server hands back only a trace, never the ciphertext. With no ciphertext there is nothing for that selection function to key off. FirstSubBytes predicts from the plaintext you chose, which you always know, and its output depends on exactly one plaintext byte and one key byte. It also yields the original key directly, where a last-round attack recovers round key 10 and needs the key schedule run backwards afterwards.

    Tried: Use np.argmax(attack.scores, axis=1) instead of axis=0 to extract the best key guess.

    After the discriminant collapses the time dimension, the scores array is 256 guesses by 16 key positions. Taking the argmax over the guess dimension gives one best guess per byte. Taking it over the other axis returns a 256-element array of position indices, which is not a key at all.

    Learn more

    The scared CPAAttack computes Pearson correlation between the predicted Hamming weight of SBox[plaintext[i] XOR k] for every key guess k in 0..255, and the actual trace samples. The maxabs discriminant then collapses the time axis by taking the largest absolute correlation per guess, so the key byte with the highest score is the correct guess. Running this for all 16 byte positions recovers the full AES key.

    The format of the flag is the recovered key hex-encoded and wrapped in the picoCTF format. For background on the AES round function and S-Box see the AES for CTF guide.

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{...}

The flag is the 16-byte AES key recovered by CPA, hex-encoded and wrapped in the standard format.

Key takeaway

Power analysis exploits the fact that a CPU draws measurably different current depending on the data it processes, which leaks key-dependent information with no mathematical weakness in the cipher at all. Correlation Power Analysis matches observed traces against a predicted leakage model, usually the Hamming weight of an intermediate value, to isolate one key byte at a time. The attack breaks otherwise-secure implementations in smart cards, embedded devices, and hardware security modules, which is why masking, shuffling, and constant-time implementations are required wherever hardware handles secret keys.

Related reading

Useful tools for Cryptography

Where to go next