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



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

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

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

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

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

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



    Немного об архитектуре статических анализаторов


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

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



    Многие производители статических анализаторов заявляют, что используют универсальное внутреннее представление для всех поддерживаемых анализатором языков программирования. Таким образом они могут анализировать программный код, разработанный на нескольких языках, как единое целое, а не как отдельные компоненты. «Целостный подход» к анализу позволяет избежать пропуска дефектов, которые возникают на границе взаимодействия отдельных составляющих программного продукта.

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

    Оптимизационные преобразования для статических анализаторов


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

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

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

    Очевидно, что каждое из таких преобразований «теряет» дефекты, которые должен обнаруживать анализатор, но это «плата» за эффективность по памяти и производительности.

    Что ищет статический анализатор кода?


    Условно дефекты, которые так или иначе интересуют злоумышленников, а следовательно, и аудиторов, можно разделить на следующие группы:

    • ошибки валидирования,
    • ошибки утечки информации,
    • ошибки аутентификации.

    Ошибки валидирования возникают в результате того, что входные данные недостаточно полно проверяются на корректность. Злоумышленник может подсунуть в качестве входных данных совсем не то, чего ожидает программа, и тем самым получить несанкционированный доступ к управлению. Наиболее известные ошибки валидирования данных — это injections и XSS. Вместо валидных данных злоумышленник подает на вход программе специальным образом подготовленные данные, которые несут в себе небольшую программу. Это программа, попадая на обработку, выполняется. Результатом ее выполнения может быть передача управления другой программе, порча данных и многое, многое другое. Также в результате ошибок валидирования может выполняться подмена сайта, с которым работает пользователь. Ошибки валидирования качественно можно обнаруживать статическими методами анализа кода.

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

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

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

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

    Ошибки работы с памятью


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

    К типичным ошибкам работы с памятью можно отнести use-after-free, double-free, null-pointer-dereference и их разновидности, например, out-of-bounds-Read и out-of-bounds-Write.

    Когда очередной анализатор не справился с обнаружением утечки памяти, можно услышать, что такие дефекты сложно эксплуатировать. Злоумышленник должен обладать высокой квалификацией и применить немало сноровки, чтобы, во-первых, узнать о наличии такого дефекта в коде, а, во-вторых, сделать эксплоит. Ну, и дальше аргументация такая: «Вы уверены, что ваш программный продукт интересен гуру такого уровня?»… Однако история знает случаи, когда ошибки работы с памятью успешно эксплуатировались и наносили немалый ущерб. В качестве примеров можно привести такие известные ситуации, как:

    1. CVE-2014-0160 — ошибка в библиотеке openssl — потенциальная компрометация приватных ключей потребовала перевыпуска всех сертификатов и перегенерации паролей.
    2. CVE-2015-2712 — ошибка в реализации js в mozilla firefox — bounds check.
    3. CVE-2010-1117 — use after free в internet explorer — remotely exploitable.
    4. CVE-2018-4913 — use after free in Acrobat Reader — code execution.

    Еще злоумышленники любят эксплуатировать дефекты, связанные с неправильной синхронизацией работы нитей или процессов. Такие дефекты сложно идентифицировать в статике, потому что промоделировать состояние машины без понятия «время» — непростая задача. Здесь имеются ввиду ошибки типа race-condition. А сегодня параллелизм используется везде, даже в совсем небольших приложениях.

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


    Как правильно эксплуатировать статический анализатор, правильно и эффективно работать с информацией, которую он выдает, читайте дальше в нашем блоге.
    Ростелеком-Солар
    365,71
    Безопасность по имени Солнце
    Поделиться публикацией

    Комментарии 13

      +6
      Ошибки работы с памятью

      Их сложно обнаружить

      Что-то PVS-Studio в каждом своем отчете пестрит обнаружениями use-after-free и подобных, включая null pointer dereference. Может, это и непросто, но по крайней мере, выполнимо, подчас довольно легко (если вместо !=null стояло ==null).

        0
        Я бы сказала, что обнаружение ошибок по шаблонам исходного кода выходит за рамки данной публикации, так как ничего не имеет общего с анализом данных во внутреннем представлении.
          +1

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

        0
        • ошибки валидирования,
        • ошибки утечки информации,
        • ошибки аутентификации.
        Интересно, почему обычно в этой и аналогичных статьях не говорят про опечатки и Copy-Paste. У нас их есть… :) См. в таблице список диагностик, которые напрямую или косвенно выявляют опечатки и Copy-Paste. И да, опечатки могут являться дефектами безопасности.
          +3
          Наверно потому, что это ортогональные вещи. Опечатки могут приводить к ошибкам проверки, но злоумышленников интересует именно ошибки проверки вне зависимости от причины их происхождения, а не опечатки как таковые.
            0
            Моё личное мнение, что граница между всеми этими вещами более размыта, чем хотят продемонстрировать. Возьмем, например, утечку памяти. С одной стороны, это просто ошибка. С другой, это ошибка, которая может привести к отказу в обслуживании. И ещё вопрос, как её классифицировать (валидация, утечки информации, аутентификация).

            Или вот, например, уязвимость CVE-2012-2122 в MySQL, описанная в этой заметке. Значение типа int записали в char и потеряли значения старших бит. Можно сказать, что это ошибка валидации. Но, с другой стороны, это просто неаккуратность, которая при определённых условиях проявляет себя. Я плохо представляю, как искать такую ошибку с позиции философии «ошибка валидации». Вот проверка. Вроде работает. Как ошибку то найти? Зато понятно, как искать такую ошибку с точки зрения банального усечения данных.

            Написал, пожалуй, путанно. Вот что я хотел сказать: я считаю, что это не ортогональные вещи. Невозможно заранее выделить, что является опечаткой/просто багом или уязвимостью. И всё равно злоумышленнику/специалисту по безопасности приходится работать со всеми подозрительными местами.
            +1
            Эта статья — о уязвимостях, которые могут быть использованы злоумышленником, то есть нештатных ситуациях.
            Ваш анализатор (imho) — больше о ошибках программиста, соответствии результата работы программы той задаче, которая ставилась при разработке, то есть процессу штатного выполнения.
            Разумеется, это пересекающиеся множества.
              +1
              Восприятие нашего инструмента (просто поиск ошибок) — это наша недоработка в плане презентации инструмента, а не реальное положение дел. Мы недавно это осознали и начали учиться позиционировать PVS-Studio и как SAST решение.

              Оказалось, что почти каждой нашей диагностике соответствует тот или иной CWE ID. А раз найденная ошибка классифицируется согласно CWE, значит она потенциально может являться уязвимостью. Таким образом получается, что почти каждая диагностика, реализованная в PVS-Studio, способна выявлять уязвимости.
                0
                PVS-Studio — неплохой инструмент для решения тех задач, которые он умеет решать. Посыл статьи таков — поймите, какие задачи задачи вам надо решить в рамках статического анализа, возьмите именно те инструменты, которые эти задачи решают, остальные задачи могут быть решены вручную или как-то еще.
                0
                Это верное замечание. Спасибо.
                +1
                Данная классификация приведена как пример и не является стандартом, полной или еще какой-то… Скорее это даже просто мое видение, как можно разделять уязвимости по классами.
                0
                Чем более разнородны языки программирования, тем больше разнородных составляющих в характеристике каждой вершины, а значит, внутреннее представление неэффективно по памяти.

                Насколько неэффективно по памяти? Будет ли это иметь значение при текущих объемах оперативной памяти? Внутреннее представление — это только одна часть анализатора. Так что надо как минимум измерить память при поиске уязвимостей, недостатков и понять сколько памяти потребляется на этом этапе. А если реализовывать абстрактную интерпретацию, то вообще в экспоненту можно уйти как по памяти, так и по скорости.


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

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


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

                  0
                  Наша практика показывает, что неэффективность по памяти в случае «универсального внутреннего представления на все» критична для промышленных приложений, несмотря на то, что реализация качественная. Более того даже HP Fortify, который заявляет, что у него единое NST на все поддерживаемые языки, в реальности несколько его модернизирует под каждую группу языков.
                  Никто не говорит, что надо иметь свое внутреннее представление под каждый ЯП, но в любом случае это должна быть некоторая семейственность внутренних представлений, каждое из которых заточено под свою группу ЯП.

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

                Самое читаемое