📜 ⬆️ ⬇️

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

Translator's Preface
Starting to translate this article, I assumed that the author understood the problem.
However, as some users of Habr have rightly shown (thanks to VBart ), not everything is so simple and the mention by the author of malloc, mmap and sbrk confused him even more.
In connection with this article is of more historical interest than technical.
Update The author updated his post in the same way as the discussion in the comments to this translation.


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

Error 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, email and important business documents and communication.


Bug


The fix starts here in ssl / d1_both.c :

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 */ 


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


The structure that describes the records contains type, length, and data. Let's go 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 the SSLv3 entry is a type of heartbeat. The macro n2s takes two bytes from p and puts them in payload . This is actually the length of the payload. Please note that the actual length in the SSLv3 entry is not checked.
The variable pl then receives the “heartbeat” data provided by the requester.
Further, the following occurs in the function:

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


Allocated as much memory as requested by the requester: up to 65535 + 1 + 2 + 16, to be precise.
The variable bp is the 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); 

Memcpy Translator's Note
TITLE

memcpy - copies memory area
SYNTAX

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

DESCRIPTION

The memcpy () function copies n bytes from the src memory area to the dest memory area. Memory areas cannot intersect. Use memmove (3) if memory areas overlap.
RETURN VALUES

The memcpy () function returns a pointer to dest.
COMPLIANCE

SVID 3, BSD 4.3, ISO 9899


The macro s2n does the opposite of the macro n2s : it takes a 16-bit value and puts it in two bytes. It then 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 the payload bytes from pl , the user-supplied data are copied to the newly allocated bp array. After that, all this is sent back to the user.
So where is the mistake?

User controls payload and pl



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

And, apparently, there are many different things nearby.

There are two ways to allocate memory dynamically using malloc (at least in Linux): using sbrk (2) and using mmap (2) . If sbrk is allocated memory , the old heap-grows-up rules are used, which limits what can be found with it, although using several queries (especially simultaneous) 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 have reminded me that mmap can be used instead in malloc , and that changes everything. Thank!]

Update from the author - this part is removed from the original article.
However, if mmap is used, “Bets are made!”. Any unused memory can be allocated to mmap . This is the goal of most Heartbleed attacks.

And the most important thing: the more your requested block, the more likely it will be served by mmap , and not sbrk .

Operating systems that do not use mmap to implement malloc are likely to be slightly less vulnerable.


The location of bp doesn’t really matter at all. The location of pl , however, is of paramount importance. Memory for it is almost certainly allocated with 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 to mmap () and may be available from pl . Several simultaneous queries will also make available some interesting data.

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

Heap memory allocation models make the compromise of a private key 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) 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; 


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

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 his limitations more clearly than ever before.

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



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

Source: https://habr.com/ru/post/218691/


All Articles