Writeup by toby-bro for CryptoBro en détresse

hardware side channel attacks

April 30, 2025

This challenge gives us the power consumption of a “something” that validates PIN codes. We will be going towards differential power analysis (DPA) to solve this problem.

Initially I tried calculating the mean for all the numbers at the same time, but nothing stood out particularly. So I imagined that the code that checked the PIN was probably checking the numbers one by one. So we are going to proceed the same way.

We calculated the average power consumption for all the codes that started with 0??? then all those like 1???… and compared their averages, the one that stood out the furthest from the lot gives us the right first digit.

We got this output

Analyzing digit position 1...
Digit 0 difference score: 0.065464
Digit 1 difference score: 0.057082
Digit 2 difference score: 0.063797
Digit 3 difference score: 0.053306
Digit 4 difference score: 0.057586
Digit 5 difference score: 0.057638
Digit 6 difference score: 0.053020
Digit 7 difference score: 0.053250
Digit 8 difference score: 0.062998
Digit 9 difference score: 0.524083
Most likely digit at position 1: 9 (score: 0.524083)

This was quite promising as 9 was ten times further from the average than the rest of the digits. So we went on for the second step similarly comparing all the traces of 90??, 91??

And so on and so forth until the end where we just compared 9460, 9461, 9462

Which gave us the flag.

The code to do this is the following

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

PLOT = False

traces_dir: Path = Path('traces')
trace_files: list[Path] = sorted(list(traces_dir.glob('trace_*.npy')))

all_traces: list[np.ndarray] = []
all_pins: list[str] = []
for trace_path in trace_files:
    pin: str = trace_path.stem.split('_')[1]
    trace: np.ndarray = np.load(trace_path)
    all_traces.append(trace)
    all_pins.append(pin)

all_traces_array: np.ndarray = np.array(all_traces)
print(f'Loaded {len(all_traces)} traces, shape: {all_traces_array.shape}')


def digit_by_digit_dpa() -> str:
    print('\nPerforming digit-by-digit DPA analysis...')
    pin_length: int = len(all_pins[0])
    recovered_pin: str = ''

    for position in range(pin_length):
        print(f'\nAnalyzing digit position {position+1}...')

        filtered_indices: list[int] | range
        filtered_indices = range(len(all_pins))
        if position > 0:
            filtered_indices = [i for i, pin in enumerate(all_pins) if pin.startswith(recovered_pin)]
            print(f'Narrowed down to {len(filtered_indices)} candidates starting with {recovered_pin}')

        max_diff_score: float = -1
        best_digit: int | None = None

        for digit in range(10):
            digit_indices: list[int] = [i for i in filtered_indices if all_pins[i][position] == str(digit)]

            if not digit_indices:
                continue

            other_indices: list[int] = [i for i in filtered_indices if all_pins[i][position] != str(digit)]

            if not other_indices:
                continue

            digit_avg: np.ndarray = np.mean(all_traces_array[digit_indices], axis=0)
            other_avg: np.ndarray = np.mean(all_traces_array[other_indices], axis=0)

            diff: np.ndarray = digit_avg - other_avg

            diff_score: float = np.max(np.abs(diff))

            print(f'Digit {digit} difference score: {diff_score:.6f}')

            if PLOT:
                plt.figure(figsize=(12, 6))
                plt.plot(diff)
                plt.title(f'Difference for digit {digit} at position {position+1}')
                plt.axhline(y=0, color='r', linestyle='-', alpha=0.3)
                plt.savefig(f'diffs/diff_pos{position+1}_digit{digit}.png')
                plt.close()

            if diff_score > max_diff_score:
                max_diff_score = diff_score
                best_digit = digit

        if best_digit is not None:
            recovered_pin += str(best_digit)
            print(f'Most likely digit at position {position+1}: {best_digit} (score: {max_diff_score:.6f})')
        else:
            print(f'Could not determine digit at position {position+1}')
            break

    return recovered_pin


recovered_pin: str = digit_by_digit_dpa()

print(f'\nRecovered PIN: {recovered_pin}')
print(f'Flag: FCSC{{{recovered_pin}}}')