На первый взгляд, все хорошо, не так ли? Но на самом деле здесь ошибка на ошибке. Детали мы отложим до обсуждения переполнения целых числе в грехе 3, а пока заметим, что литералы всегда имеют тип signed int. Если длина входных данных (строка input) превышает 32К, то переменная len станет отрицательна, она будет расширена до типа int с сохранением знака и окажется меньше MAX_BUF, что приведет к переполнению. Еще одна ошибка возникнет, если длина строки превосходит 64К. В этом случае мы имеем ошибку усечения: len оказывается маленьким положительным числом. Основной способ исправления – объявлять переменные для хранения размеров как имеющие тип size_t. Еще одна скрытая проблема заключается в том, что входные данные могут не заканчиваться нулем. Вот как может выглядеть исправленный код:
const size_t MAX_BUF = 256;
void LessBadCode(char* input)
{
size_t len;
char buf[MAX_BUF];
len = strlen(input);
// конечно, мы можем использовать strcpy безопасно
if(len < MAX_BUF)
strcpy(buf, input);
}
Родственные грехи
С этим грехом тесно связано переполнение целых чисел. Если вы пытаетесь устранить ошибки переполнения буфера путем использования функций работы со строками семейства strn… или вычисляете размер выделяемого из кучи буфера, то очень важно не допускать арифметических ошибок.
Ошибки при работе с форматной строкой могут дать такой же эффект, как переполнение буфера, хотя переполнением в строгом смысле не являются. Обычно такие ошибки вообще не связаны ни с какими буферами.
Вариантом переполнения буфера является запись в массив без контроля выхода за границы. Если противник сумеет прямо или косвенно подсунуть индекс массива и вы не проверите, что он принадлежит допустимому диапазону, то возможна запись по произвольному адресу в памяти. При этом не только изменяется поток выполнения программы, но могут быть затерты несмежные области памяти, а это сводит на нет все меры противодействия переполнению буфера.
Где искать ошибку
Вот на что нужно обращать внимание в первую очередь:
□ любые входные данные, будь то из сети, из файла или из командной строки;
□ передача данных из вышеупомянутых источников входных данных во внутренние структуры;
□ использование небезопасных функций работы со строками;
□ использование арифметических операций для вычисления размера буфера или числа свободных байтов в нем.
Выявление ошибки на этапе анализа кода
Обнаружить присутствие этого греха во время анализа кода может быть как совсем легко, так и очень сложно. Проще всего проанализировать все случаи употребления функций работы со строками. Надо иметь в виду, что вы можете найти много мест, где функции вызываются безопасно, но наш опыт показывает, что ошибки могут скрываться даже в правильных вызовах. Коэффициент регрессии, характерный для модификации кода с целью перехода исключительно на безопасные функции, обычно очень мал (от одной десятой до одной сотой величины, типичной для исправления ошибки), зато это позволит устранить возможность некоторых видов эксплойтов.
Добиться этого можно, например, поручив выполнение задачи компилятору. Если вы исключите объявления функций strcpy, strcat, sprintf и им подобных из заголовочных файлов, то компилятор укажет все места в коде, где они встречаются. Но имейте в виду, что некоторые приложения полностью или частично переопределяют библиотеку времени исполнения для языка С.
Сложнее отыскать переполнение кучи. Чтобы решить эту задачу, нужно помнить о возможности переполнения целых, о чем пойдет речь в грехе 3. Начать нужно с выявления всех мест, где производится выделение памяти, а затем проверить, с помощью каких арифметических операций вычислялся размер буфера.
Наилучший подход состоит в том, чтобы проследить, как используются все поступающие от пользователя данные, начиная с точки входа в приложение и далее по всем функциям. Очень важно знать, что именно может контролировать противник.
Тестирование
Одной из наиболее эффективных методик является