Диагностика ошибки Heartbleed в OpenSSL

в 19:11, , рубрики: openssl, анализ, информационная безопасность, криптография, уязвимость

Когда я писал об ошибке в 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;

Первый байт записи 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);

Макрос s2n делает обратное макросу n2s: берёт 16-разрядное значение и помещает его в два байта. Затем он устанавливает ту же самую запрошенную длину полезной нагрузки.

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

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

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

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

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

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

И самое главное: чем больше ваш запрашиваемый блок, тем больше вероятность, что он будет обслужен mmap, а не sbrk. [В этом разделе первоначально содержался мой скепсис по поводу PoC из-за природы того, как куча работает через sbrk. Однако, многие читатели напомнили мне, что вместо этого в malloc может быть использован mmap , а это всё меняет. Спасибо!]

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

Исправление

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

/* 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и. Это был мой первый язык программирования, и это был первый язык, который мне было комфортно использовать в профессиональных целях. Но теперь я вижу его ограничения более ясно, чем когда-либо прежде.

Между этой ошибкой и багом GnuTLS я думаю, мы должны сделать три вещи:

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

Учитывая то, как трудно писать безопасно на Cи, я не вижу других вариантов.

Я бы не пожалел для этого усилий. А вы?

Автор: Noospheratu

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js