Переполнение буфера
Страница 2. Выполнение программ под Windows 9x на платформе Intel x86


 

Выполнение программ под Windows 9x на платформе Intel x86

Intel x86: Под Intel x86 здесь имеется в виду процессоры 386/486/Pentium и т. д. Различия между ними здесь для нас не существенны, поэтому в дальнейшем будем называть используемый процессор "Intel 386". В процессоре имеются восемь 32-разрядных регистров общего назначения (EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP), шесть 16-разрядных селекторов сегментов (CS, DS, SS, ES, FS, GS) а также 32-разрядные регистры EFLAGS и EIP. Исполняемые инструкции для процессора и данные хранятся вместе в памяти. Значение регистра EIP (Instruction Pointer) указывает адрес в памяти следующей исполняемой инструкции. Обычно инструкции из памяти считываются одна за другой, но при выполнении инструкций вроде JMP (jump - прыжок) и CALL (вызов подпроцедуры), исполнение может переходить к другому месту кода.

Адресация памяти: У Intel 386 существуют 3 модели адресации памяти - segmented, flat и real-address mode. Здесь для нас существенно знать лишь то, что программы под Windows обычно используют модель памяти flat ("плоская"). Это означает, что любое 32-х битное число может являться адресом в памяти. Таким образом программа виртуально получает в своё распоряжение 4 гигабайта адресуемой памяти. Конечно, только небольшая часть адресов соответствует реально существующей памяти. Доступ по нелегальному адресу приведёт к ошибке. Использование модели flat также означает, что программа не должна никоим образом пользоваться сегментными регистрами (селекторами). Их следует просто игнорировать. Память представляет из себя последовательность байт, числа в которой принято хранить в формате big-endian, т. е. наименее значимый байт числа сохраняется по младшему адресу (напр. 32-битное число 0x123456781 будет храниться в памяти как последовательность байт 0x78 0x56 0x34 0x12).

Имея в своём распоряжении память и регистры, можно переносить данные с помощью команды MOV. Так, например, последовательность инструкций

MOV EAX, 2
MOV a, EAX
MOV [EBX], BYTE PTR 4

сначала поместит в регистр EAX значение 2, затем перенесёт значение этого регистра в память начиная с адреса a (т.к. EAX - это 32 битный регистр, то заняты будут адреса a, a+1, a+2 и a+3) и наконец, поместит байт со значением 4 в память по адресу, хранимому в регистре EBX ([EBX] обозначает значение, хранящееся в регистре EBX).

Стек: Для работы программы часто необходим стек - структура в памяти, в которую можно помещать значения и "вынимать" их оттуда в обратном порядке. Для этого выделяется отдельная область памяти (которую и называют "стек") и используется регистр ESP. Он указывает на "вершину стека", то есть на последний адрес в стеке, куда мы что-либо помещали. Для помещения значения в стек используется инструкция PUSH, которая уменьшает значение ESP на 4 и помещает ("пихает") заданное 32-битное значение по адресу [ESP]. Инструкция POP наоборот - "достаёт" ("выталкивает") значение по адресу [ESP] и затем увеличивает ESP на 4. Таким образом стек "растёт сверху вниз".

Для работы со стеком используется и регистр EBP, но это сейчас не важно. Важно то, что при вызове процедуры (с помощью инструкции CALL) в стек помещается текущее значение регистра EIP, а по окончании работы процедуры (с помощью инструкции RET) - это значение восстанавливается и процессор продолжает работу с того места, где он остановился перед вызовом процедуры. Важно также и то, что в стеке хранятся локальные переменные функции, но к этому мы вернёмся позже.

.EXE файлы. Наконец, о том как выполняются программы под Windows. Типичная Windows-программа хранится в файле с расширением .EXE. Типичный .EXE-файл является Portable Executable (PE) - файлом. Portable Executable - это название формата файла. Помимо собственно исполнимого кода PE-файл содержит различную служебную информацию о том, как он будет загружен, таблицы импортируемых и экспортируемых функций и проч. При запуске PE-файла Windows загружает его в память почти в том виде, в котором он хранился на диске, и запускает как отдельный процесс. Каждый процесс получает в распоряжение своё собственное 32-битное адресное пространство (например, два различных процесса могут пользоваться одним и тем же адресом 0x12345, и при этом для каждого из них это будет "его собственная" память. Они не будут замечать друг друга). То, по какому адресу в этом пространстве будет загружен сам PE-файл, называется по английски Image Base и записано в одном из заголовков PE-файла. Обычно Image Base = 0x400000. Все прочие значения в служебных заголовках файла даны как смещения относительно этого адреса. Так, например Code Start (начало исполняемого кода), равное 0x1000 означает, что после загрузки файла в память, исполнение программы начнётся с адреса 0x400000 + 0x1000 = 0x401000.

DLL: Практически каждая Windows-программа пользуется функциями из динамически загружаемых библиотек (Dynamic-Link-Libraries, DLL). Важнейшими из них являются KERNEL32.DLL и USER32.DLL, которые предоставляют основные системные процедуры. Тогда как функции внутри программы вызываются просто инструкцией "CALL func" где func - адрес вызываемой функции, функцию из DLL (т. н. импортируемую функцию) таким путём вызвать нельзя, т. к. во время компиляции программы адрес её не известен. Использование DLL происходит следующим образом:
Во-первых, в заголовке PE-файла записано имя DLL, функции из которой используются в программе. При загрузке программы в память, Windows загружает в её адресное пространство и все используемые ей DLL. DLL представляет из себя такой же PE-файл, как и сама программа, но так как DLL в отличие от EXE загружается в "чужое" адресное пространство, то адрес, по которому она "хотела бы" быть загружена (её Image Base), может оказаться занят. Тогда Windows переносит её по своему усмотрению.
Во-вторых, в EXE файле записано имя каждой импортируемой функции и оставлено место, куда Windows после загрузки соответствующей DLL проставит действительный адрес функции. Это называется таблицей импортов PE-файла. Во время компиляции местонахождение таблицы импортов известно, и поэтому можно вызывать процедуры из DLL "косвенно", указывая место, где должен быть адрес вызываемой процедуры.
Например, положим что таблица адресов функций, импортируемых из KERNEL32.DLL начинается в нашем PE-файле с адреса 0xe0d8 и содержит три функции - CloseHandle, CreateFileA и ExitProcess. Положим также, что Image Base нашего файла - 0x400000. Значит адрес процедуры CloseHandle будет находиться в загруженной программе по адресу 0x400000 + 0xe0d8 = 0x40e0d8, адрес CreateFileA - прямо за ним по адресу 0x40e0d8 + 4 = 0x40e0dc, и адрес ExitProcess - по адресу 0x40e0dc + 4 = 0x40e0e0.
Теперь если мы хотим вызвать процедуру ExitProcess, мы используем инструкцию

CALL [40e0e0h]

То, что число 0x40e0e0 дано в квадратных скобках как раз и указывает на то, что вызов происходит не по адресу 0x40e0e0, а по адресу, который хранится по адресу 0x40e0e0.

Всё, теперь можно спокойно начать переполнять буфер...

 
« Предыдущая статья   Следующая статья »