type
Post
status
Published
slug
2023/01/14/trace-new-processes-via-exec-syscalls
summary
此文章是对 bcc 官方示例文件 execsnoop.py 的学习分析
tags
BPF
Linux
eBPF
category
BPF
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM
1 代码逻辑分析
此工具代码使用
python
编写,借助 bcc 工具实现对 eBPF 函数的编译与执行。bcc 工具将 eBPF 函数,与系统程序的执行与退出追踪点绑定在一起,当追踪点被触发的时候, perf 帮助函数将相关数据通过 perf 事件传送至用户空间进行进一步的处理。通过 perf 事件传递信息的方式,不受 BPF 虚拟机编程能力的限制,为表示层提供了更多的控制。大多数 BPF 跟踪程序都使用 Perf 事件来实现这一目的。
1.1 eBPF 代码分析
#include <uapi/linux/ptrace.h> #include <linux/sched.h> #include <linux/fs.h> #define ARGSIZE 128 // 事件类型枚举 enum event_type { EVENT_ARG, EVENT_RET, }; struct data_t { u32 pid; // PID as in the userspace term (i.e. task->tgid in kernel) u32 ppid; // Parent PID as in the userspace term (i.e task->real_parent->tgid in kernel) u32 uid; // User ID char comm[TASK_COMM_LEN]; enum event_type type; char argv[ARGSIZE]; u64 start_time_ns; // program start time u64 stop_time_ns; // program stop time int retval; }; /** * @brief 使用宏 BPF-PERF-OUTPUT 声明一个名为 events 的 Perf 事件映射。 * 这个宏由 BCC 提供,用于方便地声明 Perf 事件映射 。 * 它允许将数据放入环形缓存区,以便与用户空间程序实时同步。 * * 如果你正在使用 BPF 程序收 集大量数据,期望将处理和可视化工作卸载到用户空间程序, * 那么 Perf 事件会是理想的选择。因为不再受限于 BPF 虚拟机提供的编程能力, * 所以这种方式对表示层提供了更多的控制。你会发现大多数 BPF 跟踪程序使用 Perf 事件 * 都是出于这个目的。 */ BPF_PERF_OUTPUT(events); /** * @brief 传入参数的时候只有,pid, ppid * * @param ctx * @param ptr * @param data * @return int */ static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) { // 这会尝试从用户地址空间安全地读取 size 个字节到 BPF 堆栈, // 以便 BPF 稍后可以对其进行操作。 // 为了安全,所有用户地址空间内存读取都必须通过 bpf_probe_read_user()。 bpf_probe_read_user(data->argv, sizeof(data->argv), ptr); // 获取内核中执行的程序名后,将它发送到用户空间进行聚合。 // 我们使用 perf_submit 函数实现这个功能 。 // 这个函数使用新的信息更新 Perf 事件映射。 events.perf_submit(ctx, data, sizeof(struct data_t)); return 1; } static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) { // 在变量声明的时候,如果没有确切的地址可以赋值, // 为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。 // NULL 指针是一个定义在标准库中的值为零的常量。 const char *argp = NULL; bpf_probe_read_user(&argp, sizeof(argp), ptr); if (argp) { return __submit_arg(ctx, (void *)(argp), data); } return 0; } /** * @brief 进程调用处理函数 * * @param ctx 上下文信息 * @param filename 文件描述符 * @param __argv 可执行程序的参数信息 * @param __envp 环境变量信息 * @return int */ // 当您通读 file_operations 方法列表时,您会注意到许多参数包括字符串 __user。 // 此注释是一种文档形式,指出指针是不能直接取消引用的用户空间地址。 // 对于正常的编译,__user 没有作用,但可以被外部检查软件用来发现用户空间地址的误用。 // 实质上是定义了一个指向字符串的二级指针,第二个的const 限制一级指针指向的内容不能修改, // 左边第一个的 const 限制二级指针指向的内容不能修改。 int syscall__execve(struct pt_regs *ctx, const char __user *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) { // 保留低 32 位 u32 uid = bpf_get_current_uid_gid() & 0xffffffff; // 如果指定了 UID 过滤,则只关心指定 PPID 的程序 UID_FILTER if (container_should_be_filtered()) { return 0; } // create data here and pass to submit_arg to save stack space (#555) // 在这里创建数据并传递给submit_arg以节省堆栈空间 struct data_t data = {}; struct task_struct *task; // 获取进程 pid data.pid = bpf_get_current_pid_tgid() >> 32; // 获取当前进程结构体 task = (struct task_struct *)bpf_get_current_task(); // Some kernels, like Ubuntu 4.13.0-generic, return 0 // as the real_parent->tgid. // We use the get_ppid function as a fallback in those cases. (#1883) data.ppid = task->real_parent->tgid; // 如果指定了 PPID 过滤,则只关心指定 PPID 的程序 PPID_FILTER // 获取当前运行的进程名称 bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.type = EVENT_ARG; // 获取当前进程的启动时间 data.start_time_ns = bpf_ktime_get_ns(); __submit_arg(ctx, (void *)filename, &data); // skip first arg, as we submitted filename #pragma unroll for (int i = 1; i < MAXARG; i++) { if (submit_arg(ctx, (void *)&__argv[i], &data) == 0) goto out; } // handle truncated argument list // 处理截断的参数列表 char ellipsis[] = "..."; __submit_arg(ctx, (void *)ellipsis, &data); out: return 0; } /** * @brief 进程退出处理函数 * * @param ctx 上下文信息 * @return int */ int do_ret_sys_execve(struct pt_regs *ctx) { if (container_should_be_filtered()) { return 0; } struct data_t data = {}; struct task_struct *task; u32 uid = bpf_get_current_uid_gid() & 0xffffffff; UID_FILTER data.pid = bpf_get_current_pid_tgid() >> 32; data.uid = uid; task = (struct task_struct *)bpf_get_current_task(); // Some kernels, like Ubuntu 4.13.0-generic, return 0 // as the real_parent->tgid. // We use the get_ppid function as a fallback in those cases. (#1883) data.ppid = task->real_parent->tgid; PPID_FILTER bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.type = EVENT_RET; data.retval = PT_REGS_RC(ctx); // 获取返回值 data.stop_time_ns = bpf_ktime_get_ns(); events.perf_submit(ctx, &data, sizeof(data)); // 提交 perf 事件 return 0; }
1.2 用户空间信息处理代码分析
# initialize BPF b = BPF(text=bpf_text) execve_fnname = b.get_syscall_fnname("execve") # 绑定到系统执行接口上 b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve") # 将内核程序执行事件绑定至 BPF 程序 syscall__execve 上 b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve") # 将内核程序退出事件绑定至 BPF 程序 do_ret_sys_execve 上 # header if args.time: print("%-9s" % ("TIME"), end="") if args.timestamp: print("%-8s" % ("TIME(s)"), end="") if args.print_uid: print("%-6s" % ("UID"), end="") print("%-16s %-7s %-7s %12s %3s %s" % ("PCOMM", "PID", "PPID", "EXECTIME(US)", "RET", "ARGS")) class EventType(object): EVENT_ARG = 0 EVENT_RET = 1 # 程序追踪的开始时间 start_ts = time.time() argv = defaultdict(list) # 定义参数字典,以 pid 为 key, value 为执行的后缀参数 exec_time = defaultdict(int) # This is best-effort PPID matching. Short-lived processes may exit # before we get a chance to read the PPID. # This is a fallback for when fetching the PPID from task->real_parent->tgip # returns 0, which happens in some kernel versions. def get_ppid(pid): try: with open("/proc/%d/status" % pid) as status: for line in status: if line.startswith("PPid:"): return int(line.split()[1]) except IOError: pass return 0 # process event # perf 处理函数固定的参数格式 def print_event(cpu, data, size): # 获取 perf 时间中存储的数据 event = b["events"].event(data) skip = False if event.type == EventType.EVENT_ARG: argv[event.pid].append(event.argv) exec_time[event.pid] = event.start_time_ns # 程序退出的判断分支 elif event.type == EventType.EVENT_RET: if event.retval != 0 and not args.fails: skip = True if args.name and not re.search(bytes(args.name), event.comm): skip = True if args.line and not re.search(bytes(args.line), b' '.join(argv[event.pid])): skip = True if args.quote: argv[event.pid] = [ b"\"" + arg.replace(b"\"", b"\\\"") + b"\"" for arg in argv[event.pid] ] if not skip: if args.time: printb(b"%-9s" % strftime("%H:%M:%S").encode('ascii'), nl="") if args.timestamp: printb(b"%-8.3f" % (time.time() - start_ts), nl="") if args.print_uid: printb(b"%-6d" % event.uid, nl="") ppid = event.ppid if event.ppid > 0 else get_ppid(event.pid) ppid = b"%d" % ppid if ppid > 0 else b"?" argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n') exec_time_sum_us = (event.stop_time_ns - exec_time[event.pid]) / 1000 printb(b"%-16s %-7d %-7s %12d %3d %s" % (event.comm, event.pid, ppid, exec_time_sum_us, event.retval, argv_text)) try: del(argv[event.pid]) del(exec_time[event.pid]) except Exception: pass # loop with callback to print_event # 使用 open_perf_buffer 函数,在每次从 Perf 事件映射接收到一个事件时, # 通知 BCC 需要执行 print_event 函数。 b["events"].open_perf_buffer(print_event) while 1: try: b.perf_buffer_poll() except KeyboardInterrupt: exit()
1.3 输出展示
# ./execsnoop PCOMM PID RET ARGS bash 15887 0 /usr/bin/man ls preconv 15894 0 /usr/bin/preconv -e UTF-8 man 15896 0 /usr/bin/tbl man 15897 0 /usr/bin/nroff -mandoc -rLL=169n -rLT=169n -Tutf8 man 15898 0 /usr/bin/pager -s nroff 15900 0 /usr/bin/locale charmap nroff 15901 0 /usr/bin/groff -mtty-char -Tutf8 -mandoc -rLL=169n -rLT=169n groff 15902 0 /usr/bin/troff -mtty-char -mandoc -rLL=169n -rLT=169n -Tutf8 groff 15903 0 /usr/bin/grotty
1.4 完整代码展示
#!/usr/bin/env python # @lint-avoid-python-3-compatibility-imports # # execsnoop Trace new processes via exec() syscalls. # For Linux, uses BCC, eBPF. Embedded C. # # USAGE: execsnoop [-h] [-T] [-t] [-x] [--cgroupmap CGROUPMAP] # [--mntnsmap MNTNSMAP] [-u USER] [-q] [-n NAME] [-l LINE] # [-U] [--max-args MAX_ARGS] [-P PPID] # # This currently will print up to a maximum of 19 arguments, plus the process # name, so 20 fields in total (MAXARG). # # This won't catch all new processes: an application may fork() but not exec(). # # Copyright 2016 Netflix, Inc. # Licensed under the Apache License, Version 2.0 (the "License") # # 07-Feb-2016 Brendan Gregg Created this. # 11-Aug-2022 Rocky Xing Added PPID filter support. from __future__ import print_function from bcc import BPF from bcc.containers import filter_by_containers from bcc.utils import ArgString, printb import bcc.utils as utils import argparse import re import time import pwd from collections import defaultdict from time import strftime def parse_uid(user): try: result = int(user) except ValueError: try: user_info = pwd.getpwnam(user) except KeyError: raise argparse.ArgumentTypeError( "{0!r} is not valid UID or user entry".format(user)) else: return user_info.pw_uid else: # Maybe validate if UID < 0 ? return result # arguments examples = """examples: ./execsnoop # trace all exec() syscalls ./execsnoop -x # include failed exec()s ./execsnoop -T # include time (HH:MM:SS) ./execsnoop -P 181 # only trace new processes whose parent PID is 181 ./execsnoop -U # include UID ./execsnoop -u 1000 # only trace UID 1000 ./execsnoop -u user # get user UID and trace only them ./execsnoop -t # include timestamps ./execsnoop -q # add "quotemarks" around arguments ./execsnoop -n main # only print command lines containing "main" ./execsnoop -l tpkg # only print command where arguments contains "tpkg" ./execsnoop --cgroupmap mappath # only trace cgroups in this BPF map ./execsnoop --mntnsmap mappath # only trace mount namespaces in the map """ parser = argparse.ArgumentParser( description="Trace exec() syscalls", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=examples) parser.add_argument("-T", "--time", action="store_true", help="include time column on output (HH:MM:SS)") parser.add_argument("-t", "--timestamp", action="store_true", help="include timestamp on output") parser.add_argument("-x", "--fails", action="store_true", help="include failed exec()s") parser.add_argument("--cgroupmap", help="trace cgroups in this BPF map only") parser.add_argument("--mntnsmap", help="trace mount namespaces in this BPF map only") parser.add_argument("-u", "--uid", type=parse_uid, metavar='USER', help="trace this UID only") parser.add_argument("-q", "--quote", action="store_true", help="Add quotemarks (\") around arguments." ) parser.add_argument("-n", "--name", type=ArgString, help="only print commands matching this name (regex), any arg") parser.add_argument("-l", "--line", type=ArgString, help="only print commands where arg contains this line (regex)") parser.add_argument("-U", "--print-uid", action="store_true", help="print UID column") parser.add_argument("-E", "--print-exectime", action="store_true", help="print EXECTIME column") parser.add_argument("--max-args", default="20", help="maximum number of arguments parsed and displayed, defaults to 20") parser.add_argument("-P", "--ppid", help="trace this parent PID only") parser.add_argument("--ebpf", action="store_true", help=argparse.SUPPRESS) args = parser.parse_args() # define BPF program bpf_text = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> #include <linux/fs.h> #define ARGSIZE 128 // 事件类型枚举 enum event_type { EVENT_ARG, EVENT_RET, }; struct data_t { u32 pid; // PID as in the userspace term (i.e. task->tgid in kernel) u32 ppid; // Parent PID as in the userspace term (i.e task->real_parent->tgid in kernel) u32 uid; // User ID char comm[TASK_COMM_LEN]; enum event_type type; char argv[ARGSIZE]; u64 start_time_ns; // program start time u64 stop_time_ns; // program stop time int retval; }; /** * @brief 使用宏 BPF-PERF-OUTPUT 声明一个名为 events 的 Perf 事件映射。 * 这个宏由 BCC 提供,用于方便地声明 Perf 事件映射 。 * 它允许将数据放入环形缓存区,以便与用户空间程序实时同步。 * * 如果你正在使用 BPF 程序收 集大量数据,期望将处理和可视化工作卸载到用户空间程序, * 那么 Perf 事件会是理想的选择。因为不再受限于 BPF 虚拟机提供的编程能力, * 所以这种方式对表示层提供了更多的控制。你会发现大多数 BPF 跟踪程序使用 Perf 事件 * 都是出于这个目的。 */ BPF_PERF_OUTPUT(events); /** * @brief 传入参数的时候只有,pid, ppid * * @param ctx * @param ptr * @param data * @return int */ static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) { // 这会尝试从用户地址空间安全地读取 size 个字节到 BPF 堆栈, // 以便 BPF 稍后可以对其进行操作。 // 为了安全,所有用户地址空间内存读取都必须通过 bpf_probe_read_user()。 bpf_probe_read_user(data->argv, sizeof(data->argv), ptr); // 获取内核中执行的程序名后,将它发送到用户空间进行聚合。 // 我们使用 perf_submit 函数实现这个功能 。 // 这个函数使用新的信息更新 Perf 事件映射。 events.perf_submit(ctx, data, sizeof(struct data_t)); return 1; } static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) { // 在变量声明的时候,如果没有确切的地址可以赋值, // 为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。 // NULL 指针是一个定义在标准库中的值为零的常量。 const char *argp = NULL; bpf_probe_read_user(&argp, sizeof(argp), ptr); if (argp) { return __submit_arg(ctx, (void *)(argp), data); } return 0; } /** * @brief 进程调用处理函数 * * @param ctx 上下文信息 * @param filename 文件描述符 * @param __argv 可执行程序的参数信息 * @param __envp 环境变量信息 * @return int */ // 当您通读 file_operations 方法列表时,您会注意到许多参数包括字符串 __user。 // 此注释是一种文档形式,指出指针是不能直接取消引用的用户空间地址。 // 对于正常的编译,__user 没有作用,但可以被外部检查软件用来发现用户空间地址的误用。 // 实质上是定义了一个指向字符串的二级指针,第二个的const 限制一级指针指向的内容不能修改, // 左边第一个的 const 限制二级指针指向的内容不能修改。 int syscall__execve(struct pt_regs *ctx, const char __user *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) { // 保留低 32 位 u32 uid = bpf_get_current_uid_gid() & 0xffffffff; // 如果指定了 UID 过滤,则只关心指定 PPID 的程序 UID_FILTER if (container_should_be_filtered()) { return 0; } // create data here and pass to submit_arg to save stack space (#555) // 在这里创建数据并传递给submit_arg以节省堆栈空间 struct data_t data = {}; struct task_struct *task; // 获取进程 pid data.pid = bpf_get_current_pid_tgid() >> 32; // 获取当前进程结构体 task = (struct task_struct *)bpf_get_current_task(); // Some kernels, like Ubuntu 4.13.0-generic, return 0 // as the real_parent->tgid. // We use the get_ppid function as a fallback in those cases. (#1883) data.ppid = task->real_parent->tgid; // 如果指定了 PPID 过滤,则只关心指定 PPID 的程序 PPID_FILTER // 获取当前运行的进程名称 bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.type = EVENT_ARG; // 获取当前进程的启动时间 data.start_time_ns = bpf_ktime_get_ns(); __submit_arg(ctx, (void *)filename, &data); // skip first arg, as we submitted filename #pragma unroll for (int i = 1; i < MAXARG; i++) { if (submit_arg(ctx, (void *)&__argv[i], &data) == 0) goto out; } // handle truncated argument list // 处理截断的参数列表 char ellipsis[] = "..."; __submit_arg(ctx, (void *)ellipsis, &data); out: return 0; } /** * @brief 进程退出处理函数 * * @param ctx 上下文信息 * @return int */ int do_ret_sys_execve(struct pt_regs *ctx) { if (container_should_be_filtered()) { return 0; } struct data_t data = {}; struct task_struct *task; u32 uid = bpf_get_current_uid_gid() & 0xffffffff; UID_FILTER data.pid = bpf_get_current_pid_tgid() >> 32; data.uid = uid; task = (struct task_struct *)bpf_get_current_task(); // Some kernels, like Ubuntu 4.13.0-generic, return 0 // as the real_parent->tgid. // We use the get_ppid function as a fallback in those cases. (#1883) data.ppid = task->real_parent->tgid; PPID_FILTER bpf_get_current_comm(&data.comm, sizeof(data.comm)); data.type = EVENT_RET; data.retval = PT_REGS_RC(ctx); // 获取返回值 data.stop_time_ns = bpf_ktime_get_ns(); events.perf_submit(ctx, &data, sizeof(data)); // 提交 perf 事件 return 0; } """ # 程序参数配置 bpf_text = bpf_text.replace("MAXARG", args.max_args) if args.uid: bpf_text = bpf_text.replace('UID_FILTER', 'if (uid != %s) { return 0; }' % args.uid) else: bpf_text = bpf_text.replace('UID_FILTER', '') if args.ppid: bpf_text = bpf_text.replace('PPID_FILTER', 'if (data.ppid != %s) { return 0; }' % args.ppid) else: bpf_text = bpf_text.replace('PPID_FILTER', '') bpf_text = filter_by_containers(args) + bpf_text if args.ebpf: print(bpf_text) exit() # initialize BPF b = BPF(text=bpf_text) execve_fnname = b.get_syscall_fnname("execve") # 绑定到系统执行接口上 b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve") b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve") # header if args.time: print("%-9s" % ("TIME"), end="") if args.timestamp: print("%-8s" % ("TIME(s)"), end="") if args.print_uid: print("%-6s" % ("UID"), end="") print("%-16s %-7s %-7s %12s %3s %s" % ("PCOMM", "PID", "PPID", "EXECTIME(US)", "RET", "ARGS")) class EventType(object): EVENT_ARG = 0 EVENT_RET = 1 # 程序追踪的开始时间 start_ts = time.time() argv = defaultdict(list) # 定义参数字典,以 pid 为 key, value 为执行的后缀参数 exec_time = defaultdict(int) # This is best-effort PPID matching. Short-lived processes may exit # before we get a chance to read the PPID. # This is a fallback for when fetching the PPID from task->real_parent->tgip # returns 0, which happens in some kernel versions. def get_ppid(pid): try: with open("/proc/%d/status" % pid) as status: for line in status: if line.startswith("PPid:"): return int(line.split()[1]) except IOError: pass return 0 # process event # perf 处理函数固定的参数格式 def print_event(cpu, data, size): # 获取 perf 时间中存储的数据 event = b["events"].event(data) skip = False if event.type == EventType.EVENT_ARG: argv[event.pid].append(event.argv) exec_time[event.pid] = event.start_time_ns # 程序退出的判断分支 elif event.type == EventType.EVENT_RET: if event.retval != 0 and not args.fails: skip = True if args.name and not re.search(bytes(args.name), event.comm): skip = True if args.line and not re.search(bytes(args.line), b' '.join(argv[event.pid])): skip = True if args.quote: argv[event.pid] = [ b"\"" + arg.replace(b"\"", b"\\\"") + b"\"" for arg in argv[event.pid] ] if not skip: if args.time: printb(b"%-9s" % strftime("%H:%M:%S").encode('ascii'), nl="") if args.timestamp: printb(b"%-8.3f" % (time.time() - start_ts), nl="") if args.print_uid: printb(b"%-6d" % event.uid, nl="") ppid = event.ppid if event.ppid > 0 else get_ppid(event.pid) ppid = b"%d" % ppid if ppid > 0 else b"?" argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n') exec_time_sum_us = (event.stop_time_ns - exec_time[event.pid]) / 1000 printb(b"%-16s %-7d %-7s %12d %3d %s" % (event.comm, event.pid, ppid, exec_time_sum_us, event.retval, argv_text)) try: del(argv[event.pid]) del(exec_time[event.pid]) except Exception: pass # loop with callback to print_event # 使用 open_perf_buffer 函数,在每次从 Perf 事件映射接收到一个事件时, # 通知 BCC 需要执行 print_event 函数。 b["events"].open_perf_buffer(print_event) while 1: try: b.perf_buffer_poll() except KeyboardInterrupt: exit()
2 资源文件
欢迎加入“喵星计算机技术研究院”,原创技术文章第一时间推送。
- 作者:tangcuyu
- 链接:https://expoli.tech/articles/2023/01/14/trace-new-processes-via-exec-syscalls
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章