Фобос-НТ
Содержание
22.12.2025

Кастомные детекторы в испытательной лаборатории

Тезисы

  1. Постановка проблемы – нехватка детекторов для поиска специфичных случаев.
  2. Демонстрация проблемы с помощью библиотеки libxml2 и анализатора Svace.
  3. Написание своего детектора для поиска XXE в libxml2 с помощью Svace Light API.
  4. Использование детектора «в бою». Сборка детектора и настройка анализатора для поиска XXE в libxml2.
  5. Troubleshooting. С чем сталкивались при разработке детекторов и как подходили к решению проблемы.

Введение

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

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

Проблема

Приведу простой пример. При использовании функций библиотеки libxml2 с некоторым набором параметров мы рискуем нарваться на XXE. Описание опасной конфигурации приведено в статье.

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

Возможность добавлять детекторы вручную самостоятельно позволит решить сразу несколько проблем:

  1. Разработчики анализатора могут долго рассматривать запрос на создание вашего кастомного детектора или же в принципе могут не взяться за что-то очень специфичное.
  2. Адаптация детекторов под свой Code style. Разработчик знает свой код лучше остальных, знает, как его писать и как его не писать. Возможность добавления своих детекторов позволит эффективнее следить за чистотой кода и раньше находить баги.

Поэтому проще сделать самому.

Разные анализаторы представляют разный механизм добавления детекторов. У анализатора Semgrep правила представлены в виде yml файликов, у checkmarx свой язык CxQL для написания детекторов. В своей работе мы пользуемся отечественным анализатором Svace, поэтому речь дальше пойдет про него.

Для разработки собственных детекторов Svace представляет 2 вида API (Light и IR). Сами детекторы представляют собой плагины, написанные на ЯП Java, которые хранятся в директории plugins дистрибутива Svace.

Разработка детектора

Вернемся к тому же libxml2 и попробуем написать детектор с помощью Svace Light API.

Какая в итоге проблема? Параметры XML_PARSE_NOENT и XML_PARSE_DTDLOAD не должны использоваться в качестве опций для API из библиотеки libxml2. Ниже приведу кусок кода, который иллюстрирует проблему.


// пример
#include <stdio.h>
#include <stdlib.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

int main(int argc, char **argv) {
    static const char buf[] =
        "<?xml version=\"1.0\"?>\n"
        "<!DOCTYPE root [\n"
        "<!ENTITY % remote SYSTEM \"http://localhost:8000\">\n"
        "%remote;]>\n"
        "<root/>";

    xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
     "noname.xml", NULL, XML_PARSE_NOENT | XML_PARSE_DTDLOAD);

    xmlFreeDoc(doc);

    return 0;
}

Пример весьма утрированный. Из памяти с помощью xmlReadMemory читается XML-структура. Если включена опция XML_PARSE_DTDLOAD, то парсер загрузит http://localhost:8000/evil.dtd и выполнится строка . Опция XML_PARSE_NOENT выполнит строку %remote, которая загрузит и выполнит внешний DTD.

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

Анализ тестового кода со всеми включенными детекторами

Рисунок 1. Анализ тестового кода со всеми включенными детекторами.

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

Проблему определили – с чего начать разработку и что вообще делать?

Разработку детектора начнем с разбора шаблона. Структура файлов для детектора будет выглядит следующим образом:

Структура каталога для детектора

Рисунок 2. Структура каталога для детектора.

Пройдемся по каждому файлу:

  1. build.gradle – конфигурационный файл для сборки детектора с помощью gradle. Самое важное в нем – указание зависимости от svace-api, а также включение модульной системы Java, поскольку именно с помощью нее анализатор может динамически подтягивать и запускать пользовательские детекторы;
  2. README.md – просто файлик с описанием детектора (необязательный);
  3. module-info.java – файл, описывающий Java-модуль для детектора;
  4. SvaceLightXXELibxml – исходный текст будущего детектора.

Самим выдумывать содержание этих файлов не требуется – в дистрибутиве Svace в директории plugins содержится несколько примеров реализованных детекторов, как в собранном виде (.jar), так и их исходные тексты. Шаблоны build.gradle, module-info.java и каркас для детектора можно взять как раз оттуда.

Шаблон есть, а как понять, что делать и какими методами пользоваться?

Тут помогут исходники Svace API. Как их найти – они находятся в дистрибутиве Svace в директории lib. Нужно разархивировать svace-api-sources.jar. Этот архив будет содержать описание всех сущностей и методов, которые используются для реализации детекторов. Покопавшись в описании библиотеки, для написания детектора выделили всего 2 метода из Light API (более подробное описание методов представлено в файле ./svace-api-sources/ru/isp/svaceapi/light/SvaceLightPlugin.java):

  1. hasBitmask(int mask) – определяет, соответствует ли параметр заданной битовой маске;
  2. funcCall(String regex, int argIndex, SvaceLightPattern argVal) – находит вызов функции с заданным по регулярному выражению именем и заданными аргументами.

Методы выбрали – как написать детектор?

Воспользуемся шаблоном, который есть в папке plugins.

Вначале нужно создать наш класс SvaceLightXXELibxml, который будет наследоваться от SvaceLightPlugin.

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

Для регистрации предупреждения необходимо добавить в детектор следующие обязательные методы (они есть в каждом шаблоне детекторов):

  1. name() – содержит имя детектора, которое будет отображаться в справке;
  2. registerWarningType(SvaceUserWarningRegister register) – задает все описательные атрибуты детектора (язык программирования, CWE, серьезность и другие).

Для нашего детектора я задал следующие аттрибуты:

@Override
public String name() {
    return "Svace light libxml2 XXE finder";
}

@Override
public void registerWarningType(SvaceUserWarningRegister register) {
    WarningTypeProperties props = WarningTypeProperties.create(Language.CXX).setVisibility(Visibility.Disabled).setCweSet(CWESet.of(611));
    warningXXE = register.registerWarning("XXE_LIBXML2", props);
}

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

Для функции hasBitmask необходимо указать маску, которая будет искать опасные опции. Заранее определим их в детекторе:

private final static Integer XML_PARSE_NOENT = 2; /* substitute entities */
private final static Integer XML_PARSE_DTDLOAD = 4; /* load the external subset */

Маска будет выглядеть следующим образом:

SvaceLightPattern mask = hasBitmask(XML_PARSE_NOENT | XML_PARSE_DTDLOAD);

Следующим шагом будет определение перечня функций, в которые могут передаваться наши опасные опции, которые будут влиять на парсинг XML-структур. Поскольку в этих функциях параметр options имеет разные порядковые номера, то эти функции были выделены в разные подгруппы регулярных выражений для применения в функции funcCall, чтобы оптимизировать детектор и сделать его более читаемым. Например, у функции xmlCtxtUseOptions параметр options - второй по порядку, а у xmlReadFile – третий. Итоговый список выглядит следующим образом:

private final static String[] groups = new String[]{
    "(xmlCtxtUseOptions)",
    "(xmlReadFile)",
    "(xmlCtxtReadFile|xmlParseInNodeContext|xmlReadDoc|xmlReadFd)",
    "(xmlCtxtReadDoc|xmlCtxtReadFd|xmlReadMemory)",
    "(xmlCtxtReadMemory|xmlReadIO)",
    "(xmlCtxtReadIO)",
};

Дело за малым – осталось только объединить 2 эти вещи в funcCall и проитерироваться по всему списку groups.

SvaceLightPattern funcCallPattern = funcCall(groups[n - 2], n - 1, mask);

Первый параметр funcCall – регулярное выражение из groups. Второй – индекс параметра options в искомой функции. Третий – битовая маска для параметра options. Если все 3 условия совпадут – это будет как раз то, что мы хотим найти.

Для регистрации детектора в базе Svace используется стандартная функция registerChecker. В нем указывается следующая информация:

  1. warning – значение warningXXE, содержащее информацию о предупреждении, которую мы задали с помощью функции registerWarningType;
  2. fixMessage – текстовое сообщение, которое будет выведено при срабатывании детектора;
  3. pattern – значение funcCallPattern, которое содержит созданный нами шаблон для поиска дефекта.

Итоговая функция для поиска нашего шаблона выглядит следующим образом:

private void createNArgChecker(int n) {
    SvaceLightPattern mask = hasBitmask(XML_PARSE_NOENT | XML_PARSE_DTDLOAD);
    SvaceLightPattern funcCallPattern = funcCall(groups[n - 2], n - 1, mask);
    registerChecker(warningXXE, "Using parameters XML_PARSE_NOENT or XML_PARSE_DTDLOAD can lead to XXE vulnerability.", funcCallPattern);
}

Итерироваться по каждой функции из списка group будем с помощью небольшого цикла над функцией createNArgChecker.

@Override
public void createChecker() {
    for (int i = 2; i <= 7; i++) {
        createNArgChecker(i);
    }
}

Вот и все, логика детектора готова.

Шаблон весьма простой – ищет только вызов функции с константой?

В Svace реализовано достаточное количество сложных видов анализов, результаты которых можно использовать в детекторах, написанных с использованием Svace API. Так, наш будущий плагин способен искать, как минимум, следующие случаи без каких-либо доработок кода детектора:

1. Прямая передача констант в функцию.

// Вариант 1: Прямой вызов с константами
xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
    "noname.xml", NULL, XML_PARSE_NOENT | XML_PARSE_DTDLOAD);
xmlFreeDoc(doc);

2. Константа формируется через макрос.

// Вариант 2: Константа формируется через макрос
#define GET_OPTIONS() (XML_PARSE_NOENT | XML_PARSE_DTDLOAD)

void test_macro() {
    static const char buf[] = "<root/>";
    // Использование макроса
    xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
        "noname.xml", NULL, GET_OPTIONS());
    xmlFreeDoc(doc);
}

3. Константа формируется через функцию.

// Вариант 3: Константа формируется через функцию
int get_options() {
    return XML_PARSE_NOENT | XML_PARSE_DTDLOAD;
}
void test_function() {
    static const char buf[] = "<root/>";
    // Использование функции, возвращающей битовую маску
    xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
        "noname.xml", NULL, get_options());
    xmlFreeDoc(doc);
}

4. Константа формируется через вычисления в коде.

// Вариант 4: Формирование константы через вычисления
int options = XML_PARSE_NOENT;
options |= XML_PARSE_DTDLOAD;

xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
    "noname.xml", NULL, options);

5. Константа передается через указатель.

// Вариант 5: Константа через указатель
int base_options = XML_PARSE_NOENT;
int *options_ptr = &base_options;
*options_ptr |= XML_PARSE_DTDLOAD;

xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
    "noname.xml", NULL, *options_ptr);

6. Константа передается через массив.

// Вариант 6: Константа через массив
int options_array[2];
options_array[0] = XML_PARSE_NOENT;
options_array[1] = XML_PARSE_DTDLOAD;

xmlDocPtr doc = xmlReadMemory(buf, sizeof(buf),
    "noname.xml", NULL, options_array[0] | options_array[1]);

Логику написали – как собрать детектор и что с ним делать?

Сборка осуществляется на основе конфигурационного файла build.gradle. Нужно указать в нем корректный путь до папки lib, в которой лежит нужная нам зависимость svace-api.

Этот файл достаточно шаблонный. Для моего варианта, когда исходные коды уже лежат в каталоге plugins в дистрибутиве, build.gradle выглядит следующим образом:

plugins {
    id 'java-library'
}

repositories {
    flatDir {
        dirs '../../lib'
    }
}

dependencies {
    compileOnlyApi ':svace-api'
}

sourceSets {
    main.java.srcDirs = ['src']
}

java {
    modularity.inferModulePath = true
}

compileJava {
    options.release.set(21)
}

tasks.register('copyJar', Copy) {
    from jar
    into "../"
}

jar.finalizedBy copyJar

Собираем из директории с build.gradle командой gradle clean build. Финальный .jar-файл кладем в директорию plugins.

Ура! Теперь можно использовать этот детектор для анализа.

Можно убедиться, что детектор добавился в базу правил с помощью команды svace warning –i USER*.

Префикс USER по умолчанию ставится всем детекторам, написанным с использованием Svace API, поэтому так мы увидим всю информацию только по этим детекторам. Во всем списке нужно отыскать наш.

Информация о детекторе

Рисунок 3. Информация о детекторе.

Теперь, перед анализом можно включить наш детектор, потому что по умолчанию он False. Включим детектор командой svace warning USER.XXE_LIBXML2 true.

В результате анализа получаем наше заветное найденное срабатывание.

Найденное предупреждение с помощью USER.XXE_LIBXML2

Рисунок 4. Найденное предупреждение с помощью USER.XXE_LIBXML2.

Теперь можно загрузить это в Svacer и посмотреть на предупреждение там. Увидим предупреждение в строке с функцией xmlReadMemory, в которую передаются параметры XML_PARSE_DTDLOAD и XML_PARSE_NOENT.

Выданное предупреждение детектора USER.XXE_LIBXML2

Рисунок 5. Выданное предупреждение детектора USER.XXE_LIBXML2.

Также, все описательные атрибуты детектора, которые были заданы при написании детектора, будут отражены в Svacer во вкладке Details.

Атрибуты детектора USER.XXE_LIBXML2 в веб-интерфейсе Svacer

Рисунок 6. Атрибуты детектора USER.XXE_LIBXML2 в веб-интерфейсе Svacer.

Troubleshooting

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

Когда тренировались в создании детекторов, пробовали реализовать простой детектор для поиска использования метода Set() пакета reflect в языке Go. Если искать просто Set по регулярному выражению – будем ловить много ложных сработок, которые вообще могут не иметь ничего общего с искомой функцией. Поиск reflect.Set тоже не даст никакого результата. При дебаге с помощью Svace IR API выяснилось, что искомое название функции – reflect.Value.Set.

Пока рабочего механизма отладки работы детекторов разработчики не представили – можно попробовать воспользоваться внутренним механизмом построения графа вызовов в Svace. Команде svace analyze можно указать опцию --build-call-graph-only и тогда анализатор просто построит граф вызовов функций без выполнения самого анализа. Результат можно увидеть в директории .svace-dir/analyze-res/call-graph.

В представленном JSON-файле можно будет увидеть корректное название функций, которые вызываются другими функциями (callees). Вытащите оттуда название нужной функции и примените для своего детектора.

Также, можно попробовать использовать более сложный вариант, но он позволит познакомиться с функционалом Svace IR API. Прелесть этого API в том, что он позволяет взаимодействовать с информацией об анализируемой программе: о глобальных переменных, телах реализованных функций и их инструкциях. Имея эти данные, можно написать, плагин, который будет логировать названия всех вызываемых функций в анализируемой программы. Его и можно будет использовать для отладки. Такой плагин можно реализовать с помощью функции procedureCall, который будет анализировать любой вызов функции в коде. Ниже пример, который печатает в консоль имена вызываемых функций.

@Override
public void procedureCall(SvaceIRCalledProcedure procedure, Optional<SvaceIRSymbol> retSymb, SvaceIROperand[] arguments) {
    String procedureName = procedure.name();
    System.out.println("procedure name - '" + procedureName + "'");
}

В этой статье рассмотрели базовый пример использования механизма Svace API. В следующей статье рассмотрим улучшение детектора с помощью механизма добавления атрибутов.

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