Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/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 
     16 from __future__ import print_function
     17 import ctypes as ct
     18 from bcc import BPF
     19 import argparse
     20 
     21 # arguments
     22 examples = """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 """
     30 parser = argparse.ArgumentParser(
     31     description="Sniff SSL data",
     32     formatter_class=argparse.RawDescriptionHelpFormatter,
     33     epilog=examples)
     34 parser.add_argument("-p", "--pid", type=int, help="sniff this PID only.")
     35 parser.add_argument("-c", "--comm",
     36                     help="sniff only commands matching string.")
     37 parser.add_argument("-o", "--no-openssl", action="store_false", dest="openssl",
     38                     help="do not show OpenSSL calls.")
     39 parser.add_argument("-g", "--no-gnutls", action="store_false", dest="gnutls",
     40                     help="do not show GnuTLS calls.")
     41 parser.add_argument("-n", "--no-nss", action="store_false", dest="nss",
     42                     help="do not show NSS calls.")
     43 parser.add_argument('-d', '--debug', dest='debug', action='count', default=0,
     44                     help='debug mode.')
     45 parser.add_argument("--ebpf", action="store_true",
     46                     help=argparse.SUPPRESS)
     47 args = parser.parse_args()
     48 
     49 
     50 prog = """
     51 #include <linux/ptrace.h>
     52 #include <linux/sched.h>        /* For TASK_COMM_LEN */
     53 
     54 struct 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 
     62 BPF_PERF_OUTPUT(perf_SSL_write);
     63 
     64 int 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 
     83 BPF_PERF_OUTPUT(perf_SSL_read);
     84 
     85 BPF_HASH(bufs, u32, u64);
     86 
     87 int 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 
     95 int 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 
    122 if args.pid:
    123     prog = prog.replace('FILTER', 'if (pid != %d) { return 0; }' % args.pid)
    124 else:
    125     prog = prog.replace('FILTER', '')
    126 
    127 if args.debug or args.ebpf:
    128     print(prog)
    129     if args.ebpf:
    130         exit()
    131 
    132 
    133 b = 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 #
    139 if 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 
    147 if 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 
    155 if 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
    170 TASK_COMM_LEN = 16  # linux/sched.h
    171 MAX_BUF_SIZE = 464  # Limited by the BPF stack
    172 
    173 
    174 # Max size of the whole struct: 512 bytes
    175 class 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
    186 print("%-12s %-18s %-16s %-6s %-6s" % ("FUNC", "TIME(s)", "COMM", "PID",
    187                                        "LEN"))
    188 
    189 # process event
    190 start = 0
    191 
    192 
    193 def print_event_write(cpu, data, size):
    194     print_event(cpu, data, size, "WRITE/SEND")
    195 
    196 
    197 def print_event_read(cpu, data, size):
    198     print_event(cpu, data, size, "READ/RECV")
    199 
    200 
    201 def 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 
    228 b["perf_SSL_write"].open_perf_buffer(print_event_write)
    229 b["perf_SSL_read"].open_perf_buffer(print_event_read)
    230 while 1:
    231     b.perf_buffer_poll()
    232