ebpf常见挂载点

基本上所有设备都能用的常见挂载点,目前考虑6.1内核,可惜的是pixel 6 的6.1内核还不支持fentry/fexit
未写完,在写了

Kprobe

Kprobe是比较常见的附加到内核函数的方法,kprobe/kretprobe分别负责在进入前和返回前hook内核函数
用法是使用SEC(kprobe/name),直接使用内核函数的签名即可,比如SEC(kprobe/kernel_clone),这个签名可以在/proc/kallsym中获取
获取调用参数有两种方案,最基础的方法是使用struct pt_regs *ctx作为参数,这个ctx结构体是具有架构依赖的,然后使用PT_REGS_PARM*(ctx)宏读取参数,arm64架构中这个可以填1~8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
SEC("kprobe/kernel_clone")
int bpf_prog(struct pt_regs *ctx) {
const u64 CLONE_THREAD = 0x00010000;
struct kernel_clone_args *args_ptr =
(struct kernel_clone_args *)PT_REGS_PARM1(ctx);
struct kernel_clone_args args;
int ret = bpf_probe_read_kernel(&args, sizeof(args), args_ptr);
if (ret != 0) {
bpf_printk("Failed to read kernel_clone_args: %d\n", ret);
return ret;
}
u64 flags = args.flags;
u64 id = bpf_get_current_pid_tgid();
if (args.kthread == 0) {
bpf_printk("new %s created by PID : %d",
flags & CLONE_THREAD ? "thread" : "process", id >> 32);
}
return 0;
}

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

这就是一个比较简单的探测kernel_clone这个函数的kprobe探针,使用PT_REGS_PARM1读取第一个参数(每个具体参数要查阅内核源代码中的定义),使用bpf_probe_read_kernel读取这个指针指向的参数列表结构体(直接解引用会被检查器拒绝加载,因为这是不安全的行为),然后根据flag这个掩码参数简单区分下进程和线程,通过bpf_get_current_pid_tgid获取调用clone的进程pid,使用printk输出到trace_pipe中

可以看到捕捉到了adb服务的活动,和新创建的shell的活动
除了直接声明函数还可以使用BPF_KPROBE这个宏,上述的代码可以写成如下等价形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
SEC("kprobe/kernel_clone")
int BPF_KPROBE(handle_enter_kernel_clone, struct kernel_clone_args *args_ptr) {
const u64 CLONE_THREAD = 0x00010000;
struct kernel_clone_args args;
int ret = bpf_probe_read_kernel(&args, sizeof(args), args_ptr);
if (ret != 0) {
bpf_printk("Failed to read kernel_clone_args: %d\n", ret);
return ret;
}
u64 flags = args.flags;
u64 id = bpf_get_current_pid_tgid();
if (args.kthread == 0) {
bpf_printk("new %s created by PID : %d",
flags & CLONE_THREAD ? "thread" : "process", id >> 32);
}
return 0;
}

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

这个宏的第一个参数是声明的函数名,之后接受最多五个参数,作为前5个传参寄存器中读取的参数(PT_REGS_PARM1~5),写起来会比第一种写法简洁一点,基本所有探针类型都有类似的宏
以及对于这些宏,触发探针时的上下文环境会被以*ctx保存,可以通过ctx访问上下文环境,所以不要在函数中再次使用ctx作为变量名
另外还有一种libbpf提供的语法糖ksyscall,因为linux syscall函数的命名通常遵循一定标准,ksyscall可以根据部分提供的函数名自动选择对应的函数附加(比如ksyscall/openat就会选择一种openat的实现),不过由于syscall通常由多种实现所以这种方法很容易漏掉调用,如果只是想

Uprobe

Uprobe是用来hook用户态函数的探针,原理是把目标地址的指令替换成int3(其他架构上就是对应的中断指令)跳到内核态执行hook逻辑(类似条件断点脚本),但是和调试器相比隐蔽性更强,基本上对应用侧只有扫描自身指令是否被修改这一种检测手法

Tracepoint

Syscall

如果内核开启了CONFIG_FTRACE_SYSCALLS的话就可以使用,使用方法是SEC(tp/syscall/name)宏,大部分手机的原厂镜像应该是不支持的

fentry/fexit

如果内核支持fentry可以使用,和kprobe语法类似,SEC(fentry/name),与kprobe不同的是fentry/fexit对每个内核函数提供了参数结构体,可以直接使用Args->fieldname的方法访问参数

Author

SGSG

Posted on

2025-06-28

Updated on

2025-07-01

Licensed under