В последнее время все больше говорят о статическом анализе как об одном из важных средств обеспечения качества разрабатываемых программных продуктов, особенно с точки зрения безопасности.
Статический анализ позволяет найти уязвимости и другие ошибки; его можно использовать в процессе разработки, интегрируя в индивидуальные процессы.
Однако в связи с его использованием возникает много вопросов.
В чем разница между платными и бесплатными инструментами? Почему недостаточно использовать линтер? Ведь причем здесь статистика? Давайте попробуем разобраться.
Сразу ответим на последний вопрос: статистика тут ни при чем, хотя статический анализ часто ошибочно называют статистическим.
Анализ статический, поскольку сканирование не запускает приложение.
Для начала давайте разберемся, что мы хотим искать в программном коде.
Статический анализ чаще всего используется для поиска уязвимостей — участков кода, наличие которых может привести к нарушению конфиденциальности, целостности или доступности информационной системы.
Однако те же технологии можно использовать для поиска других ошибок или особенностей кода.
Оговоримся, что в целом задача статического анализа алгоритмически неразрешима (например, по теореме Райса).
Поэтому приходится либо ограничивать условия задачи, либо допускать неточность результатов (пропускать уязвимости, давать ложные срабатывания).
Оказывается, в реальных программах работает второй вариант. Существует множество платных и бесплатных инструментов, которые утверждают, что ищут уязвимости в приложениях, написанных на разных языках программирования.
Давайте посмотрим, как обычно работает статический анализатор.
Далее мы поговорим конкретно о ядре анализатора, об алгоритмах.
Конечно, инструменты могут отличаться удобством интерфейса, набором функционала, набором плагинов для разных систем и удобством использования API. Наверное, это тема для отдельной статьи.
Промежуточное представительство
Схема работы статического анализатора состоит из трех основных этапов.
- Построение промежуточного представления (промежуточное представление еще называют внутренним представлением или моделью кода).
- Использование алгоритмов статического анализа, в результате чего модель кода дополняется новой информацией.
- Применение правил поиска уязвимостей к дополненной модели кода.
Подобно компиляторам, лексический и синтаксический анализ используются для построения внутреннего представления, чаще всего дерева разбора (AST, Abstract Syntax Tree).
Лексический анализ разбивает текст программы на минимальные смысловые элементы, в результате чего образуется поток лексем.
Синтаксический анализ проверяет, что поток токенов соответствует грамматике языка программирования, то есть полученный поток токенов корректен с точки зрения языка.
В результате синтаксического анализа строится дерево разбора — структура, моделирующая исходный текст программы.
Далее применяется семантический анализ, он проверяет выполнение более сложных условий, например, соответствие типов данных в инструкциях присваивания.
Дерево разбора можно использовать в качестве внутреннего представления.
Вы также можете получить другие модели из дерева разбора.
Например, можно перевести его в трехадресный код, который, в свою очередь, используется для построения графа потока управления (CFG).
Обычно CFG является основной моделью для алгоритмов статического анализа.
В бинарном анализе (статическом анализе двоичного или исполняемого кода) также строится модель, но здесь уже используются практики обратного инжиниринга: декомпиляция, деобфускация, обратная трансляция.
В результате можно получить те же модели, что и из исходного кода, в том числе и исходного (с помощью полной декомпиляции).
Иногда промежуточным представлением может служить сам двоичный код. Теоретически, чем ближе модель к исходному коду, тем хуже будет качество анализа.
В самом исходном коде можно осуществлять поиск только с помощью регулярных выражений, что не позволит найти даже самую простую уязвимость.
Анализ потока данных
Одним из основных алгоритмов статического анализа является анализ потоков данных.Задача такого анализа — определить в каждой точке программы некоторую информацию о данных, с которыми работает код. Информация может быть разной, например, тип данных или значение.
В зависимости от того, какую информацию необходимо определить, можно сформулировать задачу анализа потока данных.
Например, если необходимо определить, является ли выражение константой, а также значение этой константы, то решается задача распространения константы.
Если необходимо определить тип переменной, то можно говорить о проблеме распространения типов.
Если вам необходимо понять, какие переменные могут указывать на определенную область памяти (хранить одни и те же данные), то речь идет о задаче анализа алиасов.
Существует множество других задач анализа потока данных, которые можно использовать в статическом анализаторе.
Подобно этапам построения модели кода, эти задачи также используются в компиляторах.
Теория построения компилятора описывает решения проблемы внутрипроцедурного анализа потоков данных (данные должны отслеживаться в рамках одной процедуры/функции/метода).
Решения основаны на теории алгебраических решеток и других элементах математических теорий.
Задача анализа потока данных может быть решена за полиномиальное время, то есть за время, приемлемое для компьютеров, если условия задачи удовлетворяют условиям теоремы разрешимости, что не всегда имеет место на практике.
Поговорим подробнее о решении задачи внутрипроцедурного анализа потоков данных.
Для постановки конкретной задачи помимо определения необходимой информации необходимо определить правила изменения этой информации при передаче данных через инструкции в CFG. Напомним, что узлами в CFG являются базовые блоки — наборы инструкций, выполнение которых всегда происходит последовательно, а дуги указывают на возможную передачу управления между базовыми блоками.
Для каждой инструкции
определены наборы:
-
(информация, генерируемая инструкцией
), -
(информация уничтожена по инструкции
), -
(информация в пункте перед инструкцией
), -
(информация в пункте после инструкции
).
И
за каждую инструкцию
программы.
Основная система уравнений, с помощью которой решаются задачи анализа потоков данных, определяется следующим соотношением (уравнения потока данных):
Второе соотношение формулирует правила, по которым информация «объединяется» в точках слияния дуг КФГ (
– предшественники
в КФГ).
Могут использоваться операции объединения, пересечения и некоторые другие.
Искомая информация (множество значений введенных выше функций) формализуется в виде алгебраической решетки.
Функции
И
рассматриваются как монотонные отображения на решетках (функции потока).
Для уравнений потока данных решением является фиксированная точка этих отображений.
Алгоритмы решения задач анализа потока данных ищут максимальные фиксированные точки.
Существует несколько подходов к решению: итерационные алгоритмы, анализ сильно связанных компонентов, Т1-Т2-анализ, интервальный анализ, структурный анализ и так далее.
Существуют теоремы о корректности этих алгоритмов; они определяют сферу своей применимости к реальным задачам.
Повторюсь, условия теорем могут не выполняться, что приводит к усложнению алгоритмов и неточным результатам.
Межпроцедурный анализ
На практике приходится решать задачи межпроцедурного анализа потоков данных, поскольку уязвимость редко будет полностью локализована в одной функции.Существует несколько общих алгоритмов.
Встраивание (встроенных) функций .
В точке вызова функции мы встраиваем вызываемую функцию, тем самым сводя задачу межпроцедурного анализа к задаче внутрипроцедурного анализа.
Этот метод легко реализовать, но на практике при его применении быстро достигается комбинаторный взрыв.
Построение общего графа потока управления программой , в котором вызовы функций заменяются переходами на начальный адрес вызываемой функции, а инструкции возврата заменяются переходами на все инструкции, следующие за всеми инструкциями по вызову этой функции.
Такой подход добавляет большое количество нереализуемых путей выполнения, что сильно снижает точность анализа.
Алгоритм аналогичен предыдущему, но при переключении на функцию сохранение контекста – например, кадр стека.
Это решает проблему создания нереализуемых путей.
Однако алгоритм применим, когда глубина вызовов ограничена.
Построение информации о функциях (сводка функций).
Наиболее применимый алгоритм межпроцедурного анализа.
Он основан на построении сводки для каждой функции: правил, по которым преобразуется информация данных при применении заданной функции в зависимости от различных значений входных аргументов.
Готовые сводки используются для внутрипроцедурного анализа функций.
Отдельную сложность здесь составляет определение порядка обхода функций, так как при внутрипроцедурном анализе уже должны быть построены сводки по всем вызываемым функциям.
Обычно создаются специальные итерационные алгоритмы обхода графа вызовов.
Межпроцедурный анализ потоков данных представляет собой задачу экспоненциальной сложности, поэтому анализатору необходимо сделать ряд оптимизаций и допущений (невозможно найти точное решение за время, адекватное вычислительным мощностям).
Обычно при разработке анализатора необходимо искать компромисс между количеством потребляемых ресурсов, временем анализа и количеством ложных срабатываний и найденных уязвимостей.
Поэтому статический анализатор может работать долго, потреблять много ресурсов и выдавать ложные срабатывания.
Однако без этого невозможно найти критические уязвимости.
Именно этим серьезные статические анализаторы отличаются от многих открытых инструментов, которые, в том числе, могут позиционировать себя и на поиске уязвимостей.
Быстрые проверки за линейное время хороши, когда результат нужно получить быстро, например, в процессе компиляции.
Однако этот подход не может найти наиболее критичные уязвимости — например, связанные с внедрением данных.
Анализ загрязнений
Отдельно стоит остановиться на одной из задач анализа потоков данных — taint-анализе.Анализ искажений позволяет распределять флаги по всей программе.
Данная задача является ключевой для информационной безопасности, поскольку именно с ее помощью выявляются уязвимости, связанные с внедрением данных (SQL-инъекции, межсайтовый скриптинг, открытые перенаправления, подмена путей к файлам и т. д.), а также утечка конфиденциальных данных ( запись пароля в журналы событий, небезопасная передача данных).
Попробуем смоделировать проблему.
Допустим, мы хотим отслеживать n флагов –
.
Набор информации здесь будет состоять из множества подмножеств
, поскольку для каждой переменной в каждой точке программы мы хотим определить ее флаги.
Далее нам нужно определить функции потока.
В этом случае функции тока можно определить из следующих соображений.
- Указано множество правил, определяющих конструкции, приводящие к появлению или изменению набора флагов.
- Операция присваивания перемещает флаги справа налево.
- Любая операция, неизвестная наборам правил, объединяет флаги всех операндов, и полученный набор флагов добавляется к результатам операции.
Слияние определяется правилом слияния, то есть, если разные наборы флагов для одной переменной пришли из разных базовых блоков, то при слиянии они объединяются.
Здесь же появляются ложные срабатывания: алгоритм не гарантирует, что путь в CFG, на котором появился флаг, может быть выполнен.
Например, вам нужно обнаружить уязвимости SQL-инъекций.
Эта уязвимость возникает, когда непроверенный пользовательский ввод попадает в методы базы данных.
Необходимо определить, что данные пришли от пользователя, и добавить к таким данным флаг taint. Обычно правила установки флага taint указаны в базе правил анализатора.
Например, установите флаг для возвращаемого значения метода getParameter() класса Request.
Далее необходимо с помощью taint-анализа распределить флаг по всей анализируемой программе, принимая во внимание, что данные могут быть проверены и флаг может исчезнуть на одном из путей выполнения.
В анализаторе определено множество функций, сбрасывающих флаги.
Например, функция проверки данных тега HTML может снять флаг уязвимости межсайтового скриптинга (XSS).
Или функция привязки переменной к выражению SQL очищает флаг встраивания SQL.
Правила поиска уязвимостей
В результате применения вышеуказанных алгоритмов промежуточное представление дополняется информацией, необходимой для поиска уязвимостей.Например, в модели кода появляется информация о том, какие переменные имеют определенные флаги и какие данные являются константами.
Правила поиска уязвимостей формулируются в терминах модели кода.
Правила описывают, какие функции в окончательном промежуточном представлении могут указывать на наличие уязвимости.
Например, вы можете применить правило поиска уязвимостей, которое будет определять вызов метода с параметром, имеющим флаг taint. Возвращаясь к примеру SQL-инъекции, мы проверим, чтобы переменные с флагом taint не попадали в функции запроса к базе данных.
Оказывается, важной частью статического анализатора, помимо качества алгоритмов, является конфигурация и база правил: описание того, какие конструкции в коде генерируют флаги или другую информацию, какие конструкции проверяют такие данные и для чего в каких конструкциях использование таких данных имеет решающее значение.
Другие подходы
Помимо анализа потоков данных, существуют и другие подходы.Одним из известных является символическое исполнение или абстрактная интерпретация.
Эти подходы включают выполнение программы в абстрактных областях, вычисление и распространение ограничений на данные в программе.
Используя такой подход, можно не только найти уязвимость, но и рассчитать условия для входных данных, при которых уязвимость может быть использована.
Однако у этого подхода есть и серьёзные недостатки — при стандартных решениях на реальных программах алгоритмы взрываются экспоненциально, а оптимизации приводят к серьёзным потерям в качестве анализа.
выводы
В заключение, думаю, стоит подвести итоги, рассказав о плюсах и минусах статического анализа.Логично, что мы сравним его с динамическим анализом, при котором поиск уязвимостей происходит во время выполнения программы.
Несомненным преимуществом статического анализа является полный охват анализируемого кода.
Еще одним преимуществом статического анализа является то, что для его запуска нет необходимости запускать приложение в производственной среде.
Статический анализ можно реализовать на самых ранних этапах разработки, минимизируя стоимость найденных уязвимостей.
Недостатками статического анализа являются неизбежное наличие ложных срабатываний, потребление ресурсов и длительное время сканирования больших объемов кода.
Однако эти недостатки неизбежны, исходя из специфики алгоритмов.
Как мы видели, быстрый анализатор никогда не найдет настоящую уязвимость, такую как SQL-инъекция и тому подобное.
Мы писали о других трудностях использования инструментов статического анализа, которые, как оказалось, можно преодолеть в еще одна статья .
Теги: #информационная безопасность #Тестирование мобильных приложений #Тестирование ИТ-систем #Идеальный код #статический анализ кода #sast #поиск уязвимостей #анализ потоков данных
-
Роль Приложений Социальных Сетей В Общении
19 Oct, 24 -
Лауреаты Игнобелевской Премии 2007 Г.
19 Oct, 24 -
Вышел Агент Mail.ru Для Мобильных Телефонов
19 Oct, 24