Введение
Добрый день, всем читающим данную статью. Недавно эксперементируя с 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. Надеюсь, что данный материал окажется кому-нибудь полезным. Всем спасибо за прочтение.