简介
eBPF 是 Linux 内核提供的一种动态 Hook 机制,可以将自定义代码逻辑 Hook 到指定内核事件上,当内核触发该事件时就会调用相应的自定义代码逻辑。
Linux上常见的使用 eBPF 开发的程序:
top或ps:允许您查看进程及其在 CPU 和 RAM 方面占用的内容tcpdump:用于监视服务器上的网络流strace:它允许您查看进程的系统调用
除此之外 Linux 的 systemd(初始化系统)也是使用 eBPF 开发的。
eBPF 原理
在传统程序中,我们有一个源代码。从这个源代码中,我们创建程序,如果它是编译语言,则对其进行编译:

在 eBPF 中,不是一个程序,而是两个程序:
- 将在用户空间中编译的 eBPF 代码,但只能在内核空间中运行;
- 一个用户空间代码,主要加载刚刚编译到内核中的 eBPF 二进制文件。

我们稍后会看到,可以在内核空间和用户空间之间进行通信(通过 eBPF 映射),也可以在内核空间 eBPF 程序之间进行通信(通过尾部调用)。

从上面可以看出,一个完整的 eBPF 程序通常分为用户态和内核态两部分:用户态负责 eBPF 程序的加载、事件绑定以及 eBPF 程序运行结果的汇总输出;内核态运行在 eBPF 虚拟机中,负责定制和控制系统的运行状态;内核态的 eBPF 程序通过 BPF 映射(Map)用户态程序交互

bpf 系统调用:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
cmd: 操作命令,比如BPF_PROG_LOAD就是加载 eBPF 程序。attr: 操作命令对应的属性。size: 属性的大小。
eBPF 程序开发
内核态程序
内核态的 eBPF 程序在内核视角就是 eBPF 字节码,可以用 C 或 Rust 编译而来,分以下几种方案:
• libbpf:
- • 开发语言:C
- • 编译依赖:libbpf (用到为编译器提供的宏,如 SEC)。
- • 运行依赖:无
- • 内核要求:内核开启 BTF 特性,需要非常较新的发行版才会默认开启(如 RHEL 8.2+ 和 Ubuntu 20.10+ 等)。
• bcc:
- • 开发语言:C
- • 编译/运行依赖:bcc、LLVM、内核头文件(bcc 方案是目标机器上进行编译并运行的)
• Aya:
- • 开发语言:Rust
- • 编译依赖:Rust 环境、Aya
- • 运行依赖:无
eBPF 框架有两种类型:
- 那些使用两种不同编程语言的语言:一种用于内核空间,另一种用于用户空间;如 ebpf go;
- 那些在内核和用户空间中使用同一语言的语言,如 Aya。
常用程序类型:
| 类型 | 挂载点 | 用途 |
|---|---|---|
kprobe/kretprobe | 内核函数入口/返回 | 内核动态追踪 |
uprobe/uretprobe | 用户函数入口/返回 | 用户态程序追踪 |
tracepoint | 内核预定义追踪点 | 稳定的内核追踪 |
xdp | 网络驱动层 | 高性能包处理 |
socket_filter | 网络套接字 | 包过滤 |
cgroup_* | cgroup 钩子 | 容器资源控制 |
程序类型选择:
// 内核探针
SEC("kprobe/do_nanosleep")
SEC("kretprobe/do_nanosleep")
// 用户态探针
SEC("uprobe//lib/x86_64-linux-gnu/libc.so.6:malloc")
SEC("uretprobe//lib/x86_64-linux-gnu/libc.so.6:malloc")
// 追踪点
SEC("tracepoint/syscalls/sys_enter_openat")
// XDP
SEC("xdp")
用户态程序
用户态程序主要负责将内核态 eBPF 程序加载到内核并运行,然后通过 BPF 映射读取内核态 eBPF 程序输出的数据,最后做相应的业务逻辑处理,它可以用任何语言编写,下面列举了一些常用的方案:
- ebpf-go:
- 开发语言:go
- 编译依赖:go、cilium/ebpf
- 运行依赖:无
- Libbpf:
- 开发语言:C/C++
- 编译依赖:需要 clang / LLVM(运行时编译 eBPF 程序)
- 运行依赖:支持 eBPF 的 Linux 内核 + libbpf(用户态库)+ 内核 BTF(CO-RE 场景下)
- Aya:
- 开发语言:Rust
- 编译依赖:Rust 环境、Aya
- 运行依赖:无

Maps
- 映射类型
// 哈希表 - 通用键值存储
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, u64);
} hash_map SEC(".maps");
// 数组 - 索引存储
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 256);
__type(key, u32);
__type(value, u64);
} array_map SEC(".maps");
// 环形缓冲区 - 事件传输(推荐)
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16MB
} rb_map SEC(".maps");
// Per-CPU 映射 - 减少锁竞争
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, u64);
} percpu_map SEC(".maps");
// LRU 哈希 - 自动淘汰
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, u64);
} lru_map SEC(".maps");
- 常用映射操作
// 查找
void *val = bpf_map_lookup_elem(&map, &key);
if (val) {
// 找到时的处理
}
// 更新
u64 new_val = 100;
bpf_map_update_elem(&map, &key, &new_val, BPF_ANY);
// flags: BPF_ANY(存在则更新), BPF_NOEXIST(不存在则创建), BPF_EXIST(必须存在)
// 删除
bpf_map_delete_elem(&map, &key);
// 遍历(用户态使用 bpftool)
// bpftool map dump id <map_id>
辅助函数
// 获取上下文信息
u32 pid = bpf_get_current_pid_tgid() >> 32; // 获取 PID
u32 uid = bpf_get_current_uid_gid(); // 获取 UID
bpf_get_current_comm(&comm, sizeof(comm)); // 获取进程名
// 时间函数
u64 ts = bpf_ktime_get_ns(); // 单调时钟(纳秒)
u64 ts_real = bpf_ktime_get_real_ns(); // 挂钟时间
// 内存操作
bpf_probe_read_user(&buf, sizeof(buf), user_ptr); // 读用户内存
bpf_probe_read_kernel(&buf, sizeof(buf), kernel_ptr); // 读内核内存
bpf_probe_read_user_str(str, sizeof(str), user_str); // 读用户字符串
bpf_probe_read_kernel_str(str, sizeof(str), kernel_str);
// 映射操作
bpf_map_lookup_elem()
bpf_map_update_elem()
bpf_map_delete_elem()
// 事件输出
bpf_ringbuf_output() // 向 ring buffer 输出
bpf_ringbuf_submit() // 提交 ring buffer 条目
bpf_perf_event_output() // 向 perf buffer 输出(传统方式)
// 调试
bpf_printk(fmt, ...) // 输出到 /sys/kernel/debug/tracing/trace_pipe
开发实例
接下来我们开发一个简单的 demo,通过 APP 的 UID 进行过滤,跟踪指定 APP 调用了哪些 SO 库。实现思路呢就是在android_dlopen_ext和dlopen函数入口挂载 uprobe,在目标 APP 调用动态库加载接口时读取第一个参数中的 so 路径,并通过 eBPF ringbuf 将事件上报到用户态。用户态 Go 程序按指定 UID 过滤事件,最终输出进程名以及被加载的 so 路径。

环境配置
开发 eBPF 程序需要手机的内核版本高于 5.10。
go mod init hello
go get github.com/cilium/ebpf@v0.17.1
sudo apt update
sudo apt install clang llvm libelf-dev gcc make git
- 下载 bpf-tools:https://github.com/libbpf/bpftool/releases
# 推送到设备
adb push bpftool /data/local/tmp/
adb shell chmod +x /data/local/tmp/bpftool
- 获取内核符号
#进入 Android 设备
adb shell
su
cd /data/local/tmp
./bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
adb pull /data/local/tmp/vmlinux.h ./
内核态程序
内核态程序只能通过 C 语言编写,只能使用内核导出的函数或内核 bpf 库中的函数。
- hooker.c
//go:build ignore
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
char _license[] SEC("license") = "GPL";
#define MAX_SO_PATH 256
#define ARM64_PT_REGS_X0_OFFSET 0
const volatile __u32 target_uid = 0;
struct event {
__u64 ts_ns;
__u32 pid;
__u32 tid;
__u32 uid;
__s32 path_len;
char comm[16];
char so_path[MAX_SO_PATH];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
static __always_inline bool should_trace_uid(void)
{
__u32 uid = (__u32)(bpf_get_current_uid_gid() & 0xffffffff);
if (target_uid == 0)
return true;
return uid == target_uid;
}
static __always_inline const char *arm64_arg0_from_pt_regs(struct pt_regs *ctx)
{
__u64 arg0 = 0;
bpf_probe_read_kernel(&arg0, sizeof(arg0), (const void *)ctx + ARM64_PT_REGS_X0_OFFSET);
return (const char *)arg0;
}
static __always_inline int submit_dlopen_event(struct pt_regs *ctx)
{
const char *path;
__u64 pid_tgid;
struct event *evt;
long len;
if (!should_trace_uid())
return 0;
/* Android arm64 ABI: dlopen/android_dlopen_ext 第一个参数在 x0。 */
path = arm64_arg0_from_pt_regs(ctx);
if (!path)
return 0;
evt = bpf_ringbuf_reserve(&events, sizeof(*evt), 0);
if (!evt)
return 0;
len = bpf_probe_read_user_str(evt->so_path, sizeof(evt->so_path), path);
if (len <= 1) {
bpf_ringbuf_discard(evt, 0);
return 0;
}
pid_tgid = bpf_get_current_pid_tgid();
evt->ts_ns = bpf_ktime_get_ns();
evt->pid = pid_tgid >> 32;
evt->tid = (__u32)pid_tgid;
evt->uid = (__u32)(bpf_get_current_uid_gid() & 0xffffffff);
evt->path_len = (__s32)len;
bpf_get_current_comm(&evt->comm, sizeof(evt->comm));
bpf_ringbuf_submit(evt, 0);
return 0;
}
SEC("uprobe/trace_so_enter")
int trace_so_enter(struct pt_regs *ctx)
{
return submit_dlopen_event(ctx);
}
用户态程序
- main.go
package main
//go:generate sh -c "go run github.com/cilium/ebpf/cmd/bpf2go -target bpf hooker hooker.c && llvm-objcopy --remove-section=.BTF.ext --remove-section=.rel.BTF.ext hooker_bpf.o"
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
"golang.org/x/sys/unix"
)
const maxSOPath = 256
var linkerPaths = []string{
"/apex/com.android.runtime/bin/linker64",
"/system/bin/linker64",
}
var dlopenSymbols = []string{
"android_dlopen_ext",
"dlopen",
"__loader_android_dlopen_ext",
"__loader_dlopen",
}
// event 必须和 hooker.c 里的 struct event 字段顺序、大小保持一致。
type event struct {
TsNS uint64
Pid uint32
Tid uint32
UID uint32
PathLen int32
Comm [16]byte
SOPath [maxSOPath]byte
}
func main() {
uid, err := parseUID(os.Args)
if err != nil {
log.Fatal(err)
}
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal("remove memlock: ", err)
}
spec, err := loadHooker()
if err != nil {
log.Fatal("load bpf spec: ", err)
}
patchKernelVersion(spec)
if err := spec.RewriteConstants(map[string]interface{}{"target_uid": uid}); err != nil {
log.Fatal("rewrite target_uid: ", err)
}
var objs hookerObjects
if err := spec.LoadAndAssign(&objs, nil); err != nil {
log.Fatal("load eBPF objects: ", err)
}
defer objs.Close()
linkerPath, exe, err := openFirstLinker()
if err != nil {
log.Fatal("open linker: ", err)
}
var links []link.Link
for _, symbol := range dlopenSymbols {
up, err := exe.Uprobe(symbol, objs.TraceSoEnter, nil)
if err != nil {
log.Printf("skip %s: %v", symbol, err)
continue
}
links = append(links, up)
}
defer closeLinks(links)
if len(links) == 0 {
log.Fatal("no dlopen symbol attached")
}
reader, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatal("open ringbuf: ", err)
}
defer reader.Close()
log.Printf("trace uid=%d linker=%s", uid, linkerPath)
for {
record, err := reader.Read()
if err != nil {
log.Fatal("read ringbuf: ", err)
}
var evt event
if err := binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &evt); err != nil {
log.Printf("decode event: %v", err)
continue
}
fmt.Printf(
"ts=%s uid=%d pid=%d tid=%d comm=%s so=%s\n",
time.Unix(0, int64(evt.TsNS)).Format(time.RFC3339Nano),
evt.UID,
evt.Pid,
evt.Tid,
cString(evt.Comm[:]),
cString(evt.SOPath[:]),
)
}
}
func patchKernelVersion(spec *ebpf.CollectionSpec) {
var uts unix.Utsname
if err := unix.Uname(&uts); err != nil {
return
}
var major, minor, patch uint32
if n, _ := fmt.Sscanf(unix.ByteSliceToString(uts.Release[:]), "%d.%d.%d", &major, &minor, &patch); n < 2 {
return
}
if patch > 255 {
patch = 255
}
version := major<<16 | minor<<8 | patch
for _, prog := range spec.Programs {
if prog.Type == ebpf.Kprobe {
prog.KernelVersion = version
}
}
}
func parseUID(args []string) (uint32, error) {
if len(args) != 3 || args[1] != "-uid" {
return 0, fmt.Errorf("usage: %s -uid <android_app_uid>", args[0])
}
uid, err := strconv.ParseUint(args[2], 10, 32)
if err != nil || uid == 0 {
return 0, fmt.Errorf("invalid uid: %s", args[2])
}
return uint32(uid), nil
}
func openFirstLinker() (string, *link.Executable, error) {
var errs []string
for _, path := range linkerPaths {
exe, err := link.OpenExecutable(path)
if err == nil {
return path, exe, nil
}
errs = append(errs, path+": "+err.Error())
}
return "", nil, errors.New(strings.Join(errs, "; "))
}
func cString(raw []byte) string {
if idx := bytes.IndexByte(raw, 0); idx >= 0 {
raw = raw[:idx]
}
return string(raw)
}
func closeLinks(links []link.Link) {
for _, l := range links {
_ = l.Close()
}
}
编译构建
go generate
GOOS=android \
GOARCH=arm64 \
go build
测试程序
推送到目标设备:
adb push hooker /data/local/tmp/hooker
adb shell chmod 755 /data/local/tmp/hooker
查看目标 APP UID,然后指定 UID 运行程序,比如我们分析 B 站。
❯ adb shell ps | grep bili
u0_a340 11532 1163 38003560 532988 0 0 S tv.danmaku.bili
u0_a340 12014 1163 18727748 292360 0 0 S tv.danmaku.bili:ijkservice
u0_a340 12849 1163 35741940 340092 0 0 S tv.danmaku.bili:download
u0_a340 13027 1163 250286840 441840 0 0 S tv.danmaku.bili:web
Android 系统都是从 10000 开始分配 UID,所以 B 站的 UID是 10340,然后我们运行程序。
adb shell su -c '/data/local/tmp/hooker -uid 10340'
然后打开目标程序:

参考
eBPF - 简介、教程和社区资源 【第拾壹期 REVERSE 分享会】驱动逆向 & Ebpf & 某实战逆向 & Angr & 自定义ROM & Frida