This article is more than 1 year old

Anatomy of OpenSSL's Heartbleed: Just four bytes trigger horror bug

The code behind the C-bomb dropped on the world

Analysis The password-leaking OpenSSL bug dubbed Heartbleed is so bad, switching off the internet for a while sounds like a good plan.

A tiny flaw in the widely used encryption library allows anyone to trivially and secretly dip into vulnerable systems, from your bank's HTTPS server to your private VPN, to steal passwords, login cookies, private crypto-keys and much more.

How, in 2014, is this possible?

A simple script for the exploit engine Metasploit can, in a matter of seconds, extract sensitive in-memory data from systems that rely on OpenSSL 1.0.1 to 1.0.1f for TLS encryption. The bug affects about 500,000, or 17.5 per cent, of trusted HTTPS websites, we're told, as well as client software, email servers, chat services, and anything else using the aforementioned versions of OpenSSL.

A good number of popular web services have now been patched following disclosure of the vulnerability on Monday; you can use this tool to check (use at your own risk, of course), but don't forget to do more than patch your OpenSSL installation if you're affected – change your keys, dump your session cookies and evaluate your at-risk data.

Too long, didn't read: A summary

This serious flaw (CVE-2014-0160) is a missing bounds check before a memcpy() call that uses non-sanitized user input as the length parameter. An attacker can trick OpenSSL into allocating a 64KB buffer, copy more bytes than is necessary into the buffer, send that buffer back, and thus leak the contents of the victim's memory, 64KB at a time. The patch is here, and the blunder is far worse than Apple's gotofail.

The TLS heartbeat

The bug lies in OpenSSL's implementation of the TLS heartbeat extension: it's a keep-alive feature in which one end of the connection sends a payload of arbitrary data to the other end, which sends back an exact copy of that data to prove everything's OK. The heartbeat message, according to the official standard, looks like this, in C:

struct
{
  HeartbeatMessageType type;
  uint16 payload_length;
  opaque payload[HeartbeatMessage.payload_length];
  opaque padding[padding_length]; 
} HeartbeatMessage;

This HeartbeatMessage arrives via an SSL3_RECORD structure, a basic building block of SSL/TLS communications. The key fields in SSL3_RECORD are given below; length is how many bytes are in the received HeartbeatMessage and data is a pointer to that HeartbeatMessage.

struct ssl3_record_st
{
  unsigned int length;    /* How many bytes available */
  [...]
  unsigned char *data;    /* pointer to the record data */
  [...]
} SSL3_RECORD;

So just to be clear, the SSL3 record's data points to the start of the received HeartbeatMessage and length is the number of bytes in the received HeartbeatMessage. Meanwhile, inside the received HeartbeatMessage, payload_length is the number of bytes in the arbitrary payload that has to be sent back.

Whoever sends a HeartbeatMessage controls the payload_length but as we will see, this is never checked against the parent SSL3_RECORD's length field, allowing an attacker to overrun memory.

The diagram below shows how the attack works:

The Register illustration of the attack

Click to enlarge ... note: this does not include the padding bytes

In the above example, an attacker sends a four-byte HeartbeatMessage including a single byte payload, which is correctly acknowledged by the SSL3's length record. But the attacker lies in the payload_length field to claim the payload is 65535 bytes in size. The victim ignores the SSL3 record, and reads 65535 bytes from its own memory, starting from the received HeartbeatMessage payload, and copies it into a suitably sized buffer to send back to the attacker. It thus hoovers up far too many bytes, dangerously leaking information as indicated above in red.

Show me the code

The broken OpenSSL code that processes the incoming HeartbeatMessage looks like this, where p is a pointer to the start of the message:

/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;

So the message type is popped into the hbtype variable, the pointer is incremented by one byte, and the n2s() macro writes the 16-bit length of the heartbeat payload to the variable payload and increments the pointer by two bytes. Then pl becomes a pointer to the contents of the payload.

Let's say a heartbeat message with a payload_length of 65535, ie: a heartbeat with a 64KB payload, the maximum possible, is received. The code has to send back a copy of the incoming HeartbeatMessage, so it allocates a buffer big enough to hold the 64KB payload plus one byte to store the message type, two bytes to store the payload length, and some padding bytes, as per the above structure.

It constructs the reply HeartbeatMessage structure with the following code, where bp is a pointer to the start of the reply HeartbeatMessage:

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

So the code writes the response type to the start of the buffer, increments the buffer pointer, uses the s2n() macro to write the 16-bit payload length to memory and increment the buffer pointer by two bytes, and then it copies payload number of bytes from the received payload into the outgoing payload for the reply.

Remember, payload is controlled by the attacker, and it's quite large at 64KB. If the actual HeartbeatMessage sent by the attacker only has a payload of, say, one byte, and its payload_length is a lie, then the above memcpy() will read beyond the end of the received HeartbeatMessage and start reading from the victim process's memory.

And this memory will contain other juicy information, such as passwords or decrypted messages from other clients. Sending another heartbeat message leaks another 64KB, so rinse and repeat to scour the victim's system for goodies.

In fact, the bug leaks this sort of information, although we understand Yahoo! has since patched its systems:

The fix

The patch in OpenSSL 1.0.1g is essentially a bounds check, using the correct record length in the SSL3 structure (s3->rrec) that described the incoming HeartbeatMessage.

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;

OpenSSL's implementation of TLS heartbeats was committed to the project's source code 61 minutes to midnight on Saturday, 31 December, 2011. What we're experiencing now is the mother of all delayed hangovers. ®

Bootnote

There was some confusion over exactly how many bytes were leaked by Heartbleed, given that the maximum TLS record length is 16KB. However, this situation being the clusterf*ck that it is, OpenSSL will happily spaff 64KB back to you if you ask nicely.

More about

More about

More about

TIP US OFF

Send us news


Other stories you might like