-
Notifications
You must be signed in to change notification settings - Fork 286
/
shellsnoop.py
executable file
·163 lines (142 loc) · 4.75 KB
/
shellsnoop.py
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/python
# @lint-avoid-python-3-compatibility-imports
#
# shellsnoop Watch another shell session.
# For Linux, uses BCC, eBPF. Embedded C.
#
# This traces writes to STDOUT and STDERR for the specified PID and its
# children, and prints them out. This lets you watch another shell live. Due
# to a limited buffer size, some commands (eg, a vim session) are likely to
# be printed a little messed up.
#
# Copyright (c) 2016 Brendan Gregg.
# Licensed under the Apache License, Version 2.0 (the "License")
#
# Idea: from ttywatcher.
#
# 15-Oct-2016 Brendan Gregg Created this.
from __future__ import print_function
from bcc import BPF
import ctypes as ct
from subprocess import call
import argparse
from sys import argv
import sys
def usage():
print("USAGE: %s PID" % argv[0])
exit()
# arguments
examples = """examples:
./shellsnoop 181 # snoop on shell with PID 181
"""
parser = argparse.ArgumentParser(
description="Snoop output from another shell",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=examples)
parser.add_argument("-C", "--noclear", action="store_true",
help="don't clear the screen")
parser.add_argument("-s", "--shellonly", action="store_true",
help="shell output only (no sub-commands)")
parser.add_argument("-r", "--replay", action="store_true",
help="emit a replay shell script")
parser.add_argument("pid", nargs="?", default=0,
help="PID to trace")
args = parser.parse_args()
debug = 0
if args.pid == 0:
print("USAGE: %s [-hs] PID" % argv[0])
exit()
if args.replay:
args.noclear = True
# define BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
#define BUFSIZE 256
struct data_t {
u64 ts;
int count;
char buf[BUFSIZE];
};
BPF_PERF_OUTPUT(events);
BPF_HASH(children, u32, int);
TRACEPOINT_PROBE(sched, sched_process_fork)
{
if (NOCHILDREN)
return 0;
u32 pid = args->parent_pid;
u32 newpid = args->child_pid;
u32 *cp = children.lookup(&pid);
if (cp == NULL && pid != PID)
return 0;
int one = 1;
children.update(&newpid, &one);
return 0;
}
static void emit(void *ctx, const char *buf, u32 *offset, u64 ts, size_t count)
{
struct data_t data = {.ts = ts};
bpf_probe_read(&data.buf, BUFSIZE, (void *)buf + *offset);
data.count = count - *offset > BUFSIZE ? BUFSIZE : count - *offset;
*offset += BUFSIZE;
events.perf_submit(ctx, &data, sizeof(data));
}
// switch to a tracepoint when #748 is fixed
TRACEPOINT_PROBE(syscalls, sys_enter_write)
{
if (args->fd != 1 && args->fd != 2)
return 0;
u32 pid = bpf_get_current_pid_tgid();
u32 *cp = children.lookup(&pid);
if (cp == NULL && pid != PID)
return 0;
// bpf_probe_read() can only use a fixed size, so truncate to count
// in user space:
u32 offset = 0;
// unrolled loop to workaround stack size limit.
// TODO: switch to use BPF map storage and a single perf_submit().
u64 ts = bpf_ktime_get_ns();
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
if (offset < args->count) { emit(args, args->buf, &offset, ts, args->count); }
return 0;
};
"""
bpf_text = bpf_text.replace('PID', str(args.pid))
bpf_text = bpf_text.replace('NOCHILDREN', str(int(args.shellonly)))
if debug:
print(bpf_text)
# initialize BPF
b = BPF(text=bpf_text)
BUFSIZE = 256
last_ts = 0
if not args.noclear:
call("clear")
# process event
def print_event(cpu, data, size):
event = b["events"].event(data)
global last_ts
if last_ts == 0:
last_ts = event.ts
if args.replay:
delay_ms = (event.ts - last_ts) / 1000000
if delay_ms:
print("sleep %.2f" % (float(delay_ms) / 1000))
printable = event.buf[0:event.count]
printable = printable.replace('\\', '\\\\')
printable = printable.replace('\'', '\\047')
print("echo -e '%s\\c'" % printable)
last_ts = event.ts
else:
print("%s" % event.buf[0:event.count], end="")
sys.stdout.flush()
# loop with callback to print_event
b["events"].open_perf_buffer(print_event, page_cnt=64)
while 1:
b.kprobe_poll()