Текст книги "19 смертных грехов, угрожающих безопасности программ"
Автор книги: Майкл Ховард
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 5 (всего у книги 26 страниц) [доступный отрывок для чтения: 9 страниц]
Ну уж сравнение на равенство–то должно работать, правда? Увы, если вы имеете дело с комбинацией целых со знаком и без знака, то таких гарантий никто не дает, по крайней мере в случае, когда знаковый тип шире беззнакового. Та же проблема, с которой мы столкнулись при рассмотрении деления и вычисления остатка, возникает и здесь и приводит к тем же последствиям.
Операции сравнения могут преподнести и другую неожиданность – когда максимальный размер сравнивается с числом со знаком. Противник может найти способ сделать это число отрицательным, а тогда оно заведомо будет меньше верхнего предела. Либо пользуйтесь числами без знака (это рекомендуемый способ), либо делайте две проверки: сначала проверяйте, что число больше или равно нулю, а потом – что оно меньше верхнего предела.
Поразрядные операцииПоразрядные операции AND, OR и XOR (исключающее или) вроде бы должны работать, но и тут расширение со знаком путает все карты. Рассмотрим пример:
int flags = 0x7f;
char LowByte = 0x80;
if ((char)flags ^ LowByte == 0xff)
return ItWorked;
Вам кажется, что результатом операции должно быть 0xff, именно с этим значением вы и сравниваете, но настырный компилятор решает все сделать по–своему и приводит оба операнда к типу int. Вспомните, мы же говорили, что даже для поразрядных операций выполняется приведение к int, если операнды имеют более узкий тип. Поэтому flags расширяется до 0x0000007f, и тут ничего плохого нет, зато LowByte расширяется до 0xffffff80, в результате операции мы получаем 0xffffffff!
Греховность С#С# во многом похож на С++, что составляет его преимущество в случае, если вы знакомы с C/C++. Но это же и недостаток, так как для С# характерны многие из проблем, присущих С++. Один любопытный аспект С# заключается в том, что безопасность относительно типов проверяется гораздо строже, чем в C/C++. Например, следующий код не будет компилироваться:
byte a, b;
a = 255;
b = 1;
byte c = (b + a);
error CS0029: Cannot implicitly convert type 'int' to 'byte'
(ошибка CS0029: Не могу неявно преобразовать тип 'int' в 'byte')
Если вы понимаете, о чем говорит это сообщение, то подумайте о возможных последствиях такого способа исправления ошибки:
byte с = (byte) (Ь + а) ;
Безопаснее воспользоваться классом Convert:
byte d = Convert.ToByte(a + b);
Поняв, что пытается сказать компилятор, вы хотя бы задумаетесь, есть ли в вашем коде реальная проблема. К сожалению, возможности компилятора ограничены. Если бы в предыдущем примере вы избавились от ошибки, объявив a, b и с как целые со знаком, то появилась бы возможность переполнения, а компилятор ничего не сказал бы.
Еще одна приятная особенность С# состоит в том, что он по мере необходимости пользуется 64–разрядными целыми числами. Например, следующий код дал бы неверный результат на С, но правильно работает на С#:
int i = -1;
uint j = 0xffffffff; // наибольшее положительное 32-разрядное целое
if(i == j)
Console.WriteLine("Отлично!");
Причина в том, что С# приведет операнды к типу long (64–разрядное целое со знаком), который позволяет точно сохранить оба числа. Если вы решите пойти дальше и проделать то же самое с числами типа long и ulong (в С# оба занимают 64 разряда), то компилятор сообщит, что необходимо явно преобразовать их к одному типу. По мнению авторов, стандарт C/C++ следует уточнить: если компилятор поддерживает операции над 64–разрядными значениями, то он должен в этом отношении вести себя так же, как С#.
Ключевые слова checked и uncheckedВ языке С# есть ключевые слова checked и unchecked. Можно объявить checked–блок:
byte a = 1;
byte b = 255;
checked
{
byte c = (byte)(a + b);
byte d = Convert.ToByte(a + b);
Console.Write("{0} {1}n", b+1, c);
}
В данном примере приведение а + b от int к byte возбуждает исключение. В следующей строке, где вызывается Convert.ToByte, исключение возникло бы и без ключевого слова checked, но его наличие приводит к возбуждению исключения еще и при вычислении аргументов метода Console.Write(). Поскольку иногда переполнение целого допускается намеренно, то имеется также ключевое слово unchecked, отключающее контроль на переполнение.
Слова checked и unchecked можно также использовать для включения или отключения контроля в одном выражении:
checked (с = (byte) (Ь + а));
И наконец, включить контроль можно с помощью флага /checked компилятора. Если этот флаг присутствует, то нужно явно помечать словом unchecked участки кода или отдельные предложения, в которых переполнение допустимо.
Греховность Visual Basic и Visual Basic .NETVisual Basic регулярно претерпевает кардинальные модификации, а переход от Visual Basic 6.0 к Visual Basic .NET стал самым значительным шагом со времен введения объектной ориентированности в Visual Basic 3.0. Одно из самых фундаментальных изменений связано с целочисленными типами (см. табл. 3.1).
Вообще говоря, и Visual Basic 6.0, и Visual Basic .NET не подвержены угрозе исполнения произвольного кода из–за переполнения целых чисел. В Visual Basic 6.0 генерируется ошибка, если при выполнении какого–либо оператора или функции преобразования, например CInt(), возникает переполнение. В Visual Basic .NET в этом случае возбуждается исключение типа System.OverflowException. Как показано в табл. 3.1, программа на Visual Basic .NET имеет доступ ко всем целочисленным типам, определенным в каркасе .NET Framework.
Таблица 3.1. Целочисленные типы, поддерживаемые Visual Basic 6.0 и Visual Basic .NET
Хотя операции в самом языке Visual Basic, может быть, и неуязвимы для переполнения целого, но потенциальная проблема состоит в том, что вызовы Win32 API обычно принимают в качестве параметров 32–разрядные целые без знака (DWORD). Если ваша программа передает системному вызову 32–разрядное целое со знаком, то в ответ может получить отрицательное число. Аналогично вполне допустимо выполнить такую операцию, как 2 – 8046, над числами со знаком, но что, если указать в качестве одного из операндов такое число без знака, чтобы возникло переполнение? Если системный вызов возвращает некоторое значение, а потом вы выполняете манипуляции над этим значением и величиной, прямо или косвенно (после тех или иных вычислений) полученной от пользователя, и напоследок обращаетесь к другим системным вызовам, то можете оказаться в угрожаемой ситуации. Переходы от знаковых чисел к беззнаковым и обратно чреваты опасностью. Даже если переполнение целого и не приведет к исполнению произвольного кода, необработанные исключения станут причиной отказа от обслуживания. Неработающее приложение не приносит доходов заказчику.
Греховность JavaВ отличие от Visual Basic и С#, в язык Java не встроена защита от переполнений. Вот цитата из спецификации языка «Java Language Specification*, размещенной по адресу http://java.sun.com/docs/books/jls/second_edition/html/typeValues.doc.html#9151:
При выполнении встроенных операций над целыми типами переполнение или потеря значимости не индицируются. Единственные арифметические операторы, которые могут возбудить исключение (§11), – это оператор целочисленного деления / (§15.17.2) и оператор вычисления остатка % (§15.17.3). Они возбуждают исключение ArithmeticException, если правый операнд равен нулю.
В отличие от Visual Basic, Java поддерживает лишь подмножество всего диапазона целочисленных типов. Хотя 64–разрядные целые поддерживаются, но единственным беззнаковым типом является char, и он представляется в виде 16–разрядного значения без знака.
Поскольку в Java есть только знаковые типы, проверка переполнения становится непростым делом, и по сравнению с C/C++ удается лишь избежать затруднений, связанных со смешанными операциями над знаковыми и беззнаковыми величинами.
Греховность PerlПо крайней мере, два автора этой книги являются горячими сторонниками Perl. Но, несмотря на это, следует признать, что работа с целыми числами реализована в Perl странно. Внутри они представляются в виде чисел с плавающей точкой двойной точности, но тестирование позволяет выявить некоторые любопытные вещи. Рассмотрим следующий код:
$h = 4294967295;
$i = 0xffffffff;
$k = 0x80000000;
print "$h = 4294967295 – $h + 1 = ".($h + 1)."n";
print "$i = 0xffffffff – $i + 1 = ".($i + 1)."n";
printf("nИспользуется printf со спецификатором %%dn");
printf("\$i = %d, $i + 1 = %dnn", $i, $i + 1);
printf("nТестируется граничный случай деленияn");
printf("0x80000000/-1 = %dn", $k/-1);
print "0x80000000/-1 = ".($k/-1)."n";
В результате печатается следующее:
[e:projects19_sinsperl foo.pl
4294967295 = 4294967295 – 4294967295 + 1 = 4294967296
4294967295 = 0xffffffff – 4294967295 + 1 = 4294967296
Используется printf со спецификатором %d
$i = -1, $i + 1 = -1
Тестируется граничный случай деления
0x80000000/-1 = -2147483648
0x80000000/-1 = -2147483648
На первый взгляд, результат выглядит странно, особенно когда используется printf с форматной строкой (в отличие от обычной функции print). Первым делом в глаза бросается то, что мы можем присвоить переменной максимально возможное значение без знака, но после прибавления к нему 1 она либо увеличивается на единицу, либо – если печатать с помощью %d – вообще не изменяется. Загвоздка в том, что на самом деле вы оперируете числами с плавающей точкой, а спецификатор %d заставляет Perl преобразовать double в int. В действительности никакого переполнения нет, но при печати результатов создается впечатление, будто оно произошло.
В силу особенностей работы с числами в Perl мы рекомендуем быть очень осторожными при написании на этом языке приложений, в которых много математических операций. Если вы не разбираетесь досконально в арифметике с плавающей точкой, то можете столкнуться с весьма поучительными сюрпризами. Другие языки высокого уровня, например Visual Basic, тоже иногда производят преобразования в числа с плавающей точкой. Следующий код иллюстрирует, что в таком случае происходит:
print (5/4)."n";
1.25
Для большинства обычных приложений Perl ведет себя предсказуемо и работает великолепно. Но не забывайте, что вы имеете дело не с целыми числами, а с числами с плавающей точкой, – а это «две большие разницы».
Где искать ошибку
Любое приложение, в котором производятся арифметические операции, подвержено этому греху, особенно когда некоторые входные данные поступают от пользователя и их правильность не проверяется. Особое внимание обращайте на вычисление индексов массивов и размеров выделяемых буферов в программах на C/C++.
Выявление ошибки на этапе анализа кода
При написании программ на языках C/C++ надо обращать самое пристальное внимание на возможность переполнения целого числа. Теперь, когда многие разработчики осознали важность проверок размеров при прямых манипуляциях с памятью, атаки направлены нате арифметические операции, с помощью которых эти проверки выполняются. Следующими на очереди стоят С# и Java. Прямые манипуляции с памятью в этих языках запрещены, но тем не менее можно допустить почти все ошибки, которые характерны для С и С++.
Ко всем языкам относится следующее замечание: проверяйте входные данные прежде, чем каким–либо образом их использовать! В Web–серверах Microsoft IIS 4.0 и 5.0 очень серьезная ошибка имела место из–за того, что программист сначала прибавил к переменной 1, а затем проверил, не оказался ли размер слишком большим. При тех типах, что он использовал, 64К–1 + 1 оказалось равно нулю! На соответствующее извещение есть ссылка в разделе «Другие ресурсы».
C/C++Первым делом найдите все места, где выделяется память. Самое опасное – это выделение блока, размер которого вычисляется. Убедитесь, что при этом невозможно переполнение целого. Далее обратите внимание на функции, которые получают входные данные. Автор как–то встретил примерно такой код:
THING* AllocThings(int a, int b, int c, int d)
{
int bufsize;
THING* ptr;
bufsize = IntegerOverflowsRUs(a, b, c, d);
ptr = (THING*)malloc(bufsize);
return ptr;
}
Ошибка скрывалась в функции, вычисляющей размер буфера, к тому же найти ее мешали загадочные, ничего не говорящие читателю имена переменных (и литералов, которые представлены типом signed int). Если у вас есть время на доскональный анализ, проследите порядок вызова всех ваших функций вплоть до обращений к низкоуровневым библиотечным функциям или системным вызовам. И напоследок выясните, откуда поступают данные. Можете ли вы утверждать, что аргументы функций не подвергались манипуляциям? Кто контролирует аргументы: вы или потенциальный противник?
По мнению автора языка Perl, величайшим достоинством программиста является лень! Так давайте пойдем простым путем – заставим потрудиться компилятор. Включите уровень диагностики /W4 (для Visual С++) или–Wall либо–Wsign–compare (для gcc) – и вы увидите, как много в вашей программе мест, где возможны проблемы с целыми числами. Обращайте внимание на все предупреждения, касающиеся целых чисел, особенно на те, в которых говорится о сравнении знаковых и беззнаковых величин, а также об усечении.
В 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 .NETVisual Basic ухитрился превратить проблему переполнения целого в проблему отказа от обслуживания – примерно такую же ситуацию мы имеем при использовании ключевого слова checked в С#. Подозрение должны вызывать те места, где программист применяет механизм перехвата для игнорирования ошибок при работе с целыми. Убедитесь, что ошибки обрабатываются правильно. Следующее предложение в программе на Visual Basic (не Visual Basic .NET) свидетельствует о лености программиста, не пожелавшего обрабатывать ошибки, возникающие во время работы программы. Нехорошо это.
PerlOn Error Continue
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–код, что мало не покажется!
Правообладателям!
Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.Читателям!
Оплатили, но не знаете что делать дальше?