«Баги повсюду!» Об уязвимостях стандартной библиотеки Rust, которые годами никто не замечает

разработка
25 октября 2018, 17:18

Безопасность — одно из главных преимуществ Rust, но на 100 процентов надёжным его считать нельзя. В статье на Medium Сергей Давидофф рассмотрел некоторые уязвимости этого языка, которые остаются без внимания поддержки многие годы. dev.by публикует перевод статьи.

Rust — молодой язык системного программирования с выдающейся безопасностью памяти и скоростью. Код на Rust работает так же быстро, как код на С/С++, но в нём нет необъяснимых периодических сбоев в продакшне или жутких уязвимостей безопасности, которыми отличаются последние два языка.

Если, конечно, не пытаться найти их специально.

Безопасные абстракции в Rust позволяют делать много полезных вещей, не касаясь сложностей с распределением памяти и других низкоуровневых загадок. Но чтобы запускать код на современной аппаратуре, коснуться их всё-таки придётся, поэтому что-то в Rust должно отвечать за это. В языках с автоматическим управлением памятью типа Python или Go эту задачу выполняет языковая среда выполнения, и Rust здесь не исключение.

В Rust все безумные тонкости доступа к памяти лежат на плечах стандартной библиотеки. Она оперирует базовыми строительными блоками, например векторами, с помощью которых может выборочно открывать только безопасный интерфейс и скрывать потенциально небезопасные внутренние операции. Для этого нужно открыто указать такие операции (читай: воспроизводимые сбои, уязвимости безопасности), отметив блок как unsafe, вот так: unsafe { Dragons: hatch(); }

Однако Rust отличается от Python и Go тем, что в нём можно использовать unsafe за пределами стандартной библиотеки. С одной стороны, это значит, что вы можете написать библиотеку на Rust и вызвать её из других языков, например Python. Привязки к другим языкам заведомо небезопасны, поэтому возможность писать такой код на Rust — крупное преимущество над другими безопасными по памяти языками вроде Go. С другой стороны, это побуждает осмотрительно использовать unsafe. Некоторое время назад за нечто подобное изрядно прошлись по одной перспективной библиотеке. И тогда я решил выяснить, действительно ли Rust безопасен по памяти, как об этом заявляют.

Месяц я копался с популярными библиотеками Rust и рассказал, что из этого вышло, в этой статье. Вкратце, контейнеры Rust иногда действительно используют unsafe, когда на то нет объективных причин; возникает множество багов, которые приводят к «отказам в обслуживании», но перепробовав шесть различных контейнеров, создать эксплойт я так и не смог.

Очевидно, нужно было брать выше.

Абсолютная мощь, только для плохих парней

Есть один очень эффективный метод поиска уязвимостей, который я ещё не применял к Rust. Он несравнимо мощнее остальных, и использовать его могут только настоящие злодеи, которые хотят что-нибудь сломать, а не исправить. Это поиск в баг-трекере.

Большинство разработчиков на С или С++ не очень заботятся о безопасности. Им просто нужно, чтобы код работал, и работал быстро. Если им попадается баг, из-за которого программа выдаёт какую-нибудь чушь или даёт сбой, они просто исправляют этот баг и берутся за следующий.

На самом деле, многие из этих багов в С и С++ вызваны ошибками в управлении памятью. Эти баги представляют собой уязвимости удалённого выполнения кода, которые Rust призван не допустить. По-хорошему, их нужно вносить в специальные базы данных общеизвестных уязвимостей безопасности (Common Vulnerabilities and Exposures, CVE) и оповещать заинтересованных людей, которые выпустят исправления для пользователей. В реальности такие баги втихаря ремонтируют в лучшем случае к следующему релизу, а в худшем — они так и остаются неисправленными долгие годы, пока их не обнаружит кто-то извне или пока их не станет эксплуатировать какое-нибудь вредоносное ПО.

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

Один из моих любимых — баг в библиотеке libjpeg, который нашли в 2003 году, но не посчитали проблемой безопасности. Соответствующий патч увидел свет только в 2013 году, и вышел в настолько незаметном обновлении, что его всё равно никто получил. В журнале изменений даже не было записи об исправлении. Брешь в 2013 году обнаружил независимый эксперт и автор технологии поиска ошибок afl-fuzz Михал Залевски. То есть исправление появилось спустя 10 лет после того, как баг был обнаружен.

Получается, что на протяжении 10 лет любой, кому вздумалось бы просто прокрутить баг-трекер, мог украсть cookie и пароли из вашего браузера, всего лишь загрузив картинку и небольшой JavaScript-код.

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

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

Дальше в лес

Применять поиск по баг-трекеру коду на Rust было проще, чем я думал. Оказывается, на GitHub можно найти все проекты на том или ином языке, поэтому я просто ввёл с поиск «unsound», выбрал Rust и… Сколько же было багов!

Вместо «unsound» можно попробовать ввести «crash». Кроме того, я искал только открытые баги в недавно обновлённых проектах и пропускал стандартную библиотеку (там ведь всё должно быть в порядке).

Так я нашёл свою первую уязвимость нулевого дня. Её обнаружили за два месяца до меня, и по ней даже написана статья, хотя она в основном о производительности. Когда я указал поддержке контейнера, что это — уязвимость безопасности, её отремонтировали за два часа, а также предоставили бэкпорт каждой повреждённой стадии, хотя это была ещё нулевая версия контейнера.

Всё же, эксплуатировать один этот баг на деле будет хлопотно — лучше в цепи с другими эксплойтами.

Но нам есть куда расти. Усложним задачу.

Охота на зверя

Сейчас поищем что-то, что нам будет достаточно просто эксплуатировать (вроде контролируемой атаки переполнения буфера) и что ещё не признали уязвимостью безопасности.

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

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

Я обдумывал свой план действий, когда случайно натолкнулся на ветку reddit об истории уязвимостей стандартной библиотеки Rust, где и нашёл вот эту жемчужину:

ошибка сегментации по обе стороны VecDeque
https://github.com/rust-lang/rust/issues/44800

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

Ошибка присутствует в версиях Rust с 1.3 по 1.21. Она вызывает сбой, который достаточно легко обнаружить, но оставалась незамеченной два года. В журнале изменений в релизе, где баг исправлен, даже нет отдельной записи об этом. В CVE уязвимость тоже не вносили.

Для некоторых архитектур Debian так и продолжает выпускать уязвимые версии Rust. Я думаю, что у многих корпоративных пользователей тоже установлены уязвимые версии.

Как обычно, зло побеждает.

Я не ожидал найти что-то подобное в стандартной библиотеке, потому что у Rust очень тщательно продуманные и надёжные правила безопасности, а его команда безопасности постоянно совершенствует компилятор и стандартную библиотеку и не могла упустить ошибку.

Я связался с командой безопасности Rust по поводу своего вопроса, попросил сделать объявление и подать CVE. Вот что они ответили:

Здравствуй, Сергей!
Эта ошибка была исправлена ещё в сентябре; мы не поддерживаем старые версии Rust. По нашим текущим правилам, сейчас это невозможно. Но на всякий случай я подниму эту проблему на ближайшем совещании рабочей группы.

И через некоторое время:

<отрывок>
Мы обсудили это в среду вечером.
И в этом моменте решили внести изменения в правила.
По действующим правилам мы поддерживаем поддерживать только последнюю версию Rust.
Но мы все согласились в том, что если ошибка заслуживает отдельной доработки, то заслуживает и CVE.
Нам действительно следовало уделить больше внимания этому патчу.
Очевидно, это связано и с LTS-версиями.
Но у нас нет ни времени, ни планов разрабатывать обновление этого правила до выхода редакции 2018 года.
Мы бы очень хотели заняться этим, но на данный момент мы слишком загружены.

Должен признать, звучит справедливо.

Позже они подтвердили, что не собираются подавать CVE по этому багу, поэтому я решил не останавливаться и сделал это сам через http://iwantacve.org/. Уязвимость получила номер CVE-2018-1000657.

Баги повсюду!

Это подводит к ещё одной проблеме со стандартной библиотекой: безответственности при верификации. Если баг, который лежит прямо на поверхности, оставался незамеченным целых два года, вдруг что-то похожее всё ещё прячется где-то в глубинах стандартной библиотеки?

Эта проблема есть не только у Rust. Erlang, на котором программируют системы с надёжностью в 99,9999999 процентов (и это не преувеличение), уже неоднократно выпускали с неправильной реализацией ассоциативных массивов в стандартной библиотеке. Есть четыре просто потрясающие статьи, в которых рассмотрен системный подход к обнаружению таких проблем.

Чтобы стандартная библиотека Rust в действительности была такой безопасной, как об этом заявляют, нужно в разы повысить качество её тестирования и верификации. Правильность некоторых её примитивов доказали математически в рамках проекта RustBelt, но он не охватывал реализации структур данных.

Иллюстрация: Redbubble

Иллюстрация: Redbubble

Первый способ сделать это — использовать тот же самый подход, что и для верификации ассоциативных массивов в Erlang: построить их модель поведения, на основе чего автоматически генерировать тесты, а далее сверять выходные данные модели и реализации для определённых входных. В Rust уже есть такие инструменты тестирования: QuickCheck и proptest.

Второй способ — использовать фреймворки для символьных вычислений, например KLEE или SAW. Они анализируют код и вычисляют все возможные состояния программы для всех возможных путей исполнения. Это позволяет либо генерировать вводные, которые спровоцируют сбои, либо убедиться, что вероятность того или иного поведения исключена.

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

Но прежде чем навсегда отказаться от Rust, имейте в виду, что в среде выполнения Python обнаруживается около 5 уязвимостей, связанных с удалённым выполнением кода, в год — и это только те, которые были попали в CVE. Сколько ещё уязвимостей кроется в недрах среды, известно только «плохим парням».

Всё уязвимо

Однажды я сообщил о переполнении буфера в популярной библиотеке на С, которая используется в одном из крупнейших браузеров. Это была классическая уязвимость безопасности, которую можно было спровоцировать просто открытием веб-страницы. Мне ответили, что баг, сильно не афишируя, исправили в последующем обновлении, которое никто ещё не установил. Когда я сказал поддержке, что следует подать CVE, они заявили, что если бы регистрировали каждый такой баг, то им было бы некогда заниматься серьёзными делами.

Самое печальное, что уязвимость в той библиотеке нашёл полностью автоматизированный инструмент меньше, чем за день. Всё, что делал я — просто водил курсором и кликал. Представьте, сколько других эксплуатируемых багов при желании могли бы найти исследователь безопасности.

И тогда я понял и осознал тот факт, что уязвимо абсолютно всё.

Я продолжаю пользоваться тем браузером, но у меня нет выбора: любой практичный браузер работает на огромной базе С-кода. А писать безопасный код на С люди, очевидно, ещё не научились.

Именно поэтому я так надеюсь на Rust. Это единственный язык в мире, который имеет все шансы окончательно и бесповоротно вытеснить С и С++, но при этом обеспечить безопасность по памяти. Есть математическое доказательство правильности для практической части Rust и даже некоторых небезопасных примитивов стандартной библиотеки, его постоянно расширяют, и оно охватит язык ещё больше.

А значит, безопасный Rust есть. По-настоящему сложные теоретические задачи решены. Но небезопасные по своей сути элементы реализации, например языковая среда выполнения, заслуживают большего внимания.

Обсуждение