1#!/usr/bin/env python
2# @lint-avoid-python-3-compatibility-imports
3#
4# execsnoop Trace new processes via exec() syscalls.
5#           For Linux, uses BCC, eBPF. Embedded C.
6#
7# USAGE: execsnoop [-h] [-t] [-x] [-n NAME]
8#
9# This currently will print up to a maximum of 19 arguments, plus the process
10# name, so 20 fields in total (MAXARG).
11#
12# This won't catch all new processes: an application may fork() but not exec().
13#
14# Copyright 2016 Netflix, Inc.
15# Licensed under the Apache License, Version 2.0 (the "License")
16#
17# 07-Feb-2016   Brendan Gregg   Created this.
18
19from __future__ import print_function
20from bcc import BPF
21from bcc.utils import ArgString, printb
22import bcc.utils as utils
23import argparse
24import ctypes as ct
25import re
26import time
27from collections import defaultdict
28
29# arguments
30examples = """examples:
31    ./execsnoop           # trace all exec() syscalls
32    ./execsnoop -x        # include failed exec()s
33    ./execsnoop -t        # include timestamps
34    ./execsnoop -q        # add "quotemarks" around arguments
35    ./execsnoop -n main   # only print command lines containing "main"
36    ./execsnoop -l tpkg   # only print command where arguments contains "tpkg"
37"""
38parser = argparse.ArgumentParser(
39    description="Trace exec() syscalls",
40    formatter_class=argparse.RawDescriptionHelpFormatter,
41    epilog=examples)
42parser.add_argument("-t", "--timestamp", action="store_true",
43    help="include timestamp on output")
44parser.add_argument("-x", "--fails", action="store_true",
45    help="include failed exec()s")
46parser.add_argument("-q", "--quote", action="store_true",
47    help="Add quotemarks (\") around arguments."
48    )
49parser.add_argument("-n", "--name",
50    type=ArgString,
51    help="only print commands matching this name (regex), any arg")
52parser.add_argument("-l", "--line",
53    type=ArgString,
54    help="only print commands where arg contains this line (regex)")
55parser.add_argument("--max-args", default="20",
56    help="maximum number of arguments parsed and displayed, defaults to 20")
57parser.add_argument("--ebpf", action="store_true",
58    help=argparse.SUPPRESS)
59args = parser.parse_args()
60
61# define BPF program
62bpf_text = """
63#include <uapi/linux/ptrace.h>
64#include <linux/sched.h>
65#include <linux/fs.h>
66
67#define ARGSIZE  128
68
69enum event_type {
70    EVENT_ARG,
71    EVENT_RET,
72};
73
74struct data_t {
75    u32 pid;  // PID as in the userspace term (i.e. task->tgid in kernel)
76    u32 ppid; // Parent PID as in the userspace term (i.e task->real_parent->tgid in kernel)
77    char comm[TASK_COMM_LEN];
78    enum event_type type;
79    char argv[ARGSIZE];
80    int retval;
81};
82
83BPF_PERF_OUTPUT(events);
84
85static int __submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data)
86{
87    bpf_probe_read(data->argv, sizeof(data->argv), ptr);
88    events.perf_submit(ctx, data, sizeof(struct data_t));
89    return 1;
90}
91
92static int submit_arg(struct pt_regs *ctx, void *ptr, struct data_t *data)
93{
94    const char *argp = NULL;
95    bpf_probe_read(&argp, sizeof(argp), ptr);
96    if (argp) {
97        return __submit_arg(ctx, (void *)(argp), data);
98    }
99    return 0;
100}
101
102int syscall__execve(struct pt_regs *ctx,
103    const char __user *filename,
104    const char __user *const __user *__argv,
105    const char __user *const __user *__envp)
106{
107    // create data here and pass to submit_arg to save stack space (#555)
108    struct data_t data = {};
109    struct task_struct *task;
110
111    data.pid = bpf_get_current_pid_tgid() >> 32;
112
113    task = (struct task_struct *)bpf_get_current_task();
114    // Some kernels, like Ubuntu 4.13.0-generic, return 0
115    // as the real_parent->tgid.
116    // We use the get_ppid function as a fallback in those cases. (#1883)
117    data.ppid = task->real_parent->tgid;
118
119    bpf_get_current_comm(&data.comm, sizeof(data.comm));
120    data.type = EVENT_ARG;
121
122    __submit_arg(ctx, (void *)filename, &data);
123
124    // skip first arg, as we submitted filename
125    #pragma unroll
126    for (int i = 1; i < MAXARG; i++) {
127        if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)
128             goto out;
129    }
130
131    // handle truncated argument list
132    char ellipsis[] = "...";
133    __submit_arg(ctx, (void *)ellipsis, &data);
134out:
135    return 0;
136}
137
138int do_ret_sys_execve(struct pt_regs *ctx)
139{
140    struct data_t data = {};
141    struct task_struct *task;
142
143    data.pid = bpf_get_current_pid_tgid() >> 32;
144
145    task = (struct task_struct *)bpf_get_current_task();
146    // Some kernels, like Ubuntu 4.13.0-generic, return 0
147    // as the real_parent->tgid.
148    // We use the get_ppid function as a fallback in those cases. (#1883)
149    data.ppid = task->real_parent->tgid;
150
151    bpf_get_current_comm(&data.comm, sizeof(data.comm));
152    data.type = EVENT_RET;
153    data.retval = PT_REGS_RC(ctx);
154    events.perf_submit(ctx, &data, sizeof(data));
155
156    return 0;
157}
158"""
159
160bpf_text = bpf_text.replace("MAXARG", args.max_args)
161if args.ebpf:
162    print(bpf_text)
163    exit()
164
165# initialize BPF
166b = BPF(text=bpf_text)
167execve_fnname = b.get_syscall_fnname("execve")
168b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
169b.attach_kretprobe(event=execve_fnname, fn_name="do_ret_sys_execve")
170
171# header
172if args.timestamp:
173    print("%-8s" % ("TIME(s)"), end="")
174print("%-16s %-6s %-6s %3s %s" % ("PCOMM", "PID", "PPID", "RET", "ARGS"))
175
176TASK_COMM_LEN = 16      # linux/sched.h
177ARGSIZE = 128           # should match #define in C above
178
179class Data(ct.Structure):
180    _fields_ = [
181        ("pid", ct.c_uint),
182        ("ppid", ct.c_uint),
183        ("comm", ct.c_char * TASK_COMM_LEN),
184        ("type", ct.c_int),
185        ("argv", ct.c_char * ARGSIZE),
186        ("retval", ct.c_int),
187    ]
188
189class EventType(object):
190    EVENT_ARG = 0
191    EVENT_RET = 1
192
193start_ts = time.time()
194argv = defaultdict(list)
195
196# This is best-effort PPID matching. Short-lived processes may exit
197# before we get a chance to read the PPID.
198# This is a fallback for when fetching the PPID from task->real_parent->tgip
199# returns 0, which happens in some kernel versions.
200def get_ppid(pid):
201    try:
202        with open("/proc/%d/status" % pid) as status:
203            for line in status:
204                if line.startswith("PPid:"):
205                    return int(line.split()[1])
206    except IOError:
207        pass
208    return 0
209
210# process event
211def print_event(cpu, data, size):
212    event = ct.cast(data, ct.POINTER(Data)).contents
213
214    skip = False
215
216    if event.type == EventType.EVENT_ARG:
217        argv[event.pid].append(event.argv)
218    elif event.type == EventType.EVENT_RET:
219        if event.retval != 0 and not args.fails:
220            skip = True
221        if args.name and not re.search(bytes(args.name), event.comm):
222            skip = True
223        if args.line and not re.search(bytes(args.line),
224                                       b' '.join(argv[event.pid])):
225            skip = True
226        if args.quote:
227            argv[event.pid] = [
228                "\"" + arg.replace("\"", "\\\"") + "\""
229                for arg in argv[event.pid]
230            ]
231
232        if not skip:
233            if args.timestamp:
234                print("%-8.3f" % (time.time() - start_ts), end="")
235            ppid = event.ppid if event.ppid > 0 else get_ppid(event.pid)
236            ppid = b"%d" % ppid if ppid > 0 else b"?"
237            argv_text = b' '.join(argv[event.pid]).replace(b'\n', b'\\n')
238            printb(b"%-16s %-6d %-6s %3d %s" % (event.comm, event.pid,
239                   ppid, event.retval, argv_text))
240        try:
241            del(argv[event.pid])
242        except Exception:
243            pass
244
245
246# loop with callback to print_event
247b["events"].open_perf_buffer(print_event)
248while 1:
249    try:
250        b.perf_buffer_poll()
251    except KeyboardInterrupt:
252        exit()
253