本文地址:https://www.ebpf.top/post/bpf_capabilities_debug

作者:kira skyler

前言

在 Linux 操作系统中," 权能 " (capabilities) 是一种权限机制,用于 linux 系统中的全部特权,细粒度地划分为多个独立的权限位。这样,用户或进程可以仅被授予执行特定任务所需的特定权限,而不需要获取权限的全部。

在 Linux 权能系统中,权限分配分为多个不同的集合,如 继承权能(Inheritable set)、允许权能(Permitted set)、有效权能(Effective set)、绑定权能(Bounding set) 和 环境权能(Ambient set)。每个集合都控制进程或线程在不同情况下的权限。这些权能可能在不同的情况下会变化,如切换用户,新的用户很可能拥有不同的权能能力集合,如进程创建子进程、执行新的程序,不同的权能集合都会按照不同的规则变化。

示例: 授予用户 cap_chown 权能,该权能可以改变文件的属主,例如,只有拥有此权能的用户可以将系统中的文件所有者随意指定为其他用户或用户组

笔者曾经在排查公司定制操作系统中遇到一个问题正是和权能有关,运维人员反映 root 无法使用 tcpdunmp,报错 tcpdump: Couldn't change ownership of savefile

命令行中使用 tcpdump,确实会出现错误:

1
2
# tcpdump -i ens32 -w a.pcap
tcpdump: Couldn't change ownership of savefile

先使用 strace 看一下 tcpdump 的执行哪一步报错了,是系统调用 chown 返回的错误被拒绝改变用户属主,72 是笔者操作系统中 tcpdump 用户的 uid 和 gid,原来 tcpdump 在指定输出到文件中时会先改变这个文件的属主。

1
2
3
4
5
6
strace tcpdump -i ens32 -w a.pcap
......
chown("a.pcap", 72, 72)                 = -1 EPERM ( 不允许的操作 )
write(2, "tcpdump: ", 9tcpdump: )                = 9
write(2, "Couldn't change ownership of sav"..., 37Couldn't change ownership of savefile) = 37
......

对于系统调用返回的异常,笔者经常使用 ftrace 跟踪内核的调用路径,跟着调用路径,找到内核的这个这个函数返回的 EPERM,使用 ftrace 跟踪找到此处就是另外一个话题了,这也是笔者第一次遇到权能的问题。

1
2
3
4
5
6
bool capable_wrt_inode_uidgid(const struct inode *inode, int cap)
{
	struct user_namespace *ns = current_user_ns();

	return ns_capable(ns, cap) && privileged_wrt_inode_uidgid(ns, inode);
}

经过搜索引擎搜索,发现当前终端的权能确实没有 cap_chown,再经过搜索,确实这个系统中被定制了 root 没有 cap_chown 权能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@localhost ~]# capsh --print
Current: =ep cap_fowner,cap_audit_control-ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fsetid,cap_kill,
cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,
cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,
cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,
cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,
cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,
cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,
cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,
cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,
cap_bpf,cap_checkpoint_restore
Ambient set =
Current IAB: !cap_chown,!cap_fowner,!cap_audit_control
Securebits: 00/0x0/1'b0 (no-new-privs=0)
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=0(root) euid=0(root)
gid=0(root)
groups=0(root)
Guessed mode: UNCERTAIN (0)
1
2
# cat /etc/security/capability.conf
!cap_chown,!cap_fowner,!cap_audit_control root

借助 eBPF 大展身手 追踪权能变化

当想要追踪进程的权能是如何变化,如何传递的时候,传统工具几乎没有办法做到。已知权能的修改一定是通过系统调用触发的,无论是 execve 执行别的程序,还是 setuid 切换用户的时候,毕竟 linux 中应用层与内核的交互几乎都是系统调用。

如果使用 ptrace 开发一个类似于 strace 这样的工具跟踪所有进程的系统调用,恐怕机器性能就几乎没法使用了,因为 ptrace 的性能极差,会导致程序几十倍上百倍的慢,而且还需要经常读取 /proc 内的属性才能获取进程的权能。而且还有一个 securebits 这样的进程属性在笔者的 5.10 内核中通过 /proc/pid/status 是无法获取的,ptrace 也无法获取这个进程属性的变化。

eBPF 的极大优势包括:

可以跟踪所有的系统调用,通过跟踪 tracepoint/raw_syscalls/sys_entertracepoint/raw_syscalls/sys_exit 两个系统调用原始跟踪点即可直接跟踪所有的系统调用,不需要一个一个手写有哪些系统调用也可以防止内核版本不同系统调用改变。而且,相比于 ptrace 性能损耗极低。

可以在 eBPF 程序中获得当前进程的 task_struct,进程几乎所有的信息都在这个结构体里,几乎就是达摩克斯之剑。这样就可以实时获得进程的权能信息和 securebits

先看一下 cap.bpf.h,首先是 s_filter 结构体用于在跟踪时候可以过滤 piduid,接下来是定义多个系统调用的参数,这些参数将会在进入系统调用时候收集,最后是 s_event,这个结构体将会把收集到的信息传递给内用户层,包括进程的基本属性,用户态的栈,栈的信息将会展示代码是如何调用到这里的,cap_beforecap_before 权能在系统调用前后的变化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#ifndef __CAP_BPF_H_
#define __CAP_BPF_H_

#ifdef __cplusplus
extern "C" {
#endif

struct s_filter {
    pid_t pid;
    __s64 uid;
};

struct s_event_clone {
    pid_t child_pid;            // for fork, if execve child_pid = pid
};

struct s_event_execve {
    char filename[50];
};

struct s_event_capset {
    struct __user_cap_header_struct hdrp;
    struct __user_cap_data_struct datap;
};

struct s_event_setuid {
    uid_t uid;
    uid_t ruid;
    uid_t euid;
    uid_t suid;
};

struct s_event_setgid {
    gid_t pid;
    gid_t gid;
    gid_t rgid;
    gid_t egid;
    gid_t sgid;
    gid_t pgid;
};

struct s_event_prctl {
    int option;
    int arg2;
    int arg3;
    int arg4;
    int arg5;
};

struct s_cap {
    unsigned int securebits;
	__u64 cap_inheritable;
	__u64 cap_permitted;
	__u64 cap_effective;
	__u64 cap_bset;
	__u64 cap_ambient;
};

struct s_event {
    __u64 nsec;     // kerner nsec, bpf event 不保证按时间顺序传递
    int nr;         // syscall nr, -1 表示进程退出
    int retvel;
    pid_t pid;
    pid_t tgid;
    pid_t ppid;
    uid_t uid;
    char comm[20];

    __u64 ustack_sz;
	__u64 ustack[20];

    struct s_cap cap_before;
    struct s_cap cap_after;

    union {
        struct s_event_clone clone;
        struct s_event_capset capset;
        struct s_event_setuid setuid;
        struct s_event_setgid setgid;
        struct s_event_execve execve;
        struct s_event_prctl prctl;
    }data;
};

#ifdef __cplusplus
}
#endif
#endif

再看一下 cap.bpf.c 的 libbpf-core 的代码部分,首先是定义过滤的 pid 和 uid 均默认为 -1 表示不过滤,如果需要过滤将在用户层代码直接修改这两个值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "vmlinux.h"
#include "cap.bpf.h"
#include <asm/unistd_64.h>

#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>

struct s_filter filter = {
	.pid = -1,
	.uid = -1,
};

接下来是两个 perf map 结构,events 是 bpf 向用户层发送消息的结构,rsyscall_enter 是系统调用前后收集信息的结构,数量最大为 10000 表示最大可以最终 10000 个进程的权能变化,当桌面启动时候会有数千个进程执行,设定为 10000 是有必要的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct {
	__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
	__uint(key_size, sizeof(u32));
	__uint(value_size, sizeof(u32));
}events SEC(".maps");

struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__type(key, pid_t);
	__type(value, struct s_event);
	__uint(max_entries, 10000);
}rsyscall_enter SEC(".maps");

在系统调用进入的时候,首先收集 task_struct 的信息,过滤 pid 和 uid,接下来便是收集用户态调用的栈,收集权能信息,收集系统调用的参数,然后只保存到 rsyscall_entermap 中。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
__always_inline static void save_cred(struct task_struct *task, struct s_cap* cap) {
	const struct cred* real_cred = BPF_CORE_READ(task, real_cred);

	cap->securebits = BPF_CORE_READ(real_cred, securebits);

	kernel_cap_t kcap;

	kcap = BPF_CORE_READ(real_cred, cap_inheritable);
	cap->cap_inheritable = ((u64)kcap.cap[1] << 32) + kcap.cap[0];

	kcap = BPF_CORE_READ(real_cred, cap_permitted);
	cap->cap_permitted = ((u64)kcap.cap[1] << 32) + kcap.cap[0];

	kcap = BPF_CORE_READ(real_cred, cap_effective);
	cap->cap_effective = ((u64)kcap.cap[1] << 32) + kcap.cap[0];

	kcap = BPF_CORE_READ(real_cred, cap_bset);
	cap->cap_bset = ((u64)kcap.cap[1] << 32) + kcap.cap[0];

	kcap = BPF_CORE_READ(real_cred, cap_ambient);
	cap->cap_ambient = ((u64)kcap.cap[1] << 32) + kcap.cap[0];
}

SEC("tracepoint/raw_syscalls/sys_enter")
int trace_raw_syscall_sys_enter(struct trace_event_raw_sys_enter *ctx) {
	struct task_struct *task = (struct task_struct *)bpf_get_current_task();
	pid_t pid = BPF_CORE_READ(task, pid);
	pid_t tgid = BPF_CORE_READ(task, tgid);

	uid_t uid = bpf_get_current_uid_gid() >> 32;

	if (filter.pid > 0 && filter.pid != tgid)
		return 0;
	if (filter.uid > 0 && filter.uid != uid)
		return 0;

	pid_t ppid = BPF_CORE_READ(task, real_parent, pid);

	struct s_event event = {
		.nsec =  bpf_ktime_get_ns(),
		.nr = ctx->id,
		.pid = pid,
		.tgid = tgid,
		.ppid = ppid,
		.uid = uid,
	};

	bpf_get_current_comm(event.comm, sizeof(event.comm));
	event.ustack_sz = bpf_get_stack(ctx, event.ustack, sizeof(event.ustack), BPF_F_USER_STACK);

	save_cred(task, &event.cap_before);

	/* 根据系统调用 收集信息 */
	if (ctx->id == __NR_capset) {
		bpf_probe_read_user(&event.data.capset.hdrp, sizeof(struct __user_cap_header_struct), (void*)ctx->args[0]);
		bpf_probe_read_user(&event.data.capset.datap, sizeof(struct __user_cap_data_struct), (void*)ctx->args[1]);
	}
	else if (ctx->id == __NR_execve || ctx->id == __NR_execveat) {
		bpf_probe_read_user(&event.data.execve.filename, sizeof(event.data.execve.filename), (void*)ctx->args[0]);
	}
	else if (ctx->id == __NR_setuid || ctx->id == __NR_setfsuid) {
		event.data.setuid.uid = ctx->args[0];
	}
	else if (ctx->id == __NR_setreuid) {
		event.data.setuid.ruid = ctx->args[0];
		event.data.setuid.euid = ctx->args[1];
	}
	else if (ctx->id == __NR_setresuid) {
		event.data.setuid.ruid = ctx->args[0];
		event.data.setuid.euid = ctx->args[1];
		event.data.setuid.suid = ctx->args[2];
	}
	else if (ctx->id == __NR_setgid || ctx->id == __NR_setfsgid) {
		event.data.setgid.gid = ctx->args[0];
	}
	else if (ctx->id == __NR_setpgid) {
		event.data.setgid.pid = ctx->args[0];
		event.data.setgid.pgid = ctx->args[1];
	}
	else if (ctx->id == __NR_setregid) {
		event.data.setgid.rgid = ctx->args[0];
		event.data.setgid.egid = ctx->args[1];
	}
	else if (ctx->id == __NR_setresgid) {
		event.data.setgid.rgid = ctx->args[0];
		event.data.setgid.egid = ctx->args[1];
		event.data.setgid.sgid = ctx->args[2];
	}
	else if (ctx->id == __NR_clone) {
		event.data.clone.child_pid = ctx->args[0];
	}
	else if (ctx->id == __NR_prctl) {
		event.data.prctl.option = ctx->args[0];
		event.data.prctl.arg2 = ctx->args[1];
		event.data.prctl.arg3 = ctx->args[2];
		event.data.prctl.arg4 = ctx->args[3];
		event.data.prctl.arg5 = ctx->args[4];
	}

	// 第一次跟踪到的输出
	// if (bpf_map_lookup_elem(&rsyscall_enter, &pid) == NULL)
	// 	bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));

	bpf_map_update_elem(&rsyscall_enter, &pid, &event, BPF_ANY);

	return 0;
}

在系统返回时,再处理一次过滤和收集信息后就可以只过滤权能变化的消息传递给用户层了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
SEC("tracepoint/raw_syscalls/sys_exit")
int trace_raw_syscall_sys_exit(struct trace_event_raw_sys_exit *ctx) {
	struct task_struct *task = (struct task_struct *)bpf_get_current_task();
	pid_t pid = BPF_CORE_READ(task, pid);
	uid_t uid = bpf_get_current_uid_gid() >> 32;

	if (filter.pid > 0 && filter.pid != pid)
		return 0;
	if (filter.uid > 0 && filter.uid != uid)
		return 0;

	struct s_event* event = bpf_map_lookup_elem(&rsyscall_enter, &pid);
	if (event == NULL)
		return 0;

	pid_t ppid = BPF_CORE_READ(task, real_parent, pid);
	pid_t tgid = BPF_CORE_READ(task, tgid);
	event->ppid = ppid;
	event->tgid = tgid;

	struct s_cap new_cap;
	save_cred(task, &new_cap);

	if (new_cap.cap_inheritable != event->cap_before.cap_inheritable
		|| new_cap.cap_permitted != event->cap_before.cap_permitted
		|| new_cap.cap_effective != event->cap_before.cap_effective
		|| new_cap.cap_bset != event->cap_before.cap_bset
		|| new_cap.cap_ambient != event->cap_before.cap_ambient
		|| new_cap.securebits != event->cap_before.securebits)
	{
		event->cap_after = new_cap;
		event->nsec = bpf_ktime_get_ns();
		bpf_map_update_elem(&rsyscall_enter, &pid, event, BPF_EXIST);

		event->retvel = ctx->ret;
		bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event, sizeof(*event));
	}

	return 0;
}

在用户层部分只需要打印出权能的变化,调用 bcc 的符号解析解析栈信息即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void perf_buffer_sample(void *ctx, int cpu, void *data, __u32 size) {
    static BCCUstackResolver bcc_resolver;

    struct s_event* event = (struct s_event*)data;

    /**** 权能改变 输出 *****/
    struct tm timeinfo;
    long nsec_part;

    nsec_to_hms(event->nsec, &timeinfo, &nsec_part);

    // 打印时、分、秒和纳秒部分
    printf(ANSI_GREEN"Time: %02d:%02d:%02d.%09ld",
        timeinfo.tm_hour,
        timeinfo.tm_min,
        timeinfo.tm_sec,
        nsec_part);

    printf(", uid = %d, pid: %d, tgid: %d, ppid: %d, comm: %s\n", event->uid, event->pid, event->tgid, event->ppid, event->comm);
    print_event(event);
    printf(ANSI_RESET);
    print_cap(&event->cap_before);
    printf("----------------------------------\n");
    print_cap_diff(&event->cap_before, &event->cap_after);

    if (event->ustack_sz > 0) {
        printf("----------------------------------\n");
        std::cout << bcc_resolver.resolve_process(std::vector<uint64_t>(event->ustack, event->ustack + (event->ustack_sz / 8)),
                                            event->pid, true, true)
                << std::endl;
    }

    printf("\n");
}

以下是笔者开启监控时使用 ssh 终端登录收集到的一次记录信息,sshd 进程使用 prct 系统调用设置了 PR_SET_KEEPCAPS,用户态的栈解析依赖软件包安装 debuginfo 包和使用 fp 栈回溯方式,笔者的操作系统的 glibc 包使用 dwarf 栈记录形式 bpf 内核未能准确收集到足够多的栈信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Time: 01:39:16.317241701, uid = 0, pid: 3136, tgid: 3136, ppid: 824, comm: sshd
syscall: prctl
option: PR_SET_KEEPCAPS
arg2: 1
retval: 0
securebits: 0x00000000
cap_inheritable: 0x0000000000000000
cap_permitted: 0x000001ffffffffff
cap_effective: 0x000001ffffffffff
cap_bset: 0x000001ffbfffffff
cap_ambient: 0x0000000000000000
----------------------------------
securebits: 0x00000010
cap_inheritable: 0x0000000000000000
cap_permitted: 0x000001ffffffffff
cap_effective: 0x000001ffffffffff
cap_bset: 0x000001ffbfffffff
cap_ambient: 0x0000000000000000
----------------------------------
prctl+14 (/usr/lib64/libc-2.28.so) 0x7fd96a2fd22e

屠龙

笔者后来在公司内借助此工具完成了追踪桌面 GUI 中的终端进程和 sshd 进程权能不一致的 debug 工作。追踪发现 sshd 使用 login 进程设置权能,而桌面中的终端是 [email protected] 服务的子进程,是 gdm 通过 systemd 启动该服务过程中 systemd 调用 /etc/pam.d/systemd-user 的 pam 设置的权能。

以下便是 systemd 执行服务时候发生权能变化的路径,从最后的栈记录中可以轻松找到 systemd 的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Time: 01:47:23.092619576, uid = 1000, pid: 3320, tgid: 3320, ppid: 1, comm: (systemd)
syscall: prctl
option: PR_SET_SECUREBITS
arg2: 0
retval: 0
securebits: 0x00000010
cap_inheritable: 0x000000002000002c
cap_permitted: 0x000001ffffffffff
cap_effective: 0x0000000000000100
cap_bset: 0x000001ffffffffff
cap_ambient: 0x000000002000002c
----------------------------------
securebits: 0x00000000
cap_inheritable: 0x000000002000002c
cap_permitted: 0x000001ffffffffff
cap_effective: 0x0000000000000100
cap_bset: 0x000001ffffffffff
cap_ambient: 0x000000002000002c
----------------------------------
prctl+14 (/usr/lib64/libc-2.28.so) 0x7f86f5efd22e
exec_spawn+3180 (/usr/lib/systemd/systemd) 0x55b6d2b03fdc
service_spawn.lto_priv.390+1653 (/usr/lib/systemd/systemd) 0x55b6d2ac6135
service_enter_start.lto_priv.388+166 (/usr/lib/systemd/systemd) 0x55b6d2ac99b6


Time: 01:47:23.093346803, uid = 1000, pid: 3320, tgid: 3320, ppid: 1, comm: (systemd)
syscall: execve
filename: /usr/lib/systemd/systemd
retval: 0
securebits: 0x00000000
cap_inheritable: 0x000000002000002c
cap_permitted: 0x000001ffffffffff
cap_effective: 0x0000000000000100
cap_bset: 0x000001ffffffffff
cap_ambient: 0x000000002000002c
----------------------------------
securebits: 0x00000000
cap_inheritable: 0x000000002000002c
cap_permitted: 0x000000002000002c
cap_effective: 0x000000002000002c
cap_bset: 0x000001ffffffffff
cap_ambient: 0x000000002000002c
----------------------------------
__execve+11 (/usr/lib64/libc-2.28.so) 0x7f86f5ec82fb
exec_spawn+3180 (/usr/lib/systemd/systemd) 0x55b6d2b03fdc
service_spawn.lto_priv.390+1653 (/usr/lib/systemd/systemd) 0x55b6d2ac6135
service_enter_start.lto_priv.388+166 (/usr/lib/systemd/systemd) 0x55b6d2ac99b6