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

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

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

Исторически первые инструменты статического анализа (часто в их названии используется слово “lint”) применялись для поиска простейших дефектов программы. Они использовали простой поиск по сигнатурам, то есть обнаруживали совпадения с имеющимися сигнатурами из базы проверок. Такие инструменты применяются до сих пор и позволяют определять “подозрительные” конструкции в коде, которые могут вызвать падение программы при её выполнении.

Недостатков у этого метода немало. Основной состоит в том, что множество “подозрительных” конструкций в коде не всегда являются дефектами. В большинстве случаев такой код может быть синтаксически правильным и работать корректно. Соотношение “шума” и реальных дефектов в больших проектах может достигать 100:1. Таким образом, разработчику приходится тратить много времени на отсеивание шума, что отменяет плюсы автоматизированного поиска.

Тем не менее, несмотря на недостатки, инструменты данного типа используются и сегодня благодаря их простоте и низкой стоимости.

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

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

Основными достоинствами этого типа анализаторов являются меньшее количество “шума” за счет частичного моделирования выполнения программ и возможность обнаружения более сложных дефектов.

Для иллюстрации приведем процесс поиска уязвимостей инъекции кода и SQL-инъекции. Для их обнаружения находятся места в программе, откуда поступают недоверенные данные, например, GET-запрос протокола HTTP. Переменная, в которой хранятся данные из GET-запроса, помечается. В дальнейшем, если значения этой переменной присваиваются другой переменной (или проходит схожая операция), то эта другая переменная тоже помечается. Таким образом, в графе потока данных помечаются все переменные, которые зависят от недоверенного источника. Если помеченная переменная попадает в критичную системную функцию, которая отвечает за выполнение команды операционной системы (например, Runtime.exec в Java) или делает запрос к базе данных, то это означает потенциальную возможность внедрения кода или SQL-запроса.

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

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

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

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

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

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

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

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

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

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

Автор статьи — старший исследователь департамента анализа кода в ERPScan (дочерняя компания Digital Security).