Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
#include void function() { // Пример функции с собственным контекстом int var = 5; static int static_var = 5; // Инициализация статической переменной printf(“\t[in function] var = %d\n”, var); printf(“\t[in function] static_var = %d\n”, static_var); var++; // Прибавить 1 к var. static_var++; // Прибавить 1 к static_var. } 0x260 Возвращаемся к основам 83 int main() { // Функция main с собственным контекстом int i; static int static_var = 1337; // Другая статическая переменная // в другом контексте for(i=0; i < 5; i++) { // Повторить 5 раз printf(“[in main] static_var = %d\n”, static_var); function(); // Вызвать функцию. } } Переменная static_var объявлена статической в двух местах: в контек- сте main() и в контексте function(). Поскольку статические переменные являются локальными в контексте конкретной функции, можно дать им одинаковые имена, но в действительности они будут представлять два разных адреса в памяти. function просто выводит значения этих двух переменных в своем контексте и добавляет к обеим 1. Скомпили- ровав и выполнив эту программу, мы увидим разницу между статиче- скими и не статическими переменными. reader@hacking:/booksrc $ gcc static.c reader@hacking:/booksrc $ ./a.out [in main] static_var = 1337 [in function] var = 5 [in function] static_var = 5 [in main] static_var = 1337 [in function] var = 5 [in function] static_var = 6 [in main] static_var = 1337 [in function] var = 5 [in function] static_var = 7 [in main] static_var = 1337 [in function] var = 5 [in function] static_var = 8 [in main] static_var = 1337 [in function] var = 5 [in function] static_var = 9 reader@hacking:/booksrc $ Обратите внимание: static_var сохраняет свое значение между после- довательными вызовами function(). Это происходит потому, что стати- ческие переменные сохраняют свои значения, и потому, что их ини- циализация выполняется только один раз. Кроме того, поскольку ста- тические переменные являются локальными в контексте конкрет- ной функции, static_var в контексте main() всегда сохраняет значение 1337. И снова, выведя адреса этих переменных с помощью оператора адреса, мы получим лучшее представление о происходящем. Возьмем, напри- мер, программу static2.c. 84 0x200 Программирование static2.c #include void function() { // Пример функции с собственным контекстом int var = 5; static int static_var = 5; // Инициализация статической переменной printf(“\t[in function] var @ %p = %d\n”, &var, var); printf(“\t[in function] static_var @ %p = %d\n”, &static_var, static_var); var++; // Прибавить 1 к var. static_var++; // Прибавить 1 к static_var. } int main() { // Функция main с собственным контекстом int i; static int static_var = 1337; // Другая статическая, в другом контексте for(i=0; i < 5; i++) { // loop 5 times printf(“[in main] static_var @ %p = %d\n”, &static_var, static_var); function(); // Вызвать функцию. } } Результат компиляции и выполнения static2.c: reader@hacking:/booksrc $ gcc static2.c reader@hacking:/booksrc $ ./a.out [in main] static_var @ 0x804968c = 1337 [in function] var @ 0xbffff814 = 5 [in function] static_var @ 0x8049688 = 5 [in main] static_var @ 0x804968c = 1337 [in function] var @ 0xbffff814 = 5 [in function] static_var @ 0x8049688 = 6 [in main] static_var @ 0x804968c = 1337 [in function] var @ 0xbffff814 = 5 [in function] static_var @ 0x8049688 = 7 [in main] static_var @ 0x804968c = 1337 [in function] var @ 0xbffff814 = 5 [in function] static_var @ 0x8049688 = 8 [in main] static_var @ 0x804968c = 1337 [in function] var @ 0xbffff814 = 5 [in function] static_var @ 0x8049688 = 9 reader@hacking:/booksrc $ По выведенным адресам переменных видно, что static_var в main() от- личается от переменной с тем же именем в function(), потому что у них разные адреса памяти (0x804968c и 0x8049688 соответственно). Вы, на- верное, заметили, что адреса локальных переменных очень большие, например 0xbffff814, а глобальных и статических – очень маленькие, например 0x0804968c и 0x8049688. Хорошо, что вы так наблюдательны: 0x270 Сегментация памяти 85 обнаружение таких мелких фактов и выяснение причин их появления составляет один из краеугольных камней хакинга. Читайте дальше, и вы поймете почему. 0x270 Сегментация памяти Память, занимаемая скомпилированной программой, делится на пять сегментов: текст, или код (text), данные (data), bss, куча (heap) и стек (stack). Каждый сегмент представляет собой особый раздел памяти, выделенный для специальных целей. Сегмент text иногда также называют сегментом кода. В нем распола- гаются машинные команды программы. Выполнение команд в этом сегменте происходит нелинейно из-за упоминавшихся выше управля- ющих структур верхнего уровня и функций, которые компилируют- ся в инструкции ветвления, перехода и вызова функций на языке ас- семблера. При запуске программы EIP устанавливается на первую ин- струкцию в сегменте text. Затем процессор осуществляет цикл испол- нения, в котором происходит следующее: 1. Считывается команда по адресу, находящемуся в EIP. 2. К EIP прибавляется длина этой команды в байтах. 3. Выполняется команда, прочитанная на шаге 1. 4. Происходит переход к шагу 1. Если команда выполняет переход или вызов, она заменяет EIP другим адресом. Процессору это безразлично, потому что он не ориентирован на линейное выполнение команд. Если на шаге 3 EIP изменится, про- цессор перейдет к шагу 1 и прочтет ту инструкцию, которая находится по адресу, записанному в EIP. В сегменте text запись запрещена: в нем хранится только код, но не пе- ременные. Это защищает код программы от модификации: при попыт- ке записи в этот сегмент памяти программа сообщает пользователю, что происходит нечто неладное, и завершает свою работу. Другое пре- имущество доступности этого сегмента лишь для чтения состоит в том, что несколько запущенных экземпляров одной программы могут ис- пользовать его совместно, не мешая один другому. Следует также отме- тить, что размер этого сегмента памяти постоянен, потому что в нем не происходит никаких изменений. Сегмент данных и сегмент bss предназначены для хранения глобаль- ных и статических переменных программы. В сегменте data хранятся инициализированные глобальные и статические переменные, а в bss – такие же переменные без инициализации. Эти сегменты доступны для записи, но их размер также фиксирован. Вспомним, что глобальные переменные сохраняются независимо от функционального контекста (как переменная j в предыдущих примерах). Глобальные и статические 86 0x200 Программирование переменные способны сохранять свои значения, поскольку хранятся в отдельных сегментах памяти. Сегмент кучи (heap) непосредственно доступен программисту. Он мо- жет выделять в этом сегменте блоки памяти и использовать их по свое- му усмотрению. Примечательная особенность кучи – ее непостоянный размер, способный увеличиваться или уменьшаться по мере необходи- мости. Память в куче управляется алгоритмами выделения (allocator) и освобождения (deallocator): первый резервирует участки памяти для использования, а второй освобождает их, делая возможным повторное резервирование. Размер кучи увеличивается или уменьшается в зави- симости от того, сколько памяти зарезервировано для использования. Программист, таким образом, может с помощью функций выделения памяти динамически резервировать и освобождать память. Увеличе- ние размеров кучи сопровождается ее ростом вниз, в направлении стар- ших адресов. Сегмент стека (stack) – тоже переменного размера; он служит вре- менным хранилищем локальных переменных и контекстов функций при их вызове. Именно его показывает команда обратной трассиров- ки в GDB. Когда программа вызывает некоторую функцию, та получа- ет собственный набор переданных ей переменных, а код функции рас- полагается по отдельному адресу в сегменте текста (кода). Поскольку при вызове функции изменяются контекст и EIP, в стек помещаются передаваемые переменные, адрес, к которому EIP должен вернуться по завершении работы функции, и локальные переменные этой функции. Все эти данные хранятся вместе на стеке в так называемом кадре сте- ка. Стек содержит много кадров. В информатике стеком называется часто используемая абстрактная структура данных. Для нее действует правило «первым пришел – по- следним ушел» (FILO), согласно которому первый объект, помещенный на стек, будет последним, взятым с него. Наглядная аналогия – нани- зывание бусин на нитку с узлом на конце: нельзя снять первую бусин- ку, не сняв перед тем все остальные. Помещение элемента в стек назы- вают также проталкиванием (pushing), а его извлечение – выталкива- нием (popping). В соответствии со своим названием сегмент стека в памяти является стековой структурой, хранящей кадры (фреймы) стека. Адрес верши- ны стека хранится в ESP, он постоянно изменяется по мере помещения в стек новых элементов и их извлечения. Ввиду столь высокой динами- ки стека вполне логично, что его размер не фиксирован. Однако в отли- чие от кучи, при увеличении размера стека его вершина движется в па- мяти «вверх», в направлении младших адресов. Порядок FILO, действующий в стеке, может показаться странным, од- нако он очень удобен для хранения контекста. При вызове функции в кадр стека помещается группа данных. Для обращения к локальным переменным функции, располагающимся в текущем кадре стека, слу- 0x270 Сегментация памяти 87 жит регистр EBP, который называют указателем кадра (frame pointer, FP) или указателем локальной базы (local base, LB). В каждом кадре стека содержатся переданные функции параметры, ее локальные пере- менные и два указателя, необходимых для того, чтобы вернуть все на место: сохраненный указатель кадра (saved frame pointer, SFP) и адрес возврата. С помощью SFP регистр EBP возвращается в исходное состо- яние, а с помощью адреса возврата в EIP записывается адрес коман- ды, следующей за вызовом функции. В результате восстанавливается функциональный контекст предшествующего кадра стека. Приведенный ниже код stack_example.c содержит две функции: main() и test_function(). stack_example.c void test_function(int a, int b, int c, int d) { int flag; char buffer[10]; flag = 31337; buffer[0] = ‘A’; } int main() { test_function(1, 2, 3, 4); } Сначала эта программа определяет функцию test_function с четырь- мя целочисленными аргументами: a, b, c и d. Локальные переменные функции – это 4-байтная переменная flag и 10-символьный буфер buf- fer . Память этим переменным выделяется в сегменте стека, а машин- ные команды кода функции хранятся в сегменте text. Скомпилиро- вав программу, можно изучить ее внутреннее устройство с помощью GDB. Ниже показан результат дизассемблирования машинных ко- манд для функций main() и test_function(). Функция main() начинается с 0x08048357, а функция test_function() – с 0x08048344. Первые несколько команд в каждой функции (в следующем листинге выделены полужир- ным) организуют кадр стека. В совокупности эти команды называют- ся прологом процедуры или прологом функции. Они записывают в стек указатель кадра и локальные переменные. Иногда в прологе функции выполняется также некоторое выравнивание стека. Точный вид проло- га может весьма различаться в зависимости от компилятора и его оп- ций, но общая задача его команд – выстроить кадр стека. reader@hacking:/booksrc $ gcc -g stack_example.c reader@hacking:/booksrc $ gdb -q ./a.out Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1”. (gdb) disass main Dump of assembler code for function main(): 0x08048357 0x08048358 0x0804835a 0x0804835d 88 0x200 Программирование 0x08048360 0x08048365 0x08048367 0x0804838b 0x0804838c End of assembler dump (gdb) disass test_function() Dump of assembler code for function test_function: 0x08048344 0x08048345 0x08048347 0x0804834a 0x08048356 End of assembler dump (gdb) При выполнении этой программы вызывается функция main(), которая просто вызывает функцию test_function(). При вызове функции test_function() из функции main() в стек помеща- ются различные значения, образующие начало кадра стека. При этом аргументы функции проталкиваются в стек в обратном порядке (по- скольку в нем применяется принцип FILO). Аргументами функции служат 1, 2, 3 и 4, поэтому последовательность команд помещает в стек 4, 3, 2 и наконец 1. Эти значения соответствуют переменным d, c, b и a функции. Команды, помещающие эти значения в стек, в приведенном ниже результате дизассемблирования функции main() выделены полу- жирным. (gdb) disass main Dump of assembler code for function main: 0x08048357 0x08048358 0x0804835a 0x08048367 0x0804836f 0x08048377 0x0804837f 0x08048386 0x0804838b 0x0804838c End of assembler dump (gdb) 0x270 Сегментация памяти 89 Затем при выполнении команды call ассемблера в стек помещается адрес возврата, а выполнение передается на начало функции test_func- tion() с адресом 0x08048344. Адрес возврата – это местонахождение ко- манды, следующей за текущей, адрес которой содержится в EIP, а имен- но значение, сохраненное на шаге 3 обсуждавшегося цикла исполне- ния. В данном случае должен произойти возврат на адрес 0x0804838b, где в функции main() размещается команда leave. Команда call сохраняет в стеке адрес возврата и выполняет переход по содержащемуся в EIP адресу начала функции test_function(); таким об- разом, команды пролога функции test_function() завершили создание кадра стека. Теперь в стек помещается текущее значение EBP. Оно на- зывается сохраненным указателем кадра (SFP) и позволяет позже вер- нуть EBP в исходное состояние. Затем текущее значение ESP копирует- ся в EBP, чтобы установить новый указатель кадра. Этот указатель ка- дра используется для обращения к локальным переменным функции (flag и buffer). Память для этих переменных отводится в результате вы- читания из ESP. В конечном счете кадр стека выглядит примерно так, как показано на рис. 2.1. buffer flag указатель кадра стека (SFP) адрес возврата (ret) a b c d Вершина стека Указатель кадра (EBP) Младшие адреса Старшие адреса Рис. 2.1. Кадр стека С помощью GDB можно проследить, как в стеке формируется кадр. В следующем листинге точки останова установлены в main() перед об- ращением к test_function() и в начале test_function(). GDB помещает первую точку останова перед командами, отправляющими в стек ар- гументы функции, а вторую точку останова – после пролога функции test_function() После запуска программы исполнение останавливается в точке остано- ва, где исследуется содержимое регистров ESP (указатель стека), EBP (указатель кадра) и EIP (указатель команды). 90 0x200 Программирование (gdb) list main 4 5 flag = 31337; 6 buffer[0] = ‘A’; 7 } 8 9 int main() { 10 test_function(1, 2, 3, 4); 11 } (gdb) break 10 Breakpoint 1 at 0x8048367: file stack_example.c, line 10. (gdb) break test_function Breakpoint 2 at 0x804834a: file stack_example.c, line 5. (gdb) run Starting program: /home/reader/booksrc/a.out Breakpoint 1, main () at stack_example.c:10 10 test_function(1, 2, 3, 4); (gdb) i r esp ebp eip esp 0xbffff7f0 0xbffff7f0 ebp 0xbffff808 0xbffff808 eip 0x8048367 0x8048367 (gdb) x/5i $eip 0x8048367 (gdb) Эта точка останова находится как раз перед тем местом, где форми- руется кадр стека для вызова test_function(). То есть дно этого ново- го кадра стека находится по адресу, являющемуся текущим значе- нием ESP, 0xbffff7f0. Следующая точка останова расположена сразу после пролога функции test_function(), поэтому при продолжении ра- боты будет построен кадр стека. В следующем листинге показана ана- логичная информация для второй точки останова. Обращение к ло- кальным переменным (flag и buffer) происходит относительно указа- теля кадра (EBP). (gdb) cont Continuing. Breakpoint 2, test_function (a=1, b=2, c=3, d=4) at stack_example.c:5 5 flag = 31337; (gdb) i r esp ebp eip esp 0xbffff7c0 0xbffff7c0 ebp 0xbffff7e8 0xbffff7e8 eip 0x804834a 0x804834a (gdb) disass test_function Dump of assembler code for function test_function: 0x270 Сегментация памяти 91 0x08048344 0x08048345 0x08048347 0x08048356 End of assembler dump. (gdb) print $ebp-12 $1 = (void *) 0xbffff7dc (gdb) print $ebp-40 $2 = (void *) 0xbffff7c0 (gdb) x/16xw $esp 0xbffff7c0: 1 0x00000000 0x08049548 0xbffff7d8 0x08048249 0xbffff7d0: 0xb7f9f729 0xb7fd6ff4 0xbffff808 2 0x080483b9 0xbffff7e0: 0xb7fd6ff4 0xbffff89c 3 0xbffff808 4 0x0804838b 0xbffff7f0: 5 |