1#!/usr/bin/env python 2# 3# sslsniff Captures data on read/recv or write/send functions of OpenSSL, 4# GnuTLS and NSS 5# For Linux, uses BCC, eBPF. 6# 7# USAGE: sslsniff.py [-h] [-p PID] [-c COMM] [-o] [-g] [-d] 8# 9# Licensed under the Apache License, Version 2.0 (the "License") 10# 11# 12-Aug-2016 Adrian Lopez Created this. 12# 13-Aug-2016 Mark Drayton Fix SSL_Read 13# 17-Aug-2016 Adrian Lopez Capture GnuTLS and add options 14# 15 16from __future__ import print_function 17import ctypes as ct 18from bcc import BPF 19import argparse 20 21# arguments 22examples = """examples: 23 ./sslsniff # sniff OpenSSL and GnuTLS functions 24 ./sslsniff -p 181 # sniff PID 181 only 25 ./sslsniff -c curl # sniff curl command only 26 ./sslsniff --no-openssl # don't show OpenSSL calls 27 ./sslsniff --no-gnutls # don't show GnuTLS calls 28 ./sslsniff --no-nss # don't show NSS calls 29""" 30parser = argparse.ArgumentParser( 31 description="Sniff SSL data", 32 formatter_class=argparse.RawDescriptionHelpFormatter, 33 epilog=examples) 34parser.add_argument("-p", "--pid", type=int, help="sniff this PID only.") 35parser.add_argument("-c", "--comm", 36 help="sniff only commands matching string.") 37parser.add_argument("-o", "--no-openssl", action="store_false", dest="openssl", 38 help="do not show OpenSSL calls.") 39parser.add_argument("-g", "--no-gnutls", action="store_false", dest="gnutls", 40 help="do not show GnuTLS calls.") 41parser.add_argument("-n", "--no-nss", action="store_false", dest="nss", 42 help="do not show NSS calls.") 43parser.add_argument('-d', '--debug', dest='debug', action='count', default=0, 44 help='debug mode.') 45parser.add_argument("--ebpf", action="store_true", 46 help=argparse.SUPPRESS) 47args = parser.parse_args() 48 49 50prog = """ 51#include <linux/ptrace.h> 52#include <linux/sched.h> /* For TASK_COMM_LEN */ 53 54struct probe_SSL_data_t { 55 u64 timestamp_ns; 56 u32 pid; 57 char comm[TASK_COMM_LEN]; 58 char v0[464]; 59 u32 len; 60}; 61 62BPF_PERF_OUTPUT(perf_SSL_write); 63 64int probe_SSL_write(struct pt_regs *ctx, void *ssl, void *buf, int num) { 65 u32 pid = bpf_get_current_pid_tgid(); 66 FILTER 67 68 struct probe_SSL_data_t __data = {0}; 69 __data.timestamp_ns = bpf_ktime_get_ns(); 70 __data.pid = pid; 71 __data.len = num; 72 73 bpf_get_current_comm(&__data.comm, sizeof(__data.comm)); 74 75 if ( buf != 0) { 76 bpf_probe_read(&__data.v0, sizeof(__data.v0), buf); 77 } 78 79 perf_SSL_write.perf_submit(ctx, &__data, sizeof(__data)); 80 return 0; 81} 82 83BPF_PERF_OUTPUT(perf_SSL_read); 84 85BPF_HASH(bufs, u32, u64); 86 87int probe_SSL_read_enter(struct pt_regs *ctx, void *ssl, void *buf, int num) { 88 u32 pid = bpf_get_current_pid_tgid(); 89 FILTER 90 91 bufs.update(&pid, (u64*)&buf); 92 return 0; 93} 94 95int probe_SSL_read_exit(struct pt_regs *ctx, void *ssl, void *buf, int num) { 96 u32 pid = bpf_get_current_pid_tgid(); 97 FILTER 98 99 u64 *bufp = bufs.lookup(&pid); 100 if (bufp == 0) { 101 return 0; 102 } 103 104 struct probe_SSL_data_t __data = {0}; 105 __data.timestamp_ns = bpf_ktime_get_ns(); 106 __data.pid = pid; 107 __data.len = PT_REGS_RC(ctx); 108 109 bpf_get_current_comm(&__data.comm, sizeof(__data.comm)); 110 111 if (bufp != 0) { 112 bpf_probe_read(&__data.v0, sizeof(__data.v0), (char *)*bufp); 113 } 114 115 bufs.delete(&pid); 116 117 perf_SSL_read.perf_submit(ctx, &__data, sizeof(__data)); 118 return 0; 119} 120""" 121 122if args.pid: 123 prog = prog.replace('FILTER', 'if (pid != %d) { return 0; }' % args.pid) 124else: 125 prog = prog.replace('FILTER', '') 126 127if args.debug or args.ebpf: 128 print(prog) 129 if args.ebpf: 130 exit() 131 132 133b = BPF(text=prog) 134 135# It looks like SSL_read's arguments aren't available in a return probe so you 136# need to stash the buffer address in a map on the function entry and read it 137# on its exit (Mark Drayton) 138# 139if args.openssl: 140 b.attach_uprobe(name="ssl", sym="SSL_write", fn_name="probe_SSL_write", 141 pid=args.pid or -1) 142 b.attach_uprobe(name="ssl", sym="SSL_read", fn_name="probe_SSL_read_enter", 143 pid=args.pid or -1) 144 b.attach_uretprobe(name="ssl", sym="SSL_read", 145 fn_name="probe_SSL_read_exit", pid=args.pid or -1) 146 147if args.gnutls: 148 b.attach_uprobe(name="gnutls", sym="gnutls_record_send", 149 fn_name="probe_SSL_write", pid=args.pid or -1) 150 b.attach_uprobe(name="gnutls", sym="gnutls_record_recv", 151 fn_name="probe_SSL_read_enter", pid=args.pid or -1) 152 b.attach_uretprobe(name="gnutls", sym="gnutls_record_recv", 153 fn_name="probe_SSL_read_exit", pid=args.pid or -1) 154 155if args.nss: 156 b.attach_uprobe(name="nspr4", sym="PR_Write", fn_name="probe_SSL_write", 157 pid=args.pid or -1) 158 b.attach_uprobe(name="nspr4", sym="PR_Send", fn_name="probe_SSL_write", 159 pid=args.pid or -1) 160 b.attach_uprobe(name="nspr4", sym="PR_Read", fn_name="probe_SSL_read_enter", 161 pid=args.pid or -1) 162 b.attach_uretprobe(name="nspr4", sym="PR_Read", 163 fn_name="probe_SSL_read_exit", pid=args.pid or -1) 164 b.attach_uprobe(name="nspr4", sym="PR_Recv", fn_name="probe_SSL_read_enter", 165 pid=args.pid or -1) 166 b.attach_uretprobe(name="nspr4", sym="PR_Recv", 167 fn_name="probe_SSL_read_exit", pid=args.pid or -1) 168 169# define output data structure in Python 170TASK_COMM_LEN = 16 # linux/sched.h 171MAX_BUF_SIZE = 464 # Limited by the BPF stack 172 173 174# Max size of the whole struct: 512 bytes 175class Data(ct.Structure): 176 _fields_ = [ 177 ("timestamp_ns", ct.c_ulonglong), 178 ("pid", ct.c_uint), 179 ("comm", ct.c_char * TASK_COMM_LEN), 180 ("v0", ct.c_char * MAX_BUF_SIZE), 181 ("len", ct.c_uint) 182 ] 183 184 185# header 186print("%-12s %-18s %-16s %-6s %-6s" % ("FUNC", "TIME(s)", "COMM", "PID", 187 "LEN")) 188 189# process event 190start = 0 191 192 193def print_event_write(cpu, data, size): 194 print_event(cpu, data, size, "WRITE/SEND") 195 196 197def print_event_read(cpu, data, size): 198 print_event(cpu, data, size, "READ/RECV") 199 200 201def print_event(cpu, data, size, rw): 202 global start 203 event = ct.cast(data, ct.POINTER(Data)).contents 204 205 # Filter events by command 206 if args.comm: 207 if not args.comm == event.comm: 208 return 209 210 if start == 0: 211 start = event.timestamp_ns 212 time_s = (float(event.timestamp_ns - start)) / 1000000000 213 214 s_mark = "-" * 5 + " DATA " + "-" * 5 215 216 e_mark = "-" * 5 + " END DATA " + "-" * 5 217 218 truncated_bytes = event.len - MAX_BUF_SIZE 219 if truncated_bytes > 0: 220 e_mark = "-" * 5 + " END DATA (TRUNCATED, " + str(truncated_bytes) + \ 221 " bytes lost) " + "-" * 5 222 223 fmt = "%-12s %-18.9f %-16s %-6d %-6d\n%s\n%s\n%s\n\n" 224 print(fmt % (rw, time_s, event.comm.decode('utf-8', 'replace'), 225 event.pid, event.len, s_mark, 226 event.v0.decode('utf-8', 'replace'), e_mark)) 227 228b["perf_SSL_write"].open_perf_buffer(print_event_write) 229b["perf_SSL_read"].open_perf_buffer(print_event_read) 230while 1: 231 try: 232 b.perf_buffer_poll() 233 except KeyboardInterrupt: 234 exit() 235