| #!/usr/bin/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] [-q] [-n NAME] [-l LINE] | |
| # [--max-args MAX_ARGS] | |
| # | |
| # 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. | |
| from __future__ import print_function | |
| from bcc import BPF | |
| 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 -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 | |
| """ | |
| 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("-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("--max-args", default="20", | |
| help="maximum number of arguments parsed and displayed, defaults to 20") | |
| 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; | |
| char comm[TASK_COMM_LEN]; | |
| enum event_type type; | |
| char argv[ARGSIZE]; | |
| int retval; | |
| }; | |
| #if CGROUPSET | |
| BPF_TABLE_PINNED("hash", u64, u64, cgroupset, 1024, "CGROUPPATH"); | |
| #endif | |
| BPF_PERF_OUTPUT(events); | |
| static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data) | |
| { | |
| bpf_probe_read(data->argv, sizeof(data->argv), ptr); | |
| 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) | |
| { | |
| const char *argp = NULL; | |
| bpf_probe_read(&argp, sizeof(argp), ptr); | |
| if (argp) { | |
| return __submit_arg(ctx, (void *)(argp), data); | |
| } | |
| return 0; | |
| } | |
| int syscall__execve(struct pt_regs *ctx, | |
| const char __user *filename, | |
| const char __user *const __user *__argv, | |
| const char __user *const __user *__envp) | |
| { | |
| u32 uid = bpf_get_current_uid_gid() & 0xffffffff; | |
| UID_FILTER | |
| #if CGROUPSET | |
| u64 cgroupid = bpf_get_current_cgroup_id(); | |
| if (cgroupset.lookup(&cgroupid) == NULL) { | |
| return 0; | |
| } | |
| #endif | |
| // create data here and pass to submit_arg to save stack space (#555) | |
| struct data_t data = {}; | |
| struct task_struct *task; | |
| 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; | |
| bpf_get_current_comm(&data.comm, sizeof(data.comm)); | |
| data.type = EVENT_ARG; | |
| __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; | |
| } | |
| int do_ret_sys_execve(struct pt_regs *ctx) | |
| { | |
| #if CGROUPSET | |
| u64 cgroupid = bpf_get_current_cgroup_id(); | |
| if (cgroupset.lookup(&cgroupid) == NULL) { | |
| return 0; | |
| } | |
| #endif | |
| 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; | |
| bpf_get_current_comm(&data.comm, sizeof(data.comm)); | |
| data.type = EVENT_RET; | |
| data.retval = PT_REGS_RC(ctx); | |
| events.perf_submit(ctx, &data, sizeof(data)); | |
| 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.cgroupmap: | |
| bpf_text = bpf_text.replace('CGROUPSET', '1') | |
| bpf_text = bpf_text.replace('CGROUPPATH', args.cgroupmap) | |
| else: | |
| bpf_text = bpf_text.replace('CGROUPSET', '0') | |
| 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 %-6s %-6s %3s %s" % ("PCOMM", "PID", "PPID", "RET", "ARGS")) | |
| class EventType(object): | |
| EVENT_ARG = 0 | |
| EVENT_RET = 1 | |
| start_ts = time.time() | |
| argv = defaultdict(list) | |
| # 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 | |
| def print_event(cpu, data, size): | |
| event = b["events"].event(data) | |
| skip = False | |
| if event.type == EventType.EVENT_ARG: | |
| argv[event.pid].append(event.argv) | |
| 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') | |
| printb(b"%-16s %-6d %-6s %3d %s" % (event.comm, event.pid, | |
| ppid, event.retval, argv_text)) | |
| try: | |
| del(argv[event.pid]) | |
| except Exception: | |
| pass | |
| # loop with callback to print_event | |
| b["events"].open_perf_buffer(print_event) | |
| while 1: | |
| try: | |
| b.perf_buffer_poll() | |
| except KeyboardInterrupt: | |
| exit() |