Дэвид Лебланк - 19 смертных грехов, угрожающих безопасности программ
В Visual С++ самыми важными с этой точки зрения являются предупреждения С4018, С4389иС4244.
В gcc ищите предупреждения «warning: comparison between signed and unsigned integer expressions».
Относитесь с подозрением к директивам #pragma отключающим предупреждения, например:
...#pragma warning(disable : 4244)
Во вторую очередь следует искать места, где вы пытаетесь защититься от переполнения буфера (в стеке или в куче) путем проверки выхода за границы. Убедитесь, что все арифметические вычисления корректны. В следующем примере показано, как может возникнуть ошибка:
...int ConcatBuffers(char *buf1, char *buf2,
size_t len1, size_t len2) {
char buf[0xFF];
if(len1 + len2) > 0xFF) return -1;
memcpy(buf, buf1, len1);
memcpy(buf + len1, buf2, len2);
// сделать что-то с buf
return 0;
}
Здесь проверяется, что суммарный размер двух входных буферов не превышает размера выходного буфера. Но если lenl равно 0x103, а 1еп2 равно 0xfffffffc, то сумма переполняет 32–разрядный регистр процессора и оказывается равной 255 (0xff), так что проверка успешно проходит. В результате memcpy попытается записать примерно 4 Гб в буфер размером всего 255 байтов!
Не вздумайте подавлять эти надоедливые предупреждения путем приведения типов. Теперь вы знаете, насколько это рискованно и как внимательно нужно все проверять. Найдите все приведения и убедитесь, что они безопасны. О приведениях и преобразованиях в языках С и С++ см. раздел «Операции приведения» выше.
Вот еще один пример:
...int read(char* buf, size_t count) {
// Сделать что-то с памятью
}
while (true) {
BYTE buf[1024];
int skip = count – cbBytesRead;
if (skip > sizeof(buf))
skip = sizeof(buf);
if (read(buf, skip))
cbBytesRead += skip;
else
break;
...В этом фрагменте значение skip сравнивается с 1024, и если оно меньше, то в буфер buf копируется skip байтов. Проблема в том, что если skip оказывается отрицательным (скажем, -2), то оно будет заведомо меньше 1024, так что функция read попытается скопировать–2 байта, а это значение, будучи представлено как целое без знака (тип size_t), равно без малого 4 Гб. Итак, read() копирует 4 Гб в буфер размером 1 Кб. Печально!
Еще один источник неприятностей – это оператор new в языке С++. Он сопряжен с неявным умножением:
...Foo *p = new Foo(N);
Если N контролируется противником, то возможно переполнение внутри operator new при вычислении выражения N * sizeof(Foo);
С#
Хотя сам язык С# и не допускает прямых обращений к памяти, но иногда программа обращается к системным вызовам, помещенным в блок, помеченный ключевым словом unsafe (при этом ее еще надо компилировать с флагом /unsafe). Любые вычисления, результат которых передается системному вызову, нужно контролировать. Для этого полезно применить ключевое слово checked или – еще лучше – соответствующий флаг компилятора. Включите его и следите, не произойдет ли исключения. Напротив, ключевое слово unchecked используйте изредка, предварительно обдумав все последствия.
Java
В языке Java прямые обращения к памяти также запрещены, поэтому он не так опасен, как C/C++. Но проявлять беспечность все же не следует: как и в C/C++, в Java нет никакой защиты от переполнения целых, и легко можно допустить логические ошибки. О том, какие существуют программные решения, см. в разделе «Искупление греха».
Visual Basic и Visual Basic .NET
Visual Basic ухитрился превратить проблему переполнения целого в проблему отказа от обслуживания – примерно такую же ситуацию мы имеем при использовании ключевого слова checked в С#. Подозрение должны вызывать те места, где программист применяет механизм перехвата для игнорирования ошибок при работе с целыми. Убедитесь, что ошибки обрабатываются правильно. Следующее предложение в программе на Visual Basic (не Visual Basic .NET) свидетельствует о лености программиста, не пожелавшего обрабатывать ошибки, возникающие во время работы программы. Нехорошо это.
...On Error Continue
Perl
Perl – замечательный язык, но математика чисел с плавающей точкой может преподнести неожиданности. По большей части Perl делает то, чего от него ожидают, но будьте осторожны: это очень многогранный язык. В особенности это относится к случаям, когда вы обращаетесь к модулям, являющимся тонкой оберткой вокруг системных вызовов.
Тестирование
Если на вход подаются строки символов, попробуйте задать размеры так, чтобы вызвать ошибку. Часто это происходит, если длина строки составляет 64К или 64К – 1 байтов. Также ошибки возможны для длин, равных 127, 128, 255 и 32К плюс–минус единица. Если вам удалось подобрать тест так, что прибавление единицы к числу вызвало изменение знака или обращение в нуль, значит, вы не зря потрудились.
Если программа допускает прямой ввод числовых данных, например в структурированный документ, попробуйте очень большие числа и обращайте особое внимание на граничные случаи.
Примеры из реальной жизни
Поиск по запросу «integer overflow» в базе данных уязвимостей на сайте Security Focus дает больше 50 документов, а в базе CVE – 65 документов. Вот лишь несколько из них.
Ошибка в интерпретаторе Windows Script позволяет выполнить произвольный код
Цитата из бюллетеня CVE (CAN–2003–0010):
Переполнение целого в функции JsArrayFimctionHeapSort, используемой в Windows Script Engine для JScript (JScript.dll), в различных операционных системах Windows позволяет противнику выполнить произвольный код путем подготовки Web–страницы или электронного письма в формате HTML, в котором используется большой индекс массива. При этом возникает переполнение размещенного в куче буфера и открывается возможность для атаки.
Интересная особенность этой ошибки в том, что переполнение возникает в языке сценариев, не допускающем прямого обращения к памяти. Выпущенный по этому поводу бюллетень Microsoft можно найти на странице www.microsoft.com/ technet/security/bulletin/MS03–008.mspx.
Переполнение целого в конструкторе объекта SOAPParameter
Еще одна ошибка в языке сценариев (документ CAN–2004–0722 в базе CVE) описана на сайте Red Hat Linux (www.redhat.com) следующими словами:Zen Parse сообщил о некорректном контроле входных данных в конструкторе объекта SOAPParameter, что приводит к переполнению целого с последующей порчей кучи. Можно написать вредоносную программу на языке JavaScript, которая воспользуется этой ошибкой для выполнения произвольного кода.
В том же отчете читаем далее:
Во время аудита исходных текстов Крис Эванс (Chris Evans) обнаружил переполнение буфера и переполнение целого в библиотеке libpng, используемой в браузере Mozilla. Противник может создать специальный PNG–файл, при просмотре которого в этом браузере либо произойдет аварийный останов, либо будет выполнен произвольный код.
Переполнение кучи в HTR–документе, передаваемом поблочно, может скомпрометировать Web–сервер
Вскоре после того, как об этой ошибке стало известно (в июне 2002 года), последовали многочисленные атаки на уязвимые серверы IIS. Подробности можно найти на странице www.microsoft.com/technet/security/bulletin/MS02–028.mspx, но суть проблемы в том, что обработчик HTR–документов принимал от пользователя данные длиной 64К – 1, прибавлял 1 (нам ведь нужно место для завершающего нуля), после чего запрашивал буфер длиной 0 байтов. Неизвестно, то ли Билл Гейтс сказал, что 64К хватит любому, то ли это интернетовская легенда, но любой хакер сможет уместить в 64К такой shell–код, что мало не покажется!Искупление греха
По–настоящему избавиться от ошибок переполнения целого можно, только если вы хорошо понимаете суть проблемы. Но все же опишем несколько шагов, которые помогут не допустить такой ошибки. Прежде всего пользуйтесь всюду, где возможно, числами без знака. В стандарте C/C++ описан тип size_t для представления размеров, и разумные программисты им пользуются. Контролировать беззнаковые целые гораздо проще, чем знаковые. Ну нет же смысла применять число со знаком для задания размера выделяемой памяти!
Избегайте «хитроумного» кода – контроль целых должен быть прост и понятен. Вот пример чересчур заумного кода для контроля переполнения при сложении:
...int a, b, c;
c = a + b;
if(a ^ b ^ c < 0)
return BAD_INPUT;
В этом фрагменте масса проблем. Многим из нас потребуется несколько минут на то, чтобы понять, что же автор хотел сделать. А кроме того, код дает ложные срабатывания – как позитивные, так и негативные, то есть работает не всегда. Вот другой пример проверки, дающей правильный результат не во всех случаях:
...int a, b, c;
c = a * b;
if(c < 0)
return BAD_INPUT;
Даже если на входе допустимы только положительные числа, этот код все равно пропускает некоторые переполнения. Возьмем, к примеру, выражение (2 А 30 + + 1) * 8, то есть 2 А 33 + 8. После отбрасывания битов, вышедших за пределы 32 разрядов, получается 8. Это число положительно, а ошибка тем не менее есть. Безопаснее решить эту задачу, сохранив результат умножения 32–разрядных чисел в 64–разрядном, а затем проверить, равен ли хотя бы один из старших битов единице. Это и будет свидетельством переполнения.