eBPF - Ein erster Einblick

eBPF

Ein erster Einblick

Ahmet Hrnjadovic
von Ahmet Hrnjadovic
am 08. Mai 2019
Lesezeit: 11 Minuten

Keypoints

Das kann das neue Kernel-Feature

  • BPF wird im Kernelspace ausgeführt
  • Es kann zur Laufzeit in den Linux Kernel geladen werden
  • Dann kann es eingesetzt werden, um den Aufruf von Kernelfunktionen dynamisch zu tracen
  • Es wird in einer Sandbox ausgeführt, was das Schadensrisiko minimiert

BPF (Berkeley Packet Filter) ist nichts Neues. Es ist in Tools wie tcpdump für effizientes Packet-Tracing seit Jahren in Verwendung. In neueren Kernelversionen, vorallem seit Version 4.1, sind eine Reihe neuer Additionen BPF zugefügt worden, wodurch es für weitaus mehr als Paketfiltering verwendet werden kann. Dieser Artikel soll einen Einblick in die Verwendung von BPF geben und einige von BPFs Vorteilen aufzeigen.

Aufgrund der vielen Additionen zu BPF wird es oft als eBPF (extended BPF) betitelt. Wenn es um das Tracen von Events im Kernelspace geht, ist BPF effizienter als traditionelle Event-Tracing-Methoden, weil es direkt jede Kernelfunktion instrumentieren kann. Somit stehen BPF unzählige Quellen für Events zur Verfügung. BPF läuft in einer Sandbox im Kernel. Diese stellt sicher, dass die BPF-Applikation dem System keinen Schaden über ihre Applikationslogik zufügt. Das gibt BPF eine gute Eignung für das Monitoren von Events oder dem Troubleshooten auf Produktionssystemen. Ein Risiko, welches weiterhin existiert, ist ein negativer Einfluss auf die Systemperformance, wenn eine grosse Anzahl an Events gemonitored werden.

Eine existierende Sammlung von Utilities welche BPF verwenden, kann im bpfcc-tools paket für Debian gefunden werden. opensnoop ist ein nützliches Tool, welches darin enthalten ist. Damit kann das Öffnen von Dateien systemweit oder für ein einzelnes Programm gemonitored werden.

Ein weiteres nützliches Paket ist tcpretrans, welches TCP-Retransmits monitored. Eine gängige Methode ohne BPF TCP-Retransmits festzustellen, ist das Erstellen und die Analyse eines Packet Captures mit einem Tool wie tcpdump. Mit BPF, können wir eine Kprobe an die Kernelfunktion hängen, welche bei TCP-Retransmits aufgerufen wird, was eine weitaus elegantere Lösung ist. Aus tcpretrans:

# initialize BPF
b = BPF(text=bpf_text)
b.attach_kprobe(event="tcp_retransmit_skb", fn_name="trace_retransmit")

Die Funktion trace_retransmit ist eine vorher definierte C-Funktion in der Sourcedatei. Sie sammelt Informationen zum Retransmit und wird mit jedem Aufruf der Kernelfunktion ausgeführt.

Verwendung von BPF

Programme direkt in eBPF-Instruktionen zu schreiben ist zeitaufwendig und vergleichbar mit dem Programmieren in Assembler. Es gibt allerdings zugängliche Frontends mit unterschiedlichen Abstraktionsstufen und Möglichkeiten. In diesem Artikel wird bcc (BPF Compiler Collection) verwendet. Bcc bietet Frontends in Python und Lua. Folgend ein minimalistisches Beispiel, welches das Python Interface verwendet und gut zeigt, wie mit wenig Code der ptrace_attach System-Call gemonitored weren kann, um eine Art von Process Injection zu erkennen.

/usr/bin/python

from bcc import BPF

BPF(text='int kprobe__ptrace_attach(void *ctx) { bpf_trace_printk("ptrace_attach called\\n"); return 0; }').trace_print()

Dieses Programm generiert den folgenden Output wenn ptrace_attach aufgerufen wird:

# ./ptrace.py
         derusbi-6572  [003] .... 55527.716367: 0x00000001: ptrace_attach called

Der String, welcher der text Variable zugeschrieben wird, ist eingeschränkter C-Code. kprobe__ ist ein spezieller Prefix, welcher eine Kprobe (dynamisches Tracing eines Aufrufs einer Kernelfunktion) an die Kernelfunktion mit dem selben Namen wie die Funktion anhängt. Die definierte Funktion wird dann jedes Mal ausgeführt, wenn ptrace_attach aufgerufen wird.

bpf_trace_printk kann als praktische Abkürzung verwendet werden, um während der Entwicklung oder zum Debuggen Daten an den Kernel /sys/kernel/debug/tracing/trace_pipe auszugeben. Für andere Zwecke sollte diese Methode der Datenausgabe nicht verwendet werden, da trace_pipe global geteilt ist. Der Output von bpf_trace_printk hängt von den Einstellungen ab, die in /sys/kernel/debug/tracing/trace_options gesetzt sind. In diesem Fall wird der Name des Tasks derusbi, die PID 6572, die CPU-Nummer auf der der Task läuft, IRQ-Optionen, ein Timestamp in Nanosekunden, ein von BPF generisch gesetzter Wert für den Instruction Pointer Register und unsere formattierte Nachricht dokumentiert.

Wir können einer Funktion mit attach_kprobe in Python mehrere Kprobes anhängen. Weiterhin kann mit der Verwendung von trace_fields statt trace_print grössere Kontrolle über die ausgegebenen Informationen erlangt werden.

from bcc import BPF

# define BPF program
bpf_program = """
int p_event(void *ctx) {
    bpf_trace_printk("traced very meaningful event!\\n");
    return 0;
}
"""


# load BPF program
b = BPF(text=bpf_program)

#attach kprobes for sys_clone and execve
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="p_event")
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="p_event")

while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
    except ValueError:
        continue
    print("%f\t%d\t%s\t%s" % (ts, pid, task, msg))

Dieses nächste Beispiel verwendet das BPF_PERF_OUTPUT() Interface für die Ausgabe von Informationen. Das ist die präferierte Methode zum Teilen von pro Event gesammelten Daten mit dem User Space.

#!/usr/bin/python

#Adapted example from https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md
#as well as https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md

from bcc import BPF

# define BPF program
bpf_program = """
#include <linux/sched.h>

struct omg_data {
    u64 gid_pid;
    u32 pid;
    u32 gid;
    u64 ts;  //timestamp with nanosecond precision
    char procname[TASK_COMM_LEN]; //holds the name of the current process
};

BPF_PERF_OUTPUT(custom_event);    //creates a BPF table for pushing out custom event data to user space via perf ring buffer. This is the preferred
                            //method of pushing per-event data to user space. (might need to include <uapi/linux/ptrace.h> for this)

int get_thi_dete(struct pt_regs *ctx) {
    struct omg_data mdata;

    mdata.gid_pid = bpf_get_current_pid_tgid();
    mdata.pid = (u32) mdata.gid_pid;
    mdata.gid = mdata.gid_pid >> 32;

    mdata.ts = bpf_ktime_get_ns(); //gets timestamp in nanoseconds

    bpf_get_current_comm(&mdata.procname, sizeof(mdata.procname));

    custom_event.perf_submit(ctx, &mdata, sizeof(mdata));

    return 0;
}
"""

# load BPF program
b = BPF(text=bpf_program)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="get_thi_dete")


# header
print("%-18s %-6s %-6s %-16s %s" % ("TIME(s)", "PID", "TID", "TASK", "MESSAGE"))

# process event
start = 0
def print_event(cpu, omg_data, size):
    global start
    event = b["custom_event"].event(omg_data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-6d %-6d %-16s %s" % (time_s, event.gid, event.pid, event.procname, "Traced sys_clone!"))

# loop with callback to print_event
b["custom_event"].open_perf_buffer(print_event)
while 1:
    b.perf_buffer_poll()

Weil wir hier kein bpt_trace_printk() verwenden, bekommen wir auch kein vorgefertigtes Informationspaket. Das heisst, wir müssen selber Daten sammeln, welche für uns relevant sind. Wir benutzen das omg_data Struct, um Daten vom Kernelspace zum Userspace zu schicken. BPF_PERF_OUTPUT('custom_event') erstellt eine BPF-Table custom_event, um unseren massgeschneiderten Datensatz zum Userspace via den Perf-Ring-Buffer zu pushen. Aus bpf.h:

* u64 bpf_get_current_pid_tgid(void)
*  Return
*      A 64-bit integer containing the current tgid and pid, and
*      created as such:
*      *current_task*\ **->tgid << 32 \|**
*      *current_task*\ **->pid**.

Aus diesem Grund wird der zurückgegebene Wert herumgeshiftet. Um den Namen der momentanen Tasks zu erhalten, rufen wir bpf_get_current_comm() auf. Aus bpf.h:

* int bpf_get_current_comm(char *buf, u32 size_of_buf)
*  Description
*      Copy the **comm** attribute of the current task into *buf* of
*      *size_of_buf*. The **comm** attribute contains the name of
*      the executable (excluding the path) for the current task. The
*      *size_of_buf* must be strictly positive. On success, the
*      helper makes sure that the *buf* is NUL-terminated. On failure,
*      it is filled with zeroes.

perf_submit gibt den Event für den Userspace via dem Perf-Ring-Buffer frei. print_event ist die Python Funktion welche hier Events vom custom_event Stream einliest. b.perf_buffer_poll() wartet auf Events. Dieser Aufruf ist blockend.

Fazit

Die neuen Additionen zu BPF erlauben es, kompakte, wirkungsvolle und performante Tracing-Programme zu schreiben. Nun bleibt erforderlich, geeignete Use-Cases zu finden, welche das Potential von BPF ausschöpfen. Obwohl BPF zur Laufzeit in den Kernel geladen und im Kernelspace ausgeführt wird, ist das Risiko eines negativen Einflusses auf andere Systemkomponenten dank Sandboxing und statischer Code-Analyse durch den Kernel eingeschränkt.

Über den Autor

Ahmet Hrnjadovic

Ahmet Hrnjadovic arbeitet seit dem Jahr 2017 im Bereich Cybersecurity, wobei er sich auf die Bereiche Linux, sichere Programmierung und Web Application Security Testing fokussiert. (ORCID 0000-0003-1320-8655)

Links

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

×
Crypto-Malware

Crypto-Malware

Ahmet Hrnjadovic

SAML 2.0, OPenID Connect, OAuth 2.0

SAML 2.0, OPenID Connect, OAuth 2.0

Ahmet Hrnjadovic

Linux Bind-Shell in Assembler

Linux Bind-Shell in Assembler

Ahmet Hrnjadovic

Sie wollen mehr?

Weitere Artikel im Archiv

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

Sie wollen mehr?

Weitere Artikel im Archiv