← Back to Writeups
CUSTOMMediumHardware

SCA1

XESXOR9/2/20264 min read
#hardware#custom#medium#correlation_power_analysis#hamming_weight_model

SCA1

Platform: 0xl4ugh | Category: Hardware | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-01-23 | Status: Solved Techniques: correlation_power_analysis, hamming_weight_model

Summary

You are given some power traces and plaintexts for an AES encryption, get the secret. Note: wrap the key in 0xL4ugh{}

SCA1 - 0xl4ugh CTF

Description

You are given some power traces and plaintexts for an AES encryption, get the secret. Note: wrap the key in 0xL4ugh{}

Analysis

The challenge provides an NPZ file (500MB) containing:

  • traces: shape=(100000, 640), dtype=float64 - power consumption measurements during AES encryption
  • plaintexts: shape=(100000, 16), dtype=int64 - corresponding AES plaintexts (16 bytes each)

This is a classic Correlation Power Analysis (CPA) setup. The large number of traces (100,000) and the hint about power consumption indicate a side-channel attack on AES. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Attack Theory

CPA exploits the correlation between:

  1. Hypothetical power model - Hamming weight of intermediate values (S-box output in first round)
  2. Actual power consumption - measured traces

For AES first round:

intermediate = SBOX[plaintext[i] XOR key[i]]

The power consumption correlates with the Hamming weight of this intermediate value. By trying all 256 possible key byte values and computing correlations, the correct key byte will show the highest correlation.

Solution

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
"""
Side-Channel Analysis (SCA) - Correlation Power Analysis (CPA) Attack on AES
"""

import numpy as np
from tqdm import tqdm

# AES S-box
SBOX = [
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
    0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
    0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
    0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
    0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
    0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
    0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
    0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
    0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
    0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
    0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
    0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
    0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
    0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
    0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def hamming_weight(x):
    """Calculate Hamming weight (number of 1 bits)"""
    return bin(x).count("1")


def cpa_attack_byte_fast(traces, plaintexts, byte_idx):
    """
    Optimized CPA attack using vectorized operations
    """
    num_traces, num_samples = traces.shape

    # Precompute all hypothetical intermediate values
    hw_matrix = np.zeros((256, num_traces))
    for key_guess in range(256):
        hw_matrix[key_guess] = [
            hamming_weight(SBOX[plaintexts[i, byte_idx] ^ key_guess])
            for i in range(num_traces)
        ]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # Normalize traces
    traces_norm = traces - np.mean(traces, axis=0)
    traces_std = np.std(traces, axis=0)
    traces_std[traces_std == 0] = 1  # Avoid division by zero

    # Normalize hypotheses
    hw_norm = hw_matrix - np.mean(hw_matrix, axis=1, keepdims=True)
    hw_std = np.std(hw_matrix, axis=1, keepdims=True)
    hw_std[hw_std == 0] = 1

    # Calculate correlations for all keys at once
    correlations = np.abs(
        np.dot(hw_norm, traces_norm) / (num_traces * hw_std * traces_std)
    )
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # Get max correlation for each key
    max_corr_per_key = np.max(correlations, axis=1)

    best_key = np.argmax(max_corr_per_key)
    return best_key, max_corr_per_key


def main():
    print("[*] Loading data...")
    data = np.load("sca1.npz")

    traces = data['traces']
    plaintexts = data['plaintexts'].astype(np.uint8)

    print(f"[*] Traces shape: {traces.shape}")
    print(f"[*] Plaintexts shape: {plaintexts.shape}")

    print(f"\n[*] Running CPA attack...")

    key = []
    for byte_idx in tqdm(range(16), desc="Attacking key bytes"):
        best_key, correlations = cpa_attack_byte_fast(traces, plaintexts, byte_idx)
        key.append(best_key)
        print(f"  Byte {byte_idx:2d}: best=0x{best_key:02x} (corr={correlations[best_key]:.4f})")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    key_hex = "".join(f"{b:02x}" for b in key)
    key_ascii = "".join(chr(b) if 32 <= b < 127 else "." for b in key)

    print(f"\n[+] Recovered key (hex): {key_hex}")
    print(f"[+] Recovered key (ASCII): {key_ascii}")
    print(f"\n[+] Flag: 0xL4ugh{{{key_ascii}}}")


if __name__ == "__main__":
    main()

Execution Result

[*] Traces shape: (100000, 640)
[*] Plaintexts shape: (100000, 16)

[*] Running CPA attack...
  Byte  0: best=0x44 (corr=0.xxxx)  # 'D'
  Byte  1: best=0x50 (corr=0.xxxx)  # 'P'
  Byte  2: best=0x41 (corr=0.xxxx)  # 'A'
  Byte  3: best=0x34 (corr=0.xxxx)  # '4'
  Byte  4: best=0x42 (corr=0.xxxx)  # 'B'
  Byte  5: best=0x61 (corr=0.xxxx)  # 'a'
  Byte  6: best=0x62 (corr=0.xxxx)  # 'b'
  Byte  7: best=0x79 (corr=0.xxxx)  # 'y'
  Byte  8: best=0x47 (corr=0.xxxx)  # 'G'
  Byte  9: best=0x6f (corr=0.xxxx)  # 'o'
  Byte 10: best=0x67 (corr=0.xxxx)  # 'g'
  Byte 11: best=0x6f (corr=0.xxxx)  # 'o'
  Byte 12: best=0x47 (corr=0.xxxx)  # 'G'
  Byte 13: best=0x61 (corr=0.xxxx)  # 'a'
  Byte 14: best=0x67 (corr=0.xxxx)  # 'g'
  Byte 15: best=0x61 (corr=0.xxxx)  # 'a'
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

[+] Recovered key (hex): 4450413442616279476f676f47616761
[+] Recovered key (ASCII): REDACTED

References

  • Correlation Power Analysis
  • ChipWhisperer Tutorials
  • DPA Contest ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍