FlawOpen

flawopen.com/Teardowns/cve-2014-0160-heartbleed

● CVE-2014-0160 · CVSS 7.5 · Высокий
Исследования · FlawOpen

CVE-2014-0160: утечка памяти Heartbleed в OpenSSL

CVE-2014-0160 позволяла TLS- или DTLS-пиру читать до 64 КиБ за пределами записи Heartbeat, если OpenSSL доверял заявленной длине полезной нагрузки, которой в записи не было. В upstream-исправлении запись проверяется до разбора и возврата данных.[1], [2]

💡 Простыми словами (ELI5)

Представьте сотрудника, которому дали карточку с несколькими словами, но он поверил наклейке с просьбой сделать намного более длинную копию. Вместо того чтобы остановиться на краю карточки, он продолжает копировать текст из общего лотка. Heartbleed заставляла OpenSSL читать за пределами полученной записи; раскрытые байты зависели от содержимого памяти процесса.[1], [3]

Ключевые понятия и термины

Heartbeat в TLS/DTLS (RFC 6520)
Сообщение поддержания соединения в TLS или DTLS. Запрос содержит payload, который пир возвращает, а также требуемое протоколом дополнение.[3]
payload_length
16-битное поле с размером payload Heartbeat. Он должен помещаться в полученную запись после типа сообщения, поля длины и минимального дополнения.[1], [3]
memcpy()
Копирует заданное число байтов начиная с указателя источника. Функция не знает допустимую длину исходного буфера; её должен проверить вызывающий код.[1], [2]
Чтение за границей буфера (CWE-126)
Чтение после конца предназначенного исходного буфера. При Heartbleed ответ мог раскрыть до 64 КиБ памяти процесса — в зависимости от её содержимого.[5]

Связи со стандартами

Анализ первопричины

tls1_process_heartbeat() и dtls1_process_heartbeat() читали 16-битную длину полезной нагрузки Heartbeat и в пути ответа передавали её в memcpy(), не проверив, что столько байтов действительно есть в s->s3->rrec.length. Пир мог указать больше данных, чем содержала TLS- или DTLS-запись. Затем OpenSSL возвращала байты за пределами полученной записи и могла раскрыть до 64 КиБ памяти процесса; содержимое зависело от памяти, поэтому конкретный ключ, пароль или cookie не обязательно попадал в ответ. Upstream-коммит 96db902 сначала отбрасывает записи, слишком короткие для типа, поля длины и минимального дополнения, затем сравнивает 1 + 2 + payload + 16 с длиной полученной записи. Он также ограничивает ответ через SSL3_RT_MAX_PLAIN_LENGTH и использует вычисленный write_length. Аналогичные проверки внесены в обработчики TLS и DTLS.[1], [2], [3], [4]

Пошаговый сценарий атаки

Шаг 1

Пир отправляет короткую запись Heartbeat

TLS- или DTLS-пир отправляет сообщение Heartbeat, в котором заявленная длина payload больше числа байтов полезной нагрузки в записи.[1], [3]

Шаг 2

OpenSSL доверяет заявленной длине

До исправления обработчик читает тип и длину, не проверив сначала минимальный размер записи и не сопоставив payload с полученной записью.[1], [2]

Шаг 3

Ответ копирует данные за пределами записи

memcpy() копирует заявленное число байтов из буфера записи. Ответ может включить байты после полученного сообщения Heartbeat — до протокольного предела 64 КиБ.[1], [2]

Шаг 4

Раскрытие зависит от памяти процесса

Пир может получить байты из памяти процесса. Конкретный секрет не гарантирован: результат зависит от содержимого памяти и состояния конечной точки.[1], [4]

Шаг 5

Исправленный обработчик отклоняет несоответствие

Исправление проверяет заголовок и дополнение до разбора, подтверждает, что payload помещается в полученную запись, и ограничивает ответ перед копированием.[2], [1]

Исходный код: Уязвимый vs Защищённый вариант

Это фрагменты upstream-изменения OpenSSL, а не самостоятельная программа. Уязвимый код разбирает поля до проверки наличия фиксированного заголовка и минимального дополнения, а затем копирует заявленную пиром длину payload. Исправление проверяет минимальный размер записи до разбора, сверяет заявленную полезную нагрузку с полученной записью и ограничивает размер ответа. OpenSSL внесла соответствующие изменения в tls1_process_heartbeat() и dtls1_process_heartbeat().[2], [1]

Фрагмент исходного кода: ssl/t1_lib.c

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
/* Фрагмент tls1_process_heartbeat() до исправления. */
hbtype = *p++;
n2s(p, payload);
pl = p;

/* В пути ответа TLS1_HB_REQUEST: */
memcpy(bp, pl, payload);
r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT,
                     buffer, 3 + payload + padding);
БЕЗОПАСНЫЙ ИСПРАВЛЕННЫЙ ВАРИАНТ
/* Отклоняет запись, слишком короткую для типа, длины и минимального заполнения. */
if (1 + 2 + 16 > s->s3->rrec.length)
    return 0;

hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return 0;
pl = p;

/* В пути ответа TLS1_HB_REQUEST: */
unsigned int write_length = 1 + 2 + payload + padding;
if (write_length > SSL3_RT_MAX_PLAIN_LENGTH)
    return 0;
buffer = OPENSSL_malloc(write_length);
memcpy(bp, pl, payload);
r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, write_length);

Хронология инцидента

2012-03-14

В OpenSSL 1.0.1 появилась обработка TLS/DTLS Heartbeat без проверки длины записи.[3], [1]

2014-04-07

OpenSSL публикует уведомление CVE-2014-0160 и выпускает 1.0.1g; в уведомлении выражена благодарность Neel Mehta из Google Security. Исправление для бета-версии 1.0.2 выходит в beta2.[1], [2]

2014-04

Операторы устанавливают исправление на затронутые конечные точки и оценивают возможную утечку. Heartbleed.com отмечает, что обычные журналы не показывают, какие байты памяти вернул ответ; после установки исправления оценивается замена ключей, сертификатов, сессий и учётных данных.[6], [1]

Чек-лист по защите системы для инженеров

Источники