Атака на Internet - Илья Медведовский
Шрифт:
Интервал:
Закладка:
Нынешние кракеры, наверное, кусают себе локти, что родились не 10 лет назад, – ведь тогда хакером мог прослыть тот, кто умел методично перебирать адреса компьютеров и вводить в качестве имени и пароля что-нибудь типа guest/guest [10]. Видимо, большинство хостов (в том числе и военных – в те времена возможность проникновения в секретные хосты еще не являлась мифом) вскрывались именно так. Были известны стандартные входные имена, присутствующие в операционной системе при ее установке на компьютер (см. табл. 9.1). Особо продвинутые кракеры, скорее всего, догадывались вводить в качестве паролей наиболее распространенные имена, жаргонные словечки и т. п.
Интересно заметить, что большинство средств защиты многих современных ОС успешно борется именно с таким примитивным классом атак, называя его 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(). При часто используемой реализации стека, когда он «растет» вниз, оказывается, что адрес возврата в процедуру находится «дальше» (то есть имеет в стеке больший адрес), чем локальный буфер.
Возьмем, например, программу-дрозофилу:main(int argc, char *argv[] )
{
process_data(argv[1]);
}
#define FIXED 16
process_data (char *data)
{
char buf[FIXED];
strcpy (buf, data);
return;
}Для нее стек после вызова process_data() будет выглядеть примерно так, как это показано на рис. 9.1.
Рис. 9.1. Состояние стека после вызова уязвимой функцииТеперь уже не надо быть суперхакером, чтобы заметить, что адрес возврата находится не только в одном сегменте с локальными переменными, но и имеет больший адрес. Тогда, передав в качестве данных строку, имеющую заведомо больший размер, чем у отведенного под ее обработку буфера, мы сможем затереть все, что лежит в памяти выше, чем этот буфер, так как функция strcpy() будет копировать данные до тех пор, пока не встретит нуль-символ . В нашем примере достаточно передать как входной параметр строку длиной более 15 байт для выхода за границу буфера плюс еще несколько байт для изменения собственно адреса возврата.
Не случайно в приведенных выше рассуждениях ни разу не встретилось упоминания о конкретной операционной системе. Действительно, технология переполнения локального буфера весьма универсальна и будет работать практически в любой ОС (об ограничениях чуть ниже), поэтому читатель может скомпилировать программу-дрозофилу в его любимой ОС и посмотреть на результат, подав на вход, скажем, строку из 30 единиц (этого должно быть достаточно для любой ОС и любого компилятора). UNIX-системы при этом выведут что-то типа «Segmentation fault, core dumped». Информация от Windows NT (рис. 9.2) для хакера более наглядна – по ней сразу понятно, что произошло именно переполнение буфера с возможностью подмены адреса возврата, так как адрес, на котором «споткнулась» программа, был не чем иным, как 0x31313131. Это соответствует шестнадцатеричному коду для строки из четырех единиц. Если ввести строку, состоящую из неодинаковых символов, например 01234567890abcdefghijklmnopqst, то по выведенному адресу станет ясно, в каком месте строки должен стоять будущий адрес возврата.
Рис. 9.2. Информация о сбое программы в Windows NTИтак, цель – передача управления – хакером достигнута. Теперь дело за малым. Нужно выполнить следующие шаги:
1. Найти подходящую программу, которая не только содержит процедуру, похожую на process_data(), но и выполняется с большими привилегиями. Если хакеру доступны исходные тексты, то особое внимание надо обратить на программы, содержащие функции strcat(), strcpy(), sprintf(), vsprintf(), gets(), scanf() и т. п. Если исходных текстов нет, то остается ручной (или автоматизированный) поиск уязвимых программ, то есть подача на вход длинных строк и оценка результатов.
2. Определить для найденной программы, какой размер буфера надо использовать, где в буфере должен располагаться адрес возврата и т. п.
3. Написать код, на который осуществится переход. Для ОС UNIX стандартный вариант – вызов оболочки следующим образом:char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);Для Windows NT это сделать сложнее.
4. Каким-то образом внедрить свой код в систему (хороший вариант – расположить его все в той же строчке). При этом злоумышленнику надо проверить, чтобы вызываемая функция при обработке этой строки не испортила данный код. Другая проблема – если process_data() использует strcpy() или любые другие стандартные функции работы со строками, то код должен быть написан так, чтобы он не содержал нулей, потому что в противном случае его копирование остановится на первом нуле. Заметьте, что код вызова оболочки уже содержит, по крайней мере, три нуля: один в конце "/bin/sh" и два NULL. Возможен вариант, когда не обойтись без нулей (например, сам адрес возврата должен их содержать), тогда можно, например, зашифровать код так, чтобы нули исчезли, а затем в начале кода использовать его расшифровщик.