Pull to refresh
359.44
PVS-Studio
Статический анализ кода для C, C++, C# и Java

В преддверии испытаний статических анализаторов под руководством ФСТЭК России

Level of difficultyMedium
Reading time11 min
Views2K

Испытания статических анализаторов исходных кодов компилируемых и динамических языков программирования под руководством ФСТЭК России


В 2024 году вышел ГОСТ Р 71207 — Статический анализ программного обеспечения. Однако пользователям анализаторов сложно определять, насколько тот или иной инструмент соответствует критериям, изложенным в стандарте. Поэтому ФСТЭК России в 2025 году организует испытания статических анализаторов, результаты которых будут опубликованы в конце года. Ближайший этап — это выработка критериев оценки, и я решил предварительно изложить некоторые мысли по этой теме, которые, возможно, будут интересны жюри и участникам.


Испытание статических анализаторов


После выхода ГОСТ Р 71207-2024 наша команда провела различные доработки PVS-Studio, чтобы соответствовать всем нормам, предъявляемым в стандарте к анализаторам. По нашей оценке, PVS-Studio соответствует требованиям ГОСТ Р 71207—2024 и может применяться для построения процессов РБПО.


Однако в этой публикации изложено мнение нашей команды. Какова объективная оценка? Оказывается, дать ответ сложно. В ГОСТ Р 71207–2024 приводится методика проверки требований к статическому анализатору (п. 10), но осуществить её сторонним организациям сложно и трудоёмко. Разработчики статических анализаторов могут провести проверку своих инструментов по этой методике и самостоятельно, но остаётся вопрос доверия к таким испытаниям. Даже имея желание быть объективными, собственные испытания могут оказаться несбалансированными. Например, для оценки может быть отдано предпочтение типам открытых проектов (п. 10.2.в), с которыми больше приходится сталкиваться в поддержке.


Чтобы помочь справиться с затруднениями в оценке, ФСТЭК России объявила о начале масштабных испытаний статических анализаторов. 3 февраля ФСТЭК России провела установочную встречу с представителями заинтересованных организаций, а 12 февраля Виталий Лютиков в своём докладе на ТБ Форум озвучил предстоящие этапы работ (презентация, слайд N14). Полное официальное название предстоящего мероприятия — "Испытания статических анализаторов исходных кодов компилируемых и динамических языков программирования под руководством ФСТЭК России".


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


На текущий момент список участников, представляющих разработчиков статических анализаторов, следующий:



Жюри:


  • ООО НТЦ "Фобос-НТ";
  • АО "Лаборатория Касперского";
  • ООО "Базальт СПО";
  • АО "СберТех";
  • ООО "РусБИТех-Астра".

Список участников и жюри, возможно, будет расширен. Впрочем, я отвлёкся от темы, про которую хотел написать. Хочу затронуть вопросы выработки критериев оценки касательно критических ошибок.


Критические ошибки


ГОСТ Р 71207-2024 вводит понятие "критическая ошибка" (п. 3.1.13):


Критическая ошибка в программе. Ошибка, которая может привести к нарушению безопасности обрабатываемой информации.

Как я понимаю, оценка анализаторов как на малых синтетических тестах, так и поиск известных (размеченных) ошибок в открытых проектах, будет строиться вокруг ошибок, классифицируемых как критические. Это разумный подход. Анализаторы оцениваются для задач безопасной разработки, и нет смысла рассматривать такие диагностики, как сравнение двух переменных типа float с помощью оператора ==. Такое точное сравнение может быть ошибкой, но слишком мала вероятность, что это приведёт к уязвимости.


Критические типы ошибок, перечисленные в ГОСТ, хорошая основа как для фокусировки внимания разработчиков при РБПО, так и при аттестации анализаторов кода.


Примечание: таблицу соответствия детекторов PVS-Studio различным типам критических ошибок согласно ГОСТ можно посмотреть здесь.


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


Начнём с C и C++, для которых ГОСТ Р 71207 приводит наиболее обширный набор критических ошибок. Во-первых, в него входит общий список для компилируемых языков (п. 6.3):


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

Во-вторых, приводится дополнительный список именно для C и C++ (п. 6.5):


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

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


Опечатки


В Chromium мы находили опечатку в функции сравнения (в функциях сравнения вообще часто ошибки встречаются) двух объектов типа PasswordForm. Возможно, эта ошибка никак не влияет на безопасность, но это код, который при анализе заслуживает внимания и проверки.


lhs.confirmation_password_element_renderer_id ==
  rhs.confirmation_password_element_renderer_id &&
....
lhs.confirmation_password_element_renderer_id ==
  rhs.confirmation_password_element_renderer_id &&

Полный код функции.
bool operator==(const PasswordForm& lhs, const PasswordForm& rhs) {
  return lhs.scheme == rhs.scheme && lhs.signon_realm == rhs.signon_realm &&
         lhs.url == rhs.url && lhs.action == rhs.action &&
         lhs.submit_element == rhs.submit_element &&
         lhs.username_element == rhs.username_element &&
         lhs.username_element_renderer_id == rhs.username_element_renderer_id &&
         lhs.username_value == rhs.username_value &&
         lhs.all_possible_usernames == rhs.all_possible_usernames &&
         lhs.all_possible_passwords == rhs.all_possible_passwords &&
         lhs.form_has_autofilled_value == rhs.form_has_autofilled_value &&
         lhs.password_element == rhs.password_element &&
         lhs.password_element_renderer_id == rhs.password_element_renderer_id &&
         lhs.password_value == rhs.password_value &&
         lhs.new_password_element == rhs.new_password_element &&
         lhs.confirmation_password_element_renderer_id ==            // <=
             rhs.confirmation_password_element_renderer_id &&        // <=
         lhs.confirmation_password_element ==
             rhs.confirmation_password_element &&
         lhs.confirmation_password_element_renderer_id ==            // <=
             rhs.confirmation_password_element_renderer_id &&        // <=
         lhs.new_password_value == rhs.new_password_value &&
         lhs.date_created == rhs.date_created &&
         lhs.date_last_used == rhs.date_last_used &&
         lhs.date_password_modified == rhs.date_password_modified &&
         lhs.blocked_by_user == rhs.blocked_by_user && lhs.type == rhs.type &&
         lhs.times_used == rhs.times_used &&
         lhs.form_data.SameFormAs(rhs.form_data) &&
         lhs.generation_upload_status == rhs.generation_upload_status &&
         lhs.display_name == rhs.display_name && lhs.icon_url == rhs.icon_url &&
         // We compare the serialization of the origins here, as we want unique
         // origins to compare as '=='.
         lhs.federation_origin.Serialize() ==
             rhs.federation_origin.Serialize() &&
         lhs.skip_zero_click == rhs.skip_zero_click &&
         lhs.was_parsed_using_autofill_predictions ==
             rhs.was_parsed_using_autofill_predictions &&
         lhs.is_public_suffix_match == rhs.is_public_suffix_match &&
         lhs.is_affiliation_based_match == rhs.is_affiliation_based_match &&
         lhs.affiliated_web_realm == rhs.affiliated_web_realm &&
         lhs.app_display_name == rhs.app_display_name &&
         lhs.app_icon_url == rhs.app_icon_url &&
         lhs.submission_event == rhs.submission_event &&
         lhs.only_for_fallback == rhs.only_for_fallback &&
         lhs.is_new_password_reliable == rhs.is_new_password_reliable &&
         lhs.in_store == rhs.in_store &&
         lhs.moving_blocked_for_list == rhs.moving_blocked_for_list &&
         lhs.password_issues == rhs.password_issues;
}

Предупреждение PVS-Studio: V501 There are identical sub-expressions to the left and to the right of the '&&' operator. password_form.cc 265


Сейчас этот код отсутствует, и оператор сравнения реализуется по умолчанию:


friend bool operator==(const PasswordForm&, const PasswordForm&) = default;

Два раза сравнивается один и тот же член класса. Такие ошибки свидетельствуют или о лишнем коде (дубликате проверки, которую можно удалить), или об отсутствии в этом месте другого члена класса.


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


Формально рассмотренную ошибку можно отнести к категории "Ошибки непроверенного использования чувствительных данных". Но это будет очень притянутая классификация, от которой мало пользы. В общем случае (без ручной разметки) очень сложно понять, какие данные чувствительные, и что где-то что-то не проверено или проверено неправильно. Сложно представить, как найти эту ошибку, размышляя в категориях истоков, стоков и проверки. Зато просто и полезно искать как опечатку :)


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


С возражением соглашаюсь, но только частично. Во-первых, не каждый выход за границу буфера критичен. Всё относительно :)


struct header_t
{
  ....
  byte load_addr[2];
  byte init_addr[2];
  byte play_addr[2];
  ....
};
....
memcpy(info.load_addr, finfo.load_addr, 2 * 3);

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


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


Ещё одна опечатка из проекта Haiku Operation System:


#define MEMSET_BZERO(p,l)  memset((p), 0, (l))

void solv_SHA256_Final(sha2_byte digest[], SHA256_CTX* context) {
  ....
  /* Clean up state data: */
  MEMSET_BZERO(context, sizeof(context));
  usedspace = 0;
}

Предупреждение PVS-Studio: V1086 A call of the 'memset' function will lead to underflow of the buffer 'context'. sha2.c 623


Дефект в коде явно связан с безопасностью. Выражение sizeof(context) вычисляет размер указателя, а не структуры. В результате будут затёрты только первые байты, а остальные данные останутся в памяти.


Понятно, как искать такие ошибки. Но она не подходит в приведённую классификацию перечисленных критических ошибок:


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

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


Неопределённое поведение (C, C++)


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


Я не предлагаю добавить критерий "Поиск неопределённого поведения". Это очень широкая постановка задачи (тема UB очень разнообразна), которую невозможно будет реализовать на практике.


Однако, возможно, есть смысл собрать ряд частных случаев и включить их выявление в критерии оценки качества анализатора кода. Пример: искать забытый return. Не уверен, что привёл хорошую мысль. Это просто идея для жюри. Если она понравится, то можно совместно попробовать составить список примеров UB, которые реально и полезно выявлять с точки зрения безопасности работы.


Веса критических ошибок на примере C# кода


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


Одной из критических ошибок для компилируемых языков считаются "Ошибки переполнения буфера". Для C и C++ это 100% важный тип дефектов, а вот для C# опасность смотрится преувеличенной.


Что такое "Ошибки переполнения буфера" в C#? Выход за границу массива? При выходе за границу массива возникнет исключение. Конечно, остановку работы программы из-за исключения можно интерпретировать как DoS-атаки уровня приложения. Но это слабая угроза по сравнению с C и C++. Тем более исключение возникает и, например, при разыменовании нулевой ссылки в C#, но на этот случай критическая ошибка не предлагается.


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


Возможные дополнительные критерии оценки


Наличие документации по диагностикам. В ГОСТ Р 71207 (п. 8.10) хорошо написано, что документация должна содержать описание всех диагностик. При этом само описание должно включать пример некорректного кода, исправленный вариант или пояснение, как исправить, маппинг на CWE и т.д. Можно сделать это одним из дополнительных критериев оценки. Более того, раз речь идёт о ГОСТ, то критерием может быть документация на русском языке.


Выделенное подмножество предупреждений для выявления критических ошибок. ГОСТ Р 71207 ожидаемо предписывает в первую очередь выявлять и исправлять критические ошибки. В п. 5.4 говорится: "В ходе конфигурации должны быть выполнены выбор и включение типов предупреждений анализатора, соответствующих списку критических ошибок, приведённых в 6.3.". Пользователю будет достаточно сложно самостоятельно составить набор нужных предупреждений. Одним из дополнительных критериев может быть предоставление инструментом анализа подготовленного набора таких диагностик, который ориентирован на выявление критических ошибок согласно ГОСТ Р 71207.


Выявление мёртвого/недостижимого кода. Сам по себе мёртвый/недостижимый код не является потенциальной уязвимостью. Но он может свидетельствовать, что что-то работает не так, как планировалось, и косвенно свидетельствовать о наличии дефекта безопасности.


Пример ошибки из Qt Creator. С безопасностью она не связана, но является классической.
static KnownType knownClassTypeHelper(const std::string &type,
                                      std::string::size_type pos,
                                      std::string::size_type endPos)
{
  ....
  // Remaining non-template types
  switch (endPos - qPos)
  {
    ....
  case 30:
    if (!type.compare(qPos, 30, "QPatternist::SequenceType::Ptr"))
      return KT_QPatternist_SequenceType_Ptr;
    if (!type.compare(qPos, 30, "QXmlStreamNamespaceDeclaration"))
      return KT_QXmlStreamNamespaceDeclaration;
    break;
  case 32:
    break;                                                                // <=
    if (!type.compare(qPos, 32, "QPatternist::Item::Iterator::Ptr"))
      return KT_QPatternist_Item_Iterator_Ptr;
  case 34:
    break;                                                                // <=
    if (!type.compare(qPos, 34, "QPatternist::ItemSequenceCacheCell"))
      return KT_QPatternist_ItemSequenceCacheCell;
  case 37:
    break;                                                                // <=
    if (!type.compare(qPos, 37, "QNetworkHeadersPrivate::RawHeaderPair"))
      return KT_QNetworkHeadersPrivate_RawHeaderPair;
    if (!type.compare(qPos, 37, "QPatternist::AccelTree::BasicNodeData"))
      return KT_QPatternist_AccelTree_BasicNodeData;
    break;
  }

  return KT_Unknown;
}

Первое предупреждений PVS-Studio: V779 Unreachable code detected. It is possible that an error is present. symbolgroupvalue.cpp 1565


Возможно, стоит сделать выявление мёртвого/недостижимого кода одним из дополнительных критериев. К сожалению, может быть много ложных срабатываний (и срабатываний, не связанных с безопасностью). Интересно будет узнать мнение других участников испытаний.


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


Заключение


Ещё раз спасибо всем, кто принимал участие в разработке ГОСТ Р 71207-2024. Он сформулировал важные моменты, необходимые для правильного использования статических анализаторов. Например, мы многие годы продвигали подход, что анализ должен выполняться регулярно, а не от случая к случаю. Приятно, что теперь это чётко сформулировано в стандарте, и на него можно ссылаться.


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

Tags:
Hubs:
Total votes 8: ↑7 and ↓1+8
Comments0

Articles

Information

Website
pvs-studio.ru
Registered
Founded
2008
Employees
51–100 employees
Location
Россия
Representative
Андрей Карпов