
CVE-2026-39259
Проект: https://github.com/alexfru/SmallerC
Реализация scanf в SmallerC не налагает верхнюю границу на чтение строк, когда %s или %[ используется без явной ширины поля в строке формата. Среда выполнения продолжает запись в буфер назначения до тех пор, пока не встретит пробельный символ или конец файла (EOF), независимо от фактического выделенного размера буфера. Любые байты за пределами буфера попадают непосредственно в стек, перезаписывая то, что компилятор разместил над буфером: локальные переменные, сохранённые регистры, адрес возврата.
Это не новый класс уязвимостей. Неограниченные операции чтения строк через scanf документированы с ранних дней C, и любая качественная система статического анализа пометит их. Что делает эту проблему заслуживающей внимания именно в контексте SmallerC — это целевая среда. SmallerC разработан для DOS и голых металлических встраиваемых систем — платформ, которые по определению не предоставляют канареек стека, ASLR, NX-битов или любых других средств защиты, затрудняющих эксплуатацию на современных системах. Тот же самый примитив, который потребовал бы значительных исследовательских усилий для превращения в рабочий эксплойт на защищённой Linux-программе, становится значительно более простым для реализации на DOS-программе, работающей на плоском предсказуемом стеке.
Буфер — 16 байт. Входные данные — 20 непробельных байт. Спецификатор %s не имеет ограничения ширины, поэтому sscanf считывает все 20 байт плюс нулевой терминатор — всего 21 байт — в 16-байтовое выделение. 5 байт за пределами буфера повреждают соседнюю память стека. Что именно будет повреждено, зависит от решений компилятора по размещению стека для данной функции, но само перезапись является детерминированной и безусловной при каждом выполнении этого пути кода с такими входными данными.
Доказательство концепции
#include <stdio.h>
#include <string.h>
/*
* Сборка с SmallerC под DOS или bare-metal
* Демонстрирует неограниченную запись %s за пределы фиксированного буфера стека
*
* buffer — 16 байт, payload — 20 непробельных байт
* sscanf записывает 21 байт (20 + нулевой терминатор) в buffer
* повреждая 5 байт соседней памяти стека
*
* Чтобы наблюдать повреждение, проверьте память стека после вызова:
* 5 байт, расположенных сразу над buffer, будут содержать 'A' (0x41)
*/
int main() {
char buffer[16];
char canary[8];
memset(buffer, 0x00, sizeof(buffer));
memset(canary, 0xCC, sizeof(canary)); /* маркер для обнаружения перезаписи */
printf("[*] canary before: ");
for (int i = 0; i < 8; i++) printf("%02x ", (unsigned char)canary[i]);
printf("\n");
sscanf("AAAAAAAAAAAAAAAAAAAA", "%s", buffer); /* 20 байт в 16-байтовый буфер */
printf("[*] canary after: ");
for (int i = 0; i < 8; i++) printf("%02x ", (unsigned char)canary[i]);
printf("\n");
if (memcmp(canary, "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC", 8) != 0)
printf("[!] stack corruption confirmed, canary overwritten\n");
else
printf("[-] canary intact (stack layout placed it elsewhere)\n");
return 0;
}
Ожидаемый вывод на подверженной сборке:
[*] canary before: cc cc cc cc cc cc cc cc
[*] canary after: 41 41 41 41 41 cc cc cc
[!] stack corruption confirmed, canary overwritten
Расположение канарейки относительно буфера зависит от структуры стека, выбранной компилятором. Если вывод показывает канарейку нетронутой, перезапись всё равно происходит — она попадает на что-то другое выше буфера. Откорректируйте тест, проверив фактический стековый кадр с помощью отладчика, чтобы определить, куда попадают 5 повреждающих байт.
Одно важное уточнение: некоторые сообщения об уязвимостях этого класса пытаются продемонстрировать контроль над адресом возврата, добавляя в полезную нагрузку целевой адрес после нулевого байта, например "AAAAAAAAAAAAAAAAAAA\x00\x90\x04\x08". Это не работает. Спецификатор %s в sscanf воспринимает \x00 как завершитель строки и немедленно прекращает чтение при его обнаружении. Байты, следующие за нулевым байтом, никогда не обрабатываются. Для демонстрации реального контроля над адресом возврата необходимо выполнить перезапись без нулевого байта в критической части полезной нагрузки, что, в свою очередь, требует точного знания расположения стека целевой программы: расстояния от буфера до сохранённого адреса возврата, вставил ли компилятор какие-либо отступы и какие ограничения на выравнивание действуют. Ничто из этого не получается автоматически из данного теста.
Однако тест однозначно устанавливает сам примитив повреждения. Выход за границы при записи реален, воспроизводим и не зависит от состояния гонки или тайминга. На DOS- или встраиваемой целевой платформе, где расположение стека статично и предсказуемо при пересборке, переход от этого примитива к рабочему эксплойту является реалистичной исследовательской задачей, а не теоретическим упражнением.
Затрагиваемый сценарий узок, но не надуман. Программа должна быть собрана с SmallerC, использовать парсинг с помощью семейства scanf с неограниченным спецификатором %s или %[ для записи в фиксированный стековый буфер и принимать входные данные из источника, на который может влиять атакующий. Все четыре условия должны выполняться одновременно. Программы, использующие корректную ширину поля (например, %15s для char[16]), не подвержены уязвимости. Программы, которые не парсят управляемые атакующим данные, также не подвержены. Проблема является дефектом в обработке средой выполнения SmallerC отсутствующего ограничения ширины, но она становится угрозой безопасности только тогда, когда код приложения подвергает этот дефект воздействию недоверенных входных данных.
Со стороны приложения исправление простое: указать ширину поля, оставляющую место для нулевого терминатора — %15s для 16-байтового буфера, %63s для 64-байтового буфера. Это стандартная практика C, полностью поддерживаемая синтаксисом строки формата. Со стороны проекта SmallerC более долгосрочной работой является добавление регрессионных тестов, охватывающих как ограниченное, так и неограниченное поведение %s и %[ в scanf, sscanf и fscanf, с проверкой того, что явно заданные ширины полей действительно соблюдаются в реализации, а также заметное документирование опасного шаблона. Диагностика на уровне компилятора, предупреждающая, когда %s или %[ используется без ширины поля в строковом литерале формата, могла бы предотвращать подобный класс ошибок на упреждение и стала бы значимым дополнением к инструментарию.
Серьёзность: Средняя, когда внешние входные данные достигают уязвимого пути кода. Она снижается до Низкой, если входные данные являются локальными или непривилегированными. Целевая среда — в частности, отсутствие современных средств защиты на платформах, для которых предназначен SmallerC, — отличает эту проблему от общего совета «не используйте неограниченный scanf» и делает её заслуживающей отчёта на уровне проекта, а не рассмотрения исключительно как неправильного использования на уровне приложения.
Автор: Yousif Wazni