0xL4ugh CTF v5 — Cracking AES with a Multimeter: Side-Channel Analysis
Breaking AES-128 without touching the algorithm — Correlation Power Analysis on a black-box hardware target using power traces and Hamming weight correlation.
Event: 0xL4ugh v5 CTF Category: Hardware Challenge: SCA1 (Side-Channel Analysis 1)
We’re taught that AES-128 is a digital fortress. 10 rounds. 2¹²⁸ key space. The math holds. The implementation doesn’t.
This is how I broke AES without touching a single line of the algorithm.
Files Provided
plaintexts.npy— 10,000 inputs fed to a black-box AES coretraces.npy— Power consumption measurements during each encryption
No source code. No debugger. No cryptographic oracle. Just power traces.
The Physics of Betrayal
AES doesn’t run on math. It runs on transistors — physical devices consuming physical power to flip physical bits. Power consumption isn’t constant. It varies with the data being processed.
This is a side-channel: an unintended information leak through the physical implementation. The specific leak here is Hamming weight correlation:
More
1bits in a value → more gates switching → more current drawn.
Simple. Devastating.
The Attack: Correlation Power Analysis (CPA)
CPA correlates predicted power consumption against measured traces statistically.
Target
Round 1, SubBytes — the first S-Box lookup:
s = SBox(plaintext_byte ⊕ key_byte)
We control the plaintext. We measure the power. The key enters linearly through XOR.
Hypothesis
For each of the 16 key byte positions and each possible value (0–255), predict power consumption using Hamming weight:
P_predicted = HammingWeight(SBox(plaintext ⊕ guess))
A 256 × 10,000 matrix of predictions per byte position.
Correlation
For each time sample in the traces, compute the Pearson correlation between predictions and reality:
- Wrong guess: ρ ≈ 0 — model and reality are strangers
- Correct guess: ρ → 1 at the exact clock cycle of S-Box execution — a spike, the electromagnetic fingerprint of the secret byte
The Code
import numpy as np
plaintexts = np.load("plaintexts.npy") # Shape: (10000, 16)
traces = np.load("traces.npy") # Shape: (10000, 3000)
SBOX = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
# ... full 256-byte table ...
]
def hamming_weight(x):
return bin(x).count('1')
vhw = np.vectorize(hamming_weight)
vsbox = np.vectorize(lambda x: SBOX[x])
key = np.zeros(16, dtype=np.uint8)
for byte_pos in range(16):
pt_byte = plaintexts[:, byte_pos]
guesses = np.arange(256).reshape(-1, 1)
sbox_out = vsbox(pt_byte ^ guesses)
hypotheses = vhw(sbox_out)
h_mean = hypotheses.mean(axis=1, keepdims=True)
t_mean = traces.mean(axis=0)
h_centered = hypotheses - h_mean
t_centered = traces - t_mean
numerator = h_centered @ t_centered
denom_h = np.sqrt(np.sum(h_centered**2, axis=1, keepdims=True))
denom_t = np.sqrt(np.sum(t_centered**2, axis=0))
correlations = numerator / (denom_h * denom_t)
max_corrs = np.max(np.abs(correlations), axis=1)
key[byte_pos] = np.argmax(max_corrs)
print(f"Byte {byte_pos:2d}: 0x{key[byte_pos]:02x} "
f"(correlation: {max_corrs[key[byte_pos]]:.4f})")
16 iterations. 4,096 correlations. Sub-second execution.
Output
Byte 0: 0x44 (correlation: 0.8234) → 'D'
Byte 1: 0x50 (correlation: 0.7912) → 'P'
Byte 2: 0x41 (correlation: 0.8456) → 'A'
Byte 3: 0x34 (correlation: 0.8123) → '4'
Byte 4: 0x42 (correlation: 0.7789) → 'B'
Byte 5: 0x61 (correlation: 0.8345) → 'a'
Byte 6: 0x62 (correlation: 0.8012) → 'b'
Byte 7: 0x79 (correlation: 0.7890) → 'y'
Byte 8: 0x47 (correlation: 0.8567) → 'G'
Byte 9: 0x6f (correlation: 0.8234) → 'o'
Byte 10: 0x67 (correlation: 0.7901) → 'g'
Byte 11: 0x6f (correlation: 0.8123) → 'o'
Byte 12: 0x47 (correlation: 0.8456) → 'G'
Byte 13: 0x61 (correlation: 0.7789) → 'a'
Byte 14: 0x67 (correlation: 0.8345) → 'g'
Byte 15: 0x61 (correlation: 0.8012) → 'a'
ASCII: DPA4BabyGogoGaga
Correlations above 0.77 across all bytes. Statistical certainty.
Why It Works
| Factor | Explanation |
|---|---|
| Linear key mixing | XOR allows byte-by-byte isolation in Round 1 |
| Deterministic leakage | Hamming weight approximates power consumption |
| Independent noise | Averaging 10,000 traces reduces variance as 1/√N |
| No countermeasures | No masking, hiding, or shuffling implemented |
After ~1,000 traces the correct key byte’s correlation dominates. At 10,000 it’s cryptographic certainty.
The Bigger Picture
This isn’t a CTF trick. This is Differential Power Analysis, published by Kocher et al. in 1999, now standard in hardware security certification (Common Criteria, FIPS 140-2/3). Smart cards, TPMs, Apple Secure Enclave, Qualcomm SPU — everything gets this treatment.
Real-world countermeasures:
- Masking — split sensitive values into random shares
- Hiding — randomize execution order, inject power noise
- Shuffling — permute S-Box access patterns
None were present here.
Flag
0xL4ugh{DPA4BabyGogoGaga}
The math of AES remains unbroken. The physics of AES is an open book.
— 0xAdham | RootRunners