Rating:

This is a simulation of a Correlation Power Analysis attack.

The service allows us to encrypt any input, and leaks voltage measurements for the state right after the call to `sub_bytes` in the AES algorithm.

The attack itself is explained in depth [here](https://wiki.newae.com/Correlation_Power_Analysis). We just need to extract the first set of measurements (for the first iteration of AES) and ignore the rest.

We compare the actual measuremetns of the first round to the expected volatage of encrypting our inupt with each possible byte value of a key, and find the closest match.

```python
from pwn import *
import base64
from scipy import spatial
from Crypto.Cipher import AES

# export LANG=C.UTF-8

BLOCK_LEN = 16
BITS_IN_BYTE = 8
NUM_BYTE_VALUES = 2**BITS_IN_BYTE

VOLTAGE_MIN = 2.5
VOLTAGE_MAX = 5

HAMMING_WEIGHT_MIN = 0
HAMMING_WEIGHT_MAX = 8

def hamming_weight(x):
return bin(x).count('1')

def normalize(value, from_min, from_max, to_min, to_max):
old_range = from_max - from_min
new_range = to_max - to_min

return ((value - from_min) / old_range) * new_range + to_min

def weight_to_voltage(weight):
return normalize(HAMMING_WEIGHT_MAX - weight, HAMMING_WEIGHT_MIN, HAMMING_WEIGHT_MAX, VOLTAGE_MIN, VOLTAGE_MAX)

s_box = (
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,
)

expected_volt_per_key_byte = [[0 for x in range(NUM_BYTE_VALUES)] for y in range(NUM_BYTE_VALUES)]

with log.progress("Calculating expected voltage per key byte") as p:
for key_byte_value in range(NUM_BYTE_VALUES):
for plaintext_byte_value in range(NUM_BYTE_VALUES):
#p.status(f"Key byte: {key_byte_value}, plaintext byte: {plaintext_byte_value}")
value = s_box[key_byte_value ^ plaintext_byte_value]
weight = hamming_weight(value)
voltage = weight_to_voltage(weight)
expected_volt_per_key_byte[key_byte_value][plaintext_byte_value] = voltage

actual_volt_per_key_index = [[] for _ in range(BLOCK_LEN)]

r = remote("project_power.ichsa.ctf.today", 8012)

r.recvuntil("The following base64-encoded ciphertext is encrypted with aes-128-ecb:\n")
encrypted_flag = r.recvlineS(keepends = False)

log.info(f"Encrypted flag: {encrypted_flag}")

with log.progress("Reading measurements") as p:
for byte_val in range(NUM_BYTE_VALUES):
p.status(f"Byte value {byte_val}")
to_encrypt = bytes([byte_val] * BLOCK_LEN)
r.sendlineafter("Enter base64-encoded input to be encrypted: ", base64.b64encode(to_encrypt))
first_round_measurements = map(float, r.recvlineS(keepends=False).strip("[]").split(", "))

for key_index, measurement in enumerate(first_round_measurements):
actual_volt_per_key_index[key_index].append(measurement)

# Ignore remaining measurements

key = b""

with log.progress("Reconstructing key") as p:
tree = spatial.KDTree(expected_volt_per_key_byte)
for i, actual_volt_vector in enumerate(actual_volt_per_key_index):
p.status(f"Recovering character #{i}")
res = tree.query(actual_volt_vector)
key += bytes([res[1]])

log.success(f"Key: {key}")

unpad = lambda s: s[:-ord(s[len(s) - 1:])]
flag = unpad(AES.new(key, AES.MODE_ECB).decrypt(base64.b64decode(encrypted_flag))).decode('ascii')
log.success(f"Flag: {flag}")
assert(flag == "ICHSA_CTF{Wh0_n3eds_c0mpliC4t3d_m4th_Wh3n_u_HaVe_CPA}")
```

Original writeup (https://gitlab.com/kobi3028/ichsa-ctf/-/blob/master/project_power/solve.py).