446 lines
14 KiB
Python
Executable File
446 lines
14 KiB
Python
Executable File
#!/usr/bin/python
|
|
#
|
|
# sslsniff Captures data on read/recv or write/send functions of OpenSSL,
|
|
# GnuTLS and NSS
|
|
# For Linux, uses BCC, eBPF.
|
|
#
|
|
# USAGE: sslsniff.py [-h] [-p PID] [-u UID] [-x] [-c COMM] [-o] [-g] [-n] [-d]
|
|
# [--hexdump] [--max-buffer-size SIZE] [-l] [--handshake]
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
#
|
|
# 12-Aug-2016 Adrian Lopez Created this.
|
|
# 13-Aug-2016 Mark Drayton Fix SSL_Read
|
|
# 17-Aug-2016 Adrian Lopez Capture GnuTLS and add options
|
|
#
|
|
|
|
from __future__ import print_function
|
|
from bcc import BPF
|
|
import argparse
|
|
import binascii
|
|
import textwrap
|
|
import os.path
|
|
|
|
# arguments
|
|
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
|
|
./sslsniff -l # show function latency
|
|
./sslsniff -l --handshake # show SSL handshake latency
|
|
./sslsniff --extra-lib openssl:/path/libssl.so.1.1 # sniff extra library
|
|
"""
|
|
|
|
|
|
def ssllib_type(input_str):
|
|
valid_types = frozenset(['openssl', 'gnutls', 'nss'])
|
|
|
|
try:
|
|
lib_type, lib_path = input_str.split(':', 1)
|
|
except ValueError:
|
|
raise argparse.ArgumentTypeError("Invalid SSL library param: %r" % input_str)
|
|
|
|
if lib_type not in valid_types:
|
|
raise argparse.ArgumentTypeError("Invalid SSL library type: %r" % lib_type)
|
|
|
|
if not os.path.isfile(lib_path):
|
|
raise argparse.ArgumentTypeError("Invalid library path: %r" % lib_path)
|
|
|
|
return lib_type, lib_path
|
|
|
|
|
|
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",
|
|
help="do not show OpenSSL calls.")
|
|
parser.add_argument("-g", "--no-gnutls", action="store_false", dest="gnutls",
|
|
help="do not show GnuTLS calls.")
|
|
parser.add_argument("-n", "--no-nss", action="store_false", dest="nss",
|
|
help="do not show NSS calls.")
|
|
parser.add_argument('-d', '--debug', dest='debug', action='count', default=0,
|
|
help='debug mode.')
|
|
parser.add_argument("--ebpf", action="store_true",
|
|
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')
|
|
parser.add_argument("-l", "--latency", action="store_true",
|
|
help="show function latency")
|
|
parser.add_argument("--handshake", action="store_true",
|
|
help="show SSL handshake latency, enabled only if latency option is on.")
|
|
parser.add_argument("--extra-lib", type=ssllib_type, action='append',
|
|
help="Intercept calls from extra library (format: lib_type:lib_path)")
|
|
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;
|
|
u64 delta_ns;
|
|
u32 pid;
|
|
u32 tid;
|
|
u32 uid;
|
|
u32 len;
|
|
int buf_filled;
|
|
int rw;
|
|
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_rw);
|
|
|
|
BPF_HASH(start_ns, u32);
|
|
BPF_HASH(bufs, u32, u64);
|
|
|
|
int probe_SSL_rw_enter(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();
|
|
u64 ts = bpf_ktime_get_ns();
|
|
|
|
PID_FILTER
|
|
UID_FILTER
|
|
|
|
bufs.update(&tid, (u64*)&buf);
|
|
start_ns.update(&tid, &ts);
|
|
return 0;
|
|
}
|
|
|
|
static int SSL_exit(struct pt_regs *ctx, int rw) {
|
|
int ret;
|
|
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();
|
|
u64 ts = bpf_ktime_get_ns();
|
|
|
|
PID_FILTER
|
|
UID_FILTER
|
|
|
|
u64 *bufp = bufs.lookup(&tid);
|
|
if (bufp == 0)
|
|
return 0;
|
|
|
|
u64 *tsp = start_ns.lookup(&tid);
|
|
if (tsp == 0)
|
|
return 0;
|
|
|
|
int len = PT_REGS_RC(ctx);
|
|
if (len <= 0) // no data
|
|
return 0;
|
|
|
|
struct probe_SSL_data_t *data = ssl_data.lookup(&zero);
|
|
if (!data)
|
|
return 0;
|
|
|
|
data->timestamp_ns = ts;
|
|
data->delta_ns = ts - *tsp;
|
|
data->pid = pid;
|
|
data->tid = tid;
|
|
data->uid = uid;
|
|
data->len = (u32)len;
|
|
data->buf_filled = 0;
|
|
data->rw = rw;
|
|
u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len);
|
|
|
|
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);
|
|
start_ns.delete(&tid);
|
|
|
|
if (!ret)
|
|
data->buf_filled = 1;
|
|
else
|
|
buf_copy_size = 0;
|
|
|
|
perf_SSL_rw.perf_submit(ctx, data, EVENT_SIZE(buf_copy_size));
|
|
return 0;
|
|
}
|
|
|
|
int probe_SSL_read_exit(struct pt_regs *ctx) {
|
|
return (SSL_exit(ctx, 0));
|
|
}
|
|
|
|
int probe_SSL_write_exit(struct pt_regs *ctx) {
|
|
return (SSL_exit(ctx, 1));
|
|
}
|
|
|
|
BPF_PERF_OUTPUT(perf_SSL_do_handshake);
|
|
|
|
int probe_SSL_do_handshake_enter(struct pt_regs *ctx, void *ssl) {
|
|
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
u32 pid = pid_tgid >> 32;
|
|
u32 tid = (u32)pid_tgid;
|
|
u64 ts = bpf_ktime_get_ns();
|
|
|
|
PID_FILTER
|
|
UID_FILTER
|
|
|
|
start_ns.update(&tid, &ts);
|
|
return 0;
|
|
}
|
|
|
|
int probe_SSL_do_handshake_exit(struct pt_regs *ctx) {
|
|
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();
|
|
u64 ts = bpf_ktime_get_ns();
|
|
int ret;
|
|
|
|
PID_FILTER
|
|
UID_FILTER
|
|
|
|
u64 *tsp = start_ns.lookup(&tid);
|
|
if (tsp == 0)
|
|
return 0;
|
|
|
|
ret = PT_REGS_RC(ctx);
|
|
if (ret <= 0) // handshake failed
|
|
return 0;
|
|
|
|
struct probe_SSL_data_t *data = ssl_data.lookup(&zero);
|
|
if (!data)
|
|
return 0;
|
|
|
|
data->timestamp_ns = ts;
|
|
data->delta_ns = ts - *tsp;
|
|
data->pid = pid;
|
|
data->tid = tid;
|
|
data->uid = uid;
|
|
data->len = ret;
|
|
data->buf_filled = 0;
|
|
data->rw = 2;
|
|
bpf_get_current_comm(&data->comm, sizeof(data->comm));
|
|
start_ns.delete(&tid);
|
|
|
|
perf_SSL_do_handshake.perf_submit(ctx, data, EVENT_SIZE(0));
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
if args.pid:
|
|
prog = prog.replace('PID_FILTER', 'if (pid != %d) { return 0; }' % args.pid)
|
|
else:
|
|
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)
|
|
if args.ebpf:
|
|
exit()
|
|
|
|
|
|
b = BPF(text=prog)
|
|
|
|
# It looks like SSL_read's arguments aren't available in a return probe so you
|
|
# need to stash the buffer address in a map on the function entry and read it
|
|
# on its exit (Mark Drayton)
|
|
#
|
|
def attach_openssl(lib):
|
|
b.attach_uprobe(name=lib, sym="SSL_write",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="SSL_write",
|
|
fn_name="probe_SSL_write_exit", pid=args.pid or -1)
|
|
b.attach_uprobe(name=lib, sym="SSL_read",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="SSL_read",
|
|
fn_name="probe_SSL_read_exit", pid=args.pid or -1)
|
|
if args.latency and args.handshake:
|
|
b.attach_uprobe(name="ssl", sym="SSL_do_handshake",
|
|
fn_name="probe_SSL_do_handshake_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name="ssl", sym="SSL_do_handshake",
|
|
fn_name="probe_SSL_do_handshake_exit", pid=args.pid or -1)
|
|
|
|
def attach_gnutls(lib):
|
|
b.attach_uprobe(name=lib, sym="gnutls_record_send",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="gnutls_record_send",
|
|
fn_name="probe_SSL_write_exit", pid=args.pid or -1)
|
|
b.attach_uprobe(name=lib, sym="gnutls_record_recv",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="gnutls_record_recv",
|
|
fn_name="probe_SSL_read_exit", pid=args.pid or -1)
|
|
|
|
def attach_nss(lib):
|
|
b.attach_uprobe(name=lib, sym="PR_Write",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="PR_Write",
|
|
fn_name="probe_SSL_write_exit", pid=args.pid or -1)
|
|
b.attach_uprobe(name=lib, sym="PR_Send",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="PR_Send",
|
|
fn_name="probe_SSL_write_exit", pid=args.pid or -1)
|
|
b.attach_uprobe(name=lib, sym="PR_Read",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="PR_Read",
|
|
fn_name="probe_SSL_read_exit", pid=args.pid or -1)
|
|
b.attach_uprobe(name=lib, sym="PR_Recv",
|
|
fn_name="probe_SSL_rw_enter", pid=args.pid or -1)
|
|
b.attach_uretprobe(name=lib, sym="PR_Recv",
|
|
fn_name="probe_SSL_read_exit", pid=args.pid or -1)
|
|
|
|
|
|
LIB_TRACERS = {
|
|
"openssl": attach_openssl,
|
|
"gnutls": attach_gnutls,
|
|
"nss": attach_nss,
|
|
}
|
|
|
|
|
|
if args.openssl:
|
|
attach_openssl("ssl")
|
|
if args.gnutls:
|
|
attach_gnutls("gnutls")
|
|
if args.nss:
|
|
attach_nss("nspr4")
|
|
|
|
|
|
if args.extra_lib:
|
|
for lib_type, lib_path in args.extra_lib:
|
|
LIB_TRACERS[lib_type](lib_path)
|
|
|
|
# define output data structure in Python
|
|
|
|
|
|
# header
|
|
header = "%-12s %-18s %-16s %-7s %-7s" % ("FUNC", "TIME(s)", "COMM", "PID", "LEN")
|
|
|
|
if args.extra:
|
|
header += " %-7s %-7s" % ("UID", "TID")
|
|
|
|
if args.latency:
|
|
header += " %-7s" % ("LAT(ms)")
|
|
|
|
print(header)
|
|
# process event
|
|
start = 0
|
|
|
|
def print_event_rw(cpu, data, size):
|
|
print_event(cpu, data, size, "perf_SSL_rw")
|
|
|
|
def print_event_handshake(cpu, data, size):
|
|
print_event(cpu, data, size, "perf_SSL_do_handshake")
|
|
|
|
def print_event(cpu, data, size, 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:
|
|
if not args.comm == event.comm.decode('utf-8', 'replace'):
|
|
return
|
|
|
|
if start == 0:
|
|
start = event.timestamp_ns
|
|
time_s = (float(event.timestamp_ns - start)) / 1000000000
|
|
|
|
lat_str = "%.3f" % (event.delta_ns / 1000000) if event.delta_ns else "N/A"
|
|
|
|
s_mark = "-" * 5 + " DATA " + "-" * 5
|
|
|
|
e_mark = "-" * 5 + " END DATA " + "-" * 5
|
|
|
|
truncated_bytes = event.len - buf_size
|
|
if truncated_bytes > 0:
|
|
e_mark = "-" * 5 + " END DATA (TRUNCATED, " + str(truncated_bytes) + \
|
|
" bytes lost) " + "-" * 5
|
|
|
|
base_fmt = "%(func)-12s %(time)-18.9f %(comm)-16s %(pid)-7d %(len)-6d"
|
|
|
|
if args.extra:
|
|
base_fmt += " %(uid)-7d %(tid)-7d"
|
|
|
|
if args.latency:
|
|
base_fmt += " %(lat)-7s"
|
|
|
|
fmt = ''.join([base_fmt, "\n%(begin)s\n%(data)s\n%(end)s\n\n"])
|
|
if args.hexdump:
|
|
unwrapped_data = binascii.hexlify(buf)
|
|
data = textwrap.fill(unwrapped_data.decode('utf-8', 'replace'), width=32)
|
|
else:
|
|
data = buf.decode('utf-8', 'replace')
|
|
|
|
rw_event = {
|
|
0: "READ/RECV",
|
|
1: "WRITE/SEND",
|
|
2: "HANDSHAKE"
|
|
}
|
|
|
|
fmt_data = {
|
|
'func': rw_event[event.rw],
|
|
'time': time_s,
|
|
'lat': lat_str,
|
|
'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
|
|
}
|
|
|
|
# use base_fmt if no buf filled
|
|
if buf_size == 0:
|
|
print(base_fmt % fmt_data)
|
|
else:
|
|
print(fmt % fmt_data)
|
|
|
|
b["perf_SSL_rw"].open_perf_buffer(print_event_rw)
|
|
b["perf_SSL_do_handshake"].open_perf_buffer(print_event_handshake)
|
|
while 1:
|
|
try:
|
|
b.perf_buffer_poll()
|
|
except KeyboardInterrupt:
|
|
exit()
|