Eyebleed. A technical analysis of the fix (not the bug!) for the …
Heartbleed (CVE-2014-0160) is a bug found in OpenSSL due to a misimplementation of the Heartbeat functionality, and found by Neel Mehta (Google Security Team)
A lot of people (Existentialize, IOActive, etc.) already provided an analysis of the CVE-2014-0160 bug, widely known under the name “Heartbleed”, but nobody has analyzed the fix yet.
I had a look at both the code and the RFC, and it raised few questions to me.
1. The fix (and the original code)
We can see below a copy of the fixed-version of the dtls1_process_heartbeat() function.
-
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 */
-
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;
-
if (hbtype == TLS1_HB_REQUEST)
-
unsigned char *buffer, *bp;
-
unsigned int write_length = 1 /* heartbeat type */ +
-
2 /* heartbeat length */ +
-
payload + padding;
-
int r;
-
if (write_length > SSL3_RT_MAX_PLAIN_LENGTH)
-
return 0;
-
/* Allocate memory for the response, size is 1 byte
-
* message type, plus 2 bytes payload length, plus
-
* payload, plus padding
-
*/
-
buffer = OPENSSL_malloc(write_length);
-
bp = buffer;
-
/* Enter response type, length and copy payload */
-
*bp++ = TLS1_HB_RESPONSE;
-
s2n(payload, bp);
-
memcpy(bp, pl, payload); < ==== The initial bug was here, because it was copying “too-much” data from the source buffer.
-
bp += payload;
-
/* Random padding */
-
RAND_pseudo_bytes(bp, padding);
-
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, write_length);
-
if (r >= 0 && s->msg_callback)
-
s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
-
buffer, write_length,
-
s, s->msg_callback_arg);
-
OPENSSL_free(buffer);
-
if (r < 0)
-
return r;
1.1 Misleading comments & undefined constants
I found the function so hard to read that it looked like it was obfuscated JavaScript code, especially with all those undefined constants everywhere and misleading comments. For instance, what is described as “heartbeat length” is in fact the “payload length of the heartbeat message” but what intrigued me the most was the static constant for “padding length” (mentioned as “padding” grrr.)
1.2 Padding Length
After a look at the RFC 6520 “Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS) Heartbeat Extension”, I didn’t see any point mentioning that padding length was supposed to be a have static size (16) as done in the code. On the contrary, it is quite confusing to understand who is supposed to take care of it.
1.2.1 RFC 6520. Heartbeat messages
RFC 6520: Therefore, the padding_length is TLSPlaintext.length – payload_length – 3 for TLS and
DTLSPlaintext.length – payload_length – 3 for DTLS. The padding_length MUST be at least 16.
The sender of a HeartbeatMessage MUST use a random padding of at
least 16 bytes. The padding of a received HeartbeatMessage message
MUST be ignored.The Heartbeat protocol messages consist of their type and an arbitrary payload and padding.
1.2.2 My understanding
My understanding is that there are two types of messages:
HeartbeatResponse
HeartbeatRequest
And that *both* have a padding, including the padding of a received HeartbeatMessage message MUST be ignored. But by that, I understand that the RFC it means the content and not the field itself.
This is my I concluded that OpenSSL should not be the one deciding of the size of the padding buffer, which therefore would have prevented to allocate a buffer and to use memcpy().
Below is my pseudo-code interpretation of the RFC 6520 and what the code should be:
-
typedef struct heartbeat_msg_st
-
/*
-
RFC 6520: The total length of a HeartbeatMessage MUST NOT exceed 2^14 or
-
max_fragment_length when negotiated as defined in [RFC6066].
-
*/
-
char type;
-
unsigned short payload_length;
-
unsigned char payload[1]; /* Any size array. RFC 6520 only mentions “arbitrary content” which is (probably?) assumed to be non-empty. */
-
unsigned char padding[16]; /* The padding_length MUST be at least 16. */
-
HEARTBEAT_MSG;
-
int
-
dtls1_process_heartbeat(SSL *s)
-
HEARTBEAT_MSG *hb_msg = &s->s3->rrec.data[0];
-
char hb_type;
-
unsigned short payload_length;
-
unsigned int padding_length;
-
unsigned int hb_msg_length = s->s3->rrec.length;
-
if (s->msg_callback)
-
s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
-
hb_msg, hb_msg_length,
-
s, s->msg_callback_arg);
-
if (hb_msg_length < sizeof(HEARTBEAT_MSG)) return 0; // silently discard.
-
padding_length = hb_msg_length – hb_msg->payload_length – sizeof(payload_length) – sizeof(hb_type);
-
if (padding_length < 16) return 0; // silently discard.
-
hb_type = hb_msg->type;
-
payload_length = hb_msg->payload_length;
-
if (hb_type == TLS1_HB_REQUEST)
-
unsigned char *padding = (unsigned char *)&hb_msg;
-
padding = &padding[hb_msg_length – padding_length];
-
if (hb_msg_length > SSL3_RT_MAX_PLAIN_LENGTH) return 0;
-
hb_msg->type = TLS1_HB_RESPONSE;
-
/* Random padding */
-
RAND_pseudo_bytes(padding, padding_length);
-
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, hb_msg, hb_msg_length);
-
if (r >= 0 && s->msg_callback)
-
s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
-
hb_msg, hb_msg_length,
-
s, s->msg_callback_arg);
-
if (r < 0) return r;
2. Any thoughts ?
Reading a RFC can be hard, including widely deployed code such as OpenSSL. I’d be curious to hear (@msuiche) your interpretation of the RFC.
Source:
Eyebleed. A technical analysis of the fix (not the bug!) for the …