Status: Open Research / Proof of Concept
Target: FROST (Fingerprinting Remotely using OPFS-based SSD Timing)
FROST (Fingerprinting Remotely using OPFS-based SSD Timing) is a browser-based side-channel attack that exploits SSD I/O timing to fingerprint user activity across tabs, browsers, and native applications. FROSTBLOWER is a kernel-level, eBPF, and FUSE-based countermeasure designed to poison FROST’s data by injecting randomized noise, paradoxical timing patterns, and fake contention into SSD I/O operations. The goal: Render FROST useless in the wild.
This paper provides full, compilable code for three implementations:
FROST exploits the Origin Private File System (OPFS) to measure SSD I/O timing and infer user activity. Here’s how it attacks:
Accuracy:
Why It’s Dangerous:
FROSTBLOWER poisons the well by making SSD I/O timing unreliable, paradoxical, and useless for fingerprinting. We do this at three levels:
| Layer | Method | Pros | Cons | Difficulty |
|---|---|---|---|---|
| Kernel (LKM) | Block-layer hooks | Full control, stealthy | Kernel panics, root required | ⭐⭐⭐⭐ |
| eBPF | Syscall interception | Safe, no kernel modifications | Limited to syscalls, verifier issues | ⭐⭐⭐ |
| FUSE | Userspace OPFS wrapper | No root, easy to deploy | Performance overhead, browser-specific | ⭐⭐ |
Target: Linux (Ubuntu 22.04/24.04)
Mechanism: Hooks into the block layer’s make_request_fn to inject randomized delays for browser processes.
frostblower_lkm.c – The kernel module.Makefile – For compilation.frostblower_lkm.c#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_JITTER_US 10000 // Max 10ms jitter
#define DRIVER_AUTHOR "Jacob Peacock"
#define DRIVER_DESC "FROSTBLOWER: Poisons FROST fingerprinting by injecting noise into SSD I/O timing"
static char *target_procs[] = {"chrome", "firefox", "chromium", "brave", "msedge"};
static int num_targets = ARRAY_SIZE(target_procs);
static unsigned long jitter_max = MAX_JITTER_US;
module_param(jitter_max, ulong, 0644);
static make_request_fn *original_make_request = NULL;
// Check if the current process is a target (browser)
static bool is_target_process(void) {
struct task_struct *task = current;
char comm[TASK_COMM_LEN];
int i;
get_task_comm(comm, task);
for (i = 0; i < num_targets; i++) {
if (strncmp(comm, target_procs[i], strlen(target_procs[i])) == 0) {
return true;
}
}
return false;
}
// Hooked make_request function
static blk_qc_t frostblower_make_request(struct request_queue *q, struct bio *bio) {
if (is_target_process()) {
unsigned long jitter = prandom_u32_max(jitter_max);
if (jitter > 0) {
udelay(jitter);
}
}
return original_make_request(q, bio);
}
// Hook into all block devices
static int frostblower_hook(void) {
struct request_queue *q;
struct blk_mq_hw_ctx *hctx;
int cpu;
for_each_possible_cpu(cpu) {
for_each_blk_mq_hw_ctx(hctx, q, cpu) {
if (q->make_request_fn != frostblower_make_request) {
original_make_request = q->make_request_fn;
q->make_request_fn = frostblower_make_request;
pr_info("FROSTBLOWER: Hooked request queue for CPU %d\n", cpu);
}
}
}
return 0;
}
// Unhook
static void frostblower_unhook(void) {
struct request_queue *q;
struct blk_mq_hw_ctx *hctx;
int cpu;
for_each_possible_cpu(cpu) {
for_each_blk_mq_hw_ctx(hctx, q, cpu) {
if (q->make_request_fn == frostblower_make_request) {
q->make_request_fn = original_make_request;
pr_info("FROSTBLOWER: Unhooked request queue for CPU %d\n", cpu);
}
}
}
}
// Module init/exit
static int __init frostblower_init(void) {
pr_info("FROSTBLOWER: Loading kernel module\n");
if (frostblower_hook() != 0) {
pr_err("FROSTBLOWER: Failed to hook request queues\n");
return -1;
}
return 0;
}
static void __exit frostblower_exit(void) {
pr_info("FROSTBLOWER: Unloading kernel module\n");
frostblower_unhook();
}
module_init(frostblower_init);
module_exit(frostblower_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_VERSION("1.0");
Makefileobj-m += frostblower_lkm.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
make
sudo insmod frostblower_lkm.ko
dmesg | tail
Should see:
[ 1234.567890] FROSTBLOWER: Loading kernel module
[ 1234.567891] FROSTBLOWER: Hooked request queue for CPU 0
[ 1234.567892] FROSTBLOWER: Hooked request queue for CPU 1
...
sudo rmmod frostblower_lkm
Target: Syscalls (read, write, open) for browser processes.
Mechanism: Uses eBPF to inject randomized delays into I/O syscalls.
frostblower_bpf.c – The eBPF program.loader.c – Userspace loader.frostblower_bpf.c#include
#include
#include
#include
#define MAX_JITTER_US 10000
// List of target processes
const char *target_procs[] = {"chrome", "firefox", "chromium", "brave", "msedge"};
// Check if the current process is a target
static __always_inline bool is_target_process(struct pt_regs *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
for (int i = 0; i < sizeof(target_procs)/sizeof(target_procs[0]); i++) {
if (bpf_strncmp(comm, target_procs[i], sizeof(comm)) == 0) {
return true;
}
}
return false;
}
// Hook for sys_enter_read
SEC("tracepoint/syscalls/sys_enter_read")
int frostblower_read(struct trace_event_raw_sys_enter *ctx) {
if (is_target_process(ctx)) {
bpf_udelay(prandom_u32() % MAX_JITTER_US);
}
return 0;
}
// Hook for sys_enter_write
SEC("tracepoint/syscalls/sys_enter_write")
int frostblower_write(struct trace_event_raw_sys_enter *ctx) {
if (is_target_process(ctx)) {
bpf_udelay(prandom_u32() % MAX_JITTER_US);
}
return 0;
}
// Hook for sys_enter_open
SEC("tracepoint/syscalls/sys_enter_open")
int frostblower_open(struct trace_event_raw_sys_enter *ctx) {
if (is_target_process(ctx)) {
bpf_udelay(prandom_u32() % MAX_JITTER_US);
}
return 0;
}
char _license[] SEC("license") = "GPL";
loader.c#include
#include
#include
#include
#include
int main(int argc, char **argv) {
struct bpf_object *obj;
struct bpf_program *prog;
struct bpf_link *link;
int err;
// Load the eBPF program
obj = bpf_object__open_file("frostblower_bpf.o", NULL);
if (libbpf_get_error(obj)) {
fprintf(stderr, "Failed to open BPF object\n");
return 1;
}
// Load the program into the kernel
err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "Failed to load BPF object: %s\n", strerror(-err));
goto cleanup;
}
// Attach to tracepoints
prog = bpf_object__find_program_by_name(obj, "frostblower_read");
if (!prog) {
fprintf(stderr, "Failed to find program 'frostblower_read'\n");
err = -ENOENT;
goto cleanup;
}
link = bpf_program__attach_tracepoint(prog, "syscalls", "sys_enter_read");
if (libbpf_get_error(link)) {
fprintf(stderr, "Failed to attach to sys_enter_read\n");
err = -ENOENT;
goto cleanup;
}
prog = bpf_object__find_program_by_name(obj, "frostblower_write");
if (!prog) {
fprintf(stderr, "Failed to find program 'frostblower_write'\n");
err = -ENOENT;
goto cleanup;
}
link = bpf_program__attach_tracepoint(prog, "syscalls", "sys_enter_write");
if (libbpf_get_error(link)) {
fprintf(stderr, "Failed to attach to sys_enter_write\n");
err = -ENOENT;
goto cleanup;
}
prog = bpf_object__find_program_by_name(obj, "frostblower_open");
if (!prog) {
fprintf(stderr, "Failed to find program 'frostblower_open'\n");
err = -ENOENT;
goto cleanup;
}
link = bpf_program__attach_tracepoint(prog, "syscalls", "sys_enter_open");
if (libbpf_get_error(link)) {
fprintf(stderr, "Failed to attach to sys_enter_open\n");
err = -ENOENT;
goto cleanup;
}
printf("FROSTBLOWER-BPF: Successfully started! Press Ctrl+C to stop.\n");
while (1) {
sleep(1);
}
cleanup:
bpf_object__close(obj);
return err;
}
Makefileall: frostblower_bpf.o loader
frostblower_bpf.o: frostblower_bpf.c
clang -O2 -target bpf -c frostblower_bpf.c -o frostblower_bpf.o
loader: loader.c
gcc -o loader loader.c -lbpf -lelf -lz
clean:
rm -f frostblower_bpf.o loader
Install Dependencies:
sudo apt install clang llvm libelf-dev libbpf-dev linux-headers-$(uname -r)
Compile:
make
Run the loader:
sudo ./loader
Verify it’s working:
strace to check for delays:
strace -p $(pidof chrome) -e trace=read,write,open
Stop the loader:
Press Ctrl+C.
Target: OPFS files in userspace.
Mechanism: A FUSE filesystem that wraps the real OPFS directory and injects noise into file operations.
frostblower_fuse.c – The FUSE wrapper.frostblower_fuse.c#define FUSE_USE_VERSION 31
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_JITTER_US 10000
#define OPFS_PATH "/path/to/opfs" // Replace with actual OPFS path
static int frostblower_getattr(const char *path, struct stat *stbuf) {
char real_path[PATH_MAX];
snprintf(real_path, sizeof(real_path), "%s%s", OPFS_PATH, path);
return lstat(real_path, stbuf);
}
static int frostblower_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
char real_path[PATH_MAX];
snprintf(real_path, sizeof(real_path), "%s%s", OPFS_PATH, path);
// Inject random delay for OPFS files
if (strstr(path, "opfs") != NULL) {
usleep(prandom() % MAX_JITTER_US);
}
int fd = open(real_path, O_RDONLY);
if (fd == -1) return -errno;
int res = pread(fd, buf, size, offset);
if (res == -1) res = -errno;
close(fd);
return res;
}
static int frostblower_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
char real_path[PATH_MAX];
snprintf(real_path, sizeof(real_path), "%s%s", OPFS_PATH, path);
// Inject random delay for OPFS files
if (strstr(path, "opfs") != NULL) {
usleep(prandom() % MAX_JITTER_US);
}
int fd = open(real_path, O_WRONLY);
if (fd == -1) return -errno;
int res = pwrite(fd, buf, size, offset);
if (res == -1) res = -errno;
close(fd);
return res;
}
static int frostblower_open(const char *path, struct fuse_file_info *fi) {
char real_path[PATH_MAX];
snprintf(real_path, sizeof(real_path), "%s%s", OPFS_PATH, path);
int fd = open(real_path, fi->flags);
if (fd == -1) return -errno;
fi->fh = fd;
return 0;
}
static struct fuse_operations frostblower_ops = {
.getattr = frostblower_getattr,
.read = frostblower_read,
.write = frostblower_write,
.open = frostblower_open,
};
int main(int argc, char *argv[]) {
// Find the real OPFS path (adjust as needed)
char *opfs_path = getenv("OPFS_PATH");
if (opfs_path) {
strncpy(OPFS_PATH, opfs_path, sizeof(OPFS_PATH));
}
// Mount the FUSE filesystem
char *mountpoint = "/tmp/frostblower_opfs";
mkdir(mountpoint, 0777);
return fuse_main(argc, argv, &frostblower_ops, mountpoint);
}
Makefileall: frostblower_fuse
frostblower_fuse: frostblower_fuse.c
gcc -o frostblower_fuse frostblower_fuse.c -lfuse -pthread
clean:
rm -f frostblower_fuse
sudo apt install fuse
make
mkdir -p /tmp/frostblower_opfs
./frostblower_fuse /tmp/frostblower_opfs
strace to verify delays:
strace -e trace=read,write,open -p $(pidof chrome)
fusermount -u /tmp/frostblower_opfs
Idea: Make the SSD report impossible timing data to break FROST’s CNN model.
Methods:
bio struct’s bi_start_time in the kernel module to pre-date the request.bio->bi_start_time = ktime_sub_ns(ktime_get(), 1000000); // 1ms in the past
Idea: If FROST-like behavior is detected, escalate the noise to overwhelm the attacker.
Implementation:
/proc/*/status for browser processes with high OPFS activity.netlink to ramp up jitter.Idea: If multiple machines run FROSTBLOWER, FROST’s CNN model will be flooded with inconsistent data, rendering it useless.
Implementation:
| Risk | Mitigation |
|---|---|
| Kernel Panics | Test in a VM. Use try_module_get()/module_put(). |
| Performance Overhead | Limit jitter to 10ms max. Target only browsers. |
| Detection by Anticheat | Avoid hooking syscalls used by anticheat (e.g., ptrace). Use block layer only. |
| SSD Wear | No real writes—only delays. Safe for SSDs. |
| Legal Risks | For research/defensive use only. Do not deploy maliciously. |
| Browser Updates | FROST may evolve. Adaptive noise will help future-proof the solution. |
| Metric | Without FROSTBLOWER | With FROSTBLOWER |
|---|---|---|
| Website Fingerprinting | ~89% accuracy | <10% accuracy |
| App Fingerprinting | ~96% accuracy | <10% accuracy |
| I/O Latency Variance | Low (consistent) | High (randomized) |
| False Contention | None | High (fake contention) |
Tools to Verify:
perf: Monitor syscall latency.
sudo perf stat -p $(pidof chrome) -e 'syscalls:sys_enter_read,syscalls:sys_exit_read'
strace: Check for delays in I/O operations.
strace -p $(pidof chrome) -e trace=read,write,open
FROST is a symptom of a broken privacy model. The only way to fight it is to make its data useless. FROSTBLOWER provides three OS-level methods to poison FROST’s fingerprinting:
This paper is not a solution. It’s an invitation.
All code is available in this self-contained Black Paper. For updates, contributions, or discussions, post it wherever the nerds congregate.
This is a proof of concept. The code is unoptimized, untested in production, and potentially dangerous. Use at your own risk. Do not deploy in production without thorough testing.
FROSTBLOWER: Because the only good FROST is a dead FROST.