
Синтетический вариант переполнения буфера стека CWE-120 на основе CVE-2020-8597 (pppd EAP) в качестве цели статического анализа CodeQL
Намеренно уязвимая C-программа объёмом ~170 строк, используемая как цель для статического анализа CodeQL. Она воспроизводит класс ошибок CVE-2020-8597 — переполнение стекового буфера rhostname в EAP-обработчике pppd (CWE-120) — в программе, которая не разделяет ни имён функций pppd, ни глубины вызовов, ни структуры диспетчеризации.
Цель — проверка обобщённости: запрос CodeQL, написанный для обнаружения ошибки pppd, должен также сработать на этой программе без изменений. Если это так, запрос выражает класс ошибок, а не форму исходного кода.
Эта программа намеренно небезопасна и существует только для анализа. Не развёртывайте её. Ошибка, которую она повторяет, является публичной (CVE-2020-8597, раскрыта в 2020 году).
Полученная от атакующего длина копируется в буфер фиксированного размера без проверки, связывающей эту длину с размером буфера.
В каждом случае проверка границ присутствует — она просто не связывает две величины, которые имеют значение. Существует ровно два способа ошибиться в этом, и программа содержит по одному из каждого:
sizeof(dest). Предотвращает перечитывание, но ничего не делает с перезаписью. (handle_hello)sizeof(dest) — выглядит точно как граница буфера — но ограничивает другую переменную, не ту, что используется как длина копирования. (handle_stat)Второй случай сложнее, и именно им является мёртвая проверка pppd
vallen >= len + sizeof(rhostname): сравнение, которое упоминает
размер назначения, ограничивая при этом нечто, не являющееся длиной копирования. Запрос, который только спрашивает «упоминает ли какое-либо сравнение здесь sizeof(dest)?», заглушается им.
| pppd / CVE-2020-8597 | этот проект | |
|---|---|---|
| Источник | read() на PPP fd | recvfrom() на UDP-сокете |
| Диспетчеризация | глобальный struct protent *protocols[], линейное сопоставление по номеру протокола | локальный для файла const struct frame_op ops[], линейное сопоставление по 1-байтовому тегу |
| Глубина до стока | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| Назначение | char rhostname[256] | char name[64] |
| Неправильная защита | vallen ограничена длиной пакета len | vlen ограничена длиной кадра plen |
Оба сохраняют одно свойство, которое делает это потоком данных, а не grep: косвенный вызов через таблицу указателей на функции между источником и стоком.
| Обработчик | Строка | Присутствующая проверка | Вердикт |
|---|---|---|---|
handle_hello() | сток на :80 | vlen > plen - 2 — правильное значение, неправильная граница | должен сработать |
handle_echo() | копирование на :106 | vlen >= sizeof(buf) — оба правильны | должен молчать — негативный контроль |
handle_stat() | сток на :145 | hlen >= sizeof(report) — правильная граница, неправильное значение | должен сработать |
handle_stat — это различающий случай. Его проверка указывает sizeof(report), поэтому запрос, который принимает любое сравнение, упоминающее размер назначения, считает его защищённым и пропускает ошибку. Чтобы поймать его, требуется сравнить значение, которое проверяется, со значением, используемым как длина копирования — глобальная нумерация значений. Удалите это из запроса, и этот обработчик станет ложноотрицательным, тогда как все остальные места сохранят свой вердикт.
Одна UDP-датаграмма = один кадр:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. Кадр hello с объявленной
длиной от 65 до ~2045 переполняет name[64]. Кадр stat вместо этого несёт две
однобайтовые длины — длину заголовка и длину тела — и любая длина тела выше 32 переполняет report[32], что бы ни говорила длина заголовка.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (BSD-сокеты). Собирается чисто, без предупреждений.
CodeQL трассирует реальную компиляцию, поэтому собирайте с чистого состояния:
make clean
codeql database create db --language=cpp --command="make"
# или, без шага очистки:
codeql database create db --language=cpp --command="make -B"
Затем запустите запрос из части 3 против db; он должен сообщить о memcpy в
handle_hello и о том, что в handle_stat, и промолчать о handle_echo.
Запрос и инструкции по его запуску находятся в codeql/.
Исходный код меняется всякий раз, когда добавляется обработчик, поэтому пересоберите базу данных —
CodeQL делает снимок кода во время database create, и существующая db/ не
увидит новый код.