SCA1
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 encryptionplaintexts: 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:
- Hypothetical power model - Hamming weight of intermediate values (S-box output in first round)
- 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 read-only • copy allowed#!/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