Technical analysis of CVE-2014-0160 (Heartbleed) OOB read

Introduction

This will be a short technical analysis of CVE-2014-0160, or it’s well-known name OpenSSL Heartbleed.

Yes, it is a bit old… but the goal of this blog is not security advisory, it is to analyze past and present vulnerabilities for educational purposes, so let’s start…

In 2014, the bug was discovered by a team of security engineers (Riku, Antti and Matti) at Codenomicon and Neel Mehta of Google Security, who first reported it to the OpenSSL team.

The bug is very simple, it consists on an Out-Of-Bounds read due to an improper method to calculate the bytes that needs to be copied, trusting the size on a client arbitrary value, thus allowing an attacker to retrieve more bytes than the server should send, which coincides with internal process memory, like passwords, username, emails, sessions, cookies etc

Bug Analysis

The bug is located in the OpenSSL’s TLS Heartbeat extension, which is an utility to check if everything is okay by the sending of one connection end to the other an arbitrary data payload, which the other part must respond exactly the same.

struct
{
  HeartbeatMessageType type;
  uint16 payload_length;
  opaque payload[HeartbeatMessage.payload_length];
  opaque padding[padding_length]; 
} HeartbeatMessage;
That is the HeartbeatMessage structure that describes the Heartbeat message. It saves 1 byte for describing the type of hearbeat message, 2 bytes (uint16) for describing the payload length, then the heartbeat message content, and finally a padding. To deliver the Heartbeat message, we need to use a basic SSL/TLS communication block called SSL3_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 */  unsigned char seq_num[8]; /* sequence number, needed by DTLS1 */
       } SSL3_RECORD;

Ignoring the not-needed values to be analyzed, the important ones are the length and data.

The length contains the length of the total SSL3_RECORD data content, which is the Heartbeat message.

Let’s analyze the code that reads the client Heartbeat message:

/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;
As we can see, the type is saved in hbtype, then it uses the macro n2s. That macro writes the length of the heartbeat payload to the variable payload and increments the pointer by two bytes, making p a pointer to the starting of the Heartbeat data. Then pl will point there too. Response code:
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
As we can see, the response code first writes the type TLS1_HB_RESPONSE. Then by using the macro s2n it writes the payload length to memory and increments the buffer pointer by two bytes, making bp a pointer to the starting of the response Heartbeat message. It copies payload times, characters from pl (pointer to the request Heartbeat message) to bp (pointer to the response heartbeat message). But the most important thing, payload is still the same variable as in the read code, it means the payload is the client-arbitrary value. So it doesn’t mind how many characters the client sent, that the payload_lenght-bytes will be answered, thus leaking internal process memory.

Exploitation

The very simple PoC is crafting a Heartbeat message, with any valid type, and with a higher length than the payload data, described after it. The response will be your payload data and some bytes more they shouldn’t, that corresponds to internal values from the server memory after the buffer.

Patch

Some time after the vulnerability was reported, a patch appeared for OpenSSL 1.0.1g.

As we can see in the patch there is a new check in the read code:

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;

Patch: https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=731f431497f463f3a2a97236fe0187b11c44aead

Leave a Reply