Pull to refresh

Диагностика ошибки Heartbleed в OpenSSL. (Окончательный диагноз ещё не поставлен, хотя лечение уже идёт вовсю)

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


Когда я писал об ошибке в GnuTLS, я сказал, что это не последняя тяжелая ошибка в стеке TLS, которую мы увидим. Однако, я не ожидал, что всё будет так плачевно.

Ошибка в Heartbleed — это особенно неприятный баг. Она позволяет злоумышленнику читать до 64 Кб памяти, и исследователи в области безопасности говорят:

Без использования какой-либо конфиденциальной информации или учетных данных мы смогли украсть у себя секретные ключи, используемые для наших сертификатов X.509, имена пользователей и пароли, мгновенные сообщения, электронную почту и важные деловые документы и общения.


Баг


Исправление начинается здесь, в 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 */


Так, сначала мы получаем указатель на данные в записи SSLv3, которая выглядит следующим образом:
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;


Структура, описывающая записи, содержит тип, длину и данные. Вернёмся к dtls1_process_heartbeat:

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

Примечание переводчика: код n2s(c, s);
#define n2s(c,s)	((s=(((unsigned int)(c[0]))<< 8)| \
			    (((unsigned int)(c[1]))    )),c+=2)



Первый байт записи SSLv3 — это тип «сердцебиения». Макрос n2s берёт два байта из p и помещает их в payload. Это на самом деле длина (length) полезных данных. Обратите внимание, что фактическая длина в записи SSLv3 не проверяется.
Затем переменная pl получает данные «сердцебиения», предоставленные запрашивающим.
Далее в функции происходит следующее:

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;


Выделяется столько памяти, сколько попросил запрашивающий: до 65535+1+2+16, если быть точным.
Переменная bp — это указатель, используемый для доступа к этой памяти. Затем:

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

Примечание переводчика про memcpy
НАЗВАНИЕ

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

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

ОПИСАНИЕ

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

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

SVID 3, BSD 4.3, ISO 9899


Макрос s2n делает обратное макросу n2s: берёт 16-разрядное значение и помещает его в два байта. Затем он устанавливает ту же самую запрошенную длину полезной нагрузки.
Примечание переводчика: код s2n(c, s);
#define s2n(s,c)	((c[0]=(unsigned char)(((s)>> 8)&0xff), \
			  c[1]=(unsigned char)(((s)    )&0xff)),c+=2)



Затем копируются payload байт из pl, предоставленных пользователем данных, во вновь выделенный массив bp. После этого все это посылается обратно пользователю.
Так где же ошибка?

Пользователь управляет полезной нагрузкой и pl



Что если запрашивающая сторона на самом деле не передаст payload-байты, как должна была бы?
Что, если pl в действительности содержит только один байт?
Тогда memcpy будет читать из памяти всё, что было неподалёку от записи SSLv3.

И, по-видимому, неподалеку есть много разных вещей.

Существует два способа память выделяется динамически с помощью malloc (по крайней мере, в Linux): с помощью sbrk(2) и используя mmap(2). Если память выделяется sbrk, то используются старые правила heap-grows-up, что ограничивает то, что может быть найдено с его помощью, хотя, используя несколько запросов (особенно одновременных) можно все же найти некоторые интересные вещи. [В этом разделе первоначально содержался мой скепсис по поводу PoC из-за природы того, как куча работает через sbrk. Однако, многие читатели напомнили мне, что вместо этого в malloc может быть использован mmap , а это всё меняет. Спасибо!]

Update от автора - эта часть из оригинальной статьи убрана
Однако, если используется mmap, «Ставки сделаны!». Для mmap может быть выделена любая неиспользуемая память. Это — цель большинства атак, направленных против Heartbleed.

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

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


Расположение bp вообще не имеет значения, на самом деле. Расположение pl, однако, имеет огромное значение. Память под неё почти наверняка выделяется с помощью sbrk() из-за порога mmap в malloc(). Тем не менее, память под интересные материалы (например, документы или информацию пользователей), весьма вероятно, будет выделена mmap() и может быть доступна из pl. Несколько одновременных запросов также сделает доступными некоторые интересные данные.

Итак, что же это значит? Ну, модели распределения памяти для pl дикттуют нам, что мы можем прочитать. Вот что сказал об этом один из первооткрывателей уязвимости:

Модели распределения памяти в куче делают компрометацию приватного ключа маловероятной # heartbleed # dontpanic.

— Нил Мехта (@ neelmehta) 8 апреля 2014


Исправление


Наиболее важной частью исправления является эта:

/* 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 выполняет проверку, чтобы убедиться, что фактическая длина записи достаточно велика. Вот так.

Уроки


Что мы можем извлечь из этого?

Я фанат Cи. Это был мой первый язык программирования, и это был первый язык, который мне было комфортно использовать в профессиональных целях. Но теперь я вижу его ограничения более ясно, чем когда-либо прежде.

После Heartbleed и багом GnuTLS я думаю, мы должны сделать три вещи:

  • Платить деньги за аудит безопасности таких элементов критической инфраструктуры безопасности, как OpenSSL.
  • Писать много unit- и интеграционных тестов для этих библиотек.
  • Начать писать альтернативные реализации на более безопасных языках.


Учитывая то, как трудно писать безопасно на Cи, я не вижу других вариантов.
Only registered users can participate in poll. Log in, please.
Я бы не пожалел для этого усилий. А вы?
71.26% Да.176
21.46% Нет.53
7.29% Я вижу третий путь и опишу его в комментариях.18
247 users voted. 212 users abstained.
Tags:
Hubs:
+55
Comments25

Articles