Heartbleed: Your data is just a heartbeat away

On April 1st, 2014 Neel Mehta of Google’s security team reported a new vulnerability in the widely used OpenSSL library. By exploiting his finding, he was able to read out random bits of data currently held in the memory of the affected systems. This could include usernames and passwords, but also private keys used for decrypting the communication between the server and its clients. At the time, he wasn’t fully aware of it, but his discovery would go down in history as one of the most notorious software bugs of all time – the Heartbleed Bug.
Hard-Facts
What is OpenSSL?
OpenSSL is an open source software library implementing its own version of the SSL/TLS (Secure Socket Layer/Transport Layer Security) protocol stack, as well as a suite for common ciphers like AES, Blowfish, DES or RC4, hash functions like MD5 and SHA-1 and public-key cryptography like RSA, Elliptic curve or Diffie-Hellman key exchange. The core library is written in C and provides basic cryptographic functionality. Available wrappers also enable the use of OpenSSL in a variety of other programming languages.
OpenSSL also includes a command line tool with a lot of helpful crypto features. For example, if you want to quickly generate a new RSA private key with AES256 and a bit-length of 2048, you could use openssl
with the genrsa
option:
openssl genrsa -aes256 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
.+++++
........................................+++++
e is 65537 (0x010001)
Enter pass phrase: *********
Verifying - Enter pass phrase: *********
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,3E432E9F98F2874AB376B8F3B2302ADC
4gUrz8G7qS/bDDXDY9gZcaN0yZGwrDSpjs+27linxzRsjnLGGTkzWRYhVZjjMdhp
/Forobx6c5eHYzFFfnppy4qJlMooyfBXJV0/Jzt72AdKRWffEu2M4tk5rcv5KnWa
...
-----END RSA PRIVATE KEY-----
On December 23, 1998, OpenSSL was released as version 0.9.1c of the former SSLeay library. In the aftermath of the Heartbleed Bug, however, several people were dissatisfied with its review process and the overall quality of the code. This led to the development of two major forks, one managed by members of the OpenBSD project, and one under the roof of Google. LibreSSL (OpenBSD) cut the code base in half, resulting in 90,000 less C code and overall 150,000 fewer lines of content². BoringSSL (Google) is now widely used in the Chrome browser and the Android OS.
The Heartbeat Extension
Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS) are encryption protocols used to enable secure communication between two parties over the internet. TLS handles the part of reliable, DTLS that of unreliable transport protocols. For both, however, it is a non-essential task to keep a connection alive. Reliable protocols (e.g. TCP) need to constantly send data packages to maintain an active connection, otherwise, the session will time out after a predefined period of time. Unreliable protocols (e.g. UDP), on the other hand, usually have no mechanism for session management. Therefore, the only way to figure out if the other peer is still available is to perform a data-intensive renegotiation.
To provide a less resource consuming alternative, the concept of a new protocol was introduced in RFC 6520 in February 2012. This was the birth of the Heartbeat protocol.
The Heartbeat Extension provides a new protocol for TLS/DTLS allowing the usage of keep-alive functionality without performing a renegotiation and a basis for path MTU (PMTU) discovery for DTLS.
https://tools.ietf.org/html/rfc6520
Heartbleed and its main actors
The core component of the Heartbeat protocol is the HeartbeatMessage:
HeartbeatMessage
struct {
HeartbeatMessageType type;
uint16 payload_length;
opaque payload[HeartbeatMessage.payload_length];
opaque padding[padding_length];
} HeartbeatMessage;
A HeartbeatMessage must occur either in the form of a HeartbeatRequest or a HeartbeatResponse. The HeartbeatMessegeType has the length of 1 byte and is a simple enumeration with the two possible variants heartbeat_request (type = 1) or heartbeat_response (type = 2):
HeartbeatMessageType
enum {
heartbeat_request(1),
heartbeat_response(2),
(255)
} HeartbeatMessageType;
The payload can have any arbitrary value. To verify that the other peer is still alive, the content of the request message’s payload gets copied into the response message, sent back to the source and is eventually evaluated against its initial content. To avoid collisions and false positives, it is therefore important to choose a unique value for the payload content.
The padding must be a random value with a minimum size of 16 bytes. Its only purpose is to visibly complete the message – the actual value not relevant as it gets ignored by both parties.
Since the receiving side does not know the exact length of the padding, and therefore cannot know where the payload ends and the padding begins, it is necessary to send additional information in the payload_length field. As this is defined as an unsigned 16-bit integer (2 bytes = 65,535 as the highest possible number), the maximum size of the array holding the payload is limited to 65,535 bytes or 64 kilobytes.
Hey Bob, are you still there?
Let’s assume Alice wants to know if Bob is still able to receive further data via the established connection. Therefore, she sends a benign HeartbeatRequest to Bob and starts waiting for the matching HeartbeatResponse.

To process the HeartbeatMessage Bob can either use the “t1_lib.c” (TLS) or “d1_both.c” (DTLS) Iibrary. To really understand what went wrong in the original implementation of those libraries, it is best to take a look at the source code.
At the following you can see version 1.0.1f of the dtls1_process_heartbeat
function (d1_both.c / DTLS):
d1_both.c
int dtls1_process_heartbeat(SSL *s) {
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */
/* Read type and payload length first */
[1] hbtype = *p++;
[2] n2s(p, payload);
[3] pl = p;
Bob reads the first byte of the SSL message to determine the type of the received message [1]. To get the payload_length as a numeric value, Bob then converts the next two bytes of the message from network-byte order to host-byte order and stores the result in the payload
field [2]. Note that there is no further validation and the transmitted length is simply accepted as it is. At last, Bob sets variable pl
to point to the beginning of the actual payload data [3].
Not validating the payload_length is quite a big deal, since its value is later used to read the payload data for Bob’s HeartbeatResponse.
if (s->msg_callback)
s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
&s->s3->rrec.data[0], s->s3->rrec.length,
s, s->msg_callback_arg);
if (hbtype == TLS1_HB_REQUEST) {
unsigned char *buffer, *bp;
int r;
/* Allocate memory for the response, size is 1 byte
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
[4] buffer = OPENSSL_malloc(1 + 2 + payload + padding);
[5] bp = buffer;
Bob now allocates a part of his memory equal to the total size of the HeartbeatResponse. This means he needs to reserves 1 byte for the type, 2 bytes for the numeric value of the payload_length, plus the actual space for the payload and the closing padding, but does not yet fill it with any data [4]. The variable bp
is set to point to the beginning of the newly allocated buffer [5].
C is considered a low-level programming language, which is not memory-safe by design. Allocating memory for an unvalidated, user-controlled number of bytes is poor coding, but by itself not yet the devastating security vulnerability we were looking for.
[6] *bp++ = TLS1_HB_RESPONSE;
[7] s2n(payload, bp);
[8] 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);
if (r >= 0 && s->msg_callback)
s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
buffer, 3 + payload + padding,
s, s->msg_callback_arg);
OPENSSL_free(buffer);
if (r tlsext_hb_seq) {
dtls1_stop_timer(s);
s->tlsext_hb_seq++;
s->tlsext_hb_pending = 0;
}
}
return 0;
}
After Bob has finished the initialization process, he fills the empty buffer with the content of the response message. He sets the first byte to represent the type value of a HeartbeatResponse [6]. Then he converts the 2 bytes for the numeric payload_length back from host-byte order to network-byte order and appends them to the buffer [7]. Up to this point, nothing surprising happened. Bob now uses the C library function memcpy
to copy the number of bytes specified in payload_length from the memory area of the payload data to the buffer [8]. At last, he adds a new random padding and sends the final HeartbeatResponse back to Alice.

What could possibly go wrong?
Bob, naive as he is, simply trusted Alice’ input value from the payload_length and used it as the basis for reading out the data for his response message. What happens if Alice decides to forge her own malicious HeartbeatRequest with a payload that has less data than stated in the payload_length?

In the example illustrated above, Alice sent Bob a HeartbeatRequest with a payload_length of 8, but a payload consisting only of 4 bytes. Bob copies the original 4 bytes of the payload plus 4 additional bytes from the surrounding memory to the HeartbeatResponse. Since the payload_length field is limited to 65,535, Alice would be able to extract a maximum of 64 kilobytes per HeartbeatResponse from Bob’s system. If she repeats this a couple of times she might be lucky and receive some of his secret information.
Don’t trust Alice!
To resolve the Heartbleed disaster, version 1.0.1g of the OpenSSL library was released on April 7, 2014. This ensured that Alice could no longer read memory areas from Bob by sending bogus payloads.
d1_both.c
int dtls1_process_heartbeat(SSL * s) {
unsigned char * p = & s - > s3 - > rrec.data[0], * pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */
if (s - > msg_callback)
s - > msg_callback(0, s - > version, TLS1_RT_HEARTBEAT, &
s - > s3 - > rrec.data[0], s - > s3 - > rrec.length,
s, s - > msg_callback_arg);
/* Read type and payload length first */
[9] if (1 + 2 + 16 > s - > s3 - > rrec.length)
return 0; /* silently discard */
hbtype = * p++;
n2s(p, payload);
[10] if (1 + 2 + payload + 16 > s - > s3 - > rrec.length)
return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;
...
First of all, this fix added two lines to drop empty request messages before starting to process them. If the request does not have more than 1 byte for the type, 2 bytes for the payload_length and 16 bytes for the minimum padding, it is safe to say that the payload must be empty [9]. Furthermore, the necessary check to validate the payload_length against the actual payload data was finally included [10].
Conclusion
The lack of input validation has always been one of the leading causes of software vulnerabilities. Such mistakes are easily overlooked, especially if the code is not part of a proper review process. After the Heartbleed Bug, the developers of OpenSSL had to admit this as well. Thus the rest of the existing code got audited in the following months and 20 additional vulnerabilities were fixed in 2014 alone.
