Добрый день, всем читающим данную статью. Недавно эксперементируя с eBPF для разработки нового функционала своей EDR для linux-серверов, я столкнулся с огромной проблемой:
на просторах интернета есть огромный пласт статей по теории работы с eBPF, однако кратких практических статей как работать с BPF мной найдено не было.
Если быть более точным, то такие статьи есть, однако, они не дают понимания функционала.
В общем, в данной статье хотелось бы написать краткий гайд по работе с eBPF с уклоном в практику.
Что такое BPF
Описаний с различными схемами как работает BPF на просторах интернета очень много, поэтому буду краток.
В Linux есть встроенный функционал для безопасного запуска программ в пространстве ядра через виртуальную машину. Эта виртуальная машина и называется BPF. По сути, BPF открывает двери в ядро Linux нашим программам на высокоуровневых языках (Rust, Go, Python), что в свою очередь, предоставляет альтернативу написанию модулей ядра на C и с недавних пор Rust.
Сущности BPF
Полный список сущностей с которыми можно работать при написании программ можно посмотреть в документации или в специализированной литературе. Я же выделю несколько основных типов:
tracepoints - точки трассировки. Позволяют перехватывать события в системе. Список tracepoints можно найти в /sys/kernel/debug/tracing/events (напимер, tracepoints для отлавливания syscall'ов можно найти в /sys/kernel/debug/tracing/events/syscalls);
kprobes, kretprobes - зонды уровня ядра. Аналогично возволяют перехватывать некоторые события в системе. Отличием от tracepoints является то, что tracepoints зачастую обладают меньшим функционалом, однако, существуют на большинстве версий ядра, в отличии от kprobes, которые могут отличаться от версии ядер. kprobes - это зонды которые выполняют код до выполнения перехваченной команды, kretprobes в свою очередь наоборот выполянются после;
uprobes, uretprobes - зонды уровня пользователя. Аналогично kprobes и kretprobes, только работают в пользовательском пространстве;
map - объект для обмена программы bpf и программы в пользовательском пространстве данными.
Пишем простой перехватчик execve на Go
Подготовка окружения
Первым делом создадим директорию под наш проект (далее execve-tracer)
mkdir execve-tracer && cd execve-tracer
Далее инициализируем go
go mod init execve-tracer
Далее скачиваем необходимые пакеты с apt (или другого пакетного менеджера)
sudo apt install clang llvm libbpf-dev sudo apt install linux-tools-common linux-tools-$(uname -r) go get -t github.com/cilium/ebpf/cmd/bpf2go
И линкуем библиотеку (иначе будет ошибка отсутствия типов)
sudo ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/asm
На этом наше окружение настроено и можно приступать к написанию программы
Написание программы перехвата execve
Сначала создадим поддиректорию ebpf и в ней создадим файл execve.bpf.c
mkdir ebpf && touch ebpf/execve.bpf.c
После определим параметры которые будет передавать tracepoint в нашу функцию, чтобы после этого прописать соответсвующий объект в коде:
bpftrace -lv 'tracepoint:syscalls:sys_enter_execve'
В данном файле пишем следующий код (приведены подробные комментарии)
#include <linux/bpf.h> #include <linux/ptrace.h> #include <linux/sched.h> #include <bpf/bpf_helpers.h> #define TASK_COMM_LEN 16 // Это структура которую мы отправляем в user space struct event { __u32 pid; char comm[TASK_COMM_LEN]; char filename[256]; }; // структура syscall // можно посмотреть через bpftrace -lv 'tracepoint:syscalls:sys_enter_execve' // перед написанием кода struct syscalls_enter_execve_args { unsigned short common_type; unsigned char common_flags; unsigned char common_preempt_count; int common_pid; int __syscall_nr; const char *filename; const char *const *argv; const char *const *envp; }; // структура нашего канала в userspace struct { // Тип - массив __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); // Максимальный размер массива __uint(max_entries, 128); // Объяснить генератору что это - map } events SEC(".maps"); //Прикрепление к tracepoint SEC("tp/syscalls/sys_enter_execve") int monitor_execve(struct syscalls_enter_execve_args* ctx) { //пустая структура struct event evt = {}; // Получение pid из tgid evt.pid = bpf_get_current_pid_tgid() >> 32; // Получение текущей команды bpf_get_current_comm(evt.comm, sizeof(evt.comm)); // Парсинг названия файла в str bpf_probe_read_user_str(evt.filename, sizeof(evt.filename), ctx->filename); // Отправка в userspace bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); return 0; } // Лицензия char _license[] SEC("license") = "GPL";
Далее создаём файл main.go
touch main.go
И пишем следующий код (приведены комментарии)
package main // генерация кода по написанной выше программе //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpf -go-package=main EbpfMonitoring ebpf/execve.bpf.c -- -I. -O2 -Wall -g import ( "C" "bytes" "encoding/binary" "fmt" "log" "os" "os/signal" "strings" "syscall" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/perf" "github.com/cilium/ebpf/rlimit" ) //структура аналогичная стуктуре в BPF-программе (ВАЖНО чтобы совпадал даже порядок полей) type Event struct { Pid uint32 Comm [16]byte Filename [256]byte } func main() { // убираем memlock if err := rlimit.RemoveMemlock(); err != nil { log.Fatalf("Failed to remove memlock: %v", err) } // загружаем объекты objs := EbpfMonitoringObjects{} if err := LoadEbpfMonitoringObjects(&objs, nil); err != nil { log.Fatalf("Failed to load eBPF objects: %v", err) } defer objs.Close() // подключаемся к tracepoint tp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.MonitorExecve, nil) if err != nil { log.Fatalf("Failed to attach tracepoint: %v", err) } defer tp.Close() // подключаем reader к map в который BPF-программа передаёт события rd, err := perf.NewReader(objs.Events, os.Getpagesize()) if err != nil { log.Fatalf("Failed to open perf buffer: %v", err) } defer rd.Close() fmt.Println("eBPF program running...") sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) // простая фукнция обработки событий go func() { var e Event for { record, err := rd.Read() if err != nil { log.Printf("Failed to read from perf buffer: %v", err) continue } if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &e); err != nil { log.Printf("Failed to decode event: %v", err) continue } fmt.Printf("Execve: PID=%d, Process=%s, File=%s\n", e.Pid, strings.TrimRight(string(e.Comm[:]), "\x00"), strings.TrimRight(string(e.Filename[:]), "\x00"), ) } }() <-sigChan }
Далее собираем проект и наслаждаемся
sudo go generate sudo go build -o ebpf_tracer main.go ebpfmonitoring_bpf.go sudo ./ebpf_tracer
Вывод
В рамках данной статьи было:
кратко разобрано что такое eBPF;
подготовлено окружение для написания программы для работы с eBPF
подробно разобрано на примере кода как писать eBPF на execve. Надеюсь, что данный материал окажется кому-нибудь полезным. Всем спасибо за прочтение.
