Environnement : Docker (image champgoblem/iovisor-bcc:latest-src5), Arch Linux.
Le but de ce TP est d’explorer les capacités d’eBPF (extended Berkeley Packet Filter) pour monitorer un système Linux en temps réel sans modifier le code du noyau. Nous nous concentrons sur trois types d’événements : la création de répertoires, les flux réseau TCP et l’exécution de processus.
Mise en place de l’environnement
Pour éviter de polluer l’hôte et disposer de tous les outils de compilation (bcc-tools), j’ai utilisé un conteneur Docker privilégié. Cette configuration est nécessaire car eBPF doit charger du bytecode directement dans le noyau.
docker run --rm -it --privileged \
-v /usr/lib/modules:/lib/modules:ro \
-v /usr/src:/usr/src:ro \
-v /etc/localtime:/etc/localtime:ro \
-v /sys/kernel/tracing:/sys/kernel/tracing \
-v /sys/kernel/debug:/sys/kernel/debug \
-v "$PWD":/work \
--pid=host --net=host \
champgoblem/iovisor-bcc:latest-src5TIP
Sur Arch, j’ai dû préalablement récupérer les headers :
pacman -S linux-headers.
Travaux pratiques
Surveillance des appels système (mkdir)
Sur ce kernel, le tracepoint sys_enter_mkdir n’exposait pas le pathname de façon exploitable avec BCC. On a donc utilisé un kprobe sur do_mkdirat et lu la chaîne en mémoire noyau (struct filename), ce qui fonctionne de manière stable.
from bcc import BPF
BPF_SOURCE_CODE = r"""
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct data_t {
u32 pid;
char comm[TASK_COMM_LEN];
char pathname[256];
};
struct filename {
const char *name;
};
BPF_PERF_OUTPUT(events);
int kprobe__do_mkdirat(struct pt_regs *ctx, int dfd, struct filename *name, umode_t mode) {
const char *fname = NULL;
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read_kernel(&fname, sizeof(fname), &name->name);
bpf_probe_read_kernel_str(&data.pathname, sizeof(data.pathname), fname);
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
bpf = BPF(text=BPF_SOURCE_CODE)
def print_event(cpu, data, size):
event = bpf["events"].event(data)
print(
f"{event.pid} {event.comm.decode(errors='replace')} mkdir {event.pathname.decode(errors='replace')}",
flush=True,
)
bpf["events"].open_perf_buffer(print_event)
print("To test, create a directory (e.g. mkdir /work/frodo_ebpf)", flush=True)
print("CTRL-C to exit", flush=True)
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
breakRésultat du test :
Extrait de trace (création du dossier frodo_ebpf) :
502290 mkdir mkdir /work
502290 mkdir mkdir frodo_ebpf
Analyse des connexions TCP
Les outils BCC tcpconnect / tcpstates n’ont pas compilé sur ce kernel. Pour garder une preuve exploitable, on a utilisé le tracepoint sock:inet_sock_set_state (script local tcp_state_trace.py) et capturé les transitions d’état TCP.
Exemple de capture lors d’une connexion locale :
TCP_ESTABLISHED 127.0.0.1:43219 -> 127.0.0.1:16415
TCP_ESTABLISHED 127.0.0.1:16415 -> 127.0.0.1:43219
TCP_CLOSE 127.0.0.1:43219 -> 127.0.0.1:16415
TCP_CLOSE 127.0.0.1:16415 -> 127.0.0.1:43219TIP
Un petit serveur web local a été lancé pour déclencher la connexion.
Supervision de l’exécution (execve)
L’observation des nouveaux processus est cruciale pour la sécurité. Ici, la trace a été prise via un tracepoint sys_enter_execve (script local exec_tracepoint.py).
Trace collectée :
509091 exec: /usr/bin/ls
(Action déclenchée par un ls dans le shell.)
Analyse théorique et sécurité
Comparaison : eBPF vs auditd (approche noyau classique)
D’après mes recherches sur la comparaison des mécanismes de tracking, voici les différences majeures :
| Caractéristique | Auditd (standard) | eBPF (moderne) |
|---|---|---|
| Performance | Peut ralentir le système si trop de règles. | Très performant (JIT compilation). |
| Flexibilité | Limité aux événements prévus. | Programmable (on peut filtrer finement en C). |
| Complexité | Simple à configurer. | Demande des connaissances en développement noyau. |
| Intégrité | Logs plus difficiles à altérer par l’user. | Très discret, difficile à détecter pour un malware. |
Modèle de menace et limites
L’utilisation d’eBPF s’inscrit dans une stratégie de détection proactive.
-
Surface couverte : visibilité sur les syscalls, le réseau (événements TCP bas niveau) et les manipulations de fichiers.
-
Surface non couverte :
- Données chiffrées : eBPF voit passer les paquets, mais si TLS est utilisé, le contenu reste illisible sans instrumentation spécifique (e.g. interception des bibliothèques SSL).
- Attaques rootkit : si un attaquant obtient des privilèges kernel avant le chargement de nos sondes, il peut bloquer ou tromper eBPF.
-
La « Rule of 2 » : en lien avec les concepts de Google, eBPF permet de respecter cette règle en traitant des données potentiellement malveillantes (inputs noyau) dans une sandbox sécurisée (la VM eBPF) plutôt que de modifier directement le code critique du kernel.
Conclusion
Ce TP montre qu’eBPF est un outil puissant pour la sécurité moderne. Sa capacité à observer sans perturber est un atout majeur par rapport aux solutions classiques comme auditd. Cependant, la complexité de mise en œuvre et la nécessité d’un noyau récent restent des freins à son déploiement systématique.
Annexe
NOTE
Piège classique : les images BCC anciennes ne compilent pas sur des kernels récents (erreurs clang/headers). Utiliser une image à jour et monter
/usr/lib/modules+/usr/src.
WARNING
Si
/sys/kernel/tracingou/sys/kernel/debugn’est pas monté dans le conteneur, BCC ne peut pas attacher les probes (trace_pipe/kprobe_eventsintrouvables).
TIP
Certains outils BCC (
tcpconnect,tcpstates) peuvent ne pas compiler selon la version du kernel.
NOTE
mkdirpasse souvent parmkdiratcôté libc. Le tracepointsys_enter_mkdirn’est pas toujours suffisant, et il faut lire les chaînes userspace avecbpf_probe_read_*.
1) mkdir (kprobe do_mkdirat)
docker run --rm --privileged \
-v /usr/lib/modules:/lib/modules:ro \
-v /usr/src:/usr/src:ro \
-v /etc/localtime:/etc/localtime:ro \
-v /sys/kernel/tracing:/sys/kernel/tracing \
-v /sys/kernel/debug:/sys/kernel/debug \
-v "$PWD":/work \
--pid=host --net=host \
champgoblem/iovisor-bcc:latest-src5 \
bash -lc "python3 /work/mkdir_kprobe.py & pid=\$!; sleep 1; rm -rf /work/frodo_ebpf; mkdir -p /work/frodo_ebpf; sleep 2; kill \$pid"2) TCP (tracepoint sock:inet_sock_set_state)
python3 -m http.server 8000 >/tmp/http_server.log 2>&1 & srv=$!
cid=$(docker run -d --privileged \
-v /usr/lib/modules:/lib/modules:ro \
-v /usr/src:/usr/src:ro \
-v /etc/localtime:/etc/localtime:ro \
-v /sys/kernel/tracing:/sys/kernel/tracing \
-v /sys/kernel/debug:/sys/kernel/debug \
-v "$PWD":/work \
--pid=host --net=host \
champgoblem/iovisor-bcc:latest-src5 \
bash -lc "python3 /work/tcp_state_trace.py")
sleep 1
python3 - <<'PY'
import socket
s = socket.socket()
s.connect(("127.0.0.1", 8000))
s.close()
PY
sleep 2
docker logs "$cid"
docker stop "$cid" >/dev/null
kill "$srv"