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