使用eBPF在Android上进行插桩

ImKK 发布于 2024-04-25 412 次阅读


AI 摘要

这篇文章介绍了如何在Android系统上使用eBPF进行插桩。eBPF是一项革命性的技术,源自于Linux内核,可以在特权上下文中运行沙盒程序,用于安全有效地扩展内核功能,而无需修改内核源代码或加载内核模块。在内核层,常用的数据传输框架有perf和ringbuffer,其中ringbuffer是专门为eBPF定制的框架,体验性更好。文章还介绍了kprobe的使用方式,kprobe可以理解为在内核进行插桩的方式,有两种形式:kprobe和kretprobe。此外,用户层示例展示了如何在Go语言中使用eBPF。

eBPF 是一项革命性的技术,起源于 Linux 内核,它可以在特权上下文中(如操作系统内核)运行沙盒程序。它用于安全有效地扩展内核的功能,而无需通过更改内核源代码或加载内核模块的方式来实现。

eBPF中内核和用户层之间的数据传输常用的框架有两种,分别是perfringbuffer,前者是从kernel module而来的,而后者是专门为eBPF定制的,体验性更好,所有一般都使用后者  

在内核层,常规用法为首先使用bpf_ringbuf_reserve申请一个buffer,然后调用bpf_ringbuf_submit提交数据到缓冲区,更详细的可以参考文档https://www.kernel.org/doc/html/next/bpf/ringbuf.html

以下为kprobe使用方式,kprobe可以简单理解为在内核插桩,目前有两种形式,分别是kprobekretprobe,前者是在函数开始处插桩,后者则是在函数返回之前插桩,以下为内核层例子

#include "vmlinux.h"

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

struct file_data {
    u32 uid;
    u8 filename[256];
};

struct event {
    struct file_data file;
};

struct {
    __uint(type,BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries,1 << 24);
} events SEC(".maps");

const struct event *unused __attribute__((unused));

SEC("kprobe/do_sys_openat2")
int kprobe_openat(struct pt_regs *ctx)
{
    u32 uid;
    struct event *openat2data;
    char *fp = (char *)(ctx->si);

    uid = bpf_get_current_uid_gid();

    openat2data = bpf_ringbuf_reserve(&events,sizeof(struct event),0);
    if(!openat2data)
    {
        return 0;
    }
    long res = bpf_probe_read_user_str(&openat2data->file.filename,256,fp);
    bpf_printk("uid: %d, filename: %s",uid,openat2data->file.filename);
    openat2data->file.uid = uid;
    bpf_ringbuf_submit(openat2data,0);

    return 0;
}

用户层示例:

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
    "errors"
    "bytes"
    "encoding/binary"
    "fmt"

    //"github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
    "github.com/cilium/ebpf/ringbuf"
    "golang.org/x/sys/unix"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags "linux" -type event --target=amd64 bpf blog.c -- -I./headers

func main() {
    stopper := make(chan os.Signal,1)
    signal.Notify(stopper,os.Interrupt,syscall.SIGTERM)

    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err);
    }

    objs := bpfObjects{}
    if err := loadBpfObjects(&objs,nil); err != nil {
        log.Fatal(err);
    }
    defer objs.Close()

    se, err := link.Kprobe("do_sys_openat2",objs.KprobeOpenat,nil)
    if err != nil {
        log.Fatal(err)
    }
    defer se.Close()

    rd, err := ringbuf.NewReader(objs.Events)
    if err != nil {
        log.Fatal(err)
    }
    defer rd.Close()

    go func() {
        <-stopper

        if err := rd.Close(); err != nil {
            log.Fatal(err)
        }
    }()

    log.Println("Waiting for Data")

    var event bpfEvent

    for {
        record, err := rd.Read()
        if err != nil {
            if errors.Is(err,ringbuf.ErrClosed) {
                log.Println("Received signal, exiting...")
                return
            }
            log.Fatal(err)
            continue
        }
        if err := binary.Read(bytes.NewBuffer(record.RawSample),binary.LittleEndian,&event); err != nil {
            log.Fatal(err)
            continue
        }
        fmt.Printf("[%+v]: filename -> %s\n",
                   event.File.Uid,
                   unix.ByteSliceToString(event.File.Filename[:]))
    }
}

效果如下: