Как устранить первопричину уязвимостей безопасности памяти

в 13:01, , рубрики: c++, kotlin, memory safety, Rust, ruvds_перевод, безопасность памяти, устранение уязвимостей

Как устранить первопричину уязвимостей безопасности памяти - 1


Уязвимости безопасности памяти остаются серьёзной угрозой для защиты ПО. Мы, работники Google, считаем, что путь к крупномасштабному устранению этого класса уязвимостей и к защищённому ПО заключается в Safe Coding — подходе secure-by-design, отдающем приоритет переходу на безопасные по памяти языки.

В этом посте мы покажем, почему стремление к Safe Coding при создании нового кода быстро (хотя и контринтуитивно) снижает риски безопасности кодовой базы в целом, позволяя наконец-то прорваться через неподдающееся плато уязвимостей безопасности памяти и начать экспоненциальное снижение их количества с сохранением масштабируемости и экономности.

Также мы приведём обновлённую статистику того, как благодаря переходу на безопасные по памяти языки, процент уязвимостей безопасности памяти в Android упал за шесть лет с 76% до 24%.

Контринтуитивные результаты

Возьмём для примера развивающуюся кодовую базу, преимущественно написанную на небезопасных по памяти языках, в которой стабильно возникают новые уязвимости памяти. Что произойдёт, если для реализации новых фич мы постепенно будем переходить на безопасные по памяти языки, оставляя существующий код практически неизменным (за исключением устранения багов)?

Результаты можно симулировать. Спустя несколько лет, с постепенным замедлением разработки на небезопасных языках и началом новых разработок на безопасных языках, кодовая база приобрела следующий вид1:

Как устранить первопричину уязвимостей безопасности памяти - 2

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

Как устранить первопричину уязвимостей безопасности памяти - 3

Такое снижение может показаться парадоксальным: как такое возможно, если объём небезопасного по памяти кода на самом деле рос?

Математика

Ответ таится в важном наблюдении: количество уязвимостей снижается экспоненциально. У них есть период полураспада. Распределение сроков жизни уязвимостей соответствует экспоненциальному распределению. Если обозначить средний срок жизни уязвимости как λ, то:

Как устранить первопричину уязвимостей безопасности памяти - 4

Крупномасштабное исследование сроков жизни уязвимостей2, опубликованное в 2022 году в Usenix Security, подтвердило этот феномен. Исследователи выяснили, что подавляющее большинство уязвимостей прячется в новом или недавно модифицированном коде:

Как устранить первопричину уязвимостей безопасности памяти - 5

Это подтверждает и обобщает наше наблюдение, опубликованное в 2021 году: плотность багов безопасности памяти Android уменьшалась с увеличением возраста кода, и баги в основном обнаруживались в недавних изменениях.

Это позволило сделать два важных вывода:

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

Например, исходя из средних сроков жизни уязвимости, пятилетний код имеет в 3,4 раза (судя по срокам жизни из исследования) или в 7,4 раза (судя по срокам жизни, наблюдаемым в Android и Chromium) меньшую плотность уязвимостей, чем новый код.

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

Практика Android

Команда разработчиков Android начала отдавать приоритет переходу новой разработки на безопасные по памяти языки примерно в 2019 году. Это решение было вызвано растущими затратами и сложностью управления уязвимостями безопасности памяти. Нам ещё многое предстоит сделать, но результаты уже были положительными. Вот общая картина для всего кода за 2024 год:

Как устранить первопричину уязвимостей безопасности памяти - 6

Несмотря на то, что основная часть кода по-прежнему небезопасна по памяти (но, что важно, она постепенно становится старше), мы наблюдаем серьёзное и стабильное снижение уязвимостей безопасности памяти. Результаты согласуются с нашими симуляциями и даже превосходят их; вероятно, это вызвано нашими параллельными усилиями по повышению безопасности небезопасного по памяти кода. Впервые мы сообщили об этом снижении в 2022 году и продолжаем наблюдать падение общего количества уязвимостей безопасности памяти3. Стоит отметить, что данные за 2024 год интерполированы на весь год (указаны как 36 уязвимостей, но после выпуска сентябрьского бюллетеня по безопасности их насчитывалось 27).

Как устранить первопричину уязвимостей безопасности памяти - 7

Процент уязвимостей, вызванных проблемами безопасности памяти, продолжает тесно коррелировать с языком разработки, используемым при написании нового кода. Проблемы безопасности памяти, в 2019 году составлявшие 76% от всех уязвимостей Android, в 2024 году составляют 24%, что сильно ниже нормы по отрасли в 70%, и этот показатель продолжает снижаться.

Как устранить первопричину уязвимостей безопасности памяти - 8

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

Эволюция стратегий обеспечения безопасности памяти

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

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

Второе поколение: проактивное устранение. Следующий подход заключался в снижении рисков в уязвимом ПО, в том числе и в ряде стратегий устранения возможностей эксплойтов, повысивших затраты на их создание. Однако такие меры, как stack canary и control-flow integrity, обычно требовали постоянных затрат, связанных с продуктом и командами разработки, часто вызывая конфликт между безопасностью и другими требованиями к продукту:

  • Они снижали производительность, влияли на скорость исполнения, срок работы устройств от аккумуляторов, время задержек и объём используемой памяти, часто мешая развёртыванию.
  • Изобретательность нападающих, похоже, безгранична, они играют в кошки-мышки с разработчиками. Кроме того, планка разработки и применения эксплойтов постоянно понижается благодаря улучшению инструментария и другим разработкам.

Третье поколение: проактивный поиск уязвимостей.
Следующее поколение сделало упор на выявление уязвимостей. Для него применяются санитайзеры, часто в сочетании с инструментами фаззинга наподобие libfuzzer, многие из которых были созданы Google. Несмотря на свою полезность, эти методики борются с симптомами небезопасности памяти, а не с первопричиной. Обычно они требуют постоянного давления, вынуждая команды выполнять фаззинг, проверку и устранение их находок, что приводит к низкому покрытию. Даже при надлежащем внедрении, фаззинг не обеспечивает высокой уверенности в безопасности, доказательством чего стали уязвимости, найденные в коде, подвергавшемся тщательному фаззингу.

Эти методики сильно повысили надёжность продуктов всей отрасли, и мы продолжаем реагировать на уязвимости, устранять их и проактивно на них охотиться. Тем не менее, становится всё очевиднее, что такие подходы не только неэффективны для достижения приемлемого уровня рисков в сфере безопасности памяти, но и связаны с постоянными растущими затратами со стороны разработчиков, пользователей, бизнесов и продуктов. Как подчёркнуто множеством государственных организаций, в том числе и CISA, в отчёте secure-by-design, «только внедрением практик secure by design мы разорвём порочный круг постоянного создания и применения исправлений».

Четвёртое поколение: предотвращение с гарантированной надёжностью

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

Фундаментом этого сдвига стал Safe Coding, внедривший инварианты безопасности непосредственно в платформу разработки благодаря возможностям языка, статическому анализу и архитектуре API. В результате образовалась экосистема secure by design, обеспечивающая гарантии безопасности в крупных масштабах и защищённая от рисков случайного появления уязвимостей.

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

Возможность масштабирования Safe Coding лежит в его способности снижения затрат при помощи следующего:

  • Отказа от «гонки вооружений»: вместо бесконечной «гонки вооружений» разработчиков, которые пытаются повысить затраты нападающих, увеличивая собственные затраты, Safe Coding использует наш контроль над экосистемами разработки, чтобы разорвать этот цикл и сосредоточиться на изначально проактивном создании безопасного ПО.
  • Распространение гарантированной безопасности памяти: вместо того, чтобы проектировать индивидуальные способы вмешательства параллельно с управлением затратами, Safe Coding устанавливает высокую планку общего уровня безопасности благодаря, в частности, безопасным по памяти языкам, которые экономным образом снижают плотность уязвимостей. Современные безопасные по памяти языки (в особенности Rust) расширяют эти принципы безопасности по памяти и на другие классы багов.
  • Повышение продуктивности: Safe Coding повышает корректность кода и продуктивность разработчиков, сдвигая процесс нахождения багов ещё больше влево, на этапы ещё до проверки кода. Мы наблюдаем, как этот сдвиг проявляется в таких важных метриках, как частота откатов назад (экстренных возвратов к старым версиям кода из-за неожиданного бага). По статистике команды Android, частота откатов кода на Rust в два с лишним раза меньше, чем для кода на C++.

От уроков к действию

▍ Согласованность вместо переписывания кода

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

Мы рекомендуем сосредоточить инвестиции на улучшении согласованности, как это делаем мы в случае Rust ↔︎ C++ и Rust ↔︎ Kotlin. В этом году компания Google предоставила грант на миллион долларов Rust Foundation, а также занялась разработкой такого инструментария для согласованной работы кода, как Crubit и autocxx.

▍ Роль предыдущих поколений

Safe Coding продолжает снижать риски, так останется ли в будущем место для устранения проблем и проактивного обнаружения? В случае Android мы не знаем точного ответа, но ожидаем что-то подобного:

  • Более селективного использования мер проактивного устранения: мы ожидаем, что при переходе на безопасный по памяти код разработчики будут меньше полагаться на средства устранения багов, что приведёт к созданию не только более безопасного, но и более эффективного ПО. Например, после избавления от ставшей ненужной песочницы написанный на Rust генератор QR-кодов Chromium стал на 95% быстрее.
  • Снижение объёмов использования, но повышение эффективности проактивного обнаружения: мы ожидаем снижения использования методик проактивного обнаружения наподобие фаззинга, но повышение их эффективности, ведь так становится проще достичь исчерпывающего покрытия небольших, хорошо инкапсулированных блоков кода.

В заключение

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

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


  1. Для симуляции были взяты значения, схожие с показателями Android и других проектов Google. Размер кодовой базы удваивается каждые шесть лет. Средний срок жизни уязвимостей составляет 2,5 года. Для перехода к применению безопасных по памяти языков при написании нового кода требуется десять лет; мы использовали для описания этого перехода сигмоиду. Стоит отметить, что именно из-за применения сигмоиды второй график поначалу не выглядит экспоненциальным.↩︎
  2. Alexopoulos et al. «How Long Do Vulnerabilities Live in the Code? A Large-Scale Empirical Measurement Study on FOSS Vulnerability Lifetimes». USENIX Security 22.↩︎
  3. Уязвимости взяты не из симуляции, а из реальной кодовой базы, имеющей более высокую дисперсию, что видно по небольшому увеличению в 2023 году. В том году количество отчётов об уязвимостях было необычно большим, но согласующимся с ожиданиями, учитывая увеличение кодовой базы, поэтому хотя процент уязвимостей безопасности памяти продолжал снижаться, их абсолютное значение немного выросло.↩︎

Автор: ru_vds

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js