Хабрахабр

Создание плагина для Clang Static Analyzer для поиска целочисленных переполнений

1"/> <img src="http://orion-int.ru/wp-content/uploads/2019/10/sozdanie-plagina-dlya-clang-static-analyzer-dlya-poiska-celochislennyx-perepolnenij.jpg" title="рис.

Автор статьи: 0x64rem

Вступление

Я начала изучать материалы про графы потока управления, графы потока данных, символьное исполнение и т.д. Полтора года назад у меня появилась идея реализовать свой фазер в рамках дипломной работы в университете. Ничего конкретного в итоге не получилось, пока этим летом я не отправилась на летнюю программу Summer of Hack 2019 от Digital Security, где в качестве темы проекта мне было предложено расширение возможностей Clang Static Analyzer. Далее шёл поиск тулз, проба разных библиотек (Angr, Triton, Pin, Z3). Далее я расскажу вам, как проходил процесс написания плагина и опишу ход своих мыслей в течении месяца стажировки. Мне показалось, что эта тема поможет мне расставить по полкам мои теоретические знания, приступить к реализации чего-то существенного и получить рекомендации от опытных менторов.

Clang Static Analyzer

Для разработки Clang предоставляет три варианта интерфейсов для взаимодействия:

  • LibClang — высокоуровневый C интерфейс, который позволяет взаимодействовать с AST, но не полноценно. Хороший вариант, если вам требуется взаимодействие с другим языком (например, реализация bindings) или стабильный интерфейс.
  • Clang Plugins — динамические библиотеки, вызываемые во время компиляции. Даёт полноценно манипулировать AST.
  • LibTooling — библиотека для создания отдельных инструментов на основе Clang. Также даёт полный доступ к взаимодействию с AST. Полученный код можно запускать вне среды сборки проверяемого проекта.

Писать код для плагина можно на C++ или Python. Так как мы собираемся расширять возможности Clang Static Analyzer, то выбираем реализацию плагина.

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

Далее идёт знакомство с инструментом. Для моей задачи требуется детальный анализ кода, поэтому для разработки был выбран C++.

Анализатор можно вызвать через фронтенд Clang'а, добавив флаги -cc1 и -analyze к команде сборки, или через отдельный бинарь scan-build. Clang Staic Analyzer (далее CSA) — инструмент для статического анализа C/C++/Objective-C кода, работающий на основе символьного исполнения. Кроме самого анализа, CSA даёт возможность генерировать наглядные html-отчёты.

# команда, чтобы посмотреть флаги для фронтенда clang'а
clang -cc1 --help
# запуск CSA способ №1
clang++ -cc1 -x c++ -load path/to/Checker.so -analyze -analyzer-checker=test.Me -analyzer-config $BUILD_OPTIONS Checker.cpp

# запуск CSA способ №2
scan-build -load-plugin path/to/Checker.so -enable-checker test.Me $BUILD_COMMAND

# пример генерации отчёта для встроенного чекера DivideZero
clang++ -cc1 -analyze -analyzer-checker=core.DivideZero -o reports div-by-zero-test.cpp

2"/> <img src="http://orion-int.ru/wp-content/uploads/2019/10/sozdanie-plagina-dlya-clang-static-analyzer-dlya-poiska-celochislennyx-perepolnenij-1.jpg" alt="пример отчёта" title="рис.

Из структур далее можно увидеть декларации переменных, их типы, использование бинарных и унарных операторов, можно получать символьные выражения и т.д. CSA имеет отличную библиотеку для синтаксического анализа исходного кода при помощи обхода AST (Abstract Syntax Tree), CFG (Control Flow Graph). Ниже перечислен список классов, который был использован в реализации плагина, список поможет получить первичное понимание о возможностях CSA: Мой плагин будет использовать функционал AST классов, такой выбор будет обоснован далее.

  • Stmt — сюда относятся бинарные операции.

  • Decl — объявление переменных.

  • Expr — хранит левые, правые части выражений, их тип.

  • ASTContext — информация о дереве, текущей ноде.

  • Source manager — информация о фактическом коде, который соответствует части дерева.

  • RecursiveASTVisitor, ASTMatcher — классы для обхода дерева.

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

Поиск целочисленных переполнений

Для этого случая сайт llvm предоставляет списки потенциальных чекеров, также можно доработать существующие стабильные или альфа чекеры. Чтобы начать реализовывать плагин, нужно выбрать задачу, которую он будет решать. В итоге был выбран вариант создания чекера для детекта целочисленных переполнений (integer overflow). В ходе ознакомления с кодом имеющихся чекеров, стало понятно, что для более успешного освоения libclang лучше написать свой чекер с нуля, поэтому выбор делался из листа нереализованных идей. Ещё есть UBSan, но это санитайзеры, их используют не все, и этот метод — про выявление проблем во время исполнения, а плагин для CSA работает во время компиляции, анализируя исходники. В Clang уже есть функционал для предупреждения этой уязвимости (для его применения указывают флаги -ftrapv, -fwrapv и подобные), он встроен в компилятор, и такой выхлоп сыпется в warnings, а туда смотрят нечасто.

Прежде integer overflow казалось чем-то простым и не серьёзным. Далее идёт сбор материала по выбранной уязвимости. Overflow — если переменная стала больше, чем это было задумано, Underflow — меньше, чем её первоначальный тип. На самом деле, уязвимость занятная и может иметь внушительные последствия.
Целочисленные переполнения — это тип уязвимостей, в результате которых данные целочисленного типа в коде могут принимать неожиданные значения. Такие ошибки могут появляться как из-за программиста, так и из-за компилятора.

И такие приведения происходят везде и постоянно, они могут быть явными или неявными. В C++ во время операции сравнения арифметики целочисленные значения приводятся к одному типу, чаще к большему по разрядности. Есть несколько правил, по которым происходят приведения [1]:

  • Преобразование со знаком в тип со знаком, но большей разрядности: просто добавляются старшие разряды.
  • Преобразование целого со знаком в целое без знака одной разрядности: отрицательное преобразуется в положительное и примет новое значение. Пример подобной ошибки в DirectFB — CVE-2014-2977.
  • Преобразование целого со знаком в целое без знака большей разрядности: сначала разрядность расширится, затем, если число отрицательное, то оно некорректно поменяет значение. Например: 0xff (-1) станет 0xffffffff.
  • Целое без знака в целое со знаком той же разрядности: число может поменять значение, в зависимости от значения старшего бита.
  • Целое без знака в целое со знаком большей разрядности: сначала повышается разрядность беззнакового числа, потом перевод в знаковое.
  • Понижающее преобразование: биты просто усекаются. Это может сделать беззнаковые значения отрицательными и прочее. Пример такой уязвимости в PHP.

триггером для уязвимости может послужить небезопасный пользовательский ввод, некорректная арифметика, неверное приведение типа, вызванное программистом или компилятором в ходе оптимизации. Т.е. В истории уже был такой случай с классом SafeInt (очень иронично) [5, 6. Также возможен вариант time bomb, когда фрагмент кода безобиден с одной версией компилятора, но с выходом нового алгоритма оптимизации "взрывается" и вызывает непредвиденное поведение. 2]. 5.

Для наглядности можно ознакомиться с конкретными CVE, посмотреть их причины, последствия. Целочисленные переполнения открывают широкий вектор: возможно заставить выполнение пойти по другому пути (если переполнение затрагивает условные операторы), вызвать переполнение буфера. Естественно искать лучше integer overflow в опенсорных продуктах, чтобы не только описание читать, но и код посмотреть.

  • CVE-2019-3560 — Integer overflow в Fizz (проект, реализующий TLS для Facebook) можно было эксплуатировать уязвимость для DoS-атак, используя скрафченный сетевой пакет.
  • CVE-2018-14618 — Переполнение буфера в Curl'е вызывалось целочисленным переполнением из-за длины пароля.
  • CVE-2018-6092 — На 32-битных системах уязвимость в WebAssembly для Chrome позволяла осуществлять RCE через специальную HTML-страницу.

Его подход следующий: Чтобы не изобретать велосипеды, был рассмотрен код для детектирования integer overflow в статическом анализаторе CppCheck.

  1. Определить, является ли выражение бинарным оператором.
  2. Если да, то проверить, оба ли аргумента имеют целочисленный тип.
  3. Определить размер типов.
  4. Проверить при помощи вычислений, может ли значение выйти за свои границы максимума или минимума.
    Но на этом этапе это не дало ясности. Получается много разных сюжетов, и от этого систематизация информации становится сложнее. На свои места всё поставил список CWE. Всего на сайте выделено 9 типов integer overflow:
    • 190 — integer oveflow
    • 191 — integer underflow
    • 192 — integer coertion error
    • 193 — off-by-one
    • 194 — Unexpected Sign Extension
    • 195 — Signed to Unsigned Conversion Error
    • 196 — Unsigned to Signed Conversion Error
    • 197 — Numeric Truncation Error
    • 198 — Use of Incorrect Byte Ordering

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

3"/> <img src="http://orion-int.ru/wp-content/uploads/2019/10/sozdanie-plagina-dlya-clang-static-analyzer-dlya-poiska-celochislennyx-perepolnenij-2.jpg" title="рис.

Если нашли подходящую ноду, смотрим на потомков в поисках бинарной операции или Decl (объявления переменной). Конкретнее алгоритм звучит так: ходим по Cast'ам и смотрим IntegralCast (целочисленные преобразования). Во втором случае, сравнить только тип декларации. В первом случае надо проверить знак и разрядность, которые использует бинарная операция.

Реализация чекера

Нужен скелет для чекера, который может быть stand-alone библиотекой, а может быть собран как часть Сlang. Приступим к реализации. Если вы уже собрались писать свой плагин, то рекомендую сразу прочитать небольшой pdf: "Clang Static Analyzer: A Checker Developer's Guide", там отлично описаны базовые вещи, правда, что-то может быть уже не актуально, библиотека обновляется регулярно, но базу вы схватите сразу. В коде разница будет небольшой.

Если вы хотите добавить ваш чекер в вашу сборку clang, то необходимо:

  1. Написать сам чекер примерно с таким содержанием:

    namespace {
    class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { // Наследовать вы будете один из классов чекеров, которые имеют виртуальные функции. Задача реализовать их под ваши нужды
    struct CheckerOpts ; CheckerOpts Opts; //cool code
    };
    } void ento::registerSuperChecker(CheckerManager &mgr) {
    auto checker = mgr.registerChecker<SuperChecker>(); // если чекеру нужны входные параметры от пользователя, то следующие 4 строчки описывают это
    // этот вариант подходит только для встроенного чекера, для stand-alone пример кода описан ниже. AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); SuperChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker);
    //аргументы getCheckerIntegerOption: имя параметра, дефолтное значение, экземпляр чекера
    }

  2. Живут примерно тут ${llvm-source-path}/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
    и тут ${llvm-source-path}/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td.
    В первом нужно просто добавить имя файла с кодом, во втором нужно добавить структурное описание: Потом в исходниках Clang'а потребуется изменить файлы CMakeLists.txt и Checkers.td.

    #Checkers.td def SuperChecker : Checker<"SuperChecker">, HelpText<"test checker">, Documentation<HasDocumentation>;

Если непонятно, то в файле Checkers.td достаточно примеров, как и что делать.

Тогда в коде чекера должно быть примерно следующее: Скорее всего вам не захочется пересобирать Clang, и вы прибегните к варианту со сборкой библиотеки (so/dll).

namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { // Наследовать вы будете один из классов чекеров, которые имеют виртуальные функции. Задача реализовать их под ваши нужды struct CheckerOpts { string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code };
} void initializationFunction(CheckerManager &mgr){ SuperChecker *checker = mgr.registerChecker<SuperChecker>(); // если чекеру нужны входные параметры от пользователя, то следующие 4 строчки описывают это AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); TestChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); //аргументы getCheckerIntegerOption: имя параметра, дефолтное значение, экземпляр чекера
} extern "C" void clang_registerCheckers (CheckerRegistry &registry) { registry.addChecker(&initializationFunction, "test.Me", "SuperChecker description", "doc_link"); } extern "C" const char clang_analyzerAPIVersionString [] = "8.0.1";

Далее собираете свой код, можно написать свой скрипт для сборки, но если у вас возникают какие-то проблемы с этим (как это было у автора 🙂 ), то можно по-странному использовать Makefile в исходниках clang'a и команду make clangStaticAnalyzerCheckers.

Далее вызываем чекер:

  • для встроенных чекеров

    clang++ -cc1 -analyze -analyzer-checker=core.DivideZero test.cpp

  • для внешних

    clang++ -cc1 -load ${PATH_TO_CHECKER}/SuperChecker.so -analyze -analyzer-checker=test.Me -analyzer-config test.Me:UsrInp1="foo" test.Me:Inp1="bar" -analyzer-config test.Me:Inp2=123 test.cpp

    4), но написанный код умеет детектить только потенциальные переполнения. На этом этапе у нас есть уже какой-то результат (рис. А это значит — большое количество false positive срабатываний.

4"/> <img src="https://habrastorage.org/getpro/habr/post_images/ac1/79d/29b/ac179d29b330efd513343fdd5f9a2ddd.jpg" title="рис.

Чтобы это исправить мы можем:

  • Ходить по графу туда-сюда и проверять конкретные значения переменных для случаев, когда у нас есть потенциальное переполнение.
  • Во время обхода AST сразу сохранять конкретные значения для переменных и проверять их когда потребуется.
  • Использовать Taint анализ.

В итоге из предложенных вариантов, только один является рациональным относительно конкретной задачи: Чтобы подкрепить дальнейшие аргументы, стоит упомянуть, что при анализе Clang парсит также и все файлы, указанные в директиве #include, в результате размер полученного AST увеличивается.

  • Первое, требует много времени на выполнение. Хождение по дереву, поиск и подсчёт всего необходимого будет длится долго, анализировать большой проект таким кодом может стать затруднительно. Для хождения по дереву в коде мы будем использовать класс clang::RecursiveASTVisitor, который выполняет рекурсивный поиск в глубину. Оценка времени такого подхода будет , где V — множество вершин, а E — множество рёбер графа.
  • Второе — можно конечно хранить, но мы не знаем, что нам будет нужно, а что нет. Кроме этого, сами древовидные структуры, которые мы используем при анализе, требуют много памяти, поэтому расходовать такие ресурсы на что-то ещё — плохая идея.
  • Третье — хорошая идея, для такого метода можно найти достаточно исследований и примеров. Но в CSA нет готового taint'а. Есть чекер, который позже был добавлен в список альфа чекеров (alpha.security.taint.TaintPropagation) в исходниках он описан в файле GenericTaintChecker.cpp. Чекер хороший, но подходит только для известных небезопасных функций ввода-вывода из C, "помечает" только переменные, которые были аргументами или результатами опасных функций. Кроме описанных вариантов, стоит учитывать глобальные переменные, поля классов и прочее, чтобы правильно восстановить модель "распространения".

Успешно сделать это к концу срока не вышло, но это осталось задачей для доработки уже за рамками обучения в DSec. Оставшееся время на стажировке ушло на чтение GenericTaintChecker.cpp и попытки переделать его под свои нужды. Стандартными средствами CSA размер определяется по типу, и если мы работаем с битовым полем, то его размер будет иметь значение разрядности типа всего поля, а не количеству бит, указанному в декларации переменной. Так же в ходе разработки стало ясно, что определять опасные функции — отдельная задача, не всегда опасные места в проекте идут из каких-то стандартных функций, поэтому чекеру был добавлен флаг для указания списка функций, которые будут считаться "отравленными"/"помеченными" во время taint анализа.
Дополнительно была добавлена проверка, является ли переменная битовым полем.

Что в итоге?

Модифицированный класс для taint анализа, над которым предстоит ещё много работы. На данный момент реализован простой чекер, способный предупреждать только о потенциальных целочисленных переполнениях. Для этого подойдёт SMT-решатель Z3, который был добавлен в сборку Clang ещё в версии 5. После, нужно использовать SMT для определения переполнений. 0 (судя по release notes). 0. Для использования солвера необходимо, чтобы Clang был собран с опцией CLANG_ANALYZER_BUILD_Z3=ON, а при непосредственном вызове плагина CSA передаются флаги -Xanalyzer -analyzer-constraints=z3.

Репозиторий с результатами на GitHub

Ссылки:

  1. "24 греха компьютерной безопасности" Ховард М., Лебланк Д., Вьега Дж.

  2. How to Write a Checker in 24 Hours

  3. Clang Static Analyzer: A Checker Developer's Guide

  4. CSA checker development manual

  5. et al. Dietz W. Understanding integer overflow in C/C++

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть