Когда проверки в C/C++ ломаются из-за скрытых условий API

Разбор Trail of Bits: как особенности inet_ntoa() и RtlQueryRegistryValues обходят проверки и создают путь к опасным ошибкам.

2026-06-02 GIGATAP Team #security
#C/C++#Windows kernel#secure code review

Источник: Trail of Bits Blog — https://blog.trailofbits.com/2026/05/05/c/c-checklist-challenges-solved/

Две учебные ошибки и один старый вывод#

Trail of Bits опубликовала разбор двух заданий из чеклиста по безопасности C/C++: небольшого Linux-хелпера для ping и обработчика реестра на базе Windows Driver Framework. Сценарии разные, но вывод одинаковый: любая проверка безопасности зависит от того, насколько корректно разработчик понимает поведение используемого API.

Linux-пример выглядит простым. Программа проверяет IPv4-адрес, пытается блокировать SSRF-запросы в диапазоны localhost, сравнивает нормализованный адрес с разрешённым IP и затем переходит к выполнению команды. На поверхности проблема выглядит как обычная command injection. Однако более интересная ошибка связана с тем, как код попадает в этот путь после того, как входные данные вроде бы уже проверены.

В Windows-примере ситуация серьёзнее. Обработчик запросов драйвера принимает путь к разделу реестра, читает значения версии и выбирает ветку callback на основе этой версии. Trail of Bits указывает на два класса проблем: возможность управлять путём к реестру и отсутствие проверки типов при использовании RtlQueryRegistryValues в direct mode. В показанном примере ошибка способна выйти за пределы локального отказа в обслуживании и превратиться в примитив записи в память ядра.

Это не новая категория уязвимостей. Именно в этом и заключается главный урок. Речь идёт об ошибках контрактов API, ошибках границ доверия и недостаточно глубоком ревью. Они сохраняются годами, потому что локально код выглядит разумно, хотя нарушает одно важное условие, которого требует платформа.

Linux-задача с ping: проверка сломана статическим буфером#

Программа должна разрешать только строго определённый адрес. Для этого используется разбор IPv4, нормализация адреса, блокировка диапазона 127.0.0.0/8 в качестве защиты от SSRF и сравнение результата с разрешённым значением:

#define ALLOWED_IP "127.3.3.1"

Конфигурация специально выглядит противоречиво. Если localhost запрещён, а единственный разрешённый адрес находится в том же диапазоне, программа фактически должна отклонять любые входные данные. Однако задача построена вокруг особенности, которую легко пропустить при ревью: inet_ntoa() возвращает указатель на глобальный статический буфер.

Каждый следующий вызов inet_ntoa() перезаписывает результат предыдущего. Если программа сохраняет несколько результатов как указатели и считает, что они ссылаются на неизменяемые строки, очередной этап нормализации способен незаметно изменить данные, участвующие в сравнении.

Trail of Bits приводит практический результат: ввод вида 127.0.0.1 '; anything # может пройти проверки и дойти до точки выполнения команды. Суть ошибки не в том, что существует command injection. Проблема в том, что предыдущая валидация создаёт ложное ощущение безопасности: проверяется уже не то значение, которое разработчик считает сохранённым.

Главный вывод для ревьюеров прост: API, возвращающие указатели на статическую память, опасны внутри логики проверки данных. Они скрывают изменение состояния и позволяют поздним вызовам незаметно менять данные, которые использовались на предыдущих этапах проверки.

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

Windows-драйвер: путь к реестру под контролем атакующего#

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

Возникает классическая проблема confused deputy. Код с привилегиями драйвера получает запрос на чтение раздела реестра, который выбирает пользователь. Если обработчик доступен низкопривилегированному пользователю, тот может влиять на то, какие данные читает драйвер, даже если прямой доступ к этим разделам ему запрещён.

Trail of Bits выделяет два непосредственных последствия.

Во-первых, атакующий способен направить драйвер к контролируемому им ключу. Это позволяет влиять на поведение драйвера, включая выбор ветки callback на основе версии.

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

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

Отсутствие проверки типов в direct mode#

Более опасная проблема связана с использованием RtlQueryRegistryValues в direct mode.

API работает через массив записей таблицы запросов. В callback mode запись содержит указатель на функцию обратного вызова, которая получает данные из реестра. В direct mode, который включается флагом RTL_QUERY_REGISTRY_DIRECT, значение записывается напрямую в буфер, предоставленный вызывающим кодом.

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

Trail of Bits называет отсутствие флага RTL_QUERY_REGISTRY_TYPECHECK основной причиной проблемы. Без проверки типов код просто предполагает, что данные имеют нужный формат. Если атакующий способен контролировать ключ реестра, он может предоставить значение, тип или структура которого не соответствуют стековому буферу назначения.

Источник отмечает, что ошибка может эволюционировать от локального отказа в обслуживании до примитива записи в память ядра. Причина очевидна: direct mode позволяет записывать управляемые через реестр данные в буфер, выбранный кодом ядра. При отсутствии ограничений по типам такая запись уже не обязана соответствовать исходному замыслу разработчика.

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

Чего не стоит утверждать#

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

Также статья не утверждает, что любой вызов inet_ntoa() или RtlQueryRegistryValues автоматически создаёт уязвимость. Риск зависит от управления состоянием, границ доверия, времени жизни буферов, контроля над путями реестра, используемых флагов, типов целевых буферов и доступности обработчика для потенциального атакующего.

Более точное утверждение звучит так: эти API имеют контракты использования, которые легко нарушить, а последствия таких ошибок способны свести на нет проверки, выглядящие корректными при поверхностном ревью.

Что проверить#

При ревью C и C++ кода примеры Trail of Bits предлагают несколько конкретных проверок:

  • Проверьте использование API, возвращающих указатели на статические или глобальные буферы.
  • Копируйте проверяемые значения в стабильное хранилище до того, как последующие вызовы API смогут их перезаписать.
  • Изучайте промежуток между проверкой и использованием данных, особенно если нормализация выполняется несколько раз.
  • Рассматривайте пути, имена объектов, ключи реестра и имена устройств, контролируемые пользователем, как входы на границе доверия.
  • В коде Windows-ядра анализируйте вызовы RtlQueryRegistryValues на предмет использования direct mode и отсутствия проверки типов.
  • Проверяйте соответствие типов и размеров целевых буферов допустимым типам данных реестра.
  • Проводите аудит привилегированного кода, который читает данные от имени менее привилегированных пользователей без дополнительных проверок доступа.

Trail of Bits также сообщает о разработанном навыке для Claude, который превращает её чеклист по C/C++ в набор подсказок для поиска ошибок с помощью LLM. В публикации приведены команды для установки и активации навыка c-review из Trail of Bits skills marketplace. Такой инструмент полезен как вспомогательное средство ревью, но не заменяет понимание контрактов платформы.

Главный урок остаётся неизменным. В C и C++ безопасность определяется не отдельной строкой кода, а временем жизни данных, семантикой API и границами доверия по всей цепочке вызовов.