Статический анализ
Статический анализ – это семейство методов анализа кода «статически», то есть без его выполнения. Такой анализ может находить уязвимые конструкции кода и быть интегрирован в конвейеры сборки, ему доступен широкий спектр инструментов.
Инструменты статического анализа справедливо критикуют за иногда непонятный вывод, ложные срабатывания, а иногда за скорость – они могут медленно работать на больших объемах кода. Кроме того, они могут быть сложными в развертывании, что приводит к многочисленным предупреждениям. Несмотря на эти трудности, статический анализ остается мощным инструментом в вашем инструментарии. Многие организации развертывают его постепенно, используя настройки, чтобы убедиться в чистоте нового кода, и постепенно применяя правила к старому коду.
Защита в деталях
У крупных поставщиков платформ есть специальные команды, которые разрабатывают средства защиты, чтобы затруднить использование проблем безопасности памяти.
Эти средства защиты с такими названиями, как «рандомизация структуры адресного пространства» или «неисполняемые области памяти» (ASLR, NX), выходят за рамки данной книги. Различия в этих защитах означают, что одна и та же проблема порчи памяти в одном и том же коде может быть скомпилирована во что-то, что может проявиться на одной платформе, но не на другой.
Важно подчеркнуть, что чрезвычайно опасно, когда злоумышленник имеет возможность читать или записывать память способами, которые не были точно и тщательно ограничены. Если память может быть повреждена, то хорошей практикой программирования является исправление повреждения, а не споры о возможности эксплуатации уязвимости.
Память может быть повреждена таким способом, который будет работать на одной платформе, но не будет на другой. Она может быть испорчена такими способами, для которых еще не понятна уязвимость, потому что искусство превращения порчи в эксплойт продвигается вперед, как и защитные механизмы, которые затрудняют эту трансформацию. Ваше справедливое убеждение, что проблему не получится использовать для взлома, порой зависит от деталей синтаксического анализатора, которые позже могут меняться, делая вас уязвимым.
В лучшем случае вы тратите немногочисленные квалифицированные ресурсы, чтобы убедиться, что вы должны сделать то, о чем я вам только что сказал, то есть устранить повреждение. В худшем случае вы ошибочно считаете, что уязвимость, которой можно воспользоваться, таковой не является, и оставляете свой код уязвимым для атаки. (Если исправление проблем с повреждением является непосильной задачей или кажется сизифовым трудом, возможно, ваш код нуждается в рефакторинге.)
Динамический анализ, включая фаззинг
Динамический анализ – это такой анализ, который запускает код, чтобы увидеть, как он ведет себя при вводе вредоносных, неправильно сформированных или даже просто случайных входных данных.
Напомним, что инструкции процессора – это всего лишь биты, подобные тем, которые запускают влагосборники. Фаззинг, отправка случайных входных данных и наблюдение за тем, что произойдет, удивительно эффективен для поиска ошибок. Это особенно верно для кода, написанного на низкоуровневых языках, таких как C, но не ограничивается таким кодом. Поскольку синтаксический анализ является сложным процессом, когда был изобретен фаззинг, от четверти до трети программ не справлялись с тем, что мы сейчас называем тупым фаззингом [Miller, 1990]. И, чтобы было понятно, фаззинг может быть чрезвычайно простым, вплоть до cat /dev/random | target.
Конечно, неудачи синтаксического анализатора, которые возникают при простом фаззинге, как правило, выражаются в сбоях. Было бы замечательно, если бы эти случайные биты сделали что-то интересное. Фаззеры также имеют тенденцию приводить к множеству сбоев в одной и той же строке кода.
Фаззинг наиболее эффективен при работе с C-подобными языками, но это не значит, что он работает только против них. По мере того как вы применяете его к программам на языках с безопасностью типов и современными библиотеками синтаксического анализа, базовый фаззинг обнаруживает меньше проблем, но контекстно-зависимые фаззеры теперь являются обычным явлением.
Ранее в этой главе я приводил некоторые статистические данные («70 из 78 уязвимостей, обнаруженных с помощью OSS-Fuzz на прошлой неделе, являются небезопасными для памяти»). Это показывает, что фаззеры гораздо лучше находят проблемы с безопасностью памяти, чем что-то еще, и то, что они обнаруживают, как правило, имеет все шансы стать очень серьезной проблемой.
LangSec, или теоретико-языковая безопасность, является академическим движением. Это движение указывает, что сбои синтаксического анализа переплетаются с проблемами безопасности и что по мере усложнения языков и анализирующего их кода также растет возможность заставить их делать шокирующие вещи. Эта глава в значительной степени опирается на работы участников движения, хотя надо признать, что не у каждого инженера может хватить бюджета, навыков или объема контроля, чтобы действовать в соответствии со всеми их предложениями.
Простой дизайн, как и форматы, которые могут быть разобраны с помощью регулярных выражений, делает синтаксический анализ гораздо менее рискованным. И наоборот, мощные языки со сложной грамматикой (скажем, PDF или Office) более опасны. Составные объявления длины, в которых внешние и внутренние объекты имеют не обязательно совпадающие длины, более опасны, чем более простые определения. К другим некорректным шаблонам относятся самомодифицирующиеся форматы, форматы, требующие нескольких проходов, и команды в стиле eval. И, пожалуйста, случайно не напишите еще один тьюринг-полный язык.
Многие разработчики не могут даже толком определиться с выбором известного языка или формата файлов, поэтому может показаться, что рекомендации LangSec находятся в далекой-далекой галактике. Но многие из нас определяют маленькие языки, маленькие конечные автоматы или маленькие протоколы. Контракт между вызывающим и вызываемым объектами API – это язык, а простота и предсказуемость способствуют не только безопасности, но и предсказуемости, тестируемости и устойчивости.
Если вы сосредоточены именно на этих проблемах, узнайте больше на LangSec.org.
Шаблон Recognizer
Recognizer принимает входные данные и создает выходные данные, которые ограничены тем, что ожидает весь остальной код. Когда вы объединяете синтаксический анализ кода в одном месте, становится легче рассуждать об этом. Recognizer принимает допустимый вход и отбрасывает недопустимый. (Это может привести к отклонению всего сообщения или его части, передавая структуру данных, отличную от входной.) В шаблоне Recognizer допустимые входные данные определяются грамматикой, а синтаксический анализ завершается, прежде чем передать объект на валидацию или проверку на логику приложения. То, что ожидает ваш код, в идеале определяется явными грамматиками, а иногда и контрактами, поддерживаемыми модульными тестами или случайным поведением клиентов.
Может быть полезно запускать Recognizer изолированно от остальной части приложения. Как обсуждалось в главе 6, qmail довел это до уровня запуска в качестве отдельного идентификатора пользователя и передавал сообщения через файлы. Конечно, это потребует от вас десериализации этих файлов, что является еще одним парсером для защиты.
Также может быть полезно рассматривать Recognizer и Validator как связанные шаблоны. Recognizer просто анализирует (размечает и конструирует объект), в то время как Validator проверяет осмысленность этих объектов, выходящую за рамки того, что содержится в грамматике. Например, убедиться, что URL-адрес в данный момент является «действительным» (возможно, сервер возвращает сообщение HTTP OK и HTML-документ) или что электронное письмо не отклоняется при попытке доставки.
Шаблон единого парсера
Для любого формата, который вы обрабатываете, выберите и используйте один синтаксический анализатор. Конечно, для большинства форматов, которые мы анализируем, мы не писали парсер. Зачем писать свой собственный парсер или визуализатор для JPG, когда есть дюжина версий с открытым исходным кодом? Выбор небезопасного парсера приведет к нескончаемому параду «охотников за головами» у вашей двери. И хотя люди, зарабатывающие деньги премиями за обнаружение уязвимостей, не являются подонками или злодеями, выбор лучшего парсера позволит вам сосредоточиться на другой работе. Несколько вещей, на которые следует обратить внимание, включают значок Open Source Security Foundation и использование языков, безопасных для памяти. Наличие небольшого количества проблем с безопасностью может быть признаком зрелости; парад таких проблем, вероятно, позволяет хорошо предсказать ваше будущее.
Если у вас есть два синтаксических анализатора, которые принимают одни и те же входные данные, используют одну и ту же схему и выдают разные выходные данные, то по крайней мере один из них должен быть неверным. Эта неправильность приведет как минимум к непоследовательности.
В зависимости от характера непоследовательности умные злоумышленники могут воспользоваться ею. (Конечно, это также означает, что изменение устаревшей системы на использование одного синтаксического анализатора приведет к изменениям в поведении, что делает это изменение дорогостоящим.)
Таким образом, наличие в одной организации двух синтаксических анализаторов формата – это дорогостоящий способ вызвать ошибки и отличный способ стимулировать поиск крайнего. Что касается ошибок, Apple создала два парсера для своего формата plist, и различия в обработке ими комментариев привели к тому, что один (используемый при выполненим проверок безопасности) передавал файл другому, который выполнял команды, которые первым игнорировались как часть комментариев. История с датами рождения имени Йоды в Саскачеване – это еще один пример нескольких парсеров с несовместимыми валидаторами.