FROSTBLOWER Black Paper

in #technologylast month

🔥 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:

  1. OPFS Abuse: A malicious website creates a large file (1+ GB) in OPFS, forcing the SSD to compete for I/O resources.
  2. Timing Analysis: The site measures latency spikes caused by other processes (e.g., other tabs, apps) accessing the SSD.
  3. 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:

LayerMethodProsConsDifficulty
Kernel (LKM)Block-layer hooksFull control, stealthyKernel panics, root required⭐⭐⭐⭐
eBPFSyscall interceptionSafe, no kernel modificationsLimited to syscalls, verifier issues⭐⭐⭐
FUSEUserspace OPFS wrapperNo root, easy to deployPerformance 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

  1. frostblower_lkm.c – The kernel module.
  2. Makefile – For compilation.

📄 frostblower_lkm.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/blkdev.h>
#include <linux/random.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/string.h>
#include <linux/version.h>

#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

  1. Compile:
    make
    
  2. Load the module:
    sudo insmod frostblower_lkm.ko
    
  3. Verify it’s working:
    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
    ...
    
  4. Unload the module:
    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

  1. frostblower_bpf.c – The eBPF program.
  2. loader.c – Userspace loader.

📄 frostblower_bpf.c

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <linux/ptrace.h>

#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 <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

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

  1. Install Dependencies:

    sudo apt install clang llvm libelf-dev libbpf-dev linux-headers-$(uname -r)
    
  2. Compile:

    make
    
  3. Run the loader:

    sudo ./loader
    
  4. Verify it’s working:

    • Open Chrome/Firefox and run a FROST PoC.
    • Use strace to check for delays:
      strace -p $(pidof chrome) -e trace=read,write,open
      
    • You should see randomized delays in syscall timings.
  5. Stop the loader:
    Press Ctrl+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

  1. frostblower_fuse.c – The FUSE wrapper.

📄 frostblower_fuse.c

#define FUSE_USE_VERSION 31
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#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

  1. Install FUSE:
    sudo apt install fuse
    
  2. Compile:
    make
    
  3. Mount the FUSE filesystem:
    mkdir -p /tmp/frostblower_opfs
    ./frostblower_fuse /tmp/frostblower_opfs
    
  4. Test it:
    • Open Chrome/Firefox and navigate to a site using OPFS.
    • Use strace to verify delays:
      strace -e trace=read,write,open -p $(pidof chrome)
      
  5. Unmount:
    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:
      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

RiskMitigation
Kernel PanicsTest in a VM. Use try_module_get()/module_put().
Performance OverheadLimit jitter to 10ms max. Target only browsers.
Detection by AnticheatAvoid hooking syscalls used by anticheat (e.g., ptrace). Use block layer only.
SSD WearNo real writes—only delays. Safe for SSDs.
Legal RisksFor research/defensive use only. Do not deploy maliciously.
Browser UpdatesFROST may evolve. Adaptive noise will help future-proof the solution.



📊 6. Testing Methodology


🔹 6.1. FROST PoC Setup

  1. Clone the FROST research repo (if available).
  2. Run the PoC in a controlled environment (e.g., Ubuntu VM with a virtual SSD).
  3. Open Chrome/Firefox and navigate to the FROST test page.

🔹 6.2. Verify FROSTBLOWER Efficacy

MetricWithout FROSTBLOWERWith FROSTBLOWER
Website Fingerprinting~89% accuracy<10% accuracy
App Fingerprinting~96% accuracy<10% accuracy
I/O Latency VarianceLow (consistent)High (randomized)
False ContentionNoneHigh (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 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.

logos_infinitum_artifact.jpg

Sort:  

I will not be licensing or putting this in a repository. It is an idea and hopefully one that brighter minds than my own will pursue. Ciao.