12 min read

Understanding eBPF-Based Monitor Blinding Attacks: How Attackers Silently Disable Runtime Security

A deep technical analysis of the techniques adversaries use to poison BPF maps, hijack tracepoints, and render Falco, Tracee, and Tetragon completely blind—without triggering a single alert.


The False Sense of Security from eBPF Monitors

The adoption of eBPF-based runtime security tools has become one of the defining trends in cloud-native security. Organizations deploy Falco, Tracee, or Tetragon and gain kernel-level visibility into every system call, file access, network connection, and process execution across their infrastructure. The dashboards turn green, the compliance checkboxes get filled, and security teams sleep better at night.

There is a fundamental problem with this picture: eBPF-based security monitors can be silently and completely disabled by an attacker who has gained root access or the CAP_BPF capability on a node. The monitor continues running. Its process stays healthy. Its health checks pass. But it sees nothing. Every syscall, every file read, every network exfiltration sails past it without generating a single alert.

This class of attack, which we refer to collectively as monitor blinding, exploits the architectural reality that eBPF programs share the kernel's BPF subsystem with any other privileged process on the same host. An attacker who can load BPF programs or manipulate BPF maps can surgically interfere with the monitor's data plane, its control plane, or both. The monitor has no way to know this has happened.

In this article, we provide a comprehensive technical breakdown of four distinct attack surfaces that enable eBPF security monitoring bypass. We examine each technique against the three most widely deployed open-source runtime security monitors: Falco, Tracee, and Tetragon. We then discuss mitigations and explain how automated verification platforms like OZIPHR can continuously test whether your monitors are actually resilient to these attacks.

How eBPF-Based Security Monitors Work

Before dissecting the attacks, it is important to understand the shared architectural pattern that Falco, Tracee, and Tetragon all rely on. Despite their differences in rule languages, user-space implementations, and policy engines, all three follow the same fundamental design.

The Common Architecture

An eBPF security monitor consists of three layers working in concert:

  1. BPF programs attached to kernel hooks — These are small programs loaded into the kernel and attached to tracepoints (such as sys_enter and sys_exit), kprobes, LSM hooks, or raw tracepoints. They execute in kernel context every time the relevant event fires.
  2. BPF maps for configuration and data transfer — BPF programs cannot directly call into user space. Instead, they use BPF maps (key-value stores in kernel memory) to receive configuration from user space and to pass captured event data back. Critically, many monitors use BPF_MAP_TYPE_PROG_ARRAY maps for tail-call dispatch, where individual entries point to sub-programs that handle specific syscall types.
  3. A user-space daemon — This process reads events from perf buffers or ring buffers, evaluates them against rules or policies, and generates alerts. It also manages the lifecycle of BPF programs and populates configuration maps.

Falco's Implementation

Falco uses the libs driver (either kernel module or eBPF) to attach to the sys_enter and sys_exit raw tracepoints. Its eBPF driver uses two critical PROG_ARRAY maps: syscall_enter_tail_table and syscall_exit_tail_table. Each entry in these maps corresponds to a syscall number and points to the BPF program that handles that specific syscall. When a syscall fires, the entry-point program looks up the appropriate handler via bpf_tail_call() and transfers execution to it.

Tracee's Implementation

Tracee attaches to a broader set of hooks including tracepoints, kprobes, and LSM hooks. It relies heavily on a config_map to control which events are traced. This map contains a configuration structure where specific byte offsets control feature flags, filter settings, and tracing options. Tracee also uses PROG_ARRAY maps for tail-call dispatch of its event-processing pipeline.

Tetragon's Implementation

Tetragon uses a combination of kprobes, tracepoints, and LSM hooks driven by TracingPolicy custom resources. It maintains an execve_map that tracks process execution state across the system. This map is essential for Tetragon's process lifecycle tracking, which underpins its ability to attribute network and file events to specific processes and enforce process-tree-based policies.

Key Insight

All three monitors share a critical assumption: that their BPF maps and programs are safe from tampering by other privileged processes on the same host. This assumption is false. The BPF subsystem provides no isolation between programs loaded by different user-space processes.

Attack Surface 1: BPF Tracepoint Hijacking (SunnyDayBPF)

The first attack technique, demonstrated in the SunnyDayBPF research, exploits the fact that any privileged process can attach additional BPF programs to the same tracepoints that a security monitor uses. The key insight is that an attacker does not need to detach the monitor's programs. Instead, the attacker attaches their own BPF program to the same tracepoint with a carefully chosen execution order, allowing them to modify data before or after the monitor's program processes it.

The Mechanism

Consider a security monitor that attaches to sys_exit_read to inspect the contents of buffers returned by read() syscalls. The monitor captures the user-space buffer pointer, reads the buffer contents via bpf_probe_read_user(), and checks for sensitive data patterns (credentials, keys, etc.).

An attacker can attach a second BPF program to the same sys_exit_read tracepoint that executes after the monitor's program. This attacker-controlled program uses bpf_probe_write_user() to overwrite the user-space buffer with sanitized contents. By the time the calling process reads the buffer, the data appears normal. But the monitor already captured the original, unsanitized contents and generated no alert because the attacker's program effectively performed a post-hoc data substitution.

Alternatively, and more powerfully, the attacker can attach a program that executes before the monitor's program and modifies the data that the monitor will see:

// SunnyDayBPF tracepoint hijacking concept
SEC("tp/syscalls/sys_exit_read")
int hijack_read_exit(struct trace_event_raw_sys_exit *ctx) {
    // Get the return value (bytes read)
    long ret = ctx->ret;
    if (ret <= 0)
        return 0;

    // Retrieve the user buffer address saved at sys_enter_read
    u64 pid_tgid = bpf_get_current_pid_tgid();
    void **bufp = bpf_map_lookup_elem(&saved_bufs, &pid_tgid);
    if (!bufp)
        return 0;

    // Overwrite the buffer with zeros before the monitor reads it
    char zeros[256] = {};
    bpf_probe_write_user(*bufp, zeros, sizeof(zeros));

    return 0;
}

Why This Works

The BPF subsystem allows multiple programs to be attached to the same tracepoint. Execution order depends on the order of attachment (and since Linux 5.17, can be influenced by BPF_LINK_UPDATE). There is no mechanism in the kernel to prevent one BPF program from interfering with the data that another BPF program will observe. The monitor has no way to detect that another program has been interposed on its tracepoint.

This technique is especially effective against monitors that rely on reading user-space memory through bpf_probe_read_user(), because the attacker's program can modify the user-space buffer between the actual syscall return and the monitor's read of that buffer.

Attack Surface 2: BPF Map Poisoning (SunnyMapBPF)

The second attack surface is more devastating and more broadly applicable. BPF map poisoning targets the maps that security monitors use for tail-call dispatch and configuration. Unlike tracepoint hijacking, which requires careful timing, map poisoning achieves persistent, complete blindness with a handful of bpf() syscalls from user space.

Poisoning Falco's Tail-Call Tables

Falco's eBPF driver dispatches syscall handling through PROG_ARRAY maps named syscall_enter_tail_table and syscall_exit_tail_table. Each entry at index N points to the BPF program that handles syscall number N. When a syscall fires, the entry-point program calls bpf_tail_call(ctx, &syscall_enter_tail_table, syscall_nr). If the entry exists, execution transfers to the handler. If the entry is absent or zeroed out, the tail call silently fails and execution falls through — meaning the event is silently dropped.

An attacker can enumerate Falco's BPF maps by scanning /proc or using bpf(BPF_OBJ_GET_INFO_BY_FD) to find maps by name. Once the syscall_enter_tail_table map is located, the attacker simply deletes or zeroes out entries:

// SunnyMapBPF: Poisoning Falco's PROG_ARRAY maps
// Step 1: Find the map by iterating BPF object IDs
__u32 id = 0;
while (bpf_map_get_next_id(id, &id) == 0) {
    int fd = bpf_map_get_fd_by_id(id);
    struct bpf_map_info info = {};
    __u32 len = sizeof(info);
    bpf_obj_get_info_by_fd(fd, &info, &len);

    if (strcmp(info.name, "syscall_enter_t") == 0) {
        // Step 2: Delete entries for critical syscalls
        // This disables monitoring for execve, openat, connect, etc.
        __u32 keys[] = {
            59,   // __NR_execve
            257,  // __NR_openat
            42,   // __NR_connect
            322,  // __NR_execveat
        };
        for (int i = 0; i < 4; i++)
            bpf_map_delete_elem(fd, &keys[i]);
    }
    close(fd);
}

After this operation, Falco's entry-point BPF program still executes on every syscall. It still calls bpf_tail_call(). But the tail call silently fails because the target entry has been deleted, and Falco never processes the event. The user-space daemon receives no events, so no rules fire, no alerts generate, and Falco reports zero activity.

Poisoning Tracee's config_map

Tracee uses a config_map of type BPF_MAP_TYPE_HASH that stores its runtime configuration as a serialized structure. Specific byte offsets within this structure control critical behavior. Offset 216 in particular contains flags that determine which event categories are enabled. By overwriting this offset with zeros, an attacker can disable all event tracing:

// Poisoning Tracee's config_map at offset 216
__u32 key = 0;  // config_map has a single entry at key 0
char config[512];

// Read the current config
bpf_map_lookup_elem(map_fd, &key, config);

// Zero out the tracing options at offset 216
memset(config + 216, 0, 64);

// Write the poisoned config back
bpf_map_update_elem(map_fd, &key, config, BPF_ANY);

Tracee's BPF programs check these configuration flags early in their execution path. With the flags zeroed, the programs exit immediately without capturing any events. Tracee's user-space daemon continues running and reporting healthy status, but it receives no data.

Wiping Tetragon's execve_map

Tetragon's execve_map is central to its process-lifecycle tracking. Every running process has an entry in this map that associates its PID with its process metadata (binary path, parent PID, cgroup ID, namespace information). When Tetragon's kprobes and tracepoints fire, they look up the current process in execve_map to determine whether the event matches any TracingPolicy.

An attacker who wipes the execve_map — by iterating over its entries and deleting them — causes Tetragon's BPF programs to fail every process lookup. Without process context, Tetragon cannot evaluate any policy conditions, and all events are effectively discarded. Furthermore, Tetragon only populates this map when it observes a new execve call. Processes that were already running before the wipe will never be re-added, creating a permanent blind spot for those processes.

Critical Finding

BPF map poisoning can be executed in under 10 milliseconds, requires no kernel module loading, generates no log entries from the target monitor, and persists until the monitor restarts and fully re-initializes its maps. Against all three major monitors, a single unprivileged-looking bpf() syscall sequence is sufficient to achieve complete blindness.

Attack Surface 3: The BPF Map Freeze Gap

Linux kernel 5.2 introduced BPF_MAP_FREEZE, an operation that makes a BPF map read-only from user space after it has been initialized. Once frozen, any attempt to update the map from user space via bpf_map_update_elem() or bpf_map_delete_elem() will fail with -EPERM. BPF programs running in the kernel can still write to the map, but user-space processes — including attackers — cannot.

This mechanism was designed precisely to prevent the class of attacks described in the previous section. A monitor could freeze its PROG_ARRAY maps after populating them, and freeze its configuration maps after initialization. This would render BPF map poisoning impossible from user space.

The Problem: Nobody Uses It

As of their current releases, none of the three major runtime security monitors use BPF_MAP_FREEZE on their critical maps:

Monitor Critical Maps MAP_FREEZE Used?
Falco (libs) syscall_enter_tail_table, syscall_exit_tail_table No
Tracee config_map, prog_array maps No
Tetragon execve_map, policy maps No

The reasons vary. Some maps need to be updated at runtime (Tetragon's execve_map must be written to by its BPF programs). Others could be frozen after initialization but simply are not. The PROG_ARRAY maps used by Falco and Tracee for tail-call dispatch are populated once during startup and rarely changed afterward — they are ideal candidates for freezing, yet they remain writable.

This gap means that the kernel provides a mitigation for BPF map poisoning, but the monitors that would benefit most from it have not adopted it. An attacker who discovers this can be confident that map poisoning will work against virtually any deployment of these tools.

Limitations of MAP_FREEZE

Even if monitors adopted BPF_MAP_FREEZE, it would not be a complete solution. The freeze operation only prevents user-space writes. BPF programs running in the kernel can still modify frozen maps. This means that tracepoint hijacking (Attack Surface 1), where the attacker loads their own BPF program, could still potentially modify maps from the kernel side. Additionally, PROG_ARRAY maps present a complexity: if the monitor needs to dynamically load or update tail-call targets, freezing the map would prevent legitimate updates as well.

Attack Surface 4: No Integrity Verification

The fourth and most architecturally significant weakness is the complete absence of integrity verification in all three monitors. No monitor periodically checks whether its own BPF maps contain the expected values. No monitor verifies that its BPF programs are still attached to their intended hooks. No monitor compares the current state of its data plane against a known-good baseline.

What Integrity Verification Would Look Like

A monitor with integrity verification would implement something like the following:

// Hypothetical integrity verification loop
func verifyIntegrity(monitor *Monitor) error {
    // 1. Verify PROG_ARRAY entries match expected programs
    for syscallNr, expectedProgFD := range monitor.expectedTailCalls {
        actualProgFD, err := bpfMapLookupElem(
            monitor.tailCallMap, syscallNr,
        )
        if err != nil || actualProgFD != expectedProgFD {
            return fmt.Errorf(
                "TAMPER DETECTED: tail call entry %d modified",
                syscallNr,
            )
        }
    }

    // 2. Verify config map contents match expected checksum
    configData, _ := bpfMapLookupElem(monitor.configMap, 0)
    if sha256(configData) != monitor.expectedConfigHash {
        return fmt.Errorf("TAMPER DETECTED: config_map modified")
    }

    // 3. Verify BPF programs are still attached
    for _, link := range monitor.bpfLinks {
        info, err := link.Info()
        if err != nil || info.ProgID != link.expectedProgID {
            return fmt.Errorf("TAMPER DETECTED: BPF link modified")
        }
    }

    return nil
}

None of the three monitors implement anything resembling this. Once their BPF programs and maps are loaded, the user-space daemon trusts that the kernel-side data plane remains intact for the lifetime of the process. This trust is misplaced.

The Detection Gap

Without integrity verification, there is no mechanism for a monitor to distinguish between "nothing is happening on this host" and "my data plane has been compromised and I am receiving no events." Both conditions look identical from the user-space daemon's perspective: an empty event stream. The monitor continues running, consuming CPU and memory, responding to health checks, and reporting to its management plane that everything is normal.

This is the fundamental property that makes monitor blinding attacks so dangerous. They are not detected by the very tool that is supposed to detect attacks. An attacker who blinds the monitor has effectively achieved permanent, undetectable persistence on the host.

Impact: Complete Silent Blindness

The combined effect of these four attack surfaces is devastating for organizations that rely on eBPF-based runtime security as a primary detection layer. Let us be explicit about what an attacker achieves:

Real-World Implications

In a typical cloud-native environment, an attacker who compromises a single container with CAP_BPF (or escalates to root) can blind the node-level runtime security monitor and then proceed to move laterally, exfiltrate data, or establish persistence — all without triggering a single alert from the security tool that was specifically deployed to detect these actions.

Mitigation Strategies

Defending against monitor blinding requires a layered approach. No single mitigation is sufficient, but the combination of several strategies significantly raises the bar for attackers.

1. Adopt BPF_MAP_FREEZE for Static Maps

Monitor developers should call bpf(BPF_MAP_FREEZE) on all maps that do not require runtime updates from user space. This includes PROG_ARRAY tail-call tables (which are populated at startup and rarely changed) and configuration maps (which are initialized once). This single change would block the most straightforward BPF map poisoning attacks.

// Freezing a PROG_ARRAY map after initialization
// This prevents user-space BPF map poisoning
int freeze_map(int map_fd) {
    union bpf_attr attr = {};
    attr.map_fd = map_fd;
    return syscall(__NR_bpf, BPF_MAP_FREEZE, &attr,
                    sizeof(attr));
}

2. Implement Map Integrity Checksumming

Monitors should periodically read their own critical maps and verify that the contents match expected values. For PROG_ARRAY maps, this means checking that each entry still points to the correct BPF program (verifiable by prog ID). For configuration maps, a cryptographic hash of the expected contents can be computed at initialization and verified on a timer.

3. Enable Kernel Lockdown

The Linux kernel's lockdown LSM, when set to integrity or confidentiality mode, restricts several operations that are prerequisites for monitor blinding. In confidentiality mode, bpf_probe_write_user() is blocked, which prevents the tracepoint hijacking technique. However, lockdown does not prevent BPF map manipulation by root, so it is not a complete mitigation.

4. Use BPF LSM to Restrict BPF Operations

A BPF LSM program can be attached to the bpf LSM hook to implement fine-grained access control over BPF operations. A policy could restrict which processes are allowed to call bpf_map_update_elem() on maps belonging to the security monitor, effectively implementing per-map access controls:

// BPF LSM: Restricting map updates to the monitor process
SEC("lsm/bpf_map")
int BPF_PROG(restrict_map_access,
            struct bpf_map *map,
            fmode_t fmode) {
    // Allow read-only access from any process
    if (!(fmode & FMODE_WRITE))
        return 0;

    // Check if this is a protected monitor map
    if (is_protected_map(map->id)) {
        u32 pid = bpf_get_current_pid_tgid() >> 32;
        // Only allow the monitor process to write
        if (pid != monitor_pid)
            return -EPERM;
    }

    return 0;
}

5. Deploy Independent Verification

The most robust defense against monitor blinding is external verification: an independent system that tests whether the monitor is actually detecting events. This is the principle behind breach-and-attack simulation (BAS) platforms and is the core function of OZIPHR. If an external verifier generates a known-malicious action and the monitor fails to detect it, the monitor has been blinded — regardless of the specific technique used.

6. Restrict CAP_BPF Distribution

In Kubernetes environments, the CAP_BPF capability should be restricted to only the containers that require it (typically the security monitor itself). Pod Security Admission policies, OPA/Gatekeeper policies, or Kyverno policies can enforce that no workload container receives CAP_BPF. However, this does not protect against attackers who escalate from container escape to node-level root.

How OZIPHR Tests for Monitor Blinding

OZIPHR provides automated, continuous verification of runtime security monitors against the full spectrum of blinding techniques described in this article. Rather than trusting that a monitor is working based on its self-reported health, OZIPHR independently verifies detection capability by executing real attack chains and measuring monitor response.

The Verification Approach

OZIPHR's test library includes a dedicated category of monitor blinding tests that systematically exercise each attack surface:

Continuous Verification

OZIPHR runs these tests continuously, not as a one-time assessment. Monitor updates, kernel upgrades, configuration changes, and infrastructure drift can all re-introduce blinding vulnerabilities. Continuous verification ensures that a regression is detected within minutes, not months.

Detection Speed Benchmarking

Beyond binary pass/fail testing, OZIPHR measures the latency of monitor detection for each test case. This provides quantitative data on how quickly the monitor responds to each attack technique, and whether that response time degrades over time or under load. A monitor that takes 30 seconds to detect an execve while the attacker can blind it in 10 milliseconds has a fundamental timing problem that no amount of rule tuning can fix.

OZIPHR's platform dashboard surfaces per-monitor latency benchmarks, blinding test results, and overall resilience scores, giving security teams a quantitative measure of their actual detection posture rather than an assumed one.

Conclusion

eBPF-based runtime security monitors represent a significant advance in kernel-level visibility. But visibility is not the same as security, and the presence of a monitor is not proof that it is working. The attack techniques described in this article — BPF tracepoint hijacking, BPF map poisoning, the MAP_FREEZE gap, and the absence of integrity verification — demonstrate that an attacker with the right privileges can silently and completely disable these tools.

The implications are clear. Organizations cannot rely on the self-reported health of their security monitors. A monitor that says it is running tells you nothing about whether it is seeing. The only way to know whether your runtime security is actually detecting attacks is to test it by sending real attacks and verifying real detections.

This is the fundamental principle behind OZIPHR: trust, but verify. We recommend that every organization deploying eBPF-based runtime security takes the following immediate steps:

  1. Audit whether your monitor uses BPF_MAP_FREEZE on its critical maps. If not, file an issue with the project or implement a wrapper that freezes maps after initialization.
  2. Restrict CAP_BPF in your Kubernetes cluster to only the pods that absolutely require it. Enforce this with admission policies.
  3. Deploy an independent verification system that continuously tests whether your monitors are actually detecting the attacks they claim to detect.
  4. Implement defense-in-depth by not relying solely on eBPF monitors. Combine them with audit logs, network-level detection, and host-based integrity monitoring.

The attackers are not going to politely announce their presence. Your monitors need to prove they can see them, and the only way to get that proof is to test.

Verify Your Runtime Security Today

OZIPHR continuously tests whether your Falco, Tracee, or Tetragon deployment can actually detect attacks — including monitor blinding.

Request a Demo