esDynamic
Manage your attack workflows in a powerful and collaborative platform.
Expertise Modules
Executable catalog of attacks and techniques.
Infrastructure
Integrate your lab equipment and remotely manage your bench.
Lab equipments
Upgrade your lab with the latest hardware technologies.
Side Channel Attacks
Evaluate cryptography algorithms from data acquitition to result visualisation.
Fault Injection Attacks
Laser, Electromagnetic or Glitch to exploit a physical disruption.
Security Failure Analysis
Explore photoemission and thermal laser stimulation techniques.
Evaluation Lab
Our team is ready to provide expert analysis of your hardware.
Starter Kits
Build know-how via built-in use cases developed on modern chips.
Cybersecurity Training
Grow expertise with hands-on training modules guided by a coach.
esReverse
Static, dynamic and stress testing in a powerful and collaborative platform.
Extension: Intel x86, x64
Dynamic analyses for x86/x64 binaries with dedicated emulation frameworks.
Extension: ARM 32, 64
Dynamic analyses for ARM binaries with dedicated emulation frameworks.
Penetration Testing
Identify and exploit system vulnerabilities in a single platform.
Vulnerability Research
Uncover and address security gaps faster and more efficiently.
Malevolent Code Analysis
Effectively detect and neutralise harmful software.
Digital Forensics
Collaboratively analyse data to ensure thorough investigation.
Software Assessment
Our team is ready to provide expert analysis of your binary code.
Cybersecurity training
Grow expertise with hands-on training modules guided by a coach.
Semiconductor
Security Labs
Governmental agencies
Academics
Why eShard?
Our team
Careers
Youtube
Gitlab
Github
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.
Β
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 withnc saturn.picoctf.net 56776
. The flag will be of the formatpicoCTF{<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:
Β
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].
Β
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
Β
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)
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.
Β
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}
Β
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 formatpicoCTF{<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.
Β
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
Β
Γ©
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)
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.
Β
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}
Β
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 formatpicoCTF{<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.
Β
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
Β
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)
Β
found_key = np.nanargmax(attack.scores, axis=0).astype('uint8') print(f'picoCTF{{{found_key.tobytes().hex()}}}')
picoCTF{dde6d2ba7d0e35a99eeedf882dcae7d1}
Β
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.