Back to Home

Diagnosing Heartbleed errors in OpenSSL. (The final diagnosis has not yet been made, although treatment is already in full swing)

Translator's Preface Starting to translate this article · I assumed that its author figured out the problem. However · as some Habr users have correctly shown (thanks to VBart) · not everything is so ...

Diagnosing Heartbleed errors in OpenSSL. (The final diagnosis has not yet been made, although treatment is already in full swing)

Original author: Sean Cassidy (ex509)
  • Transfer
Translator's Preface
Начиная переводить данную статью, я предполагал, что её автор разобрался в проблеме.
Однако, как правильно показали некоторые пользователи Хабра (спасибо VBart), не всё так просто и упоминание автором malloc, mmap и sbrk ещё более его запутало.
В связи с эти статья представляет больше исторический интерес, нежели технический.
Update Автор обновил свой пост в том же ключе, в котором шло обсуждение в коментариях к этому переводу.


When I wrote about the error in GnuTLS , I said that this is not the last heavy error in the TLS stack that we will see. However, I did not expect that everything would be so deplorable.

A bug in Heartbleed is a particularly nasty bug. It allows an attacker to read up to 64 KB of memory, and security researchers say:

Without using any confidential information or credentials, we were able to steal the secret keys used for our X.509 certificates, usernames and passwords, instant messages, e-mail and important business documents and communications.


Bug


The fix begins here at ssl / d1_both.c :

intdtls1_process_heartbeat(SSL *s){          
    unsignedchar *p = &s->s3->rrec.data[0], *pl;
    unsignedshort hbtype;
    unsignedint payload;
    unsignedint padding = 16; /* Use minimum padding */


So, first we get a pointer to the data in the SSLv3 entry, which looks like this:
typedefstructssl3_record_st
    {int type;               /* type of record */unsignedint length;    /* How many bytes available */unsignedint off;       /* read/write offset into 'buf' */unsignedchar *data;    /* pointer to the record data */unsignedchar *input;   /* where the decode bytes are */unsignedchar *comp;    /* only used with decompression - malloc()ed */unsignedlong epoch;    /* epoch number, needed by DTLS1 */unsignedchar seq_num[8]; /* sequence number, needed by DTLS1 */
    } SSL3_RECORD;


The structure describing the records contains the type, length and data. Back to dtls1_process_heartbeat :

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

Translator's note: code n2s (c, s);
#define n2s(c,s)	((s=(((unsigned int)(c[0]))<< 8)| \
			    (((unsigned int)(c[1]))    )),c+=2)



The first byte of an SSLv3 entry is a heartbeat type. The n2s macro takes two bytes from p and puts them in payload . It is in fact the length ( the length ) of useful data. Please note that the actual length in the SSLv3 record is not checked.
Then the pl variable receives the “heartbeat” data provided by the requestor.
The following happens in the function:

unsignedchar *buffer, *bp;
int r;
/* Allocate memory for the response, size is 1 byte
 * message type, plus 2 bytes payload length, plus
 * payload, plus padding
 *//* Выделение памяти для ответа, размером в 
 * 1 байт под тип сообщения, плюс 2 байта - под длину полезной нагрузки,
 * плюс полезная нагрузка, плюс заполнение
 */
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;


As much memory is allocated as the requestor asked: up to 65535 + 1 + 2 + 16, to be precise.
The bp variable is a pointer used to access this memory. Then:

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

Translator's note about memcpy
НАЗВАНИЕ

memcpy — копирует область памяти
СИНТАКСИС

#include<string.h>void *memcpy(void *dest, constvoid *src, size_t n);

ОПИСАНИЕ

Функция memcpy() копирует n байтов из области памяти src в область памяти dest. Области памяти не могут пересекаться. Используйте memmove(3), если области памяти перекрываются.
ВОЗВРАЩАЕМЫЕ ЗНАЧЕНИЯ

Функция memcpy() возвращает указатель на dest.
СООТВЕТСТВИЕ СТАНДАРТАМ

SVID 3, BSD 4.3, ISO 9899


The s2n macro does the opposite of the n2s macro : takes a 16-bit value and puts it in two bytes. Then it sets the same requested payload length.
Translator's note: code s2n (c, s);
#define s2n(s,c)	((c[0]=(unsigned char)(((s)>> 8)&0xff), \
			  c[1]=(unsigned char)(((s)    )&0xff)),c+=2)



Then, payload bytes from pl , the data provided by the user, are copied to the newly allocated bp array . After that, all this is sent back to the user.
So where is the mistake?

The user controls the payload and pl



What if the requester did not actually send payload bytes, as it should?
What if pl actually contains only one byte?
Then memcpy will read from memory everything that was not far from the SSLv3 record.

And apparently there are a lot of different things nearby.

There are two ways memory is allocated dynamically using malloc (at least on Linux): using sbrk (2) and using mmap (2) . If memory is allocated sbrk, then the old heap-grows-up rules are used, which limits what can be found with it, although using several queries (especially simultaneous ones) you can still find some interesting things. [This section initially contained my skepticism about PoC due to the nature of how the heap works through sbrk. However, many readers reminded me that mmap can be used in malloc instead , and that changes everything. Thank!]

Update from the author - this part of the original article has been removed
Однако, если используется mmap, «Ставки сделаны!». Для mmap может быть выделена любая неиспользуемая память. Это — цель большинства атак, направленных против Heartbleed.

И самое главное: чем больше ваш запрашиваемый блок, тем больше вероятность, что он будет обслужен mmap, а не sbrk.

Операционные системы, которые не используют mmap для реализации malloc скорее всего, чуть менее уязвимы.


The location of bp doesn't matter at all, actually. Location pl , however, is of the utmost importance. Memory for it is almost certainly allocated using sbrk () due to the mmap threshold in malloc (). However, memory for interesting materials (for example, documents or user information) is very likely to be allocated by mmap () and can be accessed from pl . Several simultaneous queries will also make some interesting data available.

So what does this mean? Well, the memory allocation models for pl dictate to us what we can read. Here is what one of the discoverers of the vulnerability said about this:

Heap memory allocation models make private key compromise unlikely # heartbleed # dontpanic.

- Neil Mehta (@ neelmehta) April 8, 2014


Correction


The most important part of the fix is ​​this:

/* Read type and payload length first */if (1 + 2 + 16 > s->s3->rrec.length)
    return0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return0; /* silently discard per RFC 6520 sec. 4 */
pl = p;


This code does two things: the first check stops the “heartbeats” of zero length.
The second if does a check to make sure the actual record length is long enough. Like this.

The lessons


What can we learn from this?

I'm a C fan. It was my first programming language, and it was the first language that I was comfortable using for professional purposes. But now I see its limitations more clearly than ever before.

After Heartbleed and the GnuTLS bug, I think we should do three things:

  • Pay money for a security audit of elements of a critical security infrastructure such as OpenSSL.
  • Write a lot of unit- and integration tests for these libraries.
  • Start writing alternative implementations in safer languages.


Given how difficult it is to write safely in C, I see no other options.

Only registered users can participate in the survey. Please come in.

I would not regret for this effort. And you?

Read Next