Рассматриваемые в главе стеки процессора Intel x86 инвертированы в том смысле, что области памяти с меньшими адресами находятся на «вершине» стека. Операция push перемещает указатель вершины стека ниже (проталкивает запись в стек), в то время как операция pop – выше (выталкивает данные из стека). Данные располагаются в области памяти, отведенной под стек, начиная со дна стека, то есть с его максимального адреса, по последовательно уменьшающимся адресам памяти. Отчасти этим объясняется переполнение буфера: при записи в буфер от младших адресов к старшим возможно затирание данных, ранее сохраненных в области памяти со старшими адресами, например подмена сохраненного в стеке содержимого расширенного регистра команд EIP (Extended Instruction Pointer). Адрес доступного верхнего элемента хранится в регистре-указателе стека ESP.
...
Ошибки и защита
Изучение языка ассемблера
Для того чтобы лучше понять устройство стека, нужно знать ассемблер. Прежде всего использование регистров для работы с данными стека. Как правило, при работе со стеком используются следующие три регистра:
• EIP (Extended Instruction Pointer) – расширенный регистр указателя инструкции. Содержимое регистра указывает на следующую исполняемую машинную команду (текущий программный код). При вызове функций содержимое регистра сохраняется в стеке для дальнейшего использования;
• ESP (Extended Stack Pointer) – расширенный регистр указателя вершины стека. Содержимое регистра указывает на вершину стека (текущее положение в стеке). Добавление данных в стек и их удаление из стека осуществляются командами push и pop или с помощью непосредственных операций над содержимым регистра указателя вершины стека;
• EBP (Extended Base Pointer) – расширенный регистр базового указателя (указателя основной точки стека). Во время работы функции содержимое регистра должно оставаться неизменным. Содержимое регистра и смещение позволяет адресовать хранимые в стеке переменные и данные. Почти всегда регистр указывает на вершину стека выполняющейся функции.
В последующих секциях главы будет рассказано о записи локальных переменных в стек, использовании стека для передачи параметров функции, и показано, каким образом злоумышленник может воспользоваться переполнением буфера, чтобы выполнить злонамеренный код.
Большинство компиляторов в начале функции вставляют служебный программный код, который иногда называют прологом (prologue) функции. Назначение пролога, помимо всего прочего, – подготовить стек для работы функции. Часто именно эта часть программного кода сохраняет старое содержимое регистра EBP и загружает в него указатель текущего положения в стеке. После этих действий регистр EBP содержит указатель на вершину стека выполняющейся функции. Зная содержимое регистра EBP и добавляя к нему смещение, получают ссылку на размещенные в стеке данные. Обычно регистр EBP адресует переменные, хранимые в стеке.
Приведенный ниже пример простой программы с несколькими локальными переменными демонстрирует сказанное. Подробные комментарии в исходном тексте программы позволят читателю лучше понять, что она делает.
Пример программы
Приведенная на рис. 8.1 написанная на языке C программа (C-программа) очень проста. Она присваивает своим переменным некоторые значения.
Рис. 8.1. Пример простой программы, иллюстрирующий работу стека
В программе создаются три локальные переменные, которые будут помещены в стек: 15-байтовый буфер символов buffer и две целые переменные intl и int2. Во время инициализации главной функции программы этим переменным присваиваются значения, а по завершении своей работы программа возвращает 1. Несмотря на простоту, программа полезна для изучения машинного кода оттранслированной функции на языке C вместе с прологом, эпилогом и стеком. Рассмотрим дизассемблерный вид приведенной на рис. 8.1 программы, которая была скомпилирована как консольное приложение Windows в режиме построения окончательной версии Release. Дизассемблирование
Дизассемблирование приведенной на рис. 8.1 программы показывает, как компилятор решил несложную задачу определения, инициализации локальных переменных и записи их значений в стек. Результаты дизассемблирования приведены на рис. 8.2.
Рис. 8.2. Результаты дизассемблирования простой программы на языке C
Из рисунка 8.2 видно, что в прологе функции _main компилятор сначала сохранил старое значение регистра EBP в стеке, а затем записал в EBP адрес вершины стека функции (текущее положение в стеке). Эти стандартные действия делаются для того, чтобы каждая функция использовала свой собственный стек. Большинство, если не все, функций выполняют подобные операции в начале, а обратные им – в конце, в заключительной части программы – эпилоге.
Читать дальше
Конец ознакомительного отрывка
Купить книгу