Chip Security TestingΒ 
Binary Security AnalysisΒ 
ResourcesΒ 
Blog
Contact us
Back to all articles
Chip Security

PicoCTF: Power Analysis Challenges

7 min read
Edit by Guillaume Bethouart β€’ Mar 31, 2023
Share

Here the write-ups for three challenges of the PicoCTF 2023, running from March 14 to March 28. These challenges are a series on side-channel power analysis. I have used estraces & scared packages publicly available on github.

Β 

PowerAnalysis: Warmup


Description

This encryption algorithm leaks a "bit" of data every time it does a computation. Use this to figure out the encryption key. Download the encryption program here encrypt.py. Access the running server with nc saturn.picoctf.net 56776. The flag will be of the format picoCTF{<encryption key>} where <encryption key> is 32 lowercase hex characters comprising the 16-byte encryption key being used by the program.

A quick look on the encrypt.py file shows that the server return a kind of side-channel leakage corresponding to intermediate values during the AES encryption of the given plaintext, using a secret key. The leakage is the number of LSB bits set to 1 after the operation SBOX[plaintext ^ key].

I first tried to collect some leakages and then deduce the key values by hand, but I'm a lazy guy. And the laziest solution was (for me) to:

  • collect a dataset with plaintext and the corresponding leakage
  • apply a side-channel attack on it

Β 

Communicate with the server

To communicate with the server, I use a small tool I made called NC based on sockets. See an example below:

from netcat import NC import time port = 51011 nc = NC('saturn.picoctf.net', port, timeout=1, debug=True) nc.receive() time.sleep(0.005) resp = nc.query('000102030405060708090a0b0c0d0e0f', sleep=0.005)
<< Please provide 16 bytes of plaintext encoded as hex: >> 000102030405060708090a0b0c0d0e0f << leakage result: 9

Sending a plaintext, the server returns a leakage number in the range [0, 16].

Β 

Collect the dataset

As explained, I'll collect several couples (plaintext, leakage) in order to perform a side-channel attack. The following takes a plaintext as input (a numpy array) and returns the corresponding leakage:

def encrypt_and_leak(plaintext): plaintext = plaintext.tobytes().hex() nc = NC('saturn.picoctf.net', port, timeout=1, debug=False) nc.receive() time.sleep(0.005) resp = nc.query(plaintext, sleep=0.005) return int(resp.strip().split(': ')[1].strip())

Use it in a loop to build a dataset of 500 traces, i.e. couples (plaintext, leakage):

import numpy as np from tqdm.notebook import tqdm from estraces import read_ths_from_ram plaintexts = [] leaks = [] for _ in tqdm(range(500)): plaintext = np.random.randint(0, 256, 16, dtype='uint8') plaintexts.append(plaintext) leak = encrypt_and_leak(plaintext) leaks.append([leak, leak]) # A trace must have at least two samples to be processed by our side-channel framework ths = read_ths_from_ram(samples=np.array(leaks), plaintext=np.array(plaintexts)) ths
Trace Header Set: Name.............: RAM Format THS Reader...........: RAM reader with 500 traces. Samples shape: (500, 2) and metadatas: ['plaintext'] plaintext........: uint8

Β 

Attack

Once it's done (it takes about 3 minutes), we can perform the side-channel attack. We will target the LSB bit of the output of the SubBytes operation with a Correlation Power Analysis (CPA):

import scared attack = scared.CPAAttack(selection_function=scared.aes.selection_functions.encrypt.FirstSubBytes(), model=scared.Monobit(0), discriminant=scared.nanmax, # Positive correlation expected convergence_step=50) attack.run(scared.Container(ths))

An optional step is to plot the results for the first byte:

import matplotlib.pyplot as plt def plot_attack(attack, byte): """Plot attack results for the given byte.""" fig, axes = plt.subplots(1, 2, figsize=(20, 3)) axes[0].plot(attack.results[:, byte].T) axes[0].set_title('CPA results', loc='left') axes[1].plot(attack.convergence_traces[:, byte].T) axes[1].set_title('Scores convergence', loc='right') plt.suptitle(f'Attack results for byte {byte}') plt.show() plot_attack(attack, 0)

output_10_0.png

We can see that for the first byte, a key value is clearly correlating more than the others. A quick check confirms that is the case for all bytes.

Β 

Key extraction and flag conversion

Then the correct key should be the one with the highest score for each byte. Let's extract it:

found_key = np.nanargmax(attack.scores, axis=0).astype('uint8') print(f'picoCTF{{{found_key.tobytes().hex()}}}')
picoCTF{9dac9d914154db052d7ce1a110fbb3aa}

Β 

PowerAnalysis: Part 1


This embedded system allows you to measure the power consumption of the CPU while it is running an AES encryption algorithm. Use this information to leak the key via dynamic power analysis. Access the running server with nc saturn.picoctf.net 62527. It will encrypt any buffer you provide it, and output a trace of the CPU's power consumption during the operation. The flag will be of the format picoCTF{<encryption key>} where <encryption key> is 32 lowercase hex characters comprising the 16-byte encryption key being used by the program.

This challenge is similar to the previous one except that this time a CPU power consumption trace is returned by the server:

<< Please provide 16 bytes of plaintext encoded as hex: >> 000102030405060708090a0b0c0d0e0f << power measurement result: [101, 79, 46, 4, ..., 42]

But the attack process is the same: collect a dataset and then attack it.

Β 

Collect the dataset

First, let's define a function to collect and parse a single trace:

port = 58296 def get_trace(plaintext): nc = NC('saturn.picoctf.net', port, timeout=1, debug=False) nc.receive() time.sleep(0.005) plaintext = plaintext = plaintext.tobytes().hex() resp = nc.query(plaintext, sleep=0.1) while ']\n' not in resp: time.sleep(0.001) resp = resp + nc.receive() trace = np.fromstring(resp[28:-2], dtype='uint8', sep=', ') return trace

and then use it to collect a dataset of 500 traces:

error_counter = 0 plaintexts = [] samples = [] for _ in tqdm(range(500)): plaintext = np.random.randint(0, 256, 16, dtype='uint8') try: samples.append(get_trace(plaintext)) plaintexts.append(plaintext) except Exception: error_counter += 1 if error_counter > 10: break ths = read_ths_from_ram(samples=np.array(samples), plaintext=np.array(plaintexts)) ths
Trace Header Set: Name.............: RAM Format THS Reader...........: RAM reader with 500 traces. Samples shape: (500, 2666) and metadatas: ['plaintext'] plaintext........: uint8

Β 

Γ©

Attack

The hint claims that the power consumption is correlated with the Hamming weight of the processed bits. Therefore, we will attack with a CPA the output of the SubBytes operation (an efficient and classical selection function), using the Hamming weight model:

attack = scared.CPAAttack(selection_function=scared.aes.selection_functions.encrypt.FirstSubBytes(), model=scared.HammingWeight(), discriminant=scared.maxabs, # Works with positive or negative correlations convergence_step=50) attack.run(scared.Container(ths)) plot_attack(attack, 0)

output_19_0.png

We can see that for the first byte, a key value is clearly correlating more than the others. A quick check confirms that is the case for all bytes.

Β 

Key extraction and flag conversion

Again the correct key should be the one with the highest score for each byte:

found_key = np.nanargmax(attack.scores, axis=0).astype('uint8') print(f'picoCTF{{{found_key.tobytes().hex()}}}')
picoCTF{65cce0eab280e39d12625c7315b03fa1}

Β 

PowerAnalysis: Part 2

This embedded system allows you to measure the power consumption of the CPU while it is running an AES encryption algorithm. However, this time you have access to only a very limited number of measurements. Download the power-consumption traces for a sample of encryptions traces.zip The flag will be of the format picoCTF{<encryption key>} where <encryption key> is 32 lowercase hex characters comprising the 16-byte encryption key being used by the program.

Again the same, but this time the dataset is provided.

Β 

Read and parse dataset

The traces.zip archive contains 100 .txt files with the following content:

Plaintext: 6bb487e863faab956e3d7ede01fdd0a0 Power trace: [75, 61, 94, 134, 127, 134, 138, 139, ...., 42]

The plaintext is given as an hexadecimal string while the trace samples are given as a list of integer values.

Let's parse each trace file and build a dataset. First, we can define a function to parse a single trace:

import numpy as np def read_file(path): with open(path, 'r') as fid: lines = fid.readlines() plaintext = lines[0] plaintext = plaintext.split(': ')[1].strip() plaintext = np.frombuffer(bytes.fromhex(plaintext), dtype='uint8') samples = lines[1] samples = samples.split(': [')[1].split(']')[0].strip() samples = np.fromstring(samples, dtype='uint8', sep=', ') return plaintext, samples read_file('traces/trace00.txt')
(array([107, 180, 135, 232, 99, 250, 171, 149, 110, 61, 126, 222, 1, 253, 208, 160], dtype=uint8), array([ 75, 61, 94, ..., 12, 250, 212], dtype=uint8))

And then build a dataset containing all the traces:

import glob from estraces import read_ths_from_ram files = list(glob.glob('traces/*')) plaintexts = [] samples = [] for file in files: plaintext, trace = read_file(file) plaintexts.append(plaintext) samples.append(trace) ths = read_ths_from_ram(samples=np.array(samples), plaintext=np.array(plaintexts)) ths
Trace Header Set: Name.............: RAM Format THS Reader...........: RAM reader with 100 traces. Samples shape: (100, 2666) and metadatas: ['plaintext'] plaintext........: uint8

Β 

Attack

The hint is the same, then we can repeat the previous attack:

attack = scared.CPAAttack(selection_function=scared.aes.selection_functions.encrypt.FirstSubBytes(), model=scared.HammingWeight(), discriminant=scared.maxabs, convergence_step=10) attack.run(scared.Container(ths)) plot_attack(attack, 0)

output_29_0.png

Β 

Key extraction and flag conversion

found_key = np.nanargmax(attack.scores, axis=0).astype('uint8') print(f'picoCTF{{{found_key.tobytes().hex()}}}')
picoCTF{dde6d2ba7d0e35a99eeedf882dcae7d1}

Β 

Thanks to


the PicoCTF team for providing this nice CTF. I learned a lot in many domains I'm not familiar with. I can't wait to read the write-up for the challenges I haven't solved.

Share

Categories

All articles
(104)
Case Studies
(2)
Chip Security
(29)
Corporate News
(12)
Expert Review
(3)
Mobile App & Software
(31)
Vulnerability Research
(35)

you might also be interested in

Corporate News
Chip Security

Behind the release of esDynamic 2024.01

6 min read
Edit by Hugues Thiebeauld β€’ Mar 7, 2024
CopyRights eShard 2024.
All rights reserved
Privacy policy | Legal Notice