Heartbleed Bug Explained

As you may have heard, there is a new OpenSSL bug out there, and its a bad one. This isn't one of those bugs or hacks that you hear about in the news and safely ignore like you always do. It affects around 66% of all internet servers out there, which likely includes a website that you frequent, or have sensitive information on.

How it Works

So what is the bug exactly? To describe the bug, we need to understand only a few parts of the OpenSSL implementation. The struct below contains the SSL record:

typedef struct ssl3_record_st
    {
/*r */    int type;               /* type of record */
/*rw*/    unsigned int length;    /* How many bytes available */
/*r */    unsigned int off;       /* read/write offset into 'buf' */
/*rw*/    unsigned char *data;    /* pointer to the record data */
/*rw*/    unsigned char *input;   /* where the decode bytes are */
/*r */    unsigned char *comp;    /* only used with decompression - malloc()ed */
/*r */  unsigned long epoch;    /* epoch number, needed by DTLS1 */
/*r */  PQ_64BIT seq_num;       /* sequence number, needed by DTLS1 */
/*rw*/    unsigned int orig_len;  /* How many bytes were available before padding
                   was removed? This is used to implement the
                   MAC check in constant time for CBC records.
                 */
    } SSL3_RECORD;

This struct is used in the dtls1_process_heartbeat(SSL *s) method in the d1_both.c file to handle the heartbeats that keep the SSL connection alive. This method accesses the record's data in the struct, as shown below:

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 */
    hbtype = *p++;
    n2s(p, payload);
    pl = p;

The first byte of the SSL record is the type of heartbeat received, and the macro n2s(c, l) just takes two bytes from p, and puts them in payload. These bytes are the length of the payload in the heartbeat. The important thing to notice here is that the length in the SSL record is not checked/verified. The variable pl now points to the heartbeat data, which was sent by the client.

The fact that the SSL record length was not checked is a big deal, since later in the same function, memory is allocated using that same payload length, shown in the code below:

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
 */
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;

Since payload was assigned the first two bytes of the heartbeat data, which is provided by the client, that means it can have a maximum value of 65535 (or 64k, when referring to bytes). So essentially we're allocating the amount of memory that the client wants us to (up to 64k), but its no big deal right? Allocating user-defined amounts of data isn't very harmful by itself (although its definitely not a good thing), but here it is harmful since later in the same function we copy that much memory in to our buffer:

s2n(payload, bp);
memcpy(bp, pl, payload);

Here, s2n(c, l) moves the payload size to the bp buffer (opposite of what n2s did), and memcpy copies payload number of bytes from pl to bp. This also doesn't seem like a big deal until you consider the actual size of pl. What if pl is only a few bytes large? Or even just one byte? An attacker could have told us the payload was 64kb, when it was only 1 byte. So where would the rest of the memory be copied from if pl is much smaller than expected? Surrounding memory, which may have been recently freed from the same process.

There is potentially a lot of sensitive data in the surrounding memory, like usernames, passwords, and even private keys. This has a lot of people worried since an attacker could continually send malicious heartbeats to a server and keep getting back 64kb of its memory each time. Then a simple search for keywords like 'password' or 'credit card' and voila, the attacker stole your data without anyone ever knowing. Mark Loman, a security researcher, has already shown this attack is possible with Yahoo Mail.

Lucky for us though, the private key is most likely safe, as Neel Mehta (one of the researchers who discovered the bug) points out:

Sean Cassidy provides some very helpful insight in to these heap allocation methods:

There are two ways memory is dynamically allocated with malloc (at least on Linux): using sbrk(2) and using mmap(2). If the memory is allocated with sbrk, then it uses the old heap-grows-up rules and limits what can be found with this, although multiple requests (especially simultaneously) could still find some fun stuff1.

The allocations for bp don't matter at all, actually. The allocation for pl, however, matters a great deal. It's almost certainly allocated with sbrk because of the mmap threshold in malloc. However, interesting stuff (like documents or user info), is very likely to be allocated with mmap and might be reachable from pl. Multiple simultaneous requests will also make some interesting data available.

How its Fixed

Since this bug is so simple to execute, that means it is also simple to fix. OpenSSL has already issued an update, the 1.0.1g release. It contains the following fix:

/* 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 */
pl = p;

As you can see, the differences are the length checks, which first ensures the heartbeat is not empty (length 0), and second, checks that the heartbeat payload matches the length provided by the user. Its that simple. Four lines of code are now protecting over 66% of servers from the catastrophic Heartbleed bug.

How its Patched

Do you run a public-facing server that uses SSL? Then you should definitely patch it. I can only assume someone out there has already set up a war-dialer that checks every server it can for this bug and then exploits those that have it. So if you're running Ubuntu, execute the following:

sudo apt-get update
sudo apt-get install -y libssl1.0.0 openssl

# Verify that the Build Date is April 7th 2014 or later
openssl version -a

Then be sure to restart any and all services that use OpenSSL. If you aren't sure that the patch worked, just use this tool to find out.