Skip to content

Carter Pattison's Notebook

Carter Pattison edited this page Oct 8, 2026 · 8 revisions

Fall 2026

Week 1

Entry 1

  • Met new members of the VIP.
  • Reviewed old work, and researched future work described in presentation.
  • Looked into Jesus's PLC Work

Week 2

Entry 1

  • Contacted by Jesus, read papers and suggested material to further understand his work.

Week 3

Entry 1

  • Organized teams and presentation for the device security subteam.
  • Created and assigned slides for kickoff presentation.
  • Recorded my slides for the presentation.
  • Scheduled meeting time with Zeus.

Week 4

Entry 1

  • Finalized slides
  • Scheduled meeting with Zeus

##Week 5

Entry 1

  • Met with Zeus - discussed semester plan, division of work, literature review, and hardware limitations
  • We need a PLC, preferably a 5380.
  • We will talk with Hayden about plan.

Entry 2

  • Read evaluation paper
image * Plan for next week - get familiar with Ghidra and MCP so, when we get the hardware, we can start work

Entry 3

  • Ship board to Zeus or hook to ResearchNet?
  • Could we use it while hooked to a server?

Week 6

Entry 1

  • Looked into MCP for Ghidra
  • As well as an eval harness for this

Entry 2

image)

Week 7

Entry 1

  • Researched vulnerabilities for Rockwell Automation 5370s

Spring 2026

Week 1

  • Joined VIP Discord
  • Filled out VIP team form
  • Explored teams and familiarized myself with current operations

Week 2

  • Researched beginner side-channel attack project
  • Joined device security sub-team

Week 3

  • Met on Sunday, Jan 26th to work on slides
  • Started basic outline of groups (side channel attacks, PUFs, fault injection, and disk encryption)
  • Presented slides in class (1.29)

Week 4

Tasks completed

  • Investigated literature on side-channel attacks
  • Surveyed attack categories: timing, power analysis, electromagnetic

Takeaways

  • Power analysis and timing side-channel attacks are most relevant to our current direction of research

Papers -

Week 5

Tasks completed

How it works

The victim is a vulnerable password checker that compares the attempt to the secret byte by byte and returns false on the first mismatch, with an artificial 9 ms per-character delay to exaggerate the leak:

const SECRET: &str = "Pa55word123";

fn vulnerable_check_password(attempt: &str) -> bool {
    if attempt.len() != SECRET.len() { return false; }
    let attempt_bytes = attempt.as_bytes();
    let secret_bytes  = SECRET.as_bytes();
    for i in 0..attempt.len() {
        if attempt_bytes[i] != secret_bytes[i] { return false; }
        thread::sleep(Duration::from_millis(9));   // exaggerates the side-channel
    }
    true
}

The attacker recovers the secret one position at a time. For each position it tries every candidate from [a-zA-Z0-9], pads the rest of the string with filler, calls the checker, and keeps the candidate whose call took the longest, because a longer call means the checker matched one more byte before exiting:

for candidate in charset.chars() {
    let mut attempt = current_attempt.clone();
    attempt.push(candidate);
    while attempt.len() < SECRET.len() { attempt.push('A'); }

    let start = Instant::now();
    vulnerable_check_password(&attempt);
    let duration = start.elapsed();

    if duration > longest_duration {
        longest_duration = duration;
        probable_char = Some(candidate);
    }
}

Results

  • Recovers the full 11-character "Pa55word123" reliably with a 9 ms per-char delay
  • Attack succeeded down to ~8 ms delay; below that the OS scheduling jitter on a single measurement starts dominating the per-character signal

Challenges

  • A single Instant::now() measurement per candidate is too noisy at small delays: a context switch can swamp the signal
  • The thread::sleep based victim is artificial; we want a victim whose per-char work is real CPU work rather than a sleep

Next steps

  • Move from one-shot timing to multi-sample statistics (median or trimmed mean over many runs per candidate) so we can drop artificial delay
  • Replace thread::sleep with a real per-character workload

Week 6

Tasks completed

How it works

We don't have real power traces yet, so we simulate them. For each random plaintext byte, we compute the round-1 intermediate SBOX[plaintext ^ key], take its Hamming weight as the "leak", and add Gaussian noise:

intermediate         = plaintexts ^ key                  # (num_traces, 16)
intermediate_values  = SBOX[intermediate]
traces               = hamming_weight(intermediate_values).astype(np.float64)
traces              += np.random.normal(0, noise_std, traces.shape)

The attacker then loops over all 256 candidate values for one key byte, builds the hypothetical Hamming weight under each guess, and computes the Pearson correlation between that hypothesis and the real trace column. The correct key byte is whichever guess produces the highest correlation:

for guess in range(256):
    hypothetical_intermediate = SBOX[plaintexts[:, byte_index] ^ guess]
    hypothetical_weight       = hamming_weight(hypothetical_intermediate)
    # Pearson correlation against the observed trace column
    pearson_correlations[guess] = np.corrcoef(hypothetical_weight, traces[:, byte_index])[0, 1]
best_guess = int(np.argmax(np.abs(pearson_correlations)))

Details

  • Target operation: S-box output of round 1
  • Leakage model: Hamming weight
  • Trace source: simulated, Gaussian noise (std ≈ 1–2)
  • Number of traces: 100–500
  • Correlation: Pearson

Results

  • Recovers all 16 bytes of a random known key from simulated traces at noise std 1.0; degrades gracefully as noise grows

Challenges

  • Simulated traces are unrealistically clean - a single time sample per encryption with i.i.d. Gaussian noise; real traces are vectors of thousands of samples with structured noise

Next steps

  • Continue developing the timing attack on the password checker
  • Add visualizations (correlation-vs-guess plots per byte) to make the CPA result legible
  • Define research scope: timing attack on AES-128 + roadmap toward physical CPA on hardware

Week 7

Tasks completed

How it works

The Python victim mirrors the Rust victimso we can A/B them:

def vulnerable_check(attempt):
    if len(attempt) != len(SECRET_PASSWORD): return False
    for i in range(len(SECRET_PASSWORD)):
        if attempt[i] != SECRET_PASSWORD[i]:
            return False    # early exit -> the leak
    return True

def constant_time_check(attempt):
    if len(attempt) != len(SECRET_PASSWORD): return False
    result = 0
    for i in range(len(SECRET_PASSWORD)):
        result |= ord(attempt[i]) ^ ord(SECRET_PASSWORD[i])
    return result == 0

The attacker drops the artificial delay and instead averages over many samples per candidate (5000 perf-counter measurements, taking the median to suppress OS jitter):

sample_times = []
for _ in range(5000):
    start = time.perf_counter_ns()
    vulnerable_check(test_str)
    end   = time.perf_counter_ns()
    sample_times.append(end - start)
timings[char] = statistics.median(sample_times)
right_char = max(timings, key=timings.get)

Takeaways

  • Splitting victim and attacker into separate files (and eventually separate processes) makes the experiment much easier to iterate
  • Median over thousands of samples + a tiny time.sleep(0.0001) per character recovers the password without a 9 ms artificial delay

Next steps

  • Drop the per-char sleep entirely; replace it with real CPU work in the Rust victim so we can attack a more realistic target

Week 8

Tasks completed

Details

  • For each target key byte, the visualization plots Pearson correlation of the Hamming-weight-of-S-box-output model against the simulated trace column for every key-byte hypothesis (0–255)
  • The correct key byte shows up as a clear peak; incorrect guesses cluster near zero correlation

Takeaways

  • The plots make it obvious why "more traces" = "cleaner peak" incorrect guesses' correlations shrink while the correct guess stays roughly fixed

Challenges

  • Simulated traces are still unrealistically clean; we need a real or more realistic noisy trace source before claiming the attack "works"

Week 9

Tasks completed

Details

  • The vulnerable Python implementation deliberately keeps data-dependent branches/lookups so a timing-based analysis has something measurable to attack (vs. a constant-time reference)
  • This gives us a software target for an end-to-end timing attack on AES-128 before we move to hardware

Takeaways

  • Having a Python victim alongside the Rust victim lets us prototype attacks in Python first, then port the strongest one to Rust for cleaner timing

Next steps

  • Refine the Rust password-checker into a version with realistic per-char work that's easier for an external harness to drive

Week 10

Tasks completed

How it works

Two key changes vs. Week 5: (a) thread::sleep is replaced with real CPU work, and (b) std::hint::black_box is wrapped around the comparison and the work so the optimizer can't fold the loop away:

fn vulnerable_check_password(attempt: &str) -> bool {
    if attempt.len() != SECRET.len() { return false; }
    let attempt_bytes = attempt.as_bytes();
    let secret_bytes  = SECRET.as_bytes();
    for i in 0..attempt_bytes.len() {
        if black_box(attempt_bytes[i]) != black_box(secret_bytes[i]) {
            return false;                    // early-exit vulnerability
        }
        let _ = black_box(do_per_char_work(secret_bytes[i]));
    }
    true
}

#[inline(never)]
fn do_per_char_work(byte: u8) -> u64 {
    let mut acc: u64 = byte as u64;          // seed from input prevents constant-folding
    for _ in 0..500 {
        acc = black_box(acc.wrapping_mul(6364136223846793005)
                          .wrapping_add(1442695040888963407));
    }
    acc
}

The binary runs as a stdin/stdout oraclel]; the attacker pipes one attempt per line and times round-trip so this works as a true black-box target.

Takeaways

  • #[inline(never)] + black_box is what makes the per-char work survive --release; without them the optimizer collapses the loop and the timing channel disappears
  • Seeding acc from the byte value chains iterations to the input, which prevents the compiler from precomputing the loop

Week 11

Tasks completed

  • Spring break. reviewed prior visualizations and planned the volatile/black-box timing experiments

Week 12

Tasks completed

  • Piper uploaded a new batch of experiment files (precursor to timing_attack_volatile/)

Details

  • plan is toreplace black_box with explicit read_volatile/write_volatile so the per-char work survives aggressive optimizers, then run a full sample-count and iteration-count sweep to find where the attack actually breaks

Next steps

  • Build the visualizations for the volatile timing attack so we can see whether per-character timing differences stay separable as we shrink the per-char workload

Week 13

Tasks completed

  • Added visualizations for the volatile timing attack: per-position timing bar charts, per-character distribution plots, confidence-gap plots, and a plot showing the timing increment per matched character (https://github.com/cps-vip/side-channel-attacks/tree/main/timing_attack_volatile)
  • Added more per-byte AES-128 CPA visualizations to support the writeup
  • Piper uploaded supporting files for the iteration-count / sample-count sweep workflow

How it works

The volatile victim uses read_volatile/write_volatile instead of black_box so the per-char busy loop is guaranteed to execute on each character match:

#[inline(never)]
fn do_per_char_work(byte: u8) -> u64 {
    let mut buf: [u64; 4] = [byte as u64, 1, 2, 3];
    for i in 0..ITERS {
        let prev = unsafe { std::ptr::read_volatile(&buf[(i % 4) as usize]) };
        let next = prev.wrapping_mul(6364136223846793005)
                       .wrapping_add(1442695040888963407);
        unsafe { std::ptr::write_volatile(&mut buf[((i + 1) % 4) as usize], next) };
    }
    unsafe { std::ptr::read_volatile(&buf[0]) }
}

The attacker takes 201 samples per candidate, sorts, drops the top and bottom 20%, and averages the middle (a trimmed mean) much more robust than median against bimodal jitter:

times.sort_unstable();
let trim    = SAMPLES / 5;
let trimmed = &times[trim..SAMPLES - trim];
trimmed.iter().sum::<u64>() / trimmed.len() as u64

Challenges

  • At small ITERS the per-char work is fast enough that a single OS scheduling event swamps the signal; trimmed mean over 200+ samples per candidate is what makes it stable

Week 14

Tasks completed

  • Standardized axis labels, titles, colormaps, and figure sizes across the AES CPA and volatile-timing plots so the final-presentation slides look consistent
  • Reviewed all four "fig*" outputs (fig1_bar_chart, fig2_distributions, fig3_confidence_gap, fig4_staircase) to pick the strongest two for the slides

Next steps

  • generate the final iteration-count and sample-count sweep CSVs + sample pictures for the presentation, and build the true black-box variant where the attacker has zero source visibility into the checker

Week 15

Tasks completed

  • Delivered final presentation Wed Apr 23
  • Piper finished timing_attack_volatile_blackbox/, the password checker lives in a separate module (checker.rs) and the attacker (main.rs) only calls check_password(&str) -> bool, with no source-level access to SECRET or any internal state (https://github.com/cps-vip/side-channel-attacks/tree/main/timing_attack_volatile_blackbox)
  • Generated the final iteration-sweep and sample-sweep CSVs (iter_sweep_stats.csv, sample_sweep_stats.csv) and the matching visualizers (success rate vs. samples, success rate vs. iterations, runtime, confidence gap, char-level accuracy)
  • Produced sample result pictures

How the black-box variant works

// checker.rs — the attacker has no access to SECRET
const SECRET: &str = "r4ndomP@ss";

#[inline(never)]
pub fn check_password(attempt: &str) -> bool {
    if attempt.len() != SECRET.len() { return false; }
    let attempt_bytes = attempt.as_bytes();
    let secret_bytes  = SECRET.as_bytes();
    for i in 0..attempt_bytes.len() {
        if attempt_bytes[i] != secret_bytes[i] { return false; }
        let _ = do_per_char_work(secret_bytes[i]);
    }
    true
}

The attacker calls the oracle, takes a trimmed mean, picks the slowest candidate per position, and records both the chosen character and the runner-up so we can compute a confidence gap:

let result = checker::check_password(attempt);
let _ = unsafe { std::ptr::read_volatile(&result) };  // prevents DCE on the bool
// ...
times.sort_unstable_by(|a, b| b.1.cmp(&a.1));
let (best_char,  best_time)   = times[0];
let (second_char, second_time) = times[1];
let gap_ratio = best_time as f64 / second_time as f64;

Results

  • End-to-end black-box recovery of "r4ndomP@ss" byte by byte using only the boolean oracle and wall-clock timing
  • Sample sweep shows success rate climbing past ~95% once samples-per-candidate gets to ~50–100 with ITERS=10000; below that the gap closes and the attack picks the wrong byte
  • AES-128 CPA toy model: full 16-byte key recovery from simulated Hamming-weight traces, with per-byte correlation plots showing clean peaks at the correct guesses

Takeaways

  • The "iteration count" knob in do_per_char_work is quite important, its what controls how big the timing signal is, and the sweep makes that explicit

Future work

  • Move from simulated AES traces to traces captured from real hardware (ChipWhisperer / RPi / STM32)
  • Re-run the volatile black-box attack against a constant-time reference checker to confirm the mitigation actually closes the channel (we have constant_time_check in password_checkers.py but never benchmarked it under the same harness)
  • Begin physical CPA on hardware and iterate: crack a target → add countermeasures → crack again, with DPA as the multi-semester stretch goal
Active Notebooks
Archive
Tutorials

Clone this wiki locally