
Технический разбор и анти-POC для CVE-2022-3602, переполнения буфера в punycode в OpenSSL 3.0.x, со скриптами воспроизведения, анализом стека и оценкой мер компилятора.
Этот документ и репозиторий представляют собой разбор CVE−2022-3602, проблемы переполнения буфера в реализации punycode в OpenSSL. Это «анти-PoC» (проблема, судя по всему, не эксплуатируема), предназначенный для тех, кто поддерживает собственные сборки OpenSSL, и для разработчиков компиляторов.
В том же релизе есть отдельная CVE, CVE-2022-3786, которая также приводит к переполнению буфера, но в этом случае атакующий не может контролировать содержимое. Здесь нет воспроизведения для этой проблемы, но она может привести к отказу в обслуживании из-за краха.
Краши и переполнения буфера никогда не бывают хорошими, и если вы используете OpenSSL 3.0.x, благоразумно обновиться как можно скорее.
Пожалуйста, сообщайте о любых ошибках или пропусках через GitHub issues или pull-requests.
В функции ossl_punycode_decode есть ошибка off-by-one при обработке декодирования punycode, которая приводит к переполнению в 4 байта. Эта проблема возникает только тогда, когда OpenSSL обрабатывает цепочку сертификатов, и требует двух условий. Во-первых, сертификат CA или промежуточного удостоверяющего центра в цепочке должен содержать поле name-constraint, использующее punycode.
nameConstraints = permitted;email:xn-maccrthaigh-n7a.com
Во-вторых, конечный сертификат должен содержать поле otherName в SubjectAlternateName (SAN), указывающее строку SmtpUTF8Mailbox.
otherName = 1.3.6.1.5.5.7.8.9;UTF8:[email protected]
При срабатывании punycode из поля nameConstraints, но не punycode из поля otherName, будет обработан уязвимым парсером punycode в OpenSSL.
Дэвид Бенджамин (David Benjamin) и Мэтт Касвелл (Matt Caswell) установили, что проверка nameConstraint происходит после обычной проверки цепочки сертификатов и проверки подписи. Для большинства приложений это означает, что проблема не может быть вызвана самоподписанным сертификатом или недействительной цепочкой.
Обратите внимание, что приложения s_client и s_server из openssl предназначены для отладки и не прекращают обработку, когда цепочка недействительна.
Доверенный CA или промежуточный сертификат должен содержать вредоносную полезную нагрузку, а также должен подписать конечный сертификат, который вызывает проблему.
Могут быть среды, в которых недоверенные стороны являются CA или промежуточными удостоверяющими центрами, например, хостинг-сервис, поддерживающий частные CA, предоставленные клиентом, но это не распространено.
Для многих приложений ответ будет «нет» из-за того, как компилятор разместил стек, и из-за наличия других защит, таких как стековые канарейки / стековые куки, выравнивание, PIE, FORTIFY_SOURCE.
Проблема действительно приводит к переполнению 32 бит в стеке. Этого недостаточно для непосредственного выполнения шелл-кода, но может быть достаточно, чтобы изменить поток управления приложением. Например, переход на шелл-код, встроенный в цепочку сертификатов X509, может быть возможен, если эти данные также скопированы в стек в исполняемую область.
На всех протестированных мной платформах Linux переполнение происходит в выравнивание и безвредно. Теоретически компилятор может разместить переменные так, что переполнение произойдет в одну из других переменных функции ossl_a2ulabel.
В зависимости от инлайнинга полный список присутствующих переменных:
outptr, inptr, size, result, tmpptr, delta, seed, utfsize
и ни одна из них, как мне кажется, не дает очевидного пути к повышению привилегий или интересному контролю.
Я приложил tarball с инструментами, которые можно использовать для создания воспроизведений и переполнений с максимально возможным контролем над всеми четырьмя байтами. Эталонная строка воспроизведения (xn--ww90271...aaaa) переполняет четыре байта значениями 0xFF 0x0F 0x0F 0x0F. Если это не приводит к краху приложения, возможно (вероятно?), это приложение не уязвимо.
Скрипт run-poc можно использовать для генерации вредоносной цепочки сертификатов. Вредоносный сертификат CA генерируется из ca.cnf, а вызывающий проблему конечный сертификат — из leaf.cnf.
Сертификат CA использует следующую эталонную полезную нагрузку:
xn--ww902716aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Python-скрипт можно использовать для генерации других punycode-строк для других полезных нагрузок.
При выполнении run-poc будет запущен клиент и сервер openssl, и будет предпринята попытка эксплуатировать проблему десять раз.
Уязвимый OpenSSL, скорее всего, упадет. Это не значит, что версия OpenSSL уязвима к RCE, поскольку стековые канарейки и защита стековых куки также обычно вызывают (более безопасный) крах приложения. Также обратите внимание, что это никак не меняет серьезность другой CVE в том же релизе.
Удивительно тонко получить почти полный контроль над всеми четырьмя байтами переполнения, и для этого требуется эксплуатировать декодер punycode в OpenSSL с помощью нестандартного / недействительного punycode. В приложенном tarball есть скрипт, который может сконструировать строку, учитывающую эту тонкость. Ниже приводится объяснение того, как это работает.
Проблема безопасности в ossl_punycode_decode()
int ossl_punycode_decode(const char *pEncoded, const size_t enc_len,
unsigned int *pDecoded, unsigned int *pout_length)
ossl_punycode_decode вызывается из ossl_a2ulabel. Буфер pEncoded — это буфер более или менее произвольного размера, поступающий из цепочки сертификатов X509. Это часть, которая идет после любого xn-- в поле nameConstraint. См. [reproduction] для того, как воспроизвести такую цепочку сертификатов.
pDecoded — это массив unsigned int размером LABEL_BUF_SIZE. LABEL_BUF_SIZE равен 512, и на большинстве платформ unsigned int имеет ширину 4 байта. Таким образом, на большинстве платформ pDecoded имеет длину 2048 байт.
Внутри ossl_punycode_decode() суть проблемы — следующая неверная проверка длины:
if (written_out > max_out)
max_out соответствует *pout_length, который всегда равен 512. А written_out отслеживает, сколько unsigned int было записано в pDecoded. Поскольку written_out увеличивается позже, только после записи, эта ошибочная проверка позволяет записать 513 unsigned int в pDecoded. Конечный результат выглядит примерно так ...
pDecoded = [ ... , 'X , 'Y' , 'Z' ] 'P'
// Indices 509 510 511
Здесь, по соглашению C, индексы начинаются с нуля, поэтому слот номер 511 — это 512-й элемент массива. 'P' — это четырехбайтовая полезная нагрузка, которая была записана за пределами выделенного в стеке пространства для буфера buf в ossl_a2ulabel(), на который указывает pDecoded.
Четыре байта — это небольшое переполнение, которого недостаточно для NOP-слайда или непосредственного выполнения шелл-кода, но достаточно, чтобы изменить поток управления приложением. Например, переход на шелл-код, встроенный в цепочку сертификатов x509, может быть возможен, в зависимости от того, как эти данные (или скопированные фрагменты этих данных) хранятся и является ли эта память исполняемой. Однако для потенциального атакующего есть еще больше трудностей.
Во-первых, выравнивание и размещение стека компилятором или такие защиты, как стековые канарейки, могут сделать любую эксплуатацию полностью невозможной.