Интересно заметить, что большинство средств защиты многих современных ОС успешно борется именно с таким примитивным классом атак, называя его intruder detection (обнаружение нарушителей). В ОС после набора неправильного пароля обычно приняты задержка в несколько секунд, а также ограничение максимального числа неправильно набранных паролей подряд. Эти меры не позволяют взломщику удаленно перебирать пароли. (Естественно, что сегодня, если хакер и будет заниматься перебором, то не в реальном времени.) Но, видимо, в те далекие годы не было даже таких мер.
Таблица 9.1. Примеры имен и паролей по умолчанию в различных ОС
Технология переполнения буфера
Примерно в это же время хакерами был придуман способ передачи управления чужеродному коду, не только ставший классическим, но и по сей день являющийся основным методом эксплуатации многих уязвимостей как удаленно, так и локально. Он с успехом применялся и применяется в большинстве операционных систем. Первое нашумевшее его применение было в вирусе Морриса (см. раздел «Червь»), хотя наверняка и до этого способ открывался и переоткрывался (а может быть, и использовался) несколько раз.
Итак, одной из основных проблем, стоящей перед кракером, является необходимость исполнения написанного им (то есть вредного) кода на машине, которую он атакует. Иначе говоря, он должен указать компьютеру, с какого адреса размещается этот код, то есть занести в указатель команд (обычно он называется instruction pointer – IP) нужный ему адрес. Это, безусловно, может быть сделано штатными средствами операционной системы – путем запуска соответствующей программы. Но тут у кракера возникает две проблемы:
1. У него может не быть доступа на атакуемый компьютер, то есть возможности исполнения программ.
2. Даже если доступ (login) у него есть, то привилегий, данных ему, может оказаться недостаточно для выполнения некоторых функций того вредного кода, который он написал. Обычная цель кракера – получить полный контроль над машиной, что ему, естественно, просто так никто не даст.
Для решения этих проблем приходит в голову следующее: передать некоторому привилегированному процессу такие данные, которые интерпретировались бы им как код. При этом отсутствие доступа на компьютер решается передачей удаленных данных через демоны (сценарий 1 – любой пользователь Internet имеет такую возможность). Для выбора локальных привилегированных процессов (то есть при наличии доступа) также хорошо подходят демоны, если они запущены от имени суперпользователя или SUID root-программы (сценарий 3).
Итак, задача кракера уточнилась: ему необходима привилегированная программа, которая получает какие-то входные данные от непривилегированных пользователей. И дело за малым – осталось заставить программу исполнить эти данные как код. Как следует из названия раздела, такой прием получил название buffer overflow (в переводе «переполнение буфера», хотя более точно сказать «переполнение буфера в стеке»).
Рассмотрим его. Весьма часто в процедурах программист отводит для своих нужд некоторый локальный буфер, имеющий фиксированный размер. Этот размер обычно устанавливается исходя из здравого (или не очень здравого) смысла. Например, если читается строка с экрана, то программист может ограничить размер буфера 80 символами, имя файла на NTFS не должно содержать более 255 символов – именно такой буфер может быть отведен в этом случае и т. п.
Мы предположим, что программа получает некоторые данные извне. Пусть буфер необходим программисту для обработки этих данных. Тогда мы получим примерно следующий фрагмент кода:
process_data (char *data)
{
char buf[FIXED];
...
strcpy (buf, data);
<���необходимая обработка данных в буфере>
...
return;
}
Подробно на причинах появления такого кода мы остановились, чтобы показать, что он является весьма типичным и распространенным (пусть и не очень хорошим с точки зрения стиля) для любых приложений, а вовсе не надуманным примером. Именно поэтому ошибки переполнения буфера так часто и проявляются.
Дальнейшее уже почти ясно. Локальные переменные (к которым относится и наш буфер) обычно располагаются компилятором в стеке, куда чуть раньше им же помещается адрес возврата в процедуру, из которой была вызвана process_data(). При часто используемой реализации стека, когда он «растет» вниз, оказывается, что адрес возврата в процедуру находится «дальше» (то есть имеет в стеке больший адрес), чем локальный буфер.
Читать дальше
Конец ознакомительного отрывка
Купить книгу