Weaponizing eBPF: de la teoría al laboratorio

Empezamos con un poco de historia... eBPF viene de BPF (Berkeley Packet Filter), desarrollado en los años 90 para Linux y BSD. La idea original era filtrar paquetes de red de manera eficiente, permitiendo que los programas inspeccionaran headers de red sin salir del kernel y sin escribir código kernel complejo. Antes de BPF los filtros de paquetes eran lentos y monolíticos, con BPF disponíamos de un pequeño lenguaje de bytecode, ejecutado por una máquina virtual ligera en el kernel y nos permitía escribir “programas” que analizaban paquetes en tiempo real y solo pasaban al userland lo que te interesaba. Era seguro, rápido y muy flexible.

Posteriormente alrededor de 2014-2015 surge eBPF (Extended BPF) como una extensión masiva de BPF clásico. Sus mejoras clave:

  • Hooks genéricos: no solo red, ahora puedes engancharte a syscalls, tracepoints, funciones del kernel (kprobes), funciones de programas userland (uprobes), sockets, etc.
  • Estructuras de datos avanzadas (maps): hash, arrays, stacks, bloom filters… para compartir datos entre kernel y userland.
  • Verificación de seguridad: el bytecode pasa un verificador que garantiza que no habrá bucles infinitos ni accesos ilegales.

Y 10 años después, eBPF (Extended Berkeley Packet Filter) se ha convertido en una de las piezas más potentes e innovadoras del kernel Linux. Es decir, lo que comenzó como un simple mecanismo para filtrar paquetes de red ha evolucionado en un motor de ejecución de bytecode dentro del kernel, con aplicaciones en observabilidad, seguridad, tracing y rendimiento.

Debido a ello, los equipos de seguridad defensiva han abrazado eBPF para monitorizar llamadas al sistema, detectar anomalías en tiempo real y reducir la superficie de ataque. Pero como suele suceder, lo que potencia la defensa también puede inspirar al ataque. Investigadores han comenzado a explorar el lado oscuro de eBPF: rootkits invisibles, técnicas de evasión de EDR, persistencia en kernel space y manipulación sigilosa de syscalls.

Este artículo vamos a jugar un poco con eBPF para entender cómo un atacante podría abusar de esta tecnología con varias PoCs sencillas. Primero empezamos con la instalación:

🔹 Ubuntu/Debian (kernel moderno recomendado)

# Instalar dependencias básicas sudo apt update sudo apt install -y clang llvm libbpf-dev libelf-dev gcc make pkg-config git # Instalar bpftool (si no viene en el paquete) sudo apt install -y bpftool # Opcional: instalar bcc y bpftrace sudo apt install -y bpfcc-tools python3-bpfcc bpftrace

🔹 Fedora

sudo dnf install -y clang llvm bpftool bpftrace bcc-tools bcc-devel elfutils-libelf-devel

🔹 Arch Linux

sudo pacman -S clang llvm bpftool bcc bpftrace

🔹 Verificar

  1. Comprueba que el kernel soporta eBPF:

uname -r

(idealmente 5.x o 6.x).

  1. Comprueba que el pseudo-filesystem está montado:

mount | grep bpf

Si no aparece, móntalo con:

sudo mount -t bpf none /sys/fs/bpf/
  1. Prueba que funciona con un one-liner de bpftrace:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s called execve\n", comm); }'

Deberías ver en tiempo real qué procesos invocan execve.

Ahora que lo tenemos en nuestro sistema, vamos con las PoCs:

PoC 1: Ocultando procesos con eBPF y kprobes

Un clásico rootkit en Linux manipula la syscall getdents para que ls o ps no muestren ciertos procesos. Con eBPF podemos hacer algo similar sin modificar el kernel ni cargar módulos sospechosos.

Código eBPF (C):

// hide_pid.bpf.c
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/dirent.h>
#include <linux/uaccess.h>

#define HIDE_PID 1337   // PID a ocultar

SEC("kprobe/getdents64")
int bpf_prog(struct pt_regs *ctx) {
    struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)PT_REGS_PARM2(ctx);
    // Aquí podríamos parsear la lista de entradas y filtrar la que coincida con HIDE_PID.
    // Por simplicidad este PoC sólo demuestra el hook.
    bpf_printk("Interceptada llamada a getdents64!\n");
    return 0;
}

char _license[] SEC("license") = "GPL";
Compilamos:

clang -O2 -target bpf -c hide_pid.bpf.c -o hide_pid.bpf.o

Carga con bpftool:

sudo bpftool prog load hide_pid.bpf.o /sys/fs/bpf/hide_pid sudo bpftool prog attach pinned /sys/fs/bpf/hide_pid kprobe/getdents64

Ahora cada vez que un proceso llame a getdents64, nuestro programa eBPF se ejecutará.
La versión completa podría inspeccionar los nombres de directorio y eliminar la entrada que corresponde al PID a ocultar.

PoC 2: Persistencia avanzada: mapas “pinneados” en /sys/fs/bpf/

Normalmente, cuando un proceso userland muere, cualquier estructura de memoria que haya creado también desaparece. Pero los maps eBPF pueden “pinnearse” en el pseudo-filesystem /sys/fs/bpf/, sobreviviendo al proceso. Esto permite a un atacante almacenar datos o estados que persisten sin depender de un demonio activo.

Ejemplo conceptual seguro:

  • Crear un map y pinnearlo:

sudo bpftool map create /sys/fs/bpf/my_map type hash key 4 value 8 entries 64 name my_persistent_map sudo bpftool map update pinned /sys/fs/bpf/my_map key 42 value 666
  • Incluso si cierras el proceso que creó el map, puedes volver a leerlo:

sudo bpftool map lookup pinned /sys/fs/bpf/my_map key 42

PoC 3: Evasión práctica del EDR

Los EDR (Endpoint Detection & Response) funcionan principalmente interceptando o registrando syscalls críticas, como:

  • execve → ejecución de binarios.
  • openat → apertura de ficheros sensibles.
  • connect / accept → conexiones de red.

Tradicionalmente, los EDR se enganchan a estas llamadas desde userland usando librerías, hooking en libc, ptrace o kernel modules. Sin embargo, eBPF permite enganchar estas llamadas directamente en kernel space, antes de que el EDR las vea. Esto abre un vector conceptual de “interceptación previa”, que puede ser usado tanto para defensa como para ataques.

Veamos como eBPF intercepta la syscall antes de que el EDR la vea, permitiendo observar o analizar la actividad de manera segura en nuestro lab. El diagrama sería:

Flujo conceptual

  1. Kernel space intercepta: eBPF engancha syscalls sensibles (execve, openat, connect).
  2. Map eBPF almacena datos: contadores, rutas de ficheros, PIDs.
  3. Userland lee datos: análisis seguro, agregación, visualización en tiempo real.
  4. Persistencia opcional: objetos pineados en /sys/fs/bpf/ sobreviven al proceso.
Código del hook

a) Programa eBPF: execve_hook.bpf.c

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

// Map para contar llamadas por PID
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, u32);       // PID
    __type(value, __u64);   // contador
} execve_count SEC(".maps");

// Hook en syscall execve
SEC("kprobe/sys_execve")
int kprobe_execve(struct pt_regs *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    __u64 zero = 0, *val;

    val = bpf_map_lookup_elem(&execve_count, &pid);
    if (val)
        __sync_fetch_and_add(val, 1);
    else
        bpf_map_update_elem(&execve_count, &pid, &zero, BPF_NOEXIST);

    return 0; // syscall sigue su curso normal
}

char LICENSE[] SEC("license") = "GPL";

b) Loader en userland: execve_hook_user.c

#include <stdio.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "execve_hook.skel.h"

int main()
{
    struct execve_hook_bpf *skel;
    skel = execve_hook_bpf__open_and_load();
    if (!skel) return 1;

    if (execve_hook_bpf__attach(skel)) return 1;

    printf("Interceptando execve... Ctrl-C para salir\n");

    while (1) {
        sleep(5);
        int map_fd = bpf_map__fd(skel->maps.execve_count);
        u32 key = 0, next_key;
        __u64 value;
        while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
            if (bpf_map_lookup_elem(map_fd, &next_key, &value) == 0) {
                printf("PID %u -> execve count: %llu\n", next_key, (unsigned long long)value);
            }
            key = next_key;
        }
    }

    execve_hook_bpf__destroy(skel);
    return 0;
}

Ejemplos de mapas persistentes y agregación avanzada

a) Map persistente “pineado”

sudo bpftool map create /sys/fs/bpf/my_persistent_map type hash key 4 value 8 entries 128 name persistent_execve
  • Este mapa sobrevive al cierre del proceso que lo creó.

  • Ideal para almacenar contadores agregados de syscalls o eventos en laboratorio.

b) Actualización y lectura segura

# Actualizar un valor en el mapa sudo bpftool map update pinned /sys/fs/bpf/my_persistent_map key 42 value 100 # Leer un valor sudo bpftool map lookup pinned /sys/fs/bpf/my_persistent_map key 42
  • Permite experimentar con agregación de datos de múltiples procesos sin interferir con el kernel o userland.

Limitaciones y detección

  • Cargar programas eBPF requiere privilegios (CAP_BPF o CAP_SYS_ADMIN en kernels antiguos).
  • Blue Teams pueden monitorizar /sys/fs/bpf/, bpftool y syscalls relacionadas.
  • Herramientas como Falco y BPF LSM comienzan a detectar usos anómalos.

Aun así, el blind spot es considerable, ya que muchos EDRs todavía no inspeccionan eBPF.

Comentarios