eBPF

Simple program

In this section, we are going to create a simple eBPF program which trace the sys_enter_write. In this call, we get the PID and send data to the map:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 4);
    __type(key, int);
    __type(value, int);
} hello SEC(".maps");

static int foo(){
    bpf_printk("First BPF program.\n");
    return 0;
}

SEC("tp/syscalls/sys_enter_write")
int print_hello_world(void) {
    int key = 0;
    int *stats;
    //const pid_t pid_filter = 0;
    int pid_filter = 0;
    stats = bpf_map_lookup_elem(&hello, &key);
    if (!stats)
        return -1;

    int pid = bpf_get_current_pid_tgid() >> 32;
    if (pid_filter && pid != pid_filter)
        return 0;

    *stats = pid;

    bpf_printk("First BPF program from PID %d.\n", pid);
    return foo();
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";

We compile it:

clang -g -O2 -target bpf -c test_ebpf.c -o test_ebpf.o

Now, we create our C program for loading and attaching the C eBPF program. Also, we get the map data and print the outcome:

$ cat load_bpf.c
#include <stdio.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>

int main(void){
    const char *fileObj = "print_hello_world.o";
    struct bpf_object *obj;
    struct bpf_program *program;
    int err;
    int key = 0;
    int stats;

    obj = bpf_object__open_file(fileObj, NULL);
    if (!obj){
        printf("Failed to open %s\n", fileObj);
        return -1;
    }

    err = bpf_object__load(obj);

    if (err){
        printf("Failed to load object\n");
        return -1;
    }

    program = bpf_object__find_program_by_name(obj, "print_hello_world");
    if (!program){
        printf("Failed to find the program\n");
        return -1;
    }

    err = bpf_object__find_map_fd_by_name(obj, "hello");
    printf("%d\n", err);
    if (err < 0){
        printf("Failed to find the FD of the map\n");
        return -1;
    }

    bpf_program__attach(program);


    while(1){
        bpf_map_lookup_elem(err, &key, &stats);
        printf("%d\n", stats);
    }

    return 0;
}

We can compiel it:

gcc load_bpf.c -o load_bpf -lbpf

Before, we need to enable the trace:

# echo 1 > /sys/kernel/debug/tracing/tracing_on

We can execute the program:

$ sudo ./load_bpf
86063
86063
86063
86063
86063
86063
86063
86063
86063

And print the output:

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
      terminator-10734   [001] d... 19833.378021: bpf_trace_printk: First BPF program from PID 86063.
      terminator-10734   [001] d... 19833.378021: bpf_trace_printk: First BPF program.
      terminator-10734   [001] d... 19833.378028: bpf_trace_printk: First BPF program from PID 86063.
      terminator-10734   [001] d... 19833.378028: bpf_trace_printk: First BPF program.

With the map, the get the data:

Also, after you loaded the program, it is show with this command ̀bpftool prog show

Ring buffer skel

The main program which is executed in the User Space. this program load the eBPF program into the kernel:

#include <stdio.h>
#include <stdlib.h>
#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <linux/perf_event.h>
#include <argp.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include "common.h"

static int running = 1;

static void signalHandler(int signum){
    running = 0;
}

int handle_event(void *ctx, void *data, size_t data_sz){
    struct event *s_event = (struct event *)data; 
    printf("PID: %d\n", s_event->pid);
    printf("binPath: %s\n", s_event->filename);
    return 0;
}
int main(int argc, char *argv[]){
    const char *fileObj = "src/dns-trace.ebpf.o";
    struct bpf_object *obj;
    struct bpf_program *programExecve;
    struct ring_buffer *rb;
    int err;
    int fd_map_data;

    signal(SIGINT, signalHandler);

    /* Open and load our eBPF object */
    obj = bpf_object__open_file(fileObj, NULL);
    if (!obj){
        printf("Failed to open the file\n");
        return -1;
    }

    err = bpf_object__load(obj);
    if (err){
        printf("Failed to load object\n");
        return -1;
    }

    /* Retrieving fd of maps */
    fd_map_data = bpf_object__find_map_fd_by_name(obj, "data");
    if (!fd_map_data){
        printf("Failed to find the fd map data\n");
        bpf_object__close(obj);
        return -1;
    }

    /* Retrieving our programs */
    programExecve = bpf_object__find_program_by_name(obj, "syscall_execve");

    if (!programExecve){
        printf("Failed to find program\n");
        bpf_object__close(obj);
        return -1;
    }

    bpf_program__attach(programExecve);

    /* Start the ringbuffer */
    rb = ring_buffer__new(fd_map_data, handle_event, NULL, NULL);
    if (!rb){
        printf("Failed to create the ringbuf\n");
        bpf_object__close(obj);
        return -1;
    }

    while(running){
        err = ring_buffer__poll(rb, 100 /* timeout, ms */);
        if (err == -EINTR){
            printf("Failed to get the ringbuf\n");
            running = 0;
            break;
         }
    }

    ring_buffer__free(rb);
    bpf_object__close(obj);

    return 0;
}

And the program which is executed in the Kernel Space:

#define BPF_NO_GLOBAL_DATA
#define __TARGET_ARCH_x86
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
#include <string.h>
#include "common.h"

/*
 * Helper:
 * Issue: invalid indirect read from stack R2 off
 *   Fix: check if all variables is initialised
 * Issue: R1 invalid mem access 'inv'
 *   Fix: the value can be NULL
 */

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024 /* 256kb */);
} data SEC(".maps");


struct ctx_execve {
   __u16 common_type;          // Unsigned short
   __u8 common_flags;          // Unsigned char
   __u8 common_preempt_count;  // Unsigned char
   __s32 common_pid;           // int
   __s32 syscall_nr;           // int
   const __u8 *filename;       // unsigned char
   const __u8 *const *argv;
   const __u8 *const *envp;
};

static int execve(struct ctx_execve *ctx) {
    struct event *s_event;
    s_event = bpf_ringbuf_reserve(&data, sizeof(*s_event), 0);
    if (!s_event)
        return 0;

    s_event->pid = bpf_get_current_pid_tgid();

    bpf_probe_read_user_str(&s_event->filename, sizeof(s_event->filename), ctx->filename);

    bpf_ringbuf_submit(s_event, 0);

    return 0;
}
SEC("tp/syscalls/sys_enter_execve")
int syscall_execve(struct ctx_execve *ctx) {
    return execve(ctx);
}

char LICENSE[] SEC("license") = "GPL";

And the common.h file:

#ifndef H_COMMON
#define H_COMMON

#define FILENAME_SIZE 128

struct event {
    pid_t pid;
    char filename[FILENAME_SIZE];
};

#endif

And the Makefile:

GCC=gcc
CL=clang-11
CFLAGS=-Wall
LIBS=-lbpf

all: example.ebpf.o example

example.ebpf.o: src/example.ebpf.c
    $(CL) -g -O2 -target bpf -c src/example.ebpf.c -o src/example.ebpf.o

example: src/example.c
    $(GCC) $(CFLAGS) src/example.c -o example $(LIBS)

clean:
    rm -rf src/*.o  && rm example

Perf monitoring

With eBPF, we can attached perf event program for profiling the kernel. For doing that, we can create a perf event program.

$ cat perf.bpf.c
#define BPF_NO_GLOBAL_DATA
#define __TARGET_ARCH_x86
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024 /* 256kb */);
} data SEC(".maps");

SEC("perf_event")
int kprobe_perf_event(struct bpf_perf_event_data *ctx) {
    return 0;
}

After that, the program for load the eBPF program:

#include <stdio.h>
#include <stdlib.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <unistd.h>
#include <errno.h>
#include <linux/perf_event.h>
#include <sys/syscall.h>

int main(int argc, char *argv[]){
    const char *fileObj = "perf.bpf.o";
    struct bpf_object *obj;
    struct bpf_program *programPerf;
    int err;

    /* Open and load our eBPF object */
    obj = bpf_object__open_file(fileObj, NULL);
    if (!obj){
        printf("Failed to open the file\n");
        return -1;
    }

    err = bpf_object__load(obj);
    if (err){
        printf("Failed to load object\n");
        return -1;
    }

    /* Retrieving our programs */
    programPerf = bpf_object__find_program_by_name(obj, "kprobe_perf_event");

    if (!programPerf){
        printf("Failed to find program\n");
        bpf_object__close(obj);
        return -1;
    }
    // Example: https://github.com/torvalds/linux/blob/master/samples/bpf/trace_event_user.c#L137
    // https://elinux.org/images/d/dc/Kernel-Analysis-Using-eBPF-Daniel-Thompson-Linaro.pdf

    struct perf_event_attr attr;
    attr.type = PERF_TYPE_HARDWARE;
    attr.size = sizeof(attr);
    attr.freq = 1;
    attr.config = PERF_COUNT_HW_CPU_CYCLES;
    /*attr.disabled = 1;
    attr.exclude_kernel = 1;
    attr.exclude_hv = 1;*/

    //int pfd = sys_perf_event_open(&attr, -1, 0, -1, 0);
    int pfd = syscall(SYS_perf_event_open, &attr, -1, 0, -1, PERF_FLAG_FD_CLOEXEC);
    printf("fd: %d\n", pfd);

    if (pfd < 0){
        bpf_object__close(obj);
        return -1;
    }
    struct bpf_link *link = bpf_program__attach_perf_event(programPerf, pfd);

    bpf_link__destroy(link);
    bpf_object__close(obj);

    return 0;
}

And the Makefile:

GCC=gcc
CL=clang-11
CFLAGS=-Wall
LIBS=-lbpf -lc

all: perf.bpf.o perf

perf.bpf.o: perf.bpf.c
    $(CL) -g -O2 -target bpf -c perf.bpf.c -o perf.bpf.o

perf: perf.c
    $(GCC) $(CFLAGS) perf.c -o perf $(LIBS)

clean:
    rm -rf *.o  && rm perf

XDP

#include <stdio.h>
#include <stdlib.h>
#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <linux/perf_event.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <net/if.h> /* if_nametoindex */

static int running = 1;

static void signalHandler(int signum){
    running = 0;
}

int handle_event(void *ctx, void *data, size_t data_sz){
    return 0;
}
int main(int argc, char *argv[]){
    const char *fileObj = "src/dns-trace.ebpf.o";
    struct bpf_object *obj;
    struct bpf_program *programSkb;
    struct ring_buffer *rb;
    int err;
    int fd_map_data;
    int ifindex;

    arguments = parse_args(argc, argv); // Parsing arguments

    signal(SIGINT, signalHandler);

    /* Open and load our eBPF object */
    obj = bpf_object__open_file(fileObj, NULL);
    if (!obj){
        printf("Failed to open the file\n");
        return -1;
    }

    err = bpf_object__load(obj);
    if (err){
        printf("Failed to load object\n");
        return -1;
    }

    /* Retrieving fd of maps */
    fd_map_data = bpf_object__find_map_fd_by_name(obj, "data");
    if (!fd_map_data){
        printf("Failed to find the fd map data\n");
        bpf_object__close(obj);
        return -1;
    }

    /* Retrieving our programs */
    programSkb = bpf_object__find_program_by_name(obj, "detect_dns");

    if (!programSkb){
        printf("Failed to find program\n");
        bpf_object__close(obj);
        return -1;
    }

    ifindex = if_nametoindex("wlp0s20f3");
    if (ifindex == -1){
        printf("Failed to get the index of the interface\n");
        bpf_object__close(obj);
        return -1;
    }

    if (bpf_program__attach_xdp(programSkb, ifindex) == NULL){
        printf("Failed to attached the XPD prog\n");
        bpf_object__close(obj);
        return -1;
    }

    /* Start the ringbuffer */
    rb = ring_buffer__new(fd_map_data, handle_event, NULL, NULL);
    if (!rb){
        printf("Failed to create the ringbuf\n");
        bpf_object__close(obj);
        return -1;
    }

    while(running){
        err = ring_buffer__poll(rb, 100 /* timeout, ms */);
        if (err == -EINTR){
            printf("Failed to get the ringbuf\n");
            running = 0;
            break;
         }
    }

    ring_buffer__free(rb);
    bpf_object__close(obj);

    return 0;
}

And the program which is loaded into the Kernel space:


#define BPF_NO_GLOBAL_DATA
#define __TARGET_ARCH_x86
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
#include <string.h>

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024 /* 256kb */);
} data SEC(".maps");


int detect_dns(struct xdp_md *ctx){
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    struct iphdr *ip;
    struct udphdr *udp;
    __u16 h_proto;
    __u16 dport;

    if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) > data_end)
        return XDP_DROP;

    ip = (struct iphdr*)(data + sizeof(struct ethhdr));
    udp = (struct udphdr*)(data + sizeof(struct ethhdr) + sizeof(struct iphdr));

    h_proto = ip->protocol;
    // If not UDP packet
    if (h_proto != 17) 
        return XDP_PASS;

    // Check if DNS port
    dport = udp->dest;
    bpf_printk("Dport: %d", dport);

    return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";