Skip to content

Latest commit

 

History

History

2-openat

title date tags
eBPF系列二:例子——openat2
2021-02-12 05:24:10 -0800
linux
tracing tools
eBPF

Blog Post: eBPF系列二: 例子openat2

迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。 本文是eBPF系列的第二篇:例子——openat2。

Introduction

在计算机中运行程序、读写文件,都会涉及到文件的打开操作,Linux v5.10与文件打开相关的系统调用有open() / creat() / openat() / openat2这四类,在使用glibc v2.32时,几乎所有的文件打开操作使用的都是openat2()这个系统调用。

openat2()是POSIX标准定义的系统调用之一,用于文件的创建或打开,它有4个参数,其中第一个参数dirfd为文件夹的描述符,第二个参数pathname为文件路径。

这里实现一个eBPF程序,他能获取系统调用openat()的前两个参数信息。

Instance

这些源码在这里

Example 1

在v5.10版本的内核上,系统调用入口SYSCALL_DEFINE4(openat2...)对参数做了一些简单的检查后,调用的是do_sys_openat2()进行进一步处理,其因此可以使用kprobe hook do_sys_openat2()间接地打印openat2()的参数信息。它的第一、二个参数含义等同于openat2(),因此打印前两个参数信息即可。相关源码主要如下:

SEC("kprobe/do_sys_openat2")
int hello(struct pt_regs *ctx) {
	const int dirfd = PT_REGS_PARM1(ctx);
	const char *pathname = (char *)PT_REGS_PARM2(ctx);
	char fmt[] = "@dirfd='%d' @pathname='%s'";

	bpf_trace_printk(fmt, sizeof(fmt), dirfd, pathname);

	return 0;
}

运行:

$ make hello openat1_kern.o
$ sudo ./hello openat1_kern.o

Example 2

参数pathnamedo_sys_openat2()是个指向用户态程序空间的char类型的指针,若想把文件名复制到eBPF程序中,则需要借助bpf_probe_read_user_str()了:

char msg[256];

bpf_probe_read_user_str(msg, sizeof(msg), pathname);

Internal

Linux区分了不同特权等级下程序可访问的虚拟内存空间范围,它是通过access_ok()检查struct thread_info中的addr_limit来实现的。有一组API {get,set}_fs()可用于在kernel运行时中控制可访问的内存空间范围。

Note:

  1. struct thread_info是CPU架构专属类型,并非每类都有addr_limit,对x86来讲,它是段寄存器FS
  2. set_fs()会引起一些security bugs,因此当前Linux中在尽力去除这组API[1][2]

Example 3

这里写一写怎么直接hook系统调用的入口,即SYSCALL_DEFINE4(openat2...)

SYSCALL_DEFINE4一步步展开如下:

SYSCALL_DEFINE4
--> SYSCALL_DEFINEx
--> SYSCALL_METADATA // syscall tracepoint的封装
    __SYSCALL_DEFINEx

// for x86
__SYSCALL_DEFINEx
--> __X64_SYS_STUBx // amd64使用
    __IA32_SYS_STUBx // ia32使用

// for amd64
__X64_SYS_STUBx
--> __SYS_STUBx(x64, sys##name, SC_X86_64_REGS_TO_ARGS(x, __VA_ARGS__)))
--> long __##abi##_##name(const struct pt_regs *regs)

拼接起来,amd64架构,系统调用openat2()的入口函数名为__x64_sys_openat2(),参数类型是struct pt_regs *。因此eBPF程序这么写:

SEC("kprobe/sys_openat")
int hello(struct pt_regs *ctx) {
	char fmt[] = "@dirfd='%d' @pathname='%s'";
	struct pt_regs *real_regs = (struct pt_regs *)PT_REGS_PARM1(ctx);
	int dirfd = PT_REGS_PARM1_CORE(real_regs);
	char *pathname = (char *)PT_REGS_PARM2_CORE(real_regs);

	bpf_trace_printk(fmt, sizeof(fmt), dirfd, pathname);

	return 0;
}

代码中SEC("kprobe/sys_openat")表示kprobe的hook point为sys_openat,实际上用户态程序hello在调用load_and_attach()时候会检查kprobe的hook point前缀是否是sys_,若是对amd64则自动添加__x64_前缀

macro PT_REGS_PARMx_COREbpf_probe_read_kernel()做了封装,可以简单地认为用于获取hook func的第x个参数。因hook func的参数是struct pt_regs *,所以需要使用bpf_probe_read_kernel()取得struct pt_regs,进而获取得到系统调用SYSCALL_DEFINE4(openat2...)所示的参数信息。

Example 4

Linux内部API经常变更,使用kprobe hook特定的函数名不具有普适性。Linux为系统调用提供了tracepoint,若用tracepoint例子则这么写:

struct syscalls_enter_openat_args {
	unsigned short common_type;
	unsigned char common_flags;
	unsigned char common_preempt_count;
	int common_pid;
	long syscall_nr;
	long dfd;
	long filename_ptr;
	long flags;
	long mode;
};

SEC("tracepoint/syscalls/sys_enter_openat")
int hello(struct syscalls_enter_openat_args *ctx) {
	char fmt[] = "@dirfd='%d' @pathname='%s'";

	bpf_trace_printk(fmt, sizeof(fmt), ctx->dfd, (char *)ctx->filename_ptr);

	return 0;
}

struct syscalls_enter_openat_args成员信息来自tracefs中的文件events/syscalls/sys_enter_openat2/format

Reference