作者:Hcamael@知道創宇404實驗室
日期:2022年11月16日
相關閱讀:
在 Android 中開發 eBPF 程序學習總結(一)

在上一章的基礎上深入研究

在上一篇文章中,我自己改了一版BPF程序的代碼bpftest.c,代碼也在上一篇文章中放出來了,但是一個完整的BPF程序,還需要一個用戶態的loader,也就是需要有一個讀取BPF程序給我們數據的程序。

之前也說了,可以使用MAP來進行數據交互,在bpftest.c代碼中bpf_execve_map_update_elem(&key, &event, BPF_ANY);,把event結構體更新到key=1的map中,也就是說,把每個進行syscall調用的程序的pid,gid,還有程序名,更新到MAP中。所以我們需要一個loader,來讀取MAP,從而得到這些信息。

最開始,loader我使用的是android demo代碼中的那個,但是在使用中發現,沒辦法讀取結構體的值,也搜不到相關文章,能搜到示例代碼的value類型都是整型,并且我對android開發也不是很熟悉,所以考慮用C自己寫一個。

通過strace抓取之前這個loader的系統調用:

bpf(BPF_OBJ_GET, {pathname="/sys/fs/bpf/prog_bpftest_tracepoint_raw_syscalls_sys_enter", bpf_fd=0, file_flags=0}, 120) = 3
openat(AT_FDCWD, "/sys/kernel/tracing/events/raw_syscalls/sys_enter/id", O_RDONLY|O_CLOEXEC) = 4
read(4, "21\n", 4096)                   = 3
close(4)                                = 0
perf_event_open({type=PERF_TYPE_TRACEPOINT, size=0, config=21, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 4
ioctl(4, PERF_EVENT_IOC_SET_BPF, 3)     = 0
ioctl(4, PERF_EVENT_IOC_ENABLE, 0)      = 0
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ff104b788) = 0
bpf(BPF_OBJ_GET, {pathname="/sys/fs/bpf/map_bpftest_execve_map", bpf_fd=0, file_flags=0}, 120) = 5
nanosleep({tv_sec=0, tv_nsec=40000000}, NULL) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=5, key=0x7ff104b5f4, value=0x7ff104b5e8}, 120) = 0

通過上面的系統調用,我們就能理清楚,loader程序到底做了哪些工作。

接著我找到了Linux內核中的一個bpf_load.c,參考了一下在普通的Linux系統中,loader是怎么處理的,所以我對該程序進行了修改,增加了以下代碼:

struct androidBPF {
    char *prog_path;
    char *map_path;
    char *tp_category;
    char *tp_name;
};


static int load_prog(char *prog_path)
{
    int pfd;
    pfd = bpf_obj_get(prog_path);
    if (pfd < 0) {
        printf("bpf_prog_load() err=%d\n%s", errno, prog_path);
        return -1;
    }

    return pfd;
}

int attach_tracepoint(char *tp_category, char *tp_name)
{
    char buf[256];
    int efd, err, id;
    struct perf_event_attr attr = {};
    attr.type = PERF_TYPE_TRACEPOINT;
    attr.sample_type = PERF_SAMPLE_RAW;
    attr.sample_period = 1;
    attr.wakeup_events = 1;

    strcpy(buf, DEBUGFS);
    strcat(buf, "events/");
    strcat(buf, tp_category);
    strcat(buf, "/");
    strcat(buf, tp_name);
    strcat(buf, "/id");
    efd = open(buf, O_RDONLY, 0);
    if (efd < 0) {
        printf("failed to open %s\n", buf);
        return -1;
    }
    err = read(efd, buf, sizeof(buf));
    if (err < 0 || err >= sizeof(buf)) {
        printf("read from failed '%s'\n", strerror(errno));
        return -1;
    }
    close(efd);
    buf[err] = 0;
    id = atoi(buf);
    attr.config = id;
    efd = perf_event_open(&attr, -1/*pid*/, 0/*cpu*/, -1/*group_fd*/, 0);
    if (efd < 0) {
        printf("event %d fd %d err %s\n", id, efd, strerror(errno));
        return -1;
    }
    return efd;
}

static int load_map(char *map_path)
{
    int mfd;
    mfd = bpf_obj_get(map_path);
    if (mfd < 0) {
        printf("bpf_map_load() err=%d\n%s", errno, map_path);
        return -1;
    }

    return mfd;
}

int get_map_by_int_key(int *key, void *value)
{
    int mfd, ret;

    mfd = map_fd[prog_cnt-1];
    ret = bpf_lookup_elem(mfd, key, value);
    return ret;
}

int load_bpf_from_fs(struct androidBPF *abpf)
{
    int fd, efd, mfd;
    fd = load_prog(abpf->prog_path);
    if (fd <= 0) {
        printf("[debug] load prog error.\n");
        return fd;
    }
    prog_fd[prog_cnt] = fd;
    efd = attach_tracepoint(abpf->tp_category, abpf->tp_name);
    if (efd <= 0) {
        printf("[debug] attach_tracepoint error.\n");
        return efd;
    }
    event_fd[prog_cnt] = efd;
    ioctl(efd, PERF_EVENT_IOC_ENABLE, 0);
    ioctl(efd, PERF_EVENT_IOC_SET_BPF, fd);
    printf("[debug] load bpf prog success.\n");
    mfd = load_map(abpf->map_path);
    if (mfd <= 0) {
        printf("[debug] load_map error.\n");
        return mfd;
    }
    map_fd[prog_cnt++] = mfd;
    return 0;
}

void read_trace_pipe(int times)
{
    int trace_fd;
    trace_fd = open(DEBUGFS "trace_pipe", O_RDONLY, 0);
    if (trace_fd < 0)
        return;
    // times = 0, loop 0xffffffff
    do {
        static char buf[4096];
        ssize_t sz;
        sz = read(trace_fd, buf, sizeof(buf) - 1);
        if (sz > 0) {
            buf[sz] = 0;
            puts(buf);
        }
    } while (--times);
}

接著,我就能使用C代碼來寫loader了:

#include "bpf_load.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
  struct androidBPF abpf = {0, };
    if (argc < 3)
        return 0;
  abpf.prog_path = argv[1];
  abpf.map_path = argv[2];
  abpf.tp_category = "raw_syscalls";
  abpf.tp_name = "sys_enter";
  if (load_bpf_from_fs(&abpf) != 0) { // 用于加載 ELF 格式的 BPF 程序
    printf("The kernel didn't load the BPF program\n");
    return -1;
  }
  int key, ret;
  key = 1;
  struct event_execv value;
  for (int i = 0; i < 10; i ++) {
    memset(&value, 0, sizeof(value));
    ret = get_map_by_int_key(&key, &value);
    printf("[debug] ret = %d, pid = %d, gid = %d, comm = %s\n", ret, value.pid, value.gid, value.cmd);
  }
  // for (int i=0; i < 88; i++)
  // {
  //   printf("debug value[%d] = 0x%x\n", i, *((char *)&value+i));
  // }
  read_trace_pipe(1);
  return 0;
}

在本地的arm64機器上就能編譯了:

$ ls -alF
total 916
drwxr-xr-x 1 hehe hehe    320 Oct 31 11:54 ./
drwxr-xr-x 1 hehe hehe    416 Oct 30 22:34 ../
-rw-rw-r-- 1 hehe hehe   4025 Oct 30 22:37 bpf_helpers.h
-rw-rw-r-- 1 hehe hehe  10940 Oct 31 11:35 bpf_load.c
-rw-rw-r-- 1 hehe hehe   1112 Oct 31 11:35 bpf_load.h
-rw-rw-r-- 1 hehe hehe   3432 Oct 30 22:49 libbpf.c
-rw-rw-r-- 1 hehe hehe   5294 Oct 30 22:50 libbpf.h
-rw-r--r-- 1 hehe hehe 117176 Oct 30 22:54 libelf.so
-rwxrwxr-x 1 hehe hehe 773016 Oct 31 11:54 loader*
-rw-rw-r-- 1 hehe hehe    868 Oct 31 11:54 loader.c
$ clang loader.c bpf_load.c libbpf.c -lelf -lz -o loader -static

上面的loader只簡單實現了一下讀取map的操作,進階的玩法還可以更新map的數據,比如我只想監控curl程序,那么可以把111=>curl寫入map當中,然后在BPF程序中,從map[111]獲取value,只有當comm == map[111]的情況下,才把信息寫入map當中。

我們重新再來理解一下loader的操作:

  1. BPF_OBJ_GET prog_bpftest_tracepoint_raw_syscalls_sys_enter,獲取prog對象
  2. 讀取SEC定義的section的id,從/sys/kernel/tracing/events/raw_syscalls/sys_enter/id獲取
  3. perf_event_open打開相應時間,因為是tracepoint,所以type要設置為PERF_TYPE_TRACEPOINT,config等于上面獲取id
  4. 打開事件后,獲取了一個文件描述符,對該文件描述符進行ioctl操作,操作的命令有兩個,PERF_EVENT_IOC_SET_BPFPERF_EVENT_IOC_ENABLE,PERF_EVENT_IOC_SET_BPF設置為prog對象的文件描述符

到這里為止,表示激活了你想調用的BPF程序了,要不然默認情況下BPF都處于未激活狀態。

接下來就是對map的操作:

  1. BPF_OBJ_GET /sys/fs/bpf/map_bpftest_execve_map,獲取map對象。
  2. BPF_MAP_LOOKUP_ELEM {map_fd=5, key=0x7ff104b5f4, value=0x7ff104b5e8},從map_fd中搜索key對應的value,儲存在value的指針中返回。

目前這塊的資料太少了,只能通過一些demo和源碼來進行研究,下一篇將會研究uprobe的用法。

參考

  1. https://elixir.bootlin.com/linux/v4.14.2/source/samples/bpf/bpf_load.c

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2009/