Крис Касперский - ТЕХНИКА СЕТЕВЫХ АТАК
– Смещение от кадра стека Содержимое ячеек
– 0 A
– 1 buf[0]
– 2 buf[1]
– 3 buf[2]
– 4 buf[3]
– 5 buf[4]
– 6 B
– 7 Адрес возврата
– 8… Стек функции, вызвавшей myfunt
Попытка записи в ячейку buff[6] приведет к искажению адреса возврата, и после завершения работы функции myfunct() произойдет передача управления на совершенно незапланированный разработчиком участок кода и, скорее всего, дело кончится повисанием. Все было бы иначе, если бы компилятор располагал локальные переменные ниже ячейки, хранящей адрес возврата, но, эта область стека уже занята, - она принадлежит функции, вызвавшей myfunct. Так уж устроен стек, - он растет снизу вверх, но не наоборот.
Пример, приведенный ниже, служит наглядной иллюстрацией ошибки программиста, известной под названием «срыва стека» (на диске, прилагаемом к книге, он расположен в файле “/SRC/buff.demo.c.”)
· #include «stdio.h»
· #include «string.h»
·
· root()
· {
· printf("Hello, Root!n");
·}
·
· auth()
· {
· char user[10];
· char pass[10];
· printf("Login:"); gets( amp;user[0]);
· printf("Passw:"); gets( amp;pass[0]);
· if (!strcmp( amp;pass[0],"guest"))
· return 1;
· return 0;
·}
·
· main()
· {
· printf("Buffer Overflows Demon");
· if (auth())
· printf("Password okn");
· else
· printf("Invalid passwordn");
·}
На первый взгляд, программа как будто бы должна работать нормально. Но функция gets(), читающая строку с клавиатуры, не имеет никаких представлений о размере выделенного под нее буфера, и принимает данные до тех пор, пока не встретит символ возврата каретки. Если пользователь введет в качестве своего имени строку, превышающую десять символов [307], ее «хвост» затрет адрес возврата функции и дальнейшее выполнение программы окажется невозможным.
Например, если запустить этот пример под управлением Windows 2000, и в качестве имени пользователя ввести строку “1234567890qwerty” операционная система выдаст следующее сообщение, предлагая либо завершить работу приложения, либо запустить отладчик (если он установлен) для выяснения причин сбоя: «Исключение unknown software exception (0xc000001) в приложении по адресу 0x0012ffc0».
Рисунок 72 Реакция системы на переполнение буфера
Допустим, в программе присутствует некая функция (условно названная “root”), которая выполняет действия, необходимые злоумышленнику. Может ли он специально подобранной строкой изменить адрес возврата таким образом, чтобы вместо сообщения о неправильно набранном пароле, управление передавалось на эту функцию?
Для ответа на такой вопрос необходимо знать по какому адресу расположена интересующая злоумышленника функция, и какой именно байт из введенной строки затирает адрес возврата. Выяснить это можно с помощью дизассемблирования кода программы.
Дизассемблирование - процесс сложный и требующий от исследователя хороших знаний ассемблера, архитектуры операционной системы и техники компиляции кода. Без этого разобраться с алгоритмом работы программы практически невозможно. К сожалению, практически не существует литературы, посвященной дизассемблированию, поэтому, в большинстве случаев приходится осваивать эту тему самостоятельно [308].
Все, сказанное ниже, рассчитано на читателя средней квалификации, как минимум знающего назначение наиболее употребляемых команд микропроцессора Intel 80x86. В качестве дизассемблера выбрана IDA PRO четвертой версии [309], однако, можно воспользоваться и другими инструментами, такими как SOURCER, W32Dasm или на худой конец DumpBin, который поставляется с любым Windows-компилятором.
Результат дизассемблирования buff.demo.exe показан ниже (на диске, прилагаемом к книге, он расположен в файле “/LOG/buff.demo.lst”). Исследователь должен изучить «устройство» функции Auth, (как ее найти во много килобайтовом листинге - тема отдельного разговора). Для облегчения понимания, листинг снабжен подробными комментариями.
·.text:00401000; Segment type: Pure code
·.text:00401000 _text segment para public 'CODE' use32
·.text:00401000 assume cs:_text
·.text:00401000;org 401000h
·.text:00401000 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
·.text:00401000 Root proc near
·.text:00401000; Функции root расположена по адресу 0x401000
·.text:00401000 push ebp
·.text:00401000 ;… назначение процедуры root значение не имеет
·.text:00401000 ;… для ее вызова достаточно знать по какому адресу она расположена в памяти
·.text:00401000;… а расположена она по адресу 0x401000
·.text:00401001 mov ebp, esp
·.text:00401003 push offset aHelloRoot; "Hello, Root!n"
·.text:00401008 call _printf
·.text:0040100D add esp, 4
·.text:00401010 pop ebp
·.text:00401011 retn
·.text:00401011 Root endp
·.text:00401012
·.text:00401012; --------------- S U B R O U T I N E ---------------------------------------
·.text:00401012
·.text:00401012; Attributes: bp-based frame
·.text:00401012
·.text:00401012 auth proc near; CODE XREF: main+10p
·.text:00401012
·.text:00401012 var_18 = byte ptr -18h
·.text:00401012 var_C = byte ptr -0Ch
·.text:00401012; Так IDA обозначает локальные переменные, а цифры указывают относительное
·.text:00401012; расположение от конца кадра стека.
·.text:00401012; В Момент вызова функции указатель стека указывает на адрес возврата
·.text:00401012 push ebp
·.text:00401012; В стек заносится регистр ebp, значение указателя стека уменьшается на 4
·.text:00401013 mov ebp, esp
·.text:00401013; Открывается кадр стека:
·.text:00401013; В регистр ebp заносится значение регистра указателя стека esp.
·.text:00401013; Регистр ebp будет использоваться для адресации локальных переменных относительно конца кадра стека
·.text:00401015 sub esp, 18h
·.text:00401015; Резервируется 0x18 (24 в десятичной нотации) байт под локальные переменные
·.text:00401015; Но размер двух буферов равен 10+10=20 байтам! Откуда взялись четрые лишние байта?
·.text:00401015 ; Для ускорения доступа к данным компилятор размещает начала каждого из буферов по адресам, кратным
·.text:00401015; четырем байтам, так называемое выравнивание.
·.text:00401015; Таким образом на данный момент стек выглядит так:
·.text:00401015;
·.text:00401015; Относительный адрес Содержимое ячейки
·.text:00401015; - 0x18 буфер var_18[0]
·.text:00401015; - 0x17 буфер var_18[1]
·.text:00401015; - 0x16 буфер var_18[2]
·.text:00401015; - 0x15 буфер var_18[3]
·.text:00401015; - 0x14 буфер var_18[4]
·.text:00401015; - 0x13 буфер var_18[5]
·.text:00401015; - 0x12 буфер var_18[6]
·.text:00401015; - 0x11 буфер var_18[7]
·.text:00401015; - 0x10 буфер var_18[8]
·.text:00401015; - 0x0F буфер var_18[9]
·.text:00401015; - 0x0E дырка для выравнивания
·.text:00401015; - 0x0D дырка для выравнивания
·.text:00401015; - 0x0С буфер var_С[0] 01
·.text:00401015; - 0x0B буфер var_С[1] 02
·.text:00401015; - 0x0A буфер var_С[2] 03
·.text:00401015; - 0x09 буфер var_С[3] 04
·.text:00401015; - 0x08 буфер var_С[4] 05
·.text:00401015; - 0x07 буфер var_С[5] 06
·.text:00401015; - 0x06 буфер var_С[6] 07
·.text:00401015; - 0x05 буфер var_С[7] 08
·.text:00401015; - 0x04 буфер var_С[8] 09
·.text:00401015; - 0x03 буфер var_С[9] 10
·.text:00401015; - 0x02 дырка для выравнивания 11
·.text:00401015; - 0x01 дырка для выравнивания 12
·.text:00401015; 0x00 значение регистра ebp[0] 13
·.text:00401015; + 0x01 значение регистра ebp[1] 14
·.text:00401015; + 0x02 значение регистра ebp[2] 15
·.text:00401015; + 0x03 значение регистра ebp[3] 16
·.text:00401015; + 0x04 значение регистра eip[0] (адрес возврата) 17
·.text:00401015; + 0x05 значение регистра eip[1] (адрес возврата) 18
·.text:00401015; + 0x06 значение регистра eip[2] (адрес возврата) 19
·.text:00401015; + 0x07 значение регистра eip[3] (адрес возврата) 20
·.text:00401015; Таким образом, байты с 17 до 20 (не считая нуля завершающего строку) из буфера var_c затирают
·.text:00401015: адрес возврата сохраненный в стеке. Следовательно, строка из шестнадцати символов, включая
·.text:00401015; завершающий ноль вызовет модификацию младшего байта адреса возврата.
·.text:00401015; Остается отождествить буфер var_c - что он собой представляет имя пользователя или пароль?
·.text:00401018 push offset aLogin; "Login:"
·.text:00401018; В стек заносится смещение строки “Login”, значение указателя стека уменьшается на 4
·.text:00401018; Это первый (и единственный) аргумент функции printf
·.text:0040101D call _printf
·.text:0040101D; Вывод на экран приглашения “Login:”
·.text:00401022 add esp, 4
·.text:00401022; Значение указателя стека увеличивается на четыре, чтобы избавится от занесенного в стек смещения
·.text:00401025; строки “Login”. Си-функции не очищают стек после своего завершения
·.text:00401025 lea eax, [ebp+var_C]
·.text:00401025; В регистр eax заносится смещение буфера var_c, для последующей передачи его функции gets, читающей
·.text:00401025; строку с клавиатуры.
·.text:00401025; Следовательно, буфер var_c содержит имя пользователя
·.text:00401028 push eax
·.text:00401028; Значение eax заносится в стек
·.text:00401029 call _gets
·.text:00401029; Вызов функции _gets
·.text:0040102E add esp, 4
·.text:0040102E; Удаление двойного слова из стека (для очистки аргумента функции gets)
·.text:00401031 push offset aPassw; "Passw:"
·.text:00401031; Занесение в стек строки «Passw»
·.text:00401036 call _printf
·.text:00401036; Вывод строки “Passw” на экран с помощью функции printf