
Кастомные детекторы в испытательной лаборатории
Тезисы
- Постановка проблемы – нехватка детекторов для поиска специфичных случаев.
- Демонстрация проблемы с помощью библиотеки libxml2 и анализатора Svace.
- Написание своего детектора для поиска XXE в libxml2 с помощью Svace Light API.
- Использование детектора «в бою». Сборка детектора и настройка анализатора для поиска XXE в libxml2.
- Troubleshooting. С чем сталкивались при разработке детекторов и как подходили к решению проблемы.
Введение
Во время сертификационных испытаний в соответствии с методикой ВУ и НДВ мы, как испытательная лаборатория, должны проводить статический анализ кода. Для этого мы можем использовать разные анализаторы – они варьируются по анализируемым языкам, по применяемым методам анализа, по наборам детекторов и куче других параметров.
Анализируя достаточно большое количества кода, мы, конечно, подмечаем использование разработчиком различного Open Source кода в своем продукте. При выполнении статического анализа такого кода хочется видеть комплексный подход оценки – условно говоря, не концентрировать внимание только на ошибках разыменования нулевого указателя, но и, например, подсвечивать небезопасное использование функций, которые встречаются в Open Source библиотеках. Даже огромных баз детекторов, которые используются в анализаторах, может быть недостаточно для покрытия всех потенциальных дефектов в коде.
Проблема
Приведу простой пример. При использовании функций библиотеки libxml2 с некоторым набором параметров мы рискуем нарваться на XXE. Описание опасной конфигурации приведено в статье.
Посмотрев на описание проблемы, выглядит так, что искать это в коде весьма просто. Однако, натравливая на это анализатор, можем столкнуться с тем, что потенциальный дефект останется необнаруженным. Почему? Потому что дефект достаточно специфичный и относится к реализации конкретной библиотеки – по-своему уникальный. Из-за этого разработчики анализатора попросту могут не успевать писать такие детекторы, концентрируя внимание на более общих классах ошибок. В таком случае хотелось бы иметь возможность кастомизировать используемый инструмент самостоятельно.
Возможность добавлять детекторы вручную самостоятельно позволит решить сразу несколько проблем:
- Разработчики анализатора могут долго рассматривать запрос на создание вашего кастомного детектора или же в принципе могут не взяться за что-то очень специфичное.
- Адаптация детекторов под свой 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. Структура каталога для детектора.
Пройдемся по каждому файлу:
- build.gradle – конфигурационный файл для сборки детектора с помощью gradle. Самое важное в нем – указание зависимости от svace-api, а также включение модульной системы Java, поскольку именно с помощью нее анализатор может динамически подтягивать и запускать пользовательские детекторы;
- README.md – просто файлик с описанием детектора (необязательный);
- module-info.java – файл, описывающий Java-модуль для детектора;
- 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):
- hasBitmask(int mask) – определяет, соответствует ли параметр заданной битовой маске;
- funcCall(String regex, int argIndex, SvaceLightPattern argVal) – находит вызов функции с заданным по регулярному выражению именем и заданными аргументами.
Методы выбрали – как написать детектор?
Воспользуемся шаблоном, который есть в папке plugins.
Вначале нужно создать наш класс SvaceLightXXELibxml, который будет наследоваться от SvaceLightPlugin.
Далее, разберемся с косметикой. Нужно «зарегистрировать» наш детектор в базе Svace, чтобы перед выполнением анализа мы могли посмотреть на его описание и имели возможность включить/выключить.
Для регистрации предупреждения необходимо добавить в детектор следующие обязательные методы (они есть в каждом шаблоне детекторов):
- name() – содержит имя детектора, которое будет отображаться в справке;
- 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. В нем указывается следующая информация:
- warning – значение warningXXE, содержащее информацию о предупреждении, которую мы задали с помощью функции registerWarningType;
- fixMessage – текстовое сообщение, которое будет выведено при срабатывании детектора;
- 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.
В результате анализа получаем наше заветное найденное срабатывание.

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

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

Рисунок 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. В следующей статье рассмотрим улучшение детектора с помощью механизма добавления атрибутов.