PowerAnalysis: Part 1 picoCTF 2023 Solution

Published: April 26, 2023

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
  1. Step 1Understand the server protocol
    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, ...]
    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
    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
    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 ChosenPlainTextAttack class from scared accepts plaintext arrays and trace arrays, and runs CPA against a target function (like AES SubBytes).

  3. Step 3Run the CPA attack with scared
    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
    key = bytes(np.argmax(attack.results, axis=0))
    print("Recovered key:", key.hex())
    print("Flag: picoCTF{" + key.hex() + "}")
    PY
    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 key byte with the highest maximum absolute correlation across all time samples 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.

Flag

picoCTF{...}

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

Want more picoCTF 2023 writeups?

Useful tools for Cryptography

Related reading

What to try next