Estelle Jung's Notebook - cps-vip/cps-cosimulation-env GitHub Wiki
Went over Logistics + who and what Cyber Physical Systems are.
Interested in Security Team
Expanded on sub teams and foundational co-simulation code. Announced Kickoff presentation and divided into sub teams, where introductions were made. met up with the rest of the security sub team to work on slides.
- Finish Kickoff Presentation
1/26 - added short and long term goals 1/28 - wrote script to explain the goals of the side channel attack team
Hope to use applicable tools like ghidra, wireshark, and also learn rust & best practices.
Presenting
- - Wrote script for goals slide
Decided that side channel attacks are what i want to work on. Specifically timing based ones.
Feedback on getting started side channel attacks. Hayden talked about how we should impl
- - Researched Side Channel Attacks. Introduction to Side-Channel Attacks
- - Implemented a naive Side Channel Attack in rust with Carter in person.
presented the side channel attack to William. Look into AES.
- - Looked into AES. AES
2/12 - Make a bunch of tests for characters and figure out which is more likely -> list Hamming weight is simply the count of 1 bits in a binary number. For example, the number 13 in binary is 1101, so its Hamming weight is 3. This function takes an array of numbers and returns a same-shaped array where each value has been replaced by its bit count. In the context of the CPA attack, this is used to predict how much power the device consumed β more 1 bits generally means more power drawn.
Told to refine password checker. Think about black box and try to make it as realistic as possible. Make a binary files each and swap. Try to decrypt each other passwords using a script. Finish making up the slides on AES.
- - Make a more realistic password checker.
Presented on AES. Got feedback on providing actual statistics on the slides through graphs and charts.
Implemented single_byte_attack() and changed hamming_weight() in AES-128-CPA.py (our proof of concept on AES Correlation Power Analysis Attack):

Since each key byte is 8 bits, there are only 256 possible values (0β255). The single_byte_attack() tries all of them. First it makes a Hypothesis "guess". It XORs the known plaintext with the guess, then passes it through the AES S-Box (a fixed substitution table that's part of AES). This mimics what the real device is doing internally. Then it calculates the power usage by calculating the Hamming weight (number of 1-bits) of that intermediate result. More 1-bits typically means more power drawn, this is the physical assumption the whole attack rests on. We then compare this calculated power usage with the actual power used via pearson correlation. Where a high correlation means the guess is correct. After testing all 256 guesses, we return the byte with the highest absolute correlation and its correlation value.
βIn the old code: np.array(dtype=np.int32) is invalid, np.array() requires actual data as its first argument, not just a dtype keyword. Addditionally, calling np.append inside a loop is a well-known NumPy anti-pattern. Unlike a Python list's .append(), np.append doesn't modify the array in place. It instead creates a brand new array in memory on every single iteration. With thousands of traces, this means thousands of unnecessary memory allocations, making it very slow.
We solve these issues by flattening the input into a simple 1D sequence so it can iterate over individual numbers regardless of the input's original shape. We then uses a Python list comprehension to collect all the bit counts, building a plain Python list first is the right call here because lists actually do support efficient appending. Finally, we convert that completed list into a NumPy array in one shot and reshapes it back to match the original input's dimensions. One allocation instead of thousands.
Met with Piper on Wednesday and discussed what to do next. We were suggested to work on making separate password checker programs, exchanging binaries, and then reverse engineering them. But our first and foremost goal is to optimize the code so that sleep() isnt use, costing milliseconds of cost rather than the microseconds we should realistically be targeting at this level of analysis.
Relying on sleep() as a timing mechanism is a fundamental accuracy problem in side-channel work. It introduces arbitrary, OS-controlled delays that dwarf the actual signal you're trying to measure. Power traces and timing differences in real cryptographic operations happen on the scale of nanoseconds to microseconds. sleep() operates in milliseconds at best, and inconsistently at that, since the OS can delay or extend it depending on system load. This means the timing data collected while sleep() is present is essentially noise, making any correlation analysis unreliable before it even starts.
Removing it and replacing it with precise, hardware-level or cycle-accurate timing is a necessary first step before any of the more advanced work.
Was preoccupied by school work.
Met with Carter, Piper, Aly and discussed tasks being implemented. I expressed intent on working on a blackbox version in Rust of the Password checker. Piper mentioned working on the password checker without using sleep and blackbox.
Implemented attack_full_key() in AES-128-CPA.py:
attack_full_key() is the top-level function that ties everything together. AES-128 uses a 16-byte key, and since each byte is independent at this stage of encryption, we can crack each one separately. The function loops through all 16 byte positions and calls single_byte_attack() on each one. The best guess and its correlation value are returned, stored into recovered_key, and printed alongside the actual known byte so correctness can be verified at a glance. Once all 16 bytes are recovered, the full key is returned as a single array.
For each byte it shows the recovered value, the true value, a Y/N match indicator, and the correlation score. This makes it easy to spot which bytes the attack is confident about and which may have been weakly correlated, giving a quick diagnostic view of the attack's overall quality before doing anything further.
Busy from exams and deadlines right before break. Discussed via text on progress of subtasks.
N/A
Spring Break
N/A
Could not meet with teammates as I was out of the country until Tuesday night. They discussed focusing work more on AES and expressed interest in obtaining a Chipwhisper for actual power rather than generated. During the meeting was suggested to work on chipwhisperer tutorials using simulated data. We were encouraged to gather lots of statistics for the final presentation.
Looked into ChipWhisperer: ChipWhisperer is an open-source hardware and software platform built specifically for side-channel attacks and fault injection research. From a security perspective it matters because it lowers the barrier to performing attacks that were previously only feasible in well-funded labs. Making it both a valuable research tool and a demonstration of how accessible hardware-level cryptographic attacks have become.
The platform is primarily used for two attack classes. Power analysis, which is what the CPA code in this project implements, and fault injection, where faults are deliberately induced in a target device's operation while simultaneously monitoring side-channel information to exploit potential vulnerabilities. A key hardware design decision that makes it effective is its synchronous capture method, this reduces the required sample rate, lowers data storage requirements, and improves trace synchronization, which directly addresses the noise and timing problems that make software-based side-channel work so difficult (O'Flynn, C., & Chen, Z. D. (2014)). Source
We did not meet this week due to conflicting schedules and decided to just continue to work on our subtasks.
made an updated password checker to work on black box and no sleep implementation under timing attack refined: password_checker.rs
The function vulnerable_check_password() first checks length, which is an immediate and intentional leak. If your attempt is the wrong length it returns instantly, telling the attacker exactly how long the real password is. From there it compares bytes one at a time and returns false the moment it finds a mismatch. This early exit is the central vulnerability. A correct first character means the loop runs one iteration longer than a wrong one, and that extra iteration costs measurable time. Each additional correct character adds another iteration's worth of work, giving the attacker a clear signal to climb toward the correct password one character at a time.
We use black_box() to prevent Rust complier to optimize code. Without black_box(), it would look at the comparison loop, determine that SECRET is a compile-time constant, and potentially optimize away the entire character-by-character structure. Collapsing it into a single branch or even a constant. black_box() tells the compiler "treat this value as if it could be anything at runtime, don't optimize around it." It's applied to both the attempt bytes and the secret bytes during comparison, and also applied inside do_per_char_work() to keep the accumulator from being precomputed. The main loop reads password attempts line by line from stdin and prints either "user authenticated!" or "password is incorrect" to stdout. The attacker runs this binary as a subprocess, writes a guess, starts a timer, and stops it when a response comes back. They never read the source, they just measure wall time. Correct characters produce longer response times because more iterations of do_per_char_work() execute before the early exit fires. sleep() was the original stand-in for per-character cost but it has two fatal problems: OS-controlled so it's inconsistent, and millisecond delays are absurdly large compared to real cryptographic operations. do_per_char_work() replaces it with a tight arithmetic loop that runs 50,000 iterations of a linear congruential generator, a standard pseudorandom number formula.
Met with Carter, Aly, and Piper on Wednesday and discussed how we could get more statistics for Piper's volatile attack and then turning it into a two-file attack. I mentioned I was not done implementing my version of the attack and will need more time before I could implement statistics.
Writing attacker, I started simple: Spawn the checker, send a guess, measure wall time, pick the slowest candidate. One measurement per character, move on. It didn't work. Most likely because a single timing measurement on a general-purpose OS carries too much noise (e.g. scheduler jitter, cache misses, pipe latency) which can trivializes the ~100β200ns signal I was after. I needed more data points.
I collected 800 samples per candidate and averaged them, hoping noise would cancel out. It helped, but looking it up, it seems OS jitter tends to cluster rather than cancel, a busy system biases entire batches of measurements in the same direction. Single outliers could also drag the mean far from the true value. The password was still wrong, but the same wrong answers started appearing consistently, which at least told me the noise had structure.
To fight outliers I wrote measure_candidate():
it sorts all 800 samples, trims the top and bottom 40, and takes the median of what's left. More stable, but stability isn't accuracy. I still had no way to know whether the winner was genuinely faster or just got lucky.
I added CandidateStats to track each candidate's full distribution and wrote welch_t() to compute a Welch t-statistic between the top two candidates, essentially asking how likely the gap between them is to be real rather than chance. A t-value above 3.5 would mean I could commit confidently.
I restructured the attack into rounds. Each round checks the t-statistic between first and second place: if it's below the threshold, collect more samples for the top three and try again. crack_char() runs this loop up to MAX_ROUNDS times before committing to whoever is ahead.

The results:
round 0: best='1' mean=46364ns runner-up='Z' mean=44964ns t=0.77 round 1: best='W' mean=42854ns runner-up='Y' mean=42066ns t=0.28
The t-values never approached 3.5, and the best candidate kept changing each round. The framework was working: it seems there's nothing reliable to find.I added a calibration step upfront: 200 samples each of zero correct characters versus one correct character, compared directly:
Baseline median: 36400ns 1-correct median: 34300ns Gap: 0ns
The one-correct measurement came back faster than baseline, which shouldn't be possible. The noise on individual measurements was simply larger than the signal itself. No amount of statistics recovers a signal smaller than the noise floor of the instrument.It recovered Zn#kM which is not r4ndomP@ss. As Piper mentioned before a general-purpose OS is just too noisy and a bad environment to measure nanosecond differences.
Presentation is on Thursday, presented the results and limitations of Piper's implementation of the timing attack where she amplifies Do_per_char_work() to see just how much is necessary to get the correct character.
Our next goals is to do tutorials and be able to implement attacks and defenses using devices like the ChipWhisperer.
Was absent due to not signing up for class. But introduction slides contain same information from pervious semester, describing high level power grid a cyber physical simulation.
Described the sub teams. As I am interested in the security side, I am deciding between PUFs, Side channels, FPGA, Disk Encryption, and Side channel attacks.
- PUFs (Physically Unclonable Functions) Hardware-based "fingerprints" β tiny manufacturing variations in a chip that are unique to that specific piece of hardware and can't be replicated, even by the same manufacturer. Used for device authentication/identity β proving a specific device is really that device, without needing to store a secret key that could be stolen. Relevant to power grid security for authenticating sensors, meters, or embedded controllers. Wikipedia
- Side Channels Side channels = unintended information leaks from a physical system while it operates β things like power consumption, timing, electromagnetic emissions, or sound. Side-channel attacks exploit these leaks to extract secrets (like encryption keys) without breaking the actual cryptographic algorithm. On the defense side, this subteam likely studies how to detect/prevent such leaks; on the attack side, how such leaks could be exploited on grid hardware. Wikipedia
- FPGA (Field-Programmable Gate Array) A type of reconfigurable hardware chip β unlike a normal processor, you can "rewire" its logic circuits for a custom task. Often used in embedded/industrial control systems (like those in power grids) for speed and customization. This subteam probably focuses on implementing and testing security mechanisms (encryption, PUFs, etc.) directly on FPGA hardware, and/or studying FPGA-specific vulnerabilities. Wikipedia
- Disk Encryption Protecting data stored on physical drives/storage media so it's unreadable without the correct key. In a power grid context, likely relevant to securing data logs, firmware, or control system storage from tampering or theft. Wikipedia
Read more into PUFs and reviewed previous semester's members. Read this white paper referenced by the old members. This paper described how an SRAM PUF is reliable in nature by each chip's cells are unique making the chip irreproducible, the cells on the device are consistent as they have a preferred start up state and the key is only extractable once powered on, making it secure. While consistent there is a small degree of noise and to correct this, we implement error correction. This is achieved through an enrollment and reconstruction phase. The enrollment phase is a one time phase in which the First response is mapped to a codeword and this mapping is stored as the activation code also known as the helper data. The AC does not reveal the key and only valid for key reconstruction of the specific chip. When authenticating the device, a new instantiation of the PUF runs and then the actual PUF key without noise is extracted from the AC. This is known as the reconstruction phase.

Worked on the slides to accurately depict what a PUF does to those with 0 prior knowledge and the short and long terms goals of the PUF subteam.
The previous team attempted to extract the SRAM initial values by modifying a configuration file (memory.x) to carve out a memory region the rest of the program wasn't allowed to use, so it stayed "blank" until they read it.
...
They wrote code in Rust to dump half of one SRAM bank (65,536 bits) and stream it to a laptop over a debug cable. They hit two issues: the first ~130 memory addresses were getting corrupted because the debugger interfered with startup, fixed by adding a small delay; and trying to log without the debugger attached produced garbage output, so they had to always use the debug probe. Then they repeated this 40 times per board, on two separate boards, so they'd have enough data to see patterns rather than one lucky (or unlucky) sample.
they then wrote Python scripts to find the bit stability: for each individual bit position, across all 40 boot-ups, did it usually come back the same value? That gives a "stability score" per bit. Averaged across the whole chip, was ~4%, meaning about 4 bits out of every 100 flip when you reboot.
They then built the fuzzy extractor:
- In a controlled setting, they read the chip many times and took the "majority vote" per bit to get a clean reference pattern.
- They generate a secret key, mathematically combine it with that reference pattern using an error-correcting code (they chose a type called BCH), and save the result as "helper data." This helper data is safe to make public asit reveals nothing.
- Later, on any given boot, the chip's noisy new reading gets combined with the helper data. The BCH code's whole job is to detect and fix the small number of bit errors, spitting out the exact same secret key every time. Even though the raw SRAM reading was never identical twice.
They built and tested this pipeline in Python and it worked, partially: with their chosen settings, one board correctly regenerated the key 22 out of 40 times, the other 32 out of 40. Not reliable enough , the failure rate they predicted mathematically beforehand matched what they actually measured, which is why they're confident the underlying approach is sound and just needs better tuning (correcting more errors, or throwing out the least reliable bits before generating the key).
I cloned the repo cps-vip/pufs/tree/main and ran it myself.
Correction 1: Last week I described their pipeline but hadn't confirmed it actually worked. I ran it end to end: parse the raw SRAM dumps into bits, calculate bit stability, find BCH parameters, test the key recovery. Everything matched their numbers exactly. Board 1724 got 4.00% average bit instability and 22/40 keys recovered. Board 5223 got 4.12% instability and 32/40 keys recovered. The pipeline works as described.
Correction 2: Last week I assumed nobody checked if the two boards were actually unique from each other, but their code already does this comparison, I just didn't see it. I ran it: 50.35% of bits differ between board 1724 and board 5223's majority patterns. That's close to the ideal 50%, which is a good sign the boards really are unique. This only compares the two boards' averaged patterns, not every individual reading against every other. A more thorough check would compare all 40 readings from board 1724 against all 40 from board 5223. I haven't done that yet.
New issue I found: the way they test the 22/40 and 32/40 result is biased in its own favor. They build the reference pattern from all 40 readings (majority vote), then test that same reference against those same 40 readings. That's testing on the data used to build the answer key. A fairer test would build the reference from some trials and test only on trials it hasn't seen. I expect the real success rate is lower than 22/40 and 32/40 once this is fixed.
Sanity check: I counted zeros vs. ones in one reading and got 49.7% ones. Close to 50/50, which means the SRAM region isn't contaminated by leftover program data.
Next steps Run the proper comparison: every board 1724 reading against every board 5223 reading. Rebuild the key recovery test with a held-out set, so the reference and the test data don't overlap. Use the per-bit stability scores they already calculated to drop the least stable bits before building keys.
We are shifting from SRAM pufs to FPGA (AMD PYNQ-Z2) based puff. FGPA's are high versatile and high performance kind of integrated circuit compared to your CPU or GPU's which are application specific integrated circuits. This reconfigurable versatility is achieved through an array of fixed programmable logic blocks (PLBs) and flexible interconnects that can be configured to perform either complex operations or to serve as simple logic gates. Source: fgpas
Verilog is a Hardware description language (HDL) used for designing integrated circuits. Without it we start from state machines -> truth tables with state transitions -> K-maps -> optimized circuits but with Verilog we can go from state machines -> Verilog -> optimized circuits.
Module - the equivalent of functions with returns. This is a reserved word in Verilog in which we refer to these "black boxes" as such (things regarding inputs and outputs with its own internal logic working).
E.g. Arbiter
Direction of port can be denoted alongside ports in Verilog 2001.
Another important port type not shown is bidirectional ports. Labeled as: inout [endianness] "name of port".
Driver - is a datatype which can either store a value (register) or one that can transition electrons through (wire).
Operators are the same as C. While control statements follow the same logic as C too (if, else, repeat, while, for, case) it is important to be careful that the logic itself is feasible or outputs properly in hardware.
If-else statements can result in a latch (a memory element) when we want to have combinational logic. Therefore it is important for the output to be defined for every possible input so that the output does not depend solely on current inputs. Same for Case statements.
e.g.
always @(*) begin
if (enable == 1'b1) begin
data = 10;
end
// no else!
end
data is not defined when enable is 0
" In digital there are two types of elements, combinational and sequential. Of course we know this. But the question is "How do we model this in Verilog ?". Well Verilog provides two ways to model the combinational logic and only one way to model sequential logic. * Combinational elements can be modeled using assign and always statements.
- Sequential elements can be modeled using only always statement.
- There is a third block, which is used in test benches only: it is called Initial statement.
" Source:Variable assignment
Initial Blocks - executed once at start of simulation. Used in writing test benches, if there are multiple of these blocks all are executed parallelly at the beginning.
###Entry 3
Verilog Learning part 2
Always blocks as implied by the name always execute and it should have a delay associated with it also known as Sensitive list - tells the always block when to execute the code. THe @ symbol is reserved for this block and will be triggered at the condition in the following parathesis. the <= operator is sequential and = is combinational. Where = updates immediately and <= reads the value before the edge condition and updates the left variables in parallel.
e.g
always @(posedge clk) begin \ q1 <= d; \ q2 <= q1; \ q3 <= q2; \ end \
All three end up equal to value of the previous variable before the clock edge.
An assign statement is used for modeling only combinational logic and it is executed continuously.
Low impedance - the output actively pushes the wire to 0 or 1. Current can flow easily, so it controls the wire. High impedance - huge resistance, so almost no current flows. The output has no influence on the wire, as if you cut it off with a switch.
Functions and tasks are equivalent to functions in other programming languages. They both have the same syntax, but tasks can have delays whereas functions are used for combinational logic. Additionally functions can return a value while tasks can not.
Test Benches testing the code. '$finish' terminates and '$monitor' monitors the changes in the signal list and print s them in format we want.
Our goal is to ? get an oscillator running and read the output? I need to read more on the paper provided. I got to ssh into the server on my linux VM to work by following the guide.
I ran into the issue of the Display not being set up. This is because the VIP server is a headless server so setting up GUI was not possible. X11 is a > client/server system in which Vivado is the X client and sends requests to draw on the server screen. To fix this we need to add the flag -X or -Y in front of the server. Where -X is untrusted forwarding and -Y is trusted forwarding. This flag tells the client to connect to a small fake server on the > host machine.
I am now testing Vivado by following this provided guide. On pause until I figure out what board I can access in the server