Введение

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

Комментарии (0)