Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
(gdb) В конце показан кадр стека. Внизу кадра видны четыре аргумента функции 5, а сразу над ними – адрес возврата 4. Выше располагает- ся сохраненный указатель 0xbffff808 3, представляющий собой содер- жимое EBP в предыдущем кадре стека. Остальная память отведена для локальных переменных flag и buffer. Расчет их адресов относительно EBP точно показывает, где они находятся в стеке. Память переменной flag отмечена 2, а переменной buffer – 1. Оставшееся в стеке место – просто для заполнения. По завершении выполнения весь кадр выталкивается из стека, а в EIP помещается адрес возврата, чтобы программа могла продолжить свое выполнение. Если внутри функции происходит вызов другой функ- ции, в стек проталкивается еще один кадр, и так далее. В конце рабо- ты каждой функции ее кадр стека выталкивается, чтобы можно было продолжить выполнение предыдущей функции. Такой режим рабо- ты оправдывает организацию этого сегмента памяти в виде структуры данных типа FILO. Различные сегменты памяти организованы в указанном порядке – от младших адресов памяти к старшим. Так как нумерованный список привычнее читать сверху вниз, младшие адреса памяти располагаются вверху. В некоторых книгах принят противоположный порядок, что может сбивать с толку, поэтому в данной книге меньшие адреса памя- ти всегда оказываются вверху. В большинстве отладчиков память так- же отображается в этом стиле: младшие адреса памяти находятся ввер- ху, а старшие – внизу. Поскольку размер кучи и стека устанавливается динамически, эти два сегмента растут в противоположных направлениях, навстречу друг другу. Это сокращает непроизводительный расход памяти, позволяя увеличивать стек, когда куча невелика, и наоборот (рис. 2.2). 92 0x200 Программирование сегмент text сегмент data сегмент bss сегмент heap сегмент stack Стек растет в направлении младших адресов Куча растет в направлении старших адресов Младшие адреса Старшие адреса Рис. 2.2. Расположение сегментов в памяти 0x271 Сегменты памяти в C В C и других компилируемых языках скомпилированный код помеща- ется в сегмент текста, а переменные располагаются в других сегмен- тах. Сегмент, в который попадает переменная, зависит от того, как она определена. Переменные, определенные вне каких-либо функ- ций, считаются глобальными. Кроме того, с помощью ключевого сло- ва static любую переменную можно определить как статическую. Если статическая или глобальная переменная описана с начальным значе- нием, она помещается в сегмент данных, в противном случае – в сег- мент bss. Память в сегменте кучи нужно сначала выделить с помощью функции malloc(). Обычно обращение к памяти в куче происходит че- рез указатели. Остальные переменные функции хранятся в сегменте стека. В стеке может быть много кадров, поэтому помещаемые в стек переменные могут сохранять свою уникальность в различных функци- ональных контекстах. Программа memory_segments.c поможет разо- браться с этими концепциями в C. memory_segments.c #include int global_var; int global_initialized_var = 5; void function() { // Это демонстрационная функция int stack_var; // Переменная с таким же именем есть в main(). printf(“the function’s stack_var is at address 0x%08x\n”, &stack_var); } 0x270 Сегментация памяти 93 int main() { int stack_var; // Переменная с таким же именем есть в function() static int static_initialized_var = 5; static int static_var; int *heap_var_ptr; heap_var_ptr = (int *) malloc(4); // Эти переменные находятся в сегменте данных. printf(“global_initialized_var is at address 0x%08x\n”, &global_initialized_var); printf(“static_initialized_var is at address 0x%08x\n\n”, &static_initialized_var); // Эти переменные находятся в сегменте bss. printf(“static_var is at address 0x%08x\n”, &static_var); printf(“global_var is at address 0x%08x\n\n”, &global_var); // Эта переменная находится в сегменте кучи. printf(“heap_var is at address 0x%08x\n\n”, heap_var_ptr); // Эти переменные находятся в сегменте стека printf(“stack_var is at address 0x%08x\n”, &stack_var); function(); } Работу этого кода легко понять благодаря содержательным именам пе- ременных. Глобальные и статические переменные объявлены, как опи- сано выше, кроме того, добавлены инициализированные переменные. Переменная stack_var объявлена как в main(), так и в function(), чтобы продемонстрировать действие функционального контекста. Перемен- ная heap объявлена как указатель на целое и будет указывать на адрес памяти в сегменте кучи. Функция malloc() предназначена для выделе- ния четырех байтов в куче. Поскольку выделяемая память может быть использована под любые данные, функция malloc() возвращает указа- тель на void, который нужно привести к указателю на целое. reader@hacking:/booksrc $ gcc memory_segments.c reader@hacking:/booksrc $ ./a.out global_initialized_var is at address 0x080497ec static_initialized_var is at address 0x080497f0 static_var is at address 0x080497f8 global_var is at address 0x080497fc heap_var is at address 0x0804a008 stack_var is at address 0xbffff834 the function’s stack_var is at address 0xbffff814 reader@hacking:/booksrc $ 94 0x200 Программирование Первые две инициализируемые переменные располагаются по самым младшим адресам, так как они находятся в сегменте данных. Следу- ющие две переменные, static_var и global_var, хранятся в сегменте bss, потому что они не инициализируются. Их адреса несколько больше, чем у предыдущих переменных, потому что сегмент bss расположен ниже сегмента данных. После компиляции размер обоих этих сегмен- тов не будет изменяться, поэтому потери памяти здесь незначительны, а адреса находятся не слишком далеко друг от друга. Переменная heap хранится в памяти, выделенной в сегменте кучи, рас- полагающемся сразу после сегмента bss. Напомню, что размер этого сегмента не фиксирован и впоследствии может динамически увеличи- ваться. Наконец, у последних двух переменных stack_var очень боль- шие адреса в памяти, потому что они размещаются в сегменте стека. Размер стека тоже не фиксирован, но он начинается внизу и увеличи- вается вверх по направлению к сегменту кучи. Благодаря этому оба сег- мента динамические, и память не расходуется понапрасну. Первая пе- ременная stack_var в контексте функции main() хранится в кадре сте- ка внутри сегмента стека. У второй переменной stack_var в function() свой отдельный уникальный контекст, поэтому эта переменная хранит- ся в другом кадре стека внутри сегмента стека. Когда ближе к концу программы выполняется вызов function(), создается новый кадр стека, чтобы (среди прочего) хранить stack_var для контекста function(). По- скольку с каждым новым кадром стек растет в сторону сегмента кучи, адрес второй переменной stack_var (0xbffff814) меньше, чем адрес пер- вой переменной stack_var (0xbffff834), находящейся в контексте main(). 0x272 Работа с кучей Другие сегменты памяти используются, когда переменная объявлена соответствующим образом, и не требуют особых усилий – в отличие от сегмента кучи. Как уже отмечалось, выделение памяти в куче происхо- дит с помощью функции malloc(). Она принимает единственный аргу- мент, который задает количество памяти, которое требуется выделить в сегменте кучи, и возвращает указатель void, содержащий адрес нача- ла выделенной памяти. Если по какой-либо причине malloc() не смогла выделить память, она возвращает указатель NULL со значением 0. Пар- ная функция, которая освобождает память, называется free(). В каче- стве единственного аргумента она принимает указатель и освобожда- ет память в куче, после чего ту можно снова использовать. Работу этих довольно простых функций иллюстрирует программа heap_example.c. heap_example.c #include #include #include 0x270 Сегментация памяти 95 char *char_ptr; // Указатель на символьный тип int *int_ptr; // Указатель на целый тип int mem_size; if (argc < 2) // Если нет аргументов командной строки, mem_size = 50; // используется 50 – значение по умолчанию. else mem_size = atoi(argv[1]); printf(“\t[+] allocating %d bytes of memory on the heap for char_ptr\n”, mem_size); char_ptr = (char *) malloc(mem_size); // Выделение памяти в куче if(char_ptr == NULL) { // Проверка ошибки – сбоя malloc() fprintf(stderr, “Error: could not allocate heap memory.\n”); exit(-1); } strcpy(char_ptr, “This is memory is located on the heap.”); printf(“char_ptr (%p) --> ‘%s’\n”, char_ptr, char_ptr); printf(“\t[+] allocating 12 bytes of memory on the heap for int_ptr\n”); int_ptr = (int *) malloc(12); // Еще раз выделяем память в куче if(int_ptr == NULL) { // Проверка ошибки – сбоя malloc() fails fprintf(stderr, “Error: could not allocate heap memory.\n”); exit(-1); } *int_ptr = 31337; // Поместить 31337 по адресу, содержащемуся в int_ptr. printf(“int_ptr (%p) --> %d\n”, int_ptr, *int_ptr); printf(“\t[-] freeing char_ptr’s heap memory...\n”); free(char_ptr); // Освободить память в куче printf(“\t[+] allocating another 15 bytes for char_ptr\n”); char_ptr = (char *) malloc(15); // Еще раз выделяем память в куче if(char_ptr == NULL) { // Проверка ошибки – сбоя malloc() fprintf(stderr, “Error: could not allocate heap memory.\n”); exit(-1); } strcpy(char_ptr, “new memory”); printf(“char_ptr (%p) --> ‘%s’\n”, char_ptr, char_ptr); printf(“\t[-] freeing int_ptr’s heap memory...\n”); free(int_ptr); // Освободить память в куче printf(“\t[-] freeing char_ptr’s heap memory...\n”); free(char_ptr); // Освободить еще один блок памяти в куче } 96 0x200 Программирование Размер первого выделяемого блока памяти программа берет из аргу- мента командной строки или по умолчанию принимает значение 50. Затем с помощью функций malloc() и free() производится выделение и освобождение памяти в куче. Многочисленные операторы printf() служат для контроля происходящего во время работы программы. Так как malloc() не знает, для данных какого типа предназначена выделя- емая ей память, она возвращает указатель void, который нужно приве- сти к нужному типу. После каждого обращения к malloc() выполняет- ся проверка ошибки, чтобы выяснить, успешно ли прошло выделение памяти. Если выделить память не удалось и возвращен указатель NULL, с помощью fprintf() на стандартное устройство выводится сообщение об ошибке и программа завершается. Функция fprintf() очень похожа на printf(), но в качестве первого аргумента ей передается stderr как указатель стандартного файлового потока для отображения ошибок. Об этой функции будет подробнее рассказано позже, здесь она нужна только для правильного отображения ошибок. В остальном програм- ма достаточно проста. reader@hacking:/booksrc $ gcc -o heap_example heap_example.c reader@hacking:/booksrc $ ./heap_example [+] allocating 50 bytes of memory on the heap for char_ptr char_ptr (0x804a008) --> ‘This is memory is located on the heap.’ [+] allocating 12 bytes of memory on the heap for int_ptr int_ptr (0x804a040) --> 31337 [-] freeing char_ptr’s heap memory... [+] allocating another 15 bytes for char_ptr char_ptr (0x804a050) --> ‘new memory’ [-] freeing int_ptr’s heap memory... [-] freeing char_ptr’s heap memory... reader@hacking:/booksrc $ По этому выводу видно, что каждый новый блок получает все больший адрес памяти в куче. Несмотря на то что первые 50 байт были освобож- дены, запрос дополнительных 15 байт возвращает адрес после 12 байт, выделенных для int_ptr. Такой режим работы определяется функция- ми выделения памяти в куче, и можно продолжить его изучение, задав другой размер первоначально выделяемой памяти. reader@hacking:/booksrc $ ./heap_example 100 [+] allocating 100 bytes of memory on the heap for char_ptr char_ptr (0x804a008) --> ‘This is memory is located on the heap.’ [+] allocating 12 bytes of memory on the heap for int_ptr int_ptr (0x804a070) --> 31337 [-] freeing char_ptr’s heap memory... [+] allocating another 15 bytes for char_ptr char_ptr (0x804a008) --> ‘new memory’ [-] freeing int_ptr’s heap memory... [-] freeing char_ptr’s heap memory... reader@hacking:/booksrc $ 0x270 Сегментация памяти 97 Если выделить, а потом освободить более крупный блок памяти, то по- следние 15 байт будут выделены из этой освобожденной памяти. Поэкс- периментировав с разными значениями, можно точно выяснить, в ка- ких случаях функция выделения памяти начинает повторно исполь- зовать освободившуюся память. Часто с помощью обычных команд printf() и небольших экспериментов можно многое узнать о том, как устроена ваша система. 0x273 Функция malloc() с контролем ошибок В программе heap_example.c есть несколько проверок ошибки после вы- зова malloc(). Даже если malloc() всегда срабатывает успешно, в коде на C необходимо обрабатывать все случаи, когда может произойти ошибка при выделении памяти. Но если вызовов malloc() много, код для провер- ки ошибки приходится использовать несколько раз. От этого програм- ма приобретает неприглядный вид, а внесение изменений в код провер- ки или добавление обращений к malloc() становится обременительным. Поскольку код проверки ошибок фактически одинаков для всех обраще- ний к malloc(), вместо группы одинаковых команд в разных местах весь- ма эффективно использовать функцию. Примером служит программа errorchecked_heap.c. errorchecked_heap.c #include #include #include void *errorchecked_malloc(unsigned int); // Прототип функции // errorchecked_malloc() int main(int argc, char *argv[]) { char *char_ptr; // Указатель на символьный тип int *int_ptr; // Указатель на целый тип int mem_size; if (argc < 2) // Если нет аргументов командной строки, mem_size = 50; // используется 50 – значение по умолчанию. else mem_size = atoi(argv[1]); printf(“\t[+] allocating %d bytes of memory on the heap for char_ptr\n”, mem_size); char_ptr = (char *) errorchecked_malloc(mem_size); // Выделение памяти // в куче strcpy(char_ptr, “This is memory is located on the heap.”); printf(“char_ptr (%p) --> ‘%s’\n”, char_ptr, char_ptr); printf(“\t[+] allocating 12 bytes of memory on the heap for int_ptr\n”); 98 0x200 Программирование int_ptr = (int *) errorchecked_malloc(12); // Еще раз выделяем память // в куче *int_ptr = 31337; // Поместить 31337 по адресу, содержащемуся в int_ptr. printf(“int_ptr (%p) --> %d\n”, int_ptr, *int_ptr); printf(“\t[-] freeing char_ptr’s heap memory...\n”); free(char_ptr); // Освободить память в куче printf(“\t[+] allocating another 15 bytes for char_ptr\n”); char_ptr = (char *) errorchecked_malloc(15); // Еще раз выделяем память // в куче strcpy(char_ptr, “new memory”); printf(“char_ptr (%p) --> ‘%s’\n”, char_ptr, char_ptr); printf(“\t[-] freeing int_ptr’s heap memory...\n”); free(int_ptr); // Освободить память в куче printf(“\t[-] freeing char_ptr’s heap memory...\n”); free(char_ptr); // Освободить еще один блок памяти в куче } void *errorchecked_malloc(unsigned int size) { // Функция malloc() // с контролем ошибок void *ptr; ptr = malloc(size); if(ptr == NULL) { fprintf(stderr, “Error: could not allocate heap memory.\n”); exit(-1); } return ptr; } Программа errorchecked_heap.c в принципе идентична предыдущей heap_example.c, за исключением того, что выделение памяти в куче и проверка ошибок собраны в одну функцию. Первая строка кода (void *errorchecked_malloc(unsigned int); ) представляет собой прототип функ- ции. Благодаря ему компилятор знает, что есть функция с именем errorchecked_malloc() , которая принимает один аргумент беззнакового целого типа и возвращает указатель void. Действительный код функ- ции может находиться в любом месте, у нас он расположен после функ- ции main(). Собственно функция весьма проста: она получает размер об- ласти памяти, которую нужно выделить, и пытается зарезервировать эту память с помощью malloc(). Если выделить память не удается, код проверки ошибки выводит сообщение и завершает программу, в против- ном случае возвращается указатель на выделенную в куче область па- мяти. Таким образом, эту специальную функцию errorchecked_malloc() можно использовать вместо обычной malloc(), избавившись от необхо- димости вставлять код проверки ошибки после каждого обращения 0x280 Опираясь на основы 99 к ней. Это наглядно демонстрирует пользу программирования с помо- щью функций. 0x280 Опираясь на основы После того как вы разберетесь с основными понятиями программиро- вания на C, все остальное достаточно легко. Основу мощи C составляет использование функций. Если убрать функции из любой предыдущей программы, останутся только очень простые операторы. 0x281 Доступ к файлам В C применяются два основных способа работы с файлами: через де- скрипторы файла и файловые потоки. Дескрипторы файла использу- ются группой функций низкоуровневого ввода/вывода, а файловые по- токи (filestreams) представляют собой буферизованный ввод/вывод более высокого уровня, построенный на функциях низкого уровня. Некоторые считают, что программировать с применением функций файловых потоков проще, однако дескрипторы файла обеспечивают большую непосредственность. В данной книге нас будут интересовать функции низкоуровневого ввода/вывода, использующие дескрипторы файла. Штрих-код на обороте этой книги представляет некоторое число. Каж- дой книге, которая есть в магазине, соответствует свое число, по этому числу кассир получает сведения о книге из базы данных. Аналогичным образом дескриптор файла – это число, с помощью которого можно об- ращаться к открытым файлам. Есть четыре стандартные функции, ис- пользующие дескрипторы файла: open(), close(), read() и write(). Все они в случае возникновения ошибки возвращают –1. Функция open() открывает файл для чтения и/или записи и возвращает дескриптор файла. Этот дескриптор является целым числом, соответствующим только одному открытому файлу. Дескриптор файла передается как аргумент другим функциям в качестве указателя на открытый файл. У функции close() дескриптор файла – единственный аргумент. Для функций read() и write() аргументами являются дескриптор файла, указатель на данные, которые нужно считать или записать, и количе- ство байт, которые нужно записать или считать по этому адресу. Аргу- менты функции open() – указатель на имя файла, который нужно от- крыть, и ряд флагов, описывающих метод доступа к файлу. Об этих флагах далее будет рассказано подробно, а сейчас мы рассмотрим про- стую программу simplenote.c для записи заметок, в которой использу- ются дескрипторы файла. Программа принимает сообщение, содержа- щееся в аргументе командной строки, и дописывает его в конец фай- ла /tmp/notes. В ней используется несколько функций, в том числе уже знакомая функция выделения памяти в куче с проверкой ошиб- ки. Другие функции служат для вывода сообщения о правилах вызова |