- PVSM.RU - https://www.pvsm.ru -
Автор статьи: 0x64rem [1]
Полтора года назад у меня появилась идея реализовать свой фазер в рамках дипломной работы в университете. Я начала изучать материалы про графы потока управления, графы потока данных, символьное исполнение и т.д. Далее шёл поиск тулз, проба разных библиотек (Angr, Triton, Pin, Z3). Ничего конкретного в итоге не получилось, пока этим летом я не отправилась на летнюю программу Summer of Hack 2019 от Digital Security [2], где в качестве темы проекта мне было предложено расширение возможностей Clang Static Analyzer. Мне показалось, что эта тема поможет мне расставить по полкам мои теоретические знания, приступить к реализации чего-то существенного и получить рекомендации от опытных менторов. Далее я расскажу вам, как проходил процесс написания плагина и опишу ход своих мыслей в течении месяца стажировки.
Для разработки Clang предоставляет три варианта интерфейсов для взаимодействия:
Так как мы собираемся расширять возможности Clang Static Analyzer, то выбираем реализацию плагина. Писать код для плагина можно на C++ или Python.
Для последнего есть биндинги [4], которые разрешают парсить исходный код, перебирать ноды полученного абстрактного синтаксического дерева, также имеют доступ к свойствам нод и могут сопоставлять ноду строке исходного кода. Такой набор подойдёт для простого чекера. Подробнее ознакомиться с кодом можно в репозитории llvm [7].
Для моей задачи требуется детальный анализ кода, поэтому для разработки был выбран C++. Далее идёт знакомство с инструментом.
Clang Staic Analyzer [8] (далее CSA) — инструмент для статического анализа C/C++/Objective-C кода, работающий на основе символьного исполнения. Анализатор можно вызвать через фронтенд Clang'а, добавив флаги -cc1 и -analyze к команде сборки, или через отдельный бинарь scan-build. Кроме самого анализа, 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
CSA имеет отличную библиотеку для синтаксического анализа исходного кода при помощи обхода AST (Abstract Syntax Tree), CFG (Control Flow Graph). Из структур далее можно увидеть декларации переменных, их типы, использование бинарных и унарных операторов, можно получать символьные выражения и т.д. Мой плагин будет использовать функционал AST классов, такой выбор будет обоснован далее. Ниже перечислен список классов, который был использован в реализации плагина, список поможет получить первичное понимание о возможностях CSA:
Stmt — сюда относятся бинарные операции.
Decl — объявление переменных.
Expr — хранит левые, правые части выражений, их тип.
ASTContext — информация о дереве, текущей ноде.
Source manager — информация о фактическом коде, который соответствует части дерева.
RecursiveASTVisitor, ASTMatcher — классы для обхода дерева.
Повторюсь, что CSA предоставляет разработчику возможность детально рассмотреть структуру кода, и классы, перечисленные выше, это лишь небольшая часть доступного. Обязательно рекомендую полистать документацию [9] вашей версии Clang, если вы не знаете, как извлечь какие-то данные; скорее всего, что-то подходящее уже написано.
Чтобы начать реализовывать плагин, нужно выбрать задачу, которую он будет решать. Для этого случая сайт llvm предоставляет списки потенциальных чекеров [10], также можно доработать существующие стабильные [11] или альфа [12] чекеры. В ходе ознакомления с кодом имеющихся чекеров, стало понятно, что для более успешного освоения libclang лучше написать свой чекер с нуля, поэтому выбор делался из листа нереализованных идей [10]. В итоге был выбран вариант создания чекера для детекта целочисленных переполнений (integer overflow). В Clang уже есть функционал для предупреждения этой уязвимости (для его применения указывают флаги -ftrapv, -fwrapv и подобные), он встроен в компилятор, и такой выхлоп сыпется в warnings, а туда смотрят нечасто. Ещё есть UBSan [13], но это санитайзеры, их используют не все, и этот метод — про выявление проблем во время исполнения, а плагин для CSA работает во время компиляции, анализируя исходники.
Далее идёт сбор материала по выбранной уязвимости. Прежде integer overflow казалось чем-то простым и не серьёзным. На самом деле, уязвимость занятная и может иметь внушительные последствия.
Целочисленные переполнения — это тип уязвимостей, в результате которых данные целочисленного типа в коде могут принимать неожиданные значения. Overflow — если переменная стала больше, чем это было задумано, Underflow — меньше, чем её первоначальный тип. Такие ошибки могут появляться как из-за программиста, так и из-за компилятора.
В C++ во время операции сравнения арифметики целочисленные значения приводятся к одному типу, чаще к большему по разрядности. И такие приведения происходят везде и постоянно, они могут быть явными или неявными. Есть несколько правил, по которым происходят приведения [1]:
Т.е. триггером для уязвимости может послужить небезопасный пользовательский ввод, некорректная арифметика, неверное приведение типа, вызванное программистом или компилятором в ходе оптимизации. Также возможен вариант time bomb, когда фрагмент кода безобиден с одной версией компилятора, но с выходом нового алгоритма оптимизации "взрывается" и вызывает непредвиденное поведение. В истории уже был такой случай с классом SafeInt (очень иронично) [5, 6.5.2].
Целочисленные переполнения открывают широкий вектор: возможно заставить выполнение пойти по другому пути (если переполнение затрагивает условные операторы), вызвать переполнение буфера. Для наглядности можно ознакомиться с конкретными CVE, посмотреть их причины, последствия. Естественно искать лучше integer overflow в опенсорных продуктах, чтобы не только описание читать, но и код посмотреть.
Чтобы не изобретать велосипеды, был рассмотрен код для детектирования integer overflow в статическом анализаторе CppCheck [19]. Его подход следующий:
Рассматриваем причину для каждого варианта и понимаем, что переполнения происходят при некорректном явном/неявном приведении. И т.к. в структуре абстрактного синтаксического дерева отображаются любые приведения, будем использовать AST для анализа. На рисунке ниже (рис. 3), видно, что любая операция, вызывающая приведение в дереве, является отдельным узлом, и, бродя по дереву, мы можем проверять все приведения типов, опираясь на таблицу с преобразованиями, которые могут вызвать ошибку.
Sign G | Sign L | Sign E | Unsign G | Unsign L | Unsign E | |
---|---|---|---|---|---|---|
Sign | + | - | + | - | - | - |
Unsign | + | - | - | - | - | + |
Конкретнее алгоритм звучит так: ходим по Cast'ам и смотрим IntegralCast (целочисленные преобразования). Если нашли подходящую ноду, смотрим на потомков в поисках бинарной операции или Decl (объявления переменной). В первом случае надо проверить знак и разрядность, которые использует бинарная операция. Во втором случае, сравнить только тип декларации.
Приступим к реализации. Нужен скелет для чекера, который может быть stand-alone библиотекой, а может быть собран как часть Сlang. В коде разница будет небольшой. Если вы уже собрались писать свой плагин, то рекомендую сразу прочитать небольшой pdf: "Clang Static Analyzer: A Checker Developer's Guide" [21], там отлично описаны базовые вещи, правда, что-то может быть уже не актуально, библиотека обновляется регулярно, но базу вы схватите сразу.
Если вы хотите добавить ваш чекер в вашу сборку clang, то необходимо:
Написать сам чекер примерно с таким содержанием:
namespace {
class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { // Наследовать вы
будете один из классов чекеров, которые имеют виртуальные функции. Задача реализовать их
под ваши нужды
struct CheckerOpts { // структура для передачи входных аргументов чекера
string FlagOne;
int FlagTwo;
};
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: имя параметра, дефолтное значение, экземпляр чекера
}
Потом в исходниках Clang'а потребуется изменить файлы CMakeLists.txt
и Checkers.td
. Живут примерно тут ${llvm-source-path}/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
и тут ${llvm-source-path}/clang/include/clang/StaticAnalyzer/Checkers/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 ®istry) {
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 срабатываний.
Чтобы это исправить мы можем:
Чтобы подкрепить дальнейшие аргументы, стоит упомянуть, что при анализе Clang парсит также и все файлы, указанные в директиве #include
, в результате размер полученного AST увеличивается. В итоге из предложенных вариантов, только один является рациональным относительно конкретной задачи:
clang::RecursiveASTVisitor
, который выполняет рекурсивный поиск в глубину. Оценка времени такого подхода будет GenericTaintChecker.cpp
. Чекер хороший, но подходит только для известных небезопасных функций ввода-вывода из C, "помечает" только переменные, которые были аргументами или результатами опасных функций. Кроме описанных вариантов, стоит учитывать глобальные переменные, поля классов и прочее, чтобы правильно восстановить модель "распространения".Оставшееся время на стажировке ушло на чтение GenericTaintChecker.cpp
и попытки переделать его под свои нужды. Успешно сделать это к концу срока не вышло, но это осталось задачей для доработки уже за рамками обучения в DSec. Так же в ходе разработки стало ясно, что определять опасные функции — отдельная задача, не всегда опасные места в проекте идут из каких-то стандартных функций, поэтому чекеру был добавлен флаг для указания списка функций, которые будут считаться "отравленными"/"помеченными" во время taint анализа.
Дополнительно была добавлена проверка, является ли переменная битовым полем. Стандартными средствами CSA размер определяется по типу, и если мы работаем с битовым полем, то его размер будет иметь значение разрядности типа всего поля, а не количеству бит, указанному в декларации переменной.
На данный момент реализован простой чекер, способный предупреждать только о потенциальных целочисленных переполнениях. Модифицированный класс для taint анализа, над которым предстоит ещё много работы. После, нужно использовать SMT для определения переполнений. Для этого подойдёт SMT-решатель Z3, который был добавлен в сборку Clang ещё в версии 5.0.0 (судя по release notes [24]). Для использования солвера необходимо, чтобы Clang был собран с опцией CLANG_ANALYZER_BUILD_Z3=ON
, а при непосредственном вызове плагина CSA передаются флаги -Xanalyzer -analyzer-constraints=z3
.
Репозиторий с результатами на GitHub [25]
Ховард М., Лебланк Д., Вьега Дж. "24 греха компьютерной безопасности"
How to Write a Checker in 24 Hours [26]
Clang Static Analyzer: A Checker Developer's Guide [21]
CSA checker development manual [27]
Dietz W. et al. Understanding integer overflow in C/C++ [28]
Автор: forkyforky
Источник [29]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/334896
Ссылки в тексте:
[1] 0x64rem: https://habr.com/ru/users/0x64rem/
[2] Digital Security: https://dsec.ru
[3] LibClang: https://clang.llvm.org/doxygen/group__CINDEX.html
[4] bindings: https://en.wikipedia.org/wiki/Language_binding
[5] Clang Plugins: http://clang.llvm.org/docs/ClangPlugins.html
[6] LibTooling: http://clang.llvm.org/docs/LibTooling.html
[7] репозитории llvm: https://github.com/llvm-mirror/clang/tree/master/bindings/python
[8] Clang Staic Analyzer: https://clang.llvm.org/docs/ClangStaticAnalyzer.html
[9] документацию: https://clang.llvm.org/doxygen/dir_b7f1fb8e22c16abc1bb9b644ee717a58.html
[10] списки потенциальных чекеров: https://clang-analyzer.llvm.org/potential_checkers.html
[11] стабильные: https://clang.llvm.org/docs/analyzer/checkers.html
[12] альфа: https://clang.llvm.org/docs/analyzer/checkers.html#alpha-checkers
[13] UBSan: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
[14] CVE-2014-2977: https://github.com/DirectFB/directfb/blob/e97c8d40ae10585ad10cb55800efcd2ea13fbdf8/proxy/dispatcher/idirectfbsurface_dispatcher.c#L1714
[15] уязвимости в PHP: http://www.php-security.org/MOPB/MOPB-38-2007.html
[16] CVE-2019-3560: https://blog.semmle.com/facebook-fizz-CVE-2019-3560/
[17] CVE-2018-14618: https://curl.haxx.se/docs/CVE-2018-14618.html
[18] CVE-2018-6092: https://bugs.chromium.org/p/chromium/issues/detail?id=819869
[19] CppCheck: https://github.com/danmar/cppcheck
[20] CWE: https://cwe.mitre.org/data/definitions/190.html
[21] "Clang Static Analyzer: A Checker Developer's Guide": https://github.com/haoNoQ/clang-analyzer-guide/releases/download/v0.1/clang-analyzer-guide-v0.1.pdf
[22] класс: https://clang.llvm.org/doxygen/classclang_1_1RecursiveASTVisitor.html
[23] чекер: https://github.com/franchiotta/taintchecker/tree/master/src
[24] release notes: https://releases.llvm.org/5.0.0/tools/clang/docs/ReleaseNotes.html
[25] Репозиторий с результатами на GitHub: https://github.com/dR3m/IntOverflow-CSA-plugin
[26] How to Write a Checker in 24 Hours: https://llvm.org/devmtg/2012-11/Zaks-Rose-Checker24Hours.pdf
[27] CSA checker development manual: https://clang-analyzer.llvm.org/checker_dev_manual.html
[28] Dietz W. et al. Understanding integer overflow in C/C++: https://pdfs.semanticscholar.org/ce98/eea75f56e3f0244b3c5593495992cc946d36.pdf
[29] Источник: https://habr.com/ru/post/473412/?utm_source=habrahabr&utm_medium=rss&utm_campaign=473412
Нажмите здесь для печати.