summaryrefslogtreecommitdiff
path: root/src/sslkeylog.py
blob: 9e00e212289c4240151b78e5634e0016e6196d28 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#!/usr/bin/env python
r'''
Extract SSL/DTLS keys from programs that use OpenSSL.

Example usage, attach to an existing process, put keys in premaster.txt:

  PYTHONPATH=. \
  gdb -q -ex  'py import sslkeylog as skl; skl.start("premaster.txt")' \
    -p `pidof curl`

Run a new program while outputting keys to the default stderr
(you can set envvar SSLKEYLOGFILE to override this):

  PYTHONPATH=. gdb -q -ex 'py import sslkeylog as skl; skl.start()' \
      -ex r --args curl https://example.com


Recommended configuration: copy this file to ~/.gdb/sslkeylog.py and put
the following in your ~/.gdbinit:

    python
    import sys, os.path
    #sys.dont_write_bytecode = True # avoid *.pyc clutter
    sys.path.insert(0, os.path.expanduser('~/.gdb'))
    import sslkeylog as skl
    # Override default keylog (SSLKEYLOGFILE env or stderr)
    skl.keylog_filename = '/tmp/premaster.txt'
    end

Then you can simply execute:

    gdb -q -ex 'py skl.start()' -p `pidof curl`

To stop capturing keys, detach GDB or invoke 'skl.stop()'
'''

import gdb
from os import getenv

# Default filename for new Keylog instances.
keylog_filename = getenv('SSLKEYLOGFILE', '/dev/stderr')
_SSL_KEYLOG_HEADER = b'# Automatically generated by sslkeylog.py\n'

def _read_as_hex(value, size):
    addr = value.address
    data = gdb.selected_inferior().read_memory(addr, size)
    return b''.join(b'%02X' % ord(x) for x in data)

def _ssl_get_master_key(ssl_ptr):
    session = ssl_ptr['session']
    if session != 0 and session['master_key_length'] > 0:
        return _read_as_hex(session['master_key'], 48)
    return None

def get_keylog_line(ssl_ptr):
    '''
    Returns (client_random, master_key) for the current SSL session.
    '''
    mk = _ssl_get_master_key(ssl_ptr)
    s3 = ssl_ptr['s3']
    if s3 == 0 or mk is None:
        return

    cr = _read_as_hex(s3['client_random'], 32)
    # Maybe optimize storage by using Session ID if available?
    #sid = _read_as_hex(self.ssl_ptr['session']['session_id'], 32)
    return (cr, mk)

class SKLFinishBreakpoint(gdb.FinishBreakpoint):
    '''Breaks on points where new key material is possibly available.'''
    def __init__(self, ssl_ptr, key_listener):
        # Mark as internal, it is expected to be gone as soon as this quits.
        gdb.FinishBreakpoint.__init__(self, internal=True)
        self.ssl_ptr = ssl_ptr
        self.key_listener = key_listener

    def stop(self):
        # Attempt to recover key material.
        info = get_keylog_line(self.ssl_ptr)
        if info:
            # Line consists of a cache key and actual key log line
            self.key_listener.notify(*info)
        return False # Continue execution

class SKLBreakpoint(gdb.Breakpoint):
    '''Breaks at function entrance and registers a finish breakpoint.'''
    def __init__(self, spec, key_listener):
        gdb.Breakpoint.__init__(self, spec)
        self.key_listener = key_listener

    def stop(self):
        # Retrieve SSL* parameter.
        ssl_ptr = gdb.selected_frame().read_var('s')

        # Proceed with handshakes (finish function) before checking for keys.
        SKLFinishBreakpoint(ssl_ptr, self.key_listener)
        # Increase hit count for debugging (info breakpoints)
        # This number will be decremented when execution continues.
        self.ignore_count += 1
        return False # Continue execution

class Keylog(object):
    '''Listens for new key material and writes them to a file.'''
    def __init__(self, keylog_file):
        self.keylog_file = keylog_file
        # Remember written lines to avoid printing duplicates.
        self.written_items = set()

    def notify(self, client_random, master_key):
        '''Puts a new entry in the key log if not already known.'''
        if client_random not in self.written_items:
            line = b'CLIENT_RANDOM %s %s\n' % (client_random, master_key)
            self.keylog_file.write(line)

            # Assume client random is random enough as cache key.
            cache_key = client_random
            self.written_items.add(cache_key)

    def close():
        self.keylog_file.close()

    @classmethod
    def create(cls, filename):
        def needs_header(f):
            try:
                # Might fail for pipes (such as stdin).
                return f.tell() == 0
            except:
                return False

        # Byte output is needed for unbuffered I/O
        f = open(filename, 'ab', 0)
        if needs_header(f):
            f.write(_SSL_KEYLOG_HEADER)
        return cls(f)

# A shared Keylog instance.
_keylog_file = None

def start(sslkeylogfile=None, cont=True):
    '''
    :param sslkeylogfile: optional SSL keylog file name (overrides
    SSLKEYLOGFILE environment variable and its fallback value).
    :param cont: True to continue this process when paused.
    '''
    global keylog_filename
    if sslkeylogfile:
        keylog_filename = sslkeylogfile
    enable()

    # Continue the process when it was already started before.
    if cont and gdb.selected_thread():
        gdb.execute('continue')

def stop():
    '''Remove all breakpoints and close the key logfile.'''
    global _keylog_file
    if not _keylog_file:
        print('No active keylog session')
        return
    disable()
    _keylog_file.close()
    print('Logged %d entries in total' % _keylog_file.written_items)
    _keylog_file = None


# Remember enabled breakpoints
_locations = { name: None for name in (
    'SSL_connect',
    'SSL_do_handshake',
    'SSL_accept',
    'SSL_read',
    'SSL_write',
)}

def enable():
    '''Enable all SSL-related breakpoints.'''
    global _keylog_file
    if not _keylog_file:
        _keylog_file = Keylog.create(keylog_filename)
        print('Started logging SSL keys to %s' % keylog_filename)
    for name, breakpoint in _locations.items():
        if breakpoint:
            print('Breakpoint for %s is already active, ignoring' % name)
            continue
        _locations[name] = SKLBreakpoint(name, _keylog_file)

def disable():
    '''Disable all SSL-related breakpoints.'''
    for name, breakpoint in _locations.items():
        if breakpoint:
            msg = 'Deleting breakpoint %d' % breakpoint.number
            msg += ' (%s)' % breakpoint.location
            if breakpoint.hit_count > 0:
                msg += ' (called %d times)' % breakpoint.hit_count
            print(msg)
            breakpoint.delete()
            _locations[name] = None