Как я сделал переполнение кучи в curl

Created Diff never expires
73 removals
54 lines
120 additions
104 lines
В связи с выпуском Curl 8.4.0 мы публикуем рекомендации по безопасности и все подробности об CVE-2023-38545. Эта проблема является самой серьезной проблемой безопасности, обнаруженной в Curl за долгое время. Мы установили для него ВЫСОКИЙ уровень серьезности.
В связи с выпуском curl 8.4.0 мы публикуем рекомендации по безопасности и все подробности о CVE-2023-38545. Эта проблема является самой серьезной проблемой безопасности, обнаруженной в curl за долгое время. Для неё установили ВЫСОКИЙ приоритет.


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

Location: https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/

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

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

Так происходит переполнение буфера кучи.
Исправление

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

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

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

На сегодняшний день это самая большая выплаченная награда за найденные ошибки в curl: 4660 долларов США (плюс 1165 долларов США проекту curl, согласно политике IBB).
Классический комикс про Дилберта. Исходный URL-адрес, похоже, больше не доступен.
Классический комикс про Дилберта. Исходный URL-адрес, похоже, больше не доступен.
Переписать это?

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

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

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

потенциально и постепенно заменять части curl, как при внедрении hyper.

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

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

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

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

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

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

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