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.
Setup
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.
nc saturn.picoctf.net <PORT_FROM_INSTANCE>pip3 install pwntools numpy scaredSolution
Walk me through it- Step 1Understand the server protocolThe 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: 00000000000000000000000000000000bash# 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.
- Step 2Collect traces using pwntools and the scared libraryWrite 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") PYLearn 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
ChosenPlainTextAttackclass from scared accepts plaintext arrays and trace arrays, and runs CPA against a target function (like AES SubBytes). - Step 3Run the CPA attack with scaredBuild 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() + "}") PYLearn more
The scared
CPAAttackcomputes Pearson correlation between the predicted Hamming weight ofSBox[plaintext[i] XOR k]for every key guesskin 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.