Skip to content

Commit

Permalink
tools: improve sslsniff (send buffer & filtering)
Browse files Browse the repository at this point in the history
This makes few improvements:
    * This can send much larger data payload and also adds
      --max-buffer-size CLI option which allow changing this param.
    * Fixes dealing with non ASCII protocols, previously struct was
      defined as array of chars which made python ctypes treat it as
      NULL terminated string and it prevents from displaying any data
      past the null byte (which is very common for http2).
    * Adds more filtering and displaying options (--print-uid,
      --print-tid, --uid <uid>)

This also deals correctly with rare cases when bpf_probe_read_user fails
(so buffer should be empty and should not be displayed).
  • Loading branch information
bacher09 authored and yonghong-song committed Nov 23, 2021
1 parent 2cffe36 commit 91a7983
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 41 deletions.
45 changes: 44 additions & 1 deletion man/man8/sslsniff.8
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
.SH NAME
sslsniff \- Print data passed to OpenSSL, GnuTLS or NSS. Uses Linux eBPF/bcc.
.SH SYNOPSIS
.B sslsniff [-h] [-p PID] [-c COMM] [-o] [-g] [-n] [-d] [--hexdump]
.B sslsniff [-h] [-p PID] [-u UID] [-x] [-c COMM] [-o] [-g] [-n] [-d]
.B [--hexdump] [--max-buffer-size SIZE]
.SH DESCRIPTION
sslsniff prints data sent to write/send and read/recv functions of
OpenSSL, GnuTLS and NSS, allowing us to read plain text content before
Expand All @@ -13,11 +14,47 @@ This works reading the second parameter of both functions (*buf).
Since this uses BPF, only the root user can use this tool.
.SH REQUIREMENTS
CONFIG_BPF and bcc.
.SH OPTIONS
.TP
\-h
Print usage message.
.TP
\-p PID
Trace only functions in this process PID.
.TP
\-u UID
Trace only calls made by this UID.
.TP
\-x
Show extra fields: UID and TID.
.TP
\-c COMM
Show only processes that match this COMM exactly.
.TP
\-o, \-\-no-openssl
Do not trace OpenSSL functions.
.TP
\-g, \-\-no-gnutls
Do not trace GnuTLS functions.
.TP
\-n, \-\-no-nss
Do not trace GnuTLS functions.
.TP
\-\-hexdump
Show data as hexdump instead of trying to decode it as UTF-8
.TP
\-\-max-buffer-size SIZE
Sets maximum buffer size of intercepted data. Longer values would be truncated.
Default value is 8 Kib, maximum possible value is a bit less than 32 Kib.
.SH EXAMPLES
.TP
Print all calls to SSL write/send and read/recv system-wide:
#
.B sslsniff
.TP
Print only OpenSSL calls issued by user with UID 1000
#
.B sslsniff -u 1000 --no-nss --no-gnutls
.SH FIELDS
.TP
FUNC
Expand All @@ -34,6 +71,12 @@ Process ID calling SSL.
.TP
LEN
Bytes written or read by SSL functions.
.TP
UID
UID of the process, displayed only if launched with -x.
.TP
TID
Thread ID, displayed only if launched with -x.
.SH SOURCE
This is from bcc.
.IP
Expand Down
168 changes: 129 additions & 39 deletions tools/sslsniff.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
# GnuTLS and NSS
# For Linux, uses BCC, eBPF.
#
# USAGE: sslsniff.py [-h] [-p PID] [-c COMM] [-o] [-g] [-d]
# USAGE: sslsniff.py [-h] [-p PID] [-u UID] [-x] [-c COMM] [-o] [-g] [-n] [-d]
# [--hexdump] [--max-buffer-size SIZE]
#
# Licensed under the Apache License, Version 2.0 (the "License")
#
Expand All @@ -23,17 +24,23 @@
examples = """examples:
./sslsniff # sniff OpenSSL and GnuTLS functions
./sslsniff -p 181 # sniff PID 181 only
./sslsniff -u 1000 # sniff only UID 1000
./sslsniff -c curl # sniff curl command only
./sslsniff --no-openssl # don't show OpenSSL calls
./sslsniff --no-gnutls # don't show GnuTLS calls
./sslsniff --no-nss # don't show NSS calls
./sslsniff --hexdump # show data as hex instead of trying to decode it as UTF-8
./sslsniff -x # show process UID and TID
"""
parser = argparse.ArgumentParser(
description="Sniff SSL data",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=examples)
parser.add_argument("-p", "--pid", type=int, help="sniff this PID only.")
parser.add_argument("-u", "--uid", type=int, default=None,
help="sniff this UID only.")
parser.add_argument("-x", "--extra", action="store_true",
help="show extra fields (UID, TID)")
parser.add_argument("-c", "--comm",
help="sniff only commands matching string.")
parser.add_argument("-o", "--no-openssl", action="store_false", dest="openssl",
Expand All @@ -48,41 +55,67 @@
help=argparse.SUPPRESS)
parser.add_argument("--hexdump", action="store_true", dest="hexdump",
help="show data as hexdump instead of trying to decode it as UTF-8")
parser.add_argument('--max-buffer-size', type=int, default=8192,
help='Size of captured buffer')
args = parser.parse_args()


prog = """
#include <linux/ptrace.h>
#include <linux/sched.h> /* For TASK_COMM_LEN */
#define MAX_BUF_SIZE __MAX_BUF_SIZE__
struct probe_SSL_data_t {
u64 timestamp_ns;
u32 pid;
char comm[TASK_COMM_LEN];
char v0[464];
u32 tid;
u32 uid;
u32 len;
int buf_filled;
char comm[TASK_COMM_LEN];
u8 buf[MAX_BUF_SIZE];
};
#define BASE_EVENT_SIZE ((size_t)(&((struct probe_SSL_data_t*)0)->buf))
#define EVENT_SIZE(X) (BASE_EVENT_SIZE + ((size_t)(X)))
BPF_PERCPU_ARRAY(ssl_data, struct probe_SSL_data_t, 1);
BPF_PERF_OUTPUT(perf_SSL_write);
int probe_SSL_write(struct pt_regs *ctx, void *ssl, void *buf, int num) {
int ret;
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = pid_tgid;
u32 uid = bpf_get_current_uid_gid();
FILTER
PID_FILTER
UID_FILTER
struct probe_SSL_data_t *data = ssl_data.lookup(&zero);
if (!data)
return 0;
struct probe_SSL_data_t __data = {0};
__data.timestamp_ns = bpf_ktime_get_ns();
__data.pid = pid;
__data.len = num;
data->timestamp_ns = bpf_ktime_get_ns();
data->pid = pid;
data->tid = tid;
data->uid = uid;
data->len = num;
data->buf_filled = 0;
bpf_get_current_comm(&data->comm, sizeof(data->comm));
u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)num);
bpf_get_current_comm(&__data.comm, sizeof(__data.comm));
if (buf != 0)
ret = bpf_probe_read_user(data->buf, buf_copy_size, buf);
if ( buf != 0) {
bpf_probe_read_user(&__data.v0, sizeof(__data.v0), buf);
}
if (!ret)
data->buf_filled = 1;
else
buf_copy_size = 0;
perf_SSL_write.perf_submit(ctx, &__data, sizeof(__data));
perf_SSL_write.perf_submit(ctx, data, EVENT_SIZE(buf_copy_size));
return 0;
}
Expand All @@ -94,47 +127,74 @@
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = (u32)pid_tgid;
u32 uid = bpf_get_current_uid_gid();
FILTER
PID_FILTER
UID_FILTER
bufs.update(&tid, (u64*)&buf);
return 0;
}
int probe_SSL_read_exit(struct pt_regs *ctx, void *ssl, void *buf, int num) {
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = (u32)pid_tgid;
u32 uid = bpf_get_current_uid_gid();
int ret;
FILTER
PID_FILTER
UID_FILTER
u64 *bufp = bufs.lookup(&tid);
if (bufp == 0) {
if (bufp == 0)
return 0;
}
struct probe_SSL_data_t __data = {0};
__data.timestamp_ns = bpf_ktime_get_ns();
__data.pid = pid;
__data.len = PT_REGS_RC(ctx);
int len = PT_REGS_RC(ctx);
if (len <= 0) // read failed
return 0;
struct probe_SSL_data_t *data = ssl_data.lookup(&zero);
if (!data)
return 0;
bpf_get_current_comm(&__data.comm, sizeof(__data.comm));
data->timestamp_ns = bpf_ktime_get_ns();
data->pid = pid;
data->tid = tid;
data->uid = uid;
data->len = (u32)len;
data->buf_filled = 0;
u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len);
if (bufp != 0) {
bpf_probe_read_user(&__data.v0, sizeof(__data.v0), (char *)*bufp);
}
bpf_get_current_comm(&data->comm, sizeof(data->comm));
if (bufp != 0)
ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp);
bufs.delete(&tid);
perf_SSL_read.perf_submit(ctx, &__data, sizeof(__data));
if (!ret)
data->buf_filled = 1;
else
buf_copy_size = 0;
perf_SSL_read.perf_submit(ctx, data, EVENT_SIZE(buf_copy_size));
return 0;
}
"""

if args.pid:
prog = prog.replace('FILTER', 'if (pid != %d) { return 0; }' % args.pid)
prog = prog.replace('PID_FILTER', 'if (pid != %d) { return 0; }' % args.pid)
else:
prog = prog.replace('FILTER', '')
prog = prog.replace('PID_FILTER', '')

if args.uid is not None:
prog = prog.replace('UID_FILTER', 'if (uid != %d) { return 0; }' % args.uid)
else:
prog = prog.replace('UID_FILTER', '')

prog = prog.replace('__MAX_BUF_SIZE__', str(args.max_buffer_size))

if args.debug or args.ebpf:
print(prog)
Expand Down Expand Up @@ -179,14 +239,15 @@
fn_name="probe_SSL_read_exit", pid=args.pid or -1)

# define output data structure in Python
TASK_COMM_LEN = 16 # linux/sched.h
MAX_BUF_SIZE = 464 # Limited by the BPF stack


# header
print("%-12s %-18s %-16s %-7s %-6s" % ("FUNC", "TIME(s)", "COMM", "PID",
"LEN"))
header = "%-12s %-18s %-16s %-7s %-6s" % ("FUNC", "TIME(s)", "COMM", "PID", "LEN")

if args.extra:
header += " %-7s %-7s" % ("UID", "TID")

print(header)
# process event
start = 0

Expand All @@ -202,6 +263,16 @@ def print_event_read(cpu, data, size):
def print_event(cpu, data, size, rw, evt):
global start
event = b[evt].event(data)
if event.len <= args.max_buffer_size:
buf_size = event.len
else:
buf_size = args.max_buffer_size

if event.buf_filled == 1:
buf = bytearray(event.buf[:buf_size])
else:
buf_size = 0
buf = b""

# Filter events by command
if args.comm:
Expand All @@ -216,19 +287,38 @@ def print_event(cpu, data, size, rw, evt):

e_mark = "-" * 5 + " END DATA " + "-" * 5

truncated_bytes = event.len - MAX_BUF_SIZE
truncated_bytes = event.len - buf_size
if truncated_bytes > 0:
e_mark = "-" * 5 + " END DATA (TRUNCATED, " + str(truncated_bytes) + \
" bytes lost) " + "-" * 5

fmt = "%-12s %-18.9f %-16s %-7d %-6d\n%s\n%s\n%s\n\n"
base_fmt = "%(func)-12s %(time)-18.9f %(comm)-16s %(pid)-7d %(len)-6d"

if args.extra:
base_fmt += " %(uid)-7d %(tid)-7d"

fmt = ''.join([base_fmt, "\n%(begin)s\n%(data)s\n%(end)s\n\n"])
if args.hexdump:
unwrapped_data = binascii.hexlify(event.v0)
data = textwrap.fill(unwrapped_data.decode('utf-8', 'replace'),width=32)
unwrapped_data = binascii.hexlify(buf)
data = textwrap.fill(unwrapped_data.decode('utf-8', 'replace'), width=32)
else:
data = event.v0.decode('utf-8', 'replace')
print(fmt % (rw, time_s, event.comm.decode('utf-8', 'replace'),
event.pid, event.len, s_mark, data, e_mark))
data = buf.decode('utf-8', 'replace')

fmt_data = {
'func': rw,
'time': time_s,
'comm': event.comm.decode('utf-8', 'replace'),
'pid': event.pid,
'tid': event.tid,
'uid': event.uid,
'len': event.len,
'begin': s_mark,
'end': e_mark,
'data': data
}

print(fmt % fmt_data)


b["perf_SSL_write"].open_perf_buffer(print_event_write)
b["perf_SSL_read"].open_perf_buffer(print_event_read)
Expand Down
9 changes: 8 additions & 1 deletion tools/sslsniff_example.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,26 +105,33 @@ characters.

USAGE message:

usage: sslsniff.py [-h] [-p PID] [-c COMM] [-o] [-g] [-n] [-d] [--hexdump]
usage: sslsniff.py [-h] [-p PID] [-u UID] [-x] [-c COMM] [-o] [-g] [-n] [-d]
[--hexdump] [--max-buffer-size MAX_BUFFER_SIZE]

Sniff SSL data

optional arguments:
-h, --help show this help message and exit
-p PID, --pid PID sniff this PID only.
-u UID, --uid UID sniff this UID only.
-x, --extra show extra fields (UID, TID)
-c COMM, --comm COMM sniff only commands matching string.
-o, --no-openssl do not show OpenSSL calls.
-g, --no-gnutls do not show GnuTLS calls.
-n, --no-nss do not show NSS calls.
-d, --debug debug mode.
--hexdump show data as hexdump instead of trying to decode it as
UTF-8
--max-buffer-size MAX_BUFFER_SIZE
Size of captured buffer

examples:
./sslsniff # sniff OpenSSL and GnuTLS functions
./sslsniff -p 181 # sniff PID 181 only
./sslsniff -u 1000 # sniff only UID 1000
./sslsniff -c curl # sniff curl command only
./sslsniff --no-openssl # don't show OpenSSL calls
./sslsniff --no-gnutls # don't show GnuTLS calls
./sslsniff --no-nss # don't show NSS calls
./sslsniff --hexdump # show data as hex instead of trying to decode it as UTF-8
./sslsniff -x # show process UID and TID

0 comments on commit 91a7983

Please sign in to comment.