Anatomy of a data leakage bug – the OpenSSL “heartbleed” buffer overflow


An information disclosure vulnerability has been found, and promptly patched, in OpenSSL.

OpenSSL is a very widely used encryption library, responsible for putting the S in HTTPS, and the padlock in the address bar, for many websites.

The bug only exists in the OpenSSL 1.0.1 source code (from version 1.0.1 to 1.0.1f inclusive), because the faulty code relates to a fairly new feature known as the TLS Heartbeat Extension.

The heartbeat extension was first documented in RFC 6520 in February 2012.

TLS heartbeats are used as “keep alive” packets so that the ends of an encrypted connection can agree to keep the session open even when they don’t have any official data to exchange.

Because the heartbeats consist of a reply and a matching response, they allow either end to confirm not only that the session is open, but also that end-to-end connectivity is working properly.

Sending heartbeat requests

The RFC 6520 standard explicitly restricts the maxium size of a heartbeat request to 214 bytes (16KBytes), but OpenSSL itself generates far shorter requests.

Don’t worry if you don’t understand C; but if you do, the OpenSSL heartbeat request code looks like this:

unsigned int payload = 18; /* Sequence number + random bytes */
unsigned int padding = 16; /* Use minimum padding */

/* Check if padding is too long, payload and padding
* must not exceed 2^14 - 3 = 16381 bytes in total.

OPENSSL_assert(payload + padding <= 16381);

/* Create HeartBeat message, we just use a sequence number
 * as payload to distuingish different messages and add
 * some random stuff.
 *  - Message Type, 1 byte
 *  - Payload Length, 2 bytes (unsigned int)
 *  - Payload, the sequence number (2 bytes uint)
 *  - Payload, random bytes (16 bytes uint)
 *  - Padding

buf = OPENSSL_malloc(1 + 2 + payload + padding);
p = buf;
/* Message Type */
/* Payload length (18 bytes here) */
s2n(payload, p);
/* Sequence number */
s2n(s->tlsext_hb_seq, p);
/* 16 random bytes */
RAND_pseudo_bytes(p, 16);
p += 16;
/* Random padding */
RAND_pseudo_bytes(p, padding);

ret = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buf, 3 + payload + padding);

The reason that the code says that “payload and padding must not exceed 16381 bytes in total” is that the 16KByte (16384 byte) maximum heartbeat request size includes one byte to signal that this is a TLS1_HB_REQUEST, and two bytes to denote the length of the payload data in the request.

As the code stands, the OPENSSL_assert to verify that payload + padding <= 16381 is redundant, because the payload size is hard-wired to 18 bytes and the padding size to 16.

But the programmer has tried to do the right thing: put in the check anyway, in case someone changes those payload or padding sizes in the future without considering the consequences.

The code then transmits a heartbeat request consisting of:

  • The single byte 0x01 (denoting that this is a TLS1_HB_REQUEST).
  • Two bytes containing the 16-bit representation of 34 (size of payload plus padding).
  • Two bytes of payload consising of a 16-bit sequence number.
  • 16 bytes of random data making up the rest of the 18-byte payload.
  • 16 further random padding bytes, required by the standard.

Replying to heartbeat requests

When vulnerable versions of OpenSSL 1.0.1 respond to a heartbeat request, they aren’t quite so careful in processing the received data.

Heartbeat replies are supposed to contain a copy of the payload data from the request, as a way of verifying that the encrypted circuit is still working both ways.

It turns out that you can send a small heartbeat request, but sneakily set your payload length field to 0xFFFF (65535 bytes).

Then, OpenSSL will uncomplainingly copy 65535 bytes from your request packet, even though you didn’t send across that many bytes:

/* Allocate memory for the response, size is 1 byte
 * message type, plus 2 bytes payload length, plus
 * payload, plus padding
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;

/* Enter response type, length and copy payload */
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
/* Random padding */
RAND_pseudo_bytes(bp, padding);

r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);

That means OpenSSL runs off the end of your data and scoops up whatever else is next to it in memory at the other end of the connection, for a potential data leakage of approximately 64KB each time you send a malformed heartbeat request.

This bug has been rather melodramatically named “heartbleed,” for reasons that should now be obvious.

According to the Finnish National Cyber Security Centre, the sort of data that “bleeds” when the bug is triggered varies enormously, but may include message contents, user credentials, session keys and even copies of a server’s own private keys.

That’s not good!

Fixing the problem

Fortunately, there’s a fix already: simply upgrade to OpenSSL 1.0.1g.

If you don’t want to or can’t do that, you can rebuild your current version of OpenSSL from source without TLS Heartbeat support, by adding -DOPENSSL_NO_HEARTBEATS at compile time.

Both of these immunise you from this flaw.

The new OpenSSL version includes a bounds check to make sure the payload length you specified isn’t longer that the data you actually sent:

/* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
        return 0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
        return 0; /* silently discard per RFC 6520 sec. 4 */

The -DOPENSSL_NO_HEARTBEATS compile-time option simply omits the buggy code altogether from vulnerable versions.

The lessons to learn

If you’re a programmer, remember: always double-check the data that the other side sent you before you use it to control the operation of your own code.

And remember: buffer overflows should always be treated as serious, even if they don’t lead to remote code execution.

Data leakage bugs can be just as disastrous, as this flaw demonstrates.

For further information…

If you’d like to know more about the main sorts of vulnerablity, from RCE (remote code execution) through EoP (elevation of privilege) to Information Disclosure, please listen to our Techknow podcast, Understanding Vulnerabilities:

(Audio player not working? Listen on Soundcloud.)