October 11, 2023

КАК Я СДЕЛАЛ ПЕРЕПОЛНЕНИЕ КУЧИ В CURL

В связи с выпуском Curl 8.4.0 мы публикуем рекомендации по безопасности и все подробности об CVE-2023-38545 . Эта проблема является самой серьезной проблемой безопасности, обнаруженной в Curl за долгое время. Мы установили уровень серьезности HIGH .

При этом рекомендация содержит все необходимые подробности. Я решил использовать несколько дополнительных слов и расширить объяснения для всех, кто хочет понять, как работает этот недостаток и как он произошел.

Фон

Curl поддерживает SOCKS5 с августа 2002 года .

SOCKS5 — это прокси-протокол. Это достаточно простой протокол настройки сетевого взаимодействия через выделенного «посредника». Например, этот протокол обычно используется при настройке связи через Tor, а также для доступа в Интернет изнутри организаций и компаний.

SOCKS5 имеет два разных режима разрешения имен хостов. Либо клиент разрешает имя хоста локально и передает пункт назначения в качестве разрешенного адреса, либо клиент передает все имя хоста прокси-серверу и позволяет самому прокси разрешить хост удаленно .

В начале 2020 года я решил себе старую, давнюю проблему с Curl: преобразовать функцию, подключающуюся к прокси-серверу SOCKS5, из блокирующего вызова в неблокирующий конечный автомат. Это, например, очень заметно, когда приложение выполняет большое количество параллельных передач, которые выполняются через SOCKS5.

14 февраля 2020 года я выполнил основной коммит для этого изменения в мастере. Он был выпущен в версии 7.69.0 как первый выпуск с этим улучшением. И, как следствие, также первый выпуск, уязвимый для CVE-2023-38545.

Менее разумное решение

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

В верхней части функции я сделал это :

boolss5_resolve_local =
  (тип прокси == CURLPROXY_SOCKS5) ? ИСТИНА: ЛОЖЬ;

Эта логическая переменная содержит информацию о том, должен ли Curl разрешить хост или просто передать имя прокси. Это назначение выполняется сверху и, следовательно, для каждого вызова во время работы конечного автомата.

Конечный автомат запускается в состоянии INIT, в котором и кроется основной баг сегодняшнего сюжетного времени. Ошибка унаследована от функции, существовавшей до того, как она была превращена в конечный автомат.

if(!socks5_resolve_local && имя_хоста_len > 255) {
  Socks5_resolve_local = ИСТИНА;
}

SOCKS5 допускает длину поля имени хоста до 255 байт, что означает, что прокси-сервер SOCKS5 не может разрешить более длинное имя хоста. При обнаружении слишком длинного имени хоста. код Curl принимает неправильное решение вместо этого переключиться в режим локального разрешения. Для этой цели локальная переменная устанавливается в значение TRUE. (Это условие является остатком кода, добавленного давным-давно. Я думаю, что было совершенно неправильно переключать режим таким образом, поскольку пользователь, запросивший удаленное разрешение, Curl должен придерживаться этого или потерпеть неудачу. Простое переключение вряд ли сработает, даже в «хороших» ситуациях.)

Затем конечный автомат переключает состояние и продолжает работу.

Проблема вызывает

Если конечный автомат не может продолжить работу, поскольку у него больше нет данных для работы, например, если сервер SOCKS5 недостаточно быстр, он возвращается. Он вызывается снова, когда есть доступные данные для продолжения работы. Мгновение спустя.

Но теперь еще раз взгляните на локальную переменную socks5_resolve_local в верхней части функции. Ему снова присваивается значение в зависимости от режима прокси — измененное значение не запоминается из-за слишком длинного имени хоста . Теперь он снова содержит значение, говорящее, что прокси-сервер должен разрешить имя удаленно. Но имя слишком длинное…

Curl создает кадр протокола в буфере памяти и копирует место назначения в этот буфер. Поскольку код ошибочно считает, что он должен передать имя хоста, даже если имя хоста слишком длинное, копия памяти может переполнить выделенный целевой буфер. Конечно, в зависимости от длины имени хоста и размера целевого буфера.

Целевой буфер

Выделенная область памяти, которую Curl использует для построения кадра протокола для отправки на прокси, такая же, как и обычный буфер загрузки. Он просто повторно используется для этой цели перед началом передачи. По умолчанию размер буфера загрузки составляет 16 КБ, но по запросу приложения его также можно установить на другой размер. Инструмент Curl устанавливает размер буфера равным 100 КБ. Минимальный допустимый размер — 1024 байта.

Если размер буфера установлен меньше 65541 байт, такое переполнение возможно. Чем меньше размер, тем больше возможное переполнение.

Длина имени хоста

Имя хоста в URL-адресе не имеет ограничения по реальному размеру, но анализатор URL-адресов libcurl отказывается принимать имена длиной более 65535 байт. DNS принимает только имена хостов длиной до 253 байт. Таким образом, законное имя длиной более 253 байтов является необычным. Настоящее имя длиной более 1024 практически не встречается.

Таким образом, чтобы вызвать эту ошибку, злоумышленнику необходимо ввести в это уравнение сверхдлинное имя хоста. Чтобы использовать его в атаке. Имя должно быть длиннее целевого буфера, чтобы копия памяти перезаписывала память кучи.

Содержание имени хоста

Поле имени хоста URL-адреса может содержать только подмножество октетов. Диапазон значений байтов просто недействителен и может привести к тому, что анализатор URL-адресов отклонит его. Если libcurl создан для использования библиотеки IDN, она также может отклонять недопустимые имена хостов. Таким образом, эта ошибка может возникнуть только в том случае, если в имени хоста используется правильный набор байтов.

Атака

Злоумышленник, контролирующий HTTPS-сервер, к которому libcurl с помощью клиента обращается через прокси-сервер SOCKS5 (используя режим прокси-резольвера), может заставить его вернуть созданное перенаправление приложению через ответ HTTP 30x.

Такое 30-кратное перенаправление будет содержать заголовок Location: в стиле:

Местоположение: https://ааааааааааааааааааааааааааа/

… где имя хоста длиннее 16 КБ и до 64 КБ

Если в клиенте, использующем libcurl, включено автоматическое отслеживание перенаправления, а прокси-сервер SOCKS5 «достаточно медленный», чтобы вызвать ошибку локальной переменной, он скопирует созданное имя хоста в слишком маленький выделенный буфер и в соседнюю кучу памяти.

Затем произошло переполнение буфера кучи.

Исправление

Curl не должен переключать режим с удаленного разрешения на локальное из-за слишком длинного имени хоста. Скорее он должен возвращать ошибку, и, начиная с версии Curl 8.4.0, так оно и есть.

Теперь у нас также есть специальный тестовый пример для этого сценария.

Кредиты

Об этой проблеме сообщил, проанализировал и исправил Джей Сатиро.

Это самая большая награда за ошибки в Curl, выплаченная на сегодняшний день: 4660 долларов США (плюс 1165 долларов США проекту Curl, согласно политике IBB ).

Классический родственный стриптиз Дилберта. Исходный URL-адрес, похоже, больше не доступен.

Переписать?

Да, это семейство недостатков было бы невозможно, если бы Curl был написан на безопасном для памяти языке вместо C, но портирование Curl на другой язык не стоит на повестке дня. Я уверен, что новость об этой уязвимости вызовет новый поток вопросов и призывов к этому, и я могу вздохнуть, закатить глаза и попытаться ответить на этот вопрос еще раз.

Единственный подход в этом направлении, который я считаю жизнеспособным и разумным, заключается в следующем:

  1. разрешать, использовать и поддерживать больше зависимостей, написанных на языках, безопасных для памяти, и
  2. потенциально и постепенно заменять части завитка по частям, как с появлением Hyper .

Однако в настоящее время такое развитие происходит почти с ледниковой скоростью и с болезненной ясностью показывает связанные с этим проблемы. В обозримом будущем Curl останется написанным на C.

Все, кого это не устраивает, конечно, могут засучить рукава и приступить к работе.

С учетом последних двух CVE, зарегистрированных для Curl 8.4.0, совокупное общее количество говорит о том, что 41% уязвимостей безопасности, когда-либо обнаруженных в Curl, вероятно, не произошли бы, если бы мы использовали язык, безопасный для памяти. Но также: язык Rust даже не имел возможности практического использования для этой цели в то время, когда мы представили, возможно, первые 80% проблем, связанных с C.

Это горит в моей душе

Читая код сейчас невозможно не увидеть ошибку. Да, мне действительно больно признавать тот факт, что я совершил эту ошибку, не заметив этого, и что ошибка оставалась необнаруженной в коде в течение 1315 дней. Я прошу прощения. Я всего лишь человек.

Это можно было бы обнаружить с помощью более качественного набора тестов. Мы неоднократно запускали несколько статических анализаторов кода, и ни один из них не обнаружил никаких проблем в этой функции.

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

За кулисами

Чтобы узнать, как было сообщено об этой ошибке, и как мы работали над ней до того, как она была обнародована. Проверьте отчет Hackerone .