This post originally appeared on the Leaf SR blog on April 11th, 2014.
TL;DR: heartbleed is bad, but not world ending. OpenSSL is not any more vulnerable because of its freelists and would still be vulnerable without them.
We felt that there weren’t enough heartbleed write-ups yet, so we wrote another one. Unlike many of the other posts, we are not going to talk about the TLS protocol or why we think the heartbeat extension is pointless. Instead, we are going to focus on the bug itself and more specifically, why sensitive data gets leaked.
First we would like to state that, as far as complexity goes, the heartbleed vulnerability is nothing special, but that doesn’t mean it was easy to find. All bugs are easy to spot after someone else points them out to you. Hindsight is 20/20 after all. Riku, Antti and Matti at Codenomicon and Neel Mehta at Google all independently discovered this bug. Neel was also kind enough to sanity check this post before it went live (thank you Neel!) Whatever your feelings on vulnerability disclosure are, you should thank them for finding the bug and giving us all something interesting to talk about.
All of the code in this post is from openssl-1.0.1c, which is what I had running on an old virtual machine with Apache. First, let’s discuss some important OpenSSL data structures. For this discussion the ‘top’ level structure is SSL, which is defined in ssl.h as ssl_st with a typedef. Within this structure is a pointer s3 of type SSL3_STATE, which is a typedef of ssl3_state_st. Inside that structure is another structure of type SSL3_RECORD, which we reference as rrec. An SSL3_RECORD contains type and length values among other fields. The SSL3_STATE structure also contains rbuf, and wbuf of type SSL3_BUFFER. The SSL3_BUFFER structure contains a pointer to data buf, and len/offset/left members to track its usage. These buffers are allocated and deallocated often during a TLS exchange. For performance reasons, the OpenSSL developers wrote a separate freelist implementation to store them. This freelist implementation is apparently buggy but fairly easy to understand (more on this below). The freelist is actually two lists, one for read buffers (rbuf) and one for write buffers (wbuf). These lists are accessed via the SSL_CTX structure that contains wbuf_freelist and rbuf_freelist respectively. We reference the SSL_CTX structure via a pointer within the SSL structure. Heres some pseudo code to make this less confusing. Some variables have been cut for brevity.
struct SSL { SSL_CTX *ctx; SSL3_STATE *s3; } struct SSL_CTX { SSL3_BUF_FREELIST wbuf_freelist; SSL3_BUF_FREELIST rbuf_freelist; } struct SSL3_STATE { SSL3_BUFFER rbuf; /* read IO goes into here */ SSL3_BUFFER wbuf; /* write IO goes into here */ SSL3_RECORD rrec; /* each decoded record goes in here */ SSL3_RECORD wrec; /* goes out from here */ } struct SSL3_BUF_FREELIST { size_t chunklen; unsigned int len; SSL3_BUF_FREELIST_ENTRY *head; } struct SSL3_BUFFER { unsigned char *buf; /* at least SSL3_RT_MAX_PACKET_SIZE bytes, * see ssl3_setup_buffers() */ size_t len; /* buffer size */ int offset; /* where to 'copy from' */ int left; /* how many bytes left */ } struct SSL3_RECORD { /*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 */ }
2584 tls1_process_heartbeat(SSL *s) 2585 { 2586 unsigned char *p = &s->s3->rrec.data[0], *pl; 2587 unsigned short hbtype; 2588 unsigned int payload; 2589 unsigned int padding = 16; /* Use minimum padding */ 2590 2591 /* Read type and payload length first */ 2592 hbtype = *p++; 2593 n2s(p, payload); 2594 pl = p; 2595 2596 if (s->msg_callback) 2597 s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT, 2598 &s->s3->rrec.data[0], s->s3->rrec.length, 2599 s, s->msg_callback_arg); 2600 2601 if (hbtype == TLS1_HB_REQUEST) 2602 { 2603 unsigned char *buffer, *bp; 2604 int r; 2605 2606 /* Allocate memory for the response, size is 1 bytes 2607 * message type, plus 2 bytes payload length, plus 2608 * payload, plus padding 2609 */ 2610 buffer = OPENSSL_malloc(1 + 2 + payload + padding); 2611 bp = buffer; 2612 2613 /* Enter response type, length and copy payload */ 2614 *bp++ = TLS1_HB_RESPONSE; 2615 s2n(payload, bp); 2616 memcpy(bp, pl, payload); 2617 bp += payload; 2618 /* Random padding */ 2619 RAND_pseudo_bytes(bp, padding); 2620 2621 r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
static void *(*malloc_func)(size_t) = malloc; static void *default_malloc_ex(size_t num, const char *file, int line) { return malloc_func(num); } static void *(*malloc_ex_func)(size_t, const char *file, int line) = default_malloc_ex;
1677 SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth) 1678 { ... 1827 #ifndef OPENSSL_NO_BUF_FREELISTS 1828 ret->freelist_max_len = SSL_MAX_BUF_FREELIST_LEN_DEFAULT; 1829 ret->rbuf_freelist = OPENSSL_malloc(sizeof(SSL3_BUF_FREELIST)); 1830 if (!ret->rbuf_freelist) 1831 goto err; 1832 ret->rbuf_freelist->chunklen = 0; 1833 ret->rbuf_freelist->len = 0; 1834 ret->rbuf_freelist->head = NULL; 1835 ret->wbuf_freelist = OPENSSL_malloc(sizeof(SSL3_BUF_FREELIST)); 1836 if (!ret->wbuf_freelist) 1837 { 1838 OPENSSL_free(ret->rbuf_freelist); 1839 goto err; 1840 } 1841 ret->wbuf_freelist->chunklen = 0; 1842 ret->wbuf_freelist->len = 0; 1843 ret->wbuf_freelist->head = NULL;
678 static void * 679 freelist_extract(SSL_CTX *ctx, int for_read, int sz) 680 { 681 SSL3_BUF_FREELIST *list; 682 SSL3_BUF_FREELIST_ENTRY *ent = NULL; 683 void *result = NULL; 684 685 CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX); 686 list = for_read ? ctx->rbuf_freelist : ctx->wbuf_freelist; 687 if (list != NULL && sz == (int)list->chunklen) 688 ent = list->head; 689 if (ent != NULL) 690 { 691 list->head = ent->next; 692 result = ent; 693 if (--list->len == 0) 694 list->chunklen = 0; 695 } 696 CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX); 697 if (!result) 698 result = OPENSSL_malloc(sz); 699 return result; 700 }When a caller is done with a chunk it retrieved from the list a call to freelist_insert is made. This function first checks to see if the size requested is the same as the list chunklen or if the list chunklen is 0. A further check to see if the list len is already at freelist_max_len (32 by default) and size is greater than sizeof(SSL3_BUF_FREELIST_ENTRY). If these conditions are satisfied then the list chunklen is set to the requested size (it may have been 0 if not previously used), ent is assigned the value of the chunk to be inserted, the next pointer is set to the head, the list head is set to ent, the list size is incremented, and finally the mem pointer is set to NULL. If these conditions were never initially met then mem would be free’d via a call to OPENSSL_free on line 725.
702 static void 703 freelist_insert(SSL_CTX *ctx, int for_read, size_t sz, void *mem) 704 { 705 SSL3_BUF_FREELIST *list; 706 SSL3_BUF_FREELIST_ENTRY *ent; 707 708 CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX); 709 list = for_read ? ctx->rbuf_freelist : ctx->wbuf_freelist; 710 if (list != NULL && 711 (sz == list->chunklen || list->chunklen == 0) && 712 list->len < ctx->freelist_max_len && 713 sz >= sizeof(*ent)) 714 { 715 list->chunklen = sz; 716 ent = mem; 717 ent->next = list->head; 718 list->head = ent; 719 ++list->len; 720 mem = NULL; 721 } 722 723 CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX); 724 if (mem) 725 OPENSSL_free(mem); 726 }