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 core
  • traces.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 1 bits 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

FactorExplanation
Linear key mixingXOR allows byte-by-byte isolation in Round 1
Deterministic leakageHamming weight approximates power consumption
Independent noiseAveraging 10,000 traces reduces variance as 1/√N
No countermeasuresNo 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