19 смертных грехов, угрожающих безопасности программ
Шрифт:
Греховность C/C++
В программах на языках C/C++ есть масса способов переполнить буфер. Вот строки, породившие finger–червя Морриса:
char buf[20] ;
gets (buf) ;
Не существует никакого способа вызвать gets для чтения из стандартного ввода без риска переполнить буфер. Используйте вместо этого fgets. Наверное, второй по популярности способ вызвать переполнение – это воспользоваться функцией strcpy (см. предыдущий пример). А вот как еще можно напроситься на неприятности:
char buf[20];
char prefix[] = "http://";
strcpy(buf, prefix);
strncat(buf, path, sizeof(buf));
Что
char buf[MAX_PATH];
sprintf(buf, "%s – %d\n", path, errno);
Если не считать нескольких граничных случаев, функцию sprintf почти невозможно использовать безопасно. Для Microsoft Windows было выпущено извещение о критической ошибке, связанной с применением sprintf для отладочного протоколирования. Подробности см. в бюллетене MS04–011 (точная ссылка приведена в разделе «Другие ресурсы»).
А вот еще пример:
char buf [ 32] ;
strncpy(buf, data, strlen(data));
Что неверно? В последнем аргументе передана длина входного буфера, а не размер целевого буфера!
Еще один способ столкнуться с проблемой – по ошибке считать байты вместо символов. Если вы работаете с кодировкой ASCII, то между ними нет разницы, но в кодировке Unicode один символ представляется двумя байтами. Вот пример:
_snwprintf(wbuf, sizeof(wbuf), «%s\n», input);
Следующее переполнение несколько интереснее:
bool CopyStructs(InputFile* pInFile, unsigned long count)
{
unsigned long i;
m_pStructs = new Structs[count];
for(i = 0; i < count; i++)
{
if(!ReadFromFile(pInFile, &(m_pStructs[i])))
break;
}
}
Как здесь может возникнуть ошибка? Оператор new[] в языке С++ делает примерно то же, что такой код:
ptr = malloc(sizeof(type) * count);
Если значение count может поступать от пользователя, то нетрудно задать его так, чтобы при умножении возникло переполнение. Тогда будет выделен буфер гораздо меньшего размера, чем необходимо, и противник сможет его переполнить. В компиляторе С++, который будет поставляться в составе Microsoft Visual Studio 2005, реализована внутренняя проверка для недопущения такого рода ошибок. Аналогичная проблема может возникнуть во многих реализациях функции calloc, которая выполняет примерно такую же операцию. В этом и состоит коварство многих ошибок, связанных с переполнением целых чисел: опасно не само это переполнение, а вызванное им переполнение буфера. Но подробнее об этом мы расскажем в грехе 3.
Вот как еще может возникать переполнение буфера:
#define MAX_BUF 256
void BadCode(char* input)
{
short len;
char buf[MAX_BUF];
len = strlen(input);
//
if(len < MAX_BUF)
strcpy(buf, input);
}
На первый взгляд, все хорошо, не так ли? Но на самом деле здесь ошибка на ошибке. Детали мы отложим до обсуждения переполнения целых числе в грехе 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… или вычисляете размер выделяемого из кучи буфера, то очень важно не допускать арифметических ошибок.
Ошибки при работе с форматной строкой могут дать такой же эффект, как переполнение буфера, хотя переполнением в строгом смысле не являются. Обычно такие ошибки вообще не связаны ни с какими буферами.
Вариантом переполнения буфера является запись в массив без контроля выхода за границы. Если противник сумеет прямо или косвенно подсунуть индекс массива и вы не проверите, что он принадлежит допустимому диапазону, то возможна запись по произвольному адресу в памяти. При этом не только изменяется поток выполнения программы, но могут быть затерты несмежные области памяти, а это сводит на нет все меры противодействия переполнению буфера.
Где искать ошибку
Вот на что нужно обращать внимание в первую очередь:
□ любые входные данные, будь то из сети, из файла или из командной строки;
□ передача данных из вышеупомянутых источников входных данных во внутренние структуры;
□ использование небезопасных функций работы со строками;
□ использование арифметических операций для вычисления размера буфера или числа свободных байтов в нем.
Выявление ошибки на этапе анализа кода
Обнаружить присутствие этого греха во время анализа кода может быть как совсем легко, так и очень сложно. Проще всего проанализировать все случаи употребления функций работы со строками. Надо иметь в виду, что вы можете найти много мест, где функции вызываются безопасно, но наш опыт показывает, что ошибки могут скрываться даже в правильных вызовах. Коэффициент регрессии, характерный для модификации кода с целью перехода исключительно на безопасные функции, обычно очень мал (от одной десятой до одной сотой величины, типичной для исправления ошибки), зато это позволит устранить возможность некоторых видов эксплойтов.