Когда математика против защиты: как целочисленное переполнение ломает логику в коде на Си
Когда математика против защиты: как целочисленное переполнение ломает логику в коде на Си В прошлых разборах мы изучали ошибки адресации: выход за пределы массива на стеке, утечки через форматные строки и повторное использование памяти в куче. Сегодня протестируем вектор, где код внешне выглядит аккуратно, проверка размера буфера на месте, но программа все равно падает в Segmentation fault. Речь идет об Integer Overflow (целочисленном переполнении). Я собрал тестовый стенд в Parrot Security OS на ARM64 и разобрал, почему проверка if (size > MAX) может пропустить опасный объем данных. 1. В чем идея уязвимости? Представьте обработчик сетевых пакетов: сервер принимает длину полезной нагрузки, прибавляет к ней фиксированный размер служебного заголовка (например, 8 байт) и проверяет, поместится ли итоговый блок в локальный буфер на 64 байта. Логика программиста: unsigned short total_size = length + 8; if (total_size > 64) { reject(); } Если передать length = 100, программа честно вычислит 108, увидит превышение лимита и отклонит пакет. Проверка работает штатно. Но у типа unsigned short фиксированный размер - 16 бит. Его диапазон строго ограничен значениями от 0 до 65535 (0xFFFF). 2. Эксперимент: переполняем 16-битный регистр Что произойдет, если на вход передать число 65530? 1. Процессор выполняет сложение: 65530 + 8 = 65538. 2. Значение 65538 не помещается в 16 бит (0x10002). 3. Старший семнадцатый бит отбрасывается аппаратным сумматором, и в переменной total_size остается остаток от деления на 65536 - число 2! 4. Условие if (total_size > 64) проверяет двойку: 2 > 64 ложно. 5. Проверка безопасности успешно пройдена! А дальше управление переходит к функции копирования: memcpy(local_buffer, data, length); где length по-прежнему равен исходным 65530 байтам. 3. Результат в терминале Программа бодро рапортует: - [*] Входящая длина: 65530 (размер short: 2 байт) - [*] Вычисленный общий размер (length + 8): 2 - [+] Проверка пройдена! Выделяем место и копируем 65530 байт... - Segmentation fault Попытка записать 65 килобайт данных в буфер размером 64 байта мгновенно разрушает структуру памяти и приводит к аварийному завершению процесса. Математический оборот счетчика (Wrap-around) полностью обошел логический фильтр. 4. Вторая сторона медали: знаковые числа (Signedness Bug) Еще опаснее ситуация, когда разработчик использует обычный знаковый int: Если переменная длины принимает значение -1, проверка вида if (len > 64) сочтет его безопасным (-1 меньше 64). Однако стандартная функция memcpy() ожидает параметр размера типа size_t (беззнаковый 64-битный тип). При передаче в функцию -1 неявно приводится к гигантскому числу 18 446 744 073 709 551 615 (0xFFFFFFFFFFFFFFFF), что приводит к попытке чтения несуществующей памяти и моментальному падению сервиса. Как предотвратить такие уязвимости? 1. Проверять границы до выполнения операции: Вместо сложения с последующим сравнением используйте вычитание из константы предела: if (length > MAX_BUFFER_SIZE - 8) { reject(); }. В таком случае переполнение физически невозможно. 2. Использовать встроенные макросы компилятора: Современные версии GCC и Clang поддерживают встроенные функции с аппаратной проверкой флага переноса: __builtin_add_overflow(a, b, &res). 3. Статический анализ кода: Линтеры и SAST-инструменты уверенно находят неявные приведения типов (signed в unsigned) и предупреждают о риске переполнения еще на этапе сборки проекта.