使用 eBPF 追踪进程权能变化

文摘   2024-10-25 10:19   陕西  

在 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,确实会出现错误:

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

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

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跟踪找到此处就是另外一个话题了,这也是笔者第一次遇到权能的问题。

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权能

[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)
# 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权能在系统调用前后的变化。

#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表示不过滤,如果需要过滤将在用户层代码直接修改这两个值。

#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是有必要的。

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中。

__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;
}

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

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的符号解析解析栈信息即可。

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, truetrue)
                << std::endl;
    }

    printf("\n");
}

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

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进程设置权能,而桌面中的终端是user@.service服务的子进程,是gdm通过systemd启动该服务过程中systemd调用/etc/pam.d/systemd-user的pam设置的权能。

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

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




Linux内核之旅
Linux内核之旅
 最新文章