Иллюзия абсолютной защиты
На одном из прошлых конкурсов по кибербезопасности Pwn2Own исследователь продемонстрировал впечатляющую уязвимость, успешно атаковав все основные браузеры — Chrome, Firefox, Safari и Edge, которыми пользуются миллиарды людей по всему миру. Этот случай, как и многие другие, наглядно демонстрирует фундаментальный парадокс: несмотря на все усилия разработчиков, даже самые защищённые программные системы продолжают взламываться.
Самый эффективный подход к безопасности заключается в том, чтобы встраивать её в программное обеспечение с самого начала, на этапе проектирования и разработки. В отличие от физических систем, поведение которых изучается эмпирически, программные системы полностью описываются своим исходным кодом. Этот код, следующий синтаксическим и семантическим правилам языка программирования, отражает изначальные намерения программиста. Поскольку программное обеспечение работает на основе чётко определённых инструкций, мы, теоретически, можем с высокой точностью анализировать, контролировать и мониторить его поведение. Развивая всё более совершенные инструменты и процессы безопасности, мы, в пределе, должны бы были предотвращать успешные атаки злоумышленников. Но так ли это на практике? Можно ли раз и навсегда решить проблему кибербезопасности?
Гипотетический сценарий: максимально защищённая система
Давайте представим, что мы использовали все доступные инструменты и методологии для проектирования, разработки и поддержки нашей программной системы, где безопасность является первоочередным приоритетом. Мы применили комплексный подход, включивший в себя:
- Проактивные стратегии: создание моделей угроз, использование memory-safe языков программирования (таких как Rust, Go), следование строгим принципам безопасной разработки.
- Оборонительные тактики: регулярное проведение аудитов кода, применение методов статического и динамического анализа (SAST/DAST), фаззинг для выявления скрытых дефектов.
- Формальные методы: даже формальную верификацию наиболее критичных компонентов системы, математически доказывая их соответствие определённым свойствам.
Возникает резонный вопрос: достаточно ли этого? Можем ли мы, наконец, заявить о полной безопасности такой системы? Ответ, увы, отрицателен. И на то есть веские причины.
Почему гарантированная безопасность — это миф?
Существуют как мини две фундаментальные причины, по которым мы не можем гарантировать отсутствие уязвимостей в любой программной системе.
1. Неизвестные неизвестности (Unknown Unknowns)
Мы не знаем того, чего мы не знаем. Чтобы система могла противостоять атакам, мы должны понимать, какие именно свойства должны выполняться. Зачастую мы осознаём, что то или иное поведение программы является уязвимостью, лишь постфактум.
Классическим примером служит спекулятивное выполнение в процессорах. Изначально эта технология была создана исключительно для повышения производительности, и она превосходно справлялась со своей задачей. Однако потребовался взгляд специалиста по безопасности, обладающего изрядной долей любопытства, чтобы обнаружить конфликт с требованием постоянного времени выполнения (constant-time execution) для криптографических алгоритмов. Это требование означает, что операции должны выполняться за строго одинаковое время, независимо от обрабатываемых секретных данных.
Спекулятивное выполнение нарушает это свойство. Злоумышленник, измеряя микроскопические временные задержки, может восстановить секретные ключи. Как мы можем проверить или обеспечить это высокоуровневое свойство на практике? Отключить спекулятивное выполнение (уязвимость Spectre)? Контролировать кеш-промахи (Meltdown)? Запретить все зависящие от данных оптимизации в компиляторе и процессоре? Любое решение будет конкретным и специфичным.
Таким образом, атакующий всегда может эксплуатировать:
- Отсутствие свойств, о необходимости которых мы даже не подозреваем.
- Специфичность методов, с помощью которых мы пытаемся обеспечить известные нам свойства.
2. Проблема моделирования (The Modeling Gap)
Из соображений эффективности мы обычно рассуждаем о поведении системы в рамках некой упрощённой модели. На основе свойств этой модели мы делаем выводы о реальной, работающей в production-среде системе.
- Доказуемая безопасность (криптография, верификация моделей) моделирует полное поведение системы с помощью формальных методов, позволяя делать универсальные утверждения о её свойствах.
- Статический анализ (SAST) моделирует все возможные выполнения программы после парсинга её исходного или бинарного кода.
- Динамический анализ (DAST) моделирует текущее выполнение программы на определённом уровне абстракции (санитизация, песочницы, Trusted Execution Environments).
Атакующий всегда может эксплуатировать несоответствие между моделью и реальностью. Яркой иллюстрацией является запуск формально верифицированной реализации криптографического протокола на Rust на процессорах с ошибкой в микрокоде. В 2023 году была обнаружена уязвимость (CVE-2023–205932) в процессорах AMD Zen 2, которая позволяла передавать параметры таких критически важных операций, как memcpy или strcmp, в открытом виде, пересекая границы виртуальных машин, песочниц и процессов. Для эксплуатации достаточно было спровоцировать специфическую последовательность низкоуровневых событий в процессоре.
Гарантии, установленные на уровне протокола, исходного кода или даже исполняемого файла, перестают работать, если процессор ведёт себя не так, как мы предполагали. В зазоре между исходным кодом и исполняемым файлом таится неопределённое поведение (undefined behavior), являющееся причиной до 72% эксплойтов в дикой природе. В зазоре между исполняемым файлом и реальным процессом в памяти мы находим аппаратные уязвимости, такие как ошибки микрокода, побочные каналы утечки информации и атаки типа RowHammer (когда многократный доступ к определённым строкам памяти в чипах DRAM вызывает битовые переключения в соседних строках).
Без учёта всех предположений о компиляторе и аппаратном обеспечении практически невозможно делать достоверные утверждения о свойствах работающей системы.
Безопасность — это не бинарное состояние, а непрерывный процесс
Если гарантий нет, стоит ли тогда вообще тратить силы на защиту? Стоит, потому что безопасность программной системы — это не бинарное свойство («есть/нет»), которое нужно гарантировать. На практике безопасность — это, в первую очередь, эмпирическая и числовая характеристика, которую необходимо постоянно усиливать.
Истинная цель любых инструментов и процессов безопасности (включая те, что designed для формальных гарантий) — повышать общий уровень защищённости системы. Другими словами, мы стремимся снизить вероятность того, что злоумышленник сможет скомпрометировать конфиденциальность, целостность или доступность нашей системы. Если вам кажется, что ваша корпоративная система «гарантированно защищена», это лишь означает, что вы пока не столкнулись с контрпримером. Мы можем лишь асимптотически приближаться к абсолютной безопасности, но никогда не достигнем её.
Новая парадигма: безопасность как экономика затрат и стимулов
Нам следует рассматривать безопасность через призму экономики и затрат атакующего.
- Безопасность как стоимость атаки. Вернёмся к примеру с производителем смартфонов. Каждое новое jailbreak-решение заставляет его разрабатывать новые механизмы защиты. Одни из них становятся долгосрочными, прорывными и общеотраслевыми mitigation (например, аппаратная изоляция процессов), другие — лишь точечными заплатками. Однако в совокупности они неуклонно повышают стоимость следующей успешной атаки. Если несколько лет назад для взлома было достаточно получить доступ на чтение/запись в ядре, то сегодня это лишь начальная точка для многомесячной работы исследователей.
- Безопасность как функция стимула. Существует значительный спрос на jailbreak, а значит, и на преодоление защитных мер вендора. С одной стороны — предложение защитных мер от вендора. С другой — спрос на их обход со стороны сообщества. Если спроса нет, система может казаться защищённой, независимо от реальной силы обороны. Если же спрос высок, система может восприниматься извне как уязвимая, хотя внутренне мы прилагаем огромные усилия.
Интересно, что это означает: система с большим количеством публично известных уязвимостей (CVE) может технически быть намного безопаснее системы, у которой CVE нет вовсе. Отчёты об успешных атаках (через программы bug bounty, упражнения Red Team) — это единственный надёжный сигнал, показывающий текущую силу наших защитных рубежей.
Инженерная практика: закалка защит через контрпримеры
Следовательно, если наши инструменты и процессы не смогли обнаружить или предотвратить конкретную уязвимость — не стоит отчаиваться! Это не означает их неэффективность в целом. Это значит, что их необходимо закалить (harden). Мы должны выявить конкретную причину, по которой они дали сбой в данном частном случае, и исправить её. Мы усиливаем нашу оборону шаг за шагом, инкрементально и под руководством контрпримеров.
Поскольку безопасность — свойство эмпирическое, и подход должен быть эмпирическим. Вмеlet заявлений об эффективности наших защит нам следует сосредоточиться на утверждениях о нашей неспособности найти контрпримеры.
Роль сообществ: смещение фокуса
- Для защитников (Defensive Security): При оценке новой методики вместо сбора доказательств, подтверждающих её эффективность, следует активно искать свидетельства, которые её опровергают. Необходимо анализировать риски потенциальных атак, которые остаются возможными несмотря на применение этой методики. Инструменты должны быть спроектированы так, чтобы их можно было адаптировать для противодействия новым, ещё неизвестным типам атак (как, например, CodeQL позволяет добавлять новые правила детекции).
- Для атакующих (Offensive Security): Любой новый тип атаки становится контрпримером для всех существующих систем защиты. Помимо анализа самой атаки, следует добавлять анализ того, почему существующие защиты не сработали. Необходимо давать конкретные рекомендации, как эти защиты можно закалить против наиболее общей версии данной атаки.
- Для инженеров-программистов (Software Engineering): Здесь открывается пространство для автоматизации. Методы автоматизированной инженерии программного обеспечения (автоматизированная отладка, автоматическое исправление программ) могут быть использованы для локализации корневой причины сбоя в защите и автоматического внесения исправлений в инструменты, чтобы сделать их эффективными против новой угрозы.
Метафора рыболовной сети
Позвольте предложить метафору. Представьте, что поиск уязвимостей — это ловля рыбы. Наши инструменты и процессы — это рыболовная сеть. Уязвимости — это рыба. Утверждение заключается в том, что для каждой сети всегда найдётся рыба, которая проскользнёт сквозь её ячейки. Но это никоим образом не отменяет полезности сети как таковой.
Мы должны осознать, что не существует и не будет создана некая «идеальная» сеть, которая поймает всю рыбу. Вместо этого нам необходимо систематическое, поэтапное, эволюционное развитие наших защит, управляемое контрпримерами, чтобы эмпирически максимизировать их эффективность. Фокус смещается с концептуальной разработки новых защит на эмпирическое выявление и устранение ограничений существующих.
Заключение: Окончательное решение — в бесконечном процессе
Как же раз и навсегда решить проблему кибербезопасности? Мы должны принять, что абсолютных гарантий не существует — всегда может найтись неизвестная атака, которая окажется успешной. Однако мы можем асимптотически приближаться к максимальной безопасности, двигаясь методом контрпримеров.
Для широкого круга читателей это означает, что нам следует перестать пытаться подтвердить эффективность наших защит и начать безуспешно пытаться найти контрпримеры к их эффективности. Именно так — по одному контрпримеру за раз — мы и будем «решать» проблему кибербезопасности, понимая, что это не конечная цель, а бесконечный процесс укрепления и адаптации.