๐ฅ FROSTBLOWER: OS-Level Countermeasures Against FROST Fingerprinting
Status: Open Research / Proof of Concept
Target: FROST (Fingerprinting Remotely using OPFS-based SSD Timing)
๐ Abstract
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:
1. Kernel Module (LKM) โ For direct block-layer interception.
2. eBPF Program โ For syscall-level noise injection.
3. FUSE Wrapper โ For userspace OPFS poisoning.
---
๐ 1. The Threat: How FROST Works
FROST exploits the Origin Private File System (OPFS) to measure SSD I/O timing and infer user activity. Hereโs how it attacks:
- OPFS Abuse: A malicious website creates a large file (1+ GB) in OPFS, forcing the SSD to compete for I/O resources.
- Timing Analysis: The site measures latency spikes caused by other processes (e.g., other tabs, apps) accessing the SSD.
- Fingerprinting: A convolutional neural network (CNN) classifies these latency patterns to identify:
- Other open websites (even in different browsers).
- Running applications (e.g., Spotify, Discord, or a text editor).
- User activity (e.g., typing, scrolling, or video playback).
Accuracy:
- ~89% for website fingerprinting.
- ~96% for application fingerprinting.
Why Itโs Dangerous:
- No permissions required โ Runs entirely in the browser.
- Cross-process leakage โ Can fingerprint activity outside the browser.
- No mitigations โ Browser vendors (Google, Apple, Mozilla) refuse to fix it.
---
โ๏ธ 2. The Counterattack: FROSTBLOWER
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 โญโญ---
๐ ๏ธ 3. Implementation: Full Code & Deployment
๐น 3.1. Kernel Module (FROSTBLOWER-LKM)
Target: Linux (Ubuntu 22.04/24.04)
Mechanism: Hooks into the block layerโs make_request_fn to inject randomized delays for browser processes.
๐ Files
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");
๐ Makefile
obj-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
๐ Deployment
- Compile:
bash make - Load the module:
bash sudo insmod frostblower_lkm.ko - Verify itโs working:
bash 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 ... - Unload the module:
bash sudo rmmod frostblower_lkm
---
๐น 3.2. eBPF Program (FROSTBLOWER-BPF)
Target: Syscalls (read, write, open) for browser processes.
Mechanism: Uses eBPF to inject randomized delays into I/O syscalls.
๐ Files
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;
}
๐ Makefile
all: 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
๐ Deployment
- Install Dependencies:
bash sudo apt install clang llvm libelf-dev libbpf-dev linux-headers-$(uname -r) - Compile:
bash make - Run the loader:
bash sudo ./loader -
Verify itโs working:
- Open Chrome/Firefox and run a FROST PoC.
- Usestraceto check for delays:
bash strace -p $(pidof chrome) -e trace=read,write,open
- You should see randomized delays in syscall timings. -
Stop the loader:
PressCtrl+C.
---
๐น 3.3. FUSE Wrapper (FROSTBLOWER-FUSE)
Target: OPFS files in userspace.
Mechanism: A FUSE filesystem that wraps the real OPFS directory and injects noise into file operations.
๐ Files
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);
}
๐ Makefile
all: frostblower_fuse
frostblower_fuse: frostblower_fuse.c
gcc -o frostblower_fuse frostblower_fuse.c -lfuse -pthread
clean:
rm -f frostblower_fuse
๐ Deployment
- Install FUSE:
bash sudo apt install fuse - Compile:
bash make - Mount the FUSE filesystem:
bash mkdir -p /tmp/frostblower_opfs ./frostblower_fuse /tmp/frostblower_opfs - Test it:
- Open Chrome/Firefox and navigate to a site using OPFS.
- Usestraceto verify delays:
bash strace -e trace=read,write,open -p $(pidof chrome) - Unmount:
bash fusermount -u /tmp/frostblower_opfs
---
๐ฏ 4. Advanced Tactics
๐น 4.1. Paradoxical Timing
Idea: Make the SSD report impossible timing data to break FROSTโs CNN model.
Methods:
1. Negative Latency:
- Modify the bio structโs bi_start_time in the kernel module to pre-date the request.
- Example:
c
bio->bi_start_time = ktime_sub_ns(ktime_get(), 1000000); // 1ms in the past
2. Out-of-Order Completions:
- Reorder I/O operation timestamps to violate causality.
3. Quantum Noise:
- Simulate non-deterministic latency (e.g., same I/O operation takes 1ms, then 100ms, then 1ms again).
๐น 4.2. Adaptive Noise Escalation
Idea: If FROST-like behavior is detected, escalate the noise to overwhelm the attacker.
Implementation:
1. Userspace Daemon:
- Monitors /proc/*/status for browser processes with high OPFS activity.
- Communicates with the kernel module via netlink to ramp up jitter.
2. Dynamic Jitter:
- Start with 1ms jitter, escalate to 100ms if FROST is suspected.
3. Fake SSD Failures:
- Simulate I/O errors to crash FROSTโs JavaScript.
๐น 4.3. Collaborative Poisoning
Idea: If multiple machines run FROSTBLOWER, FROSTโs CNN model will be flooded with inconsistent data, rendering it useless.
Implementation:
1. P2P Noise Sync:
- Use libp2p or Tor to share random seed values between machines.
- Synchronize jitter patterns to amplify noise.
2. Targeted Poisoning:
- Focus noise on known FROST-abusing domains (e.g., via a community blocklist).
---
โ ๏ธ 5. Risks & Mitigations
Risk Mitigation Kernel Panics Test in a VM. Usetry_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.
---
๐ 6. Testing Methodology
๐น 6.1. FROST PoC Setup
- Clone the FROST research repo (if available).
- Run the PoC in a controlled environment (e.g., Ubuntu VM with a virtual SSD).
- Open Chrome/Firefox and navigate to the FROST test page.
๐น 6.2. Verify FROSTBLOWER Efficacy
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.
bash
sudo perf stat -p $(pidof chrome) -e 'syscalls:sys_enter_read,syscalls:sys_exit_read'
- strace: Check for delays in I/O operations.
bash
strace -p $(pidof chrome) -e trace=read,write,open
- FROST PoC: Run the attack and measure accuracy drop.
---
๐ 7. Ethical & Legal Considerations
- Defensive Use Only: FROSTBLOWER is a shield, not a weapon.
- No Malicious Deployment: Do not use it to attack others or disrupt legitimate services.
- Transparency: If deploying in production, disclose its use to users.
---
๐ฅ 8. Conclusion: Burn FROST to the Ground
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:
1. Kernel Module (LKM) โ For direct block-layer interception.
2. eBPF Program โ For syscall-level noise injection.
3. FUSE Wrapper โ For userspace OPFS poisoning.
This paper is not a solution. Itโs an invitation.
- Implement it.
- Improve it.
- Deploy it.
- Burn FROST to the ground.
---
๐ Appendix A: Full Code Repository
All code is available in this self-contained Black Paper. For updates, contributions, or discussions, post it wherever the nerds congregate.
---
๐ฌ Final Note
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.