Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
eip 0xb7f076f4 0xb7f076f4 |
0xb7f076f7
0xb7f076fa
0xb7f076fc
0xb7f076fe
(gdb) continue
Continuing.
Breakpoint 3, main () at char_array2.c:8 8 printf(str_a);
(gdb) i r eip eip 0x80483d7 0x80483d7
(gdb) x/5i $eip
0x260 Возвращаемся к основам
55
0x80483d7
0x80483da
0x80483dd
0x80483e3
(gdb)
Адрес, находящийся в EIP в средней точке останова, отличается от других, потому что код функции strcpy() находится в загруженной би- блиотеке. В средней точке отладчик отображает EIP для кода функции strcpy()
, тогда как в двух других точках EIP указывает на код функ- ции main(). Хочу подчеркнуть, что EIP способен «переноситься» из основного кода в код strcpy() и обратно. При каждом вызове функции запись об этом сохраняется в структуре данных, называемой стеком.
С помощью стекаEIP может возвращаться из длинных цепочек вызо- вов функций. В GDB есть команда bt (от backtrace), позволяющая про- следить цепочку вызовов, помещенную в стек. Ниже показано состоя- ние стека для каждой точки останова.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/reader/booksrc/char_array2
Error in re-setting breakpoint 4:
Function “strcpy” not defined.
Breakpoint 1, main () at char_array2.c:7 7 strcpy(str_a, “Hello, world!\n”);
(gdb) bt
#0 main () at char_array2.c:7
(gdb) cont
Continuing.
Breakpoint 4, 0xb7f076f4 in strcpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0 0xb7f076f4 in strcpy () from /lib/tls/i686/cmov/libc.so.6
#1 0x080483d7 in main () at char_array2.c:7
(gdb) cont
Continuing.
Breakpoint 3, main () at char_array2.c:8 8 printf(str_a);
(gdb) bt
#0 main () at char_array2.c:8
(gdb)
В средней точке останова обратная трассировка стека показывает вы- зов strcpy(). Можно также заметить, что во время второго прогона адрес функции strcpy() немного изменился. Это результат действия защиты от эксплойтов, включаемой по умолчанию в ядро Linux начиная с вер- сии 2.6.11. Несколько позднее мы подробно рассмотрим эту защиту.
56
0x200 Программирование
0x262 Целые числа со знаком, без знака,
длинные и короткие
По умолчанию числовые значения в C имеют знак, то есть могут быть как положительными, так и отрицательными. Напротив, беззнаковые значения не могут быть отрицательными. Поскольку все это хранится в памяти, все числовые значения должны храниться в двоичном виде, и двоичный вид беззнаковых значений понятнее всего. 32-разрядное беззнаковое целое может принимать значения от 0 (все разряды – нули) до 4 294 967 295 (все разряды – единицы). 32-разрядное целое со зна- ком содержит те же 32 бита и, следовательно, может быть одной из 2 32
возможных комбинаций. Поэтому 32-разрядные целые находятся в ди- апазоне от −2 147 483 648 до 2 147 483 647. Фактически, один из раз- рядов служит флажком, сигнализирующим о том, какое это число: по- ложительное или отрицательное. Положительные значения со знаком имеют тот же вид, что и беззнаковые, но отрицательные хранятся ина- че – в так называемом дополнительном коде (two’s complement). Пред- ставление чисел вдополнительном коде удобно для работы двоичных сумматоров: если сложить отрицательную величину в дополнительном коде с положительным числом с такой же абсолютной величиной, то в результате получится 0. Чтобы получить запись в двоичном дополни- тельном коде, нужно записать число в двоичном виде, инвертировать все его разряды и прибавить к результату 1. Звучит странно, но дей- ствует и позволяет складывать отрицательные числа с положительны- ми с помощью простых двоичных сумматоров.
Эту арифметику легко проверить с помощью pcalc – простого кальку- лятора для программистов, который показывает результаты в десятич- ном, шестнадцатеричном и двоичном форматах. Для простоты в приме- ре использованы 8-битные числа.
reader@hacking:/booksrc $ pcalc 0y01001001 73 0x49 0y1001001
reader@hacking:/booksrc $ pcalc 0y10110110 + 1 183 0xb7 0y10110111
reader@hacking:/booksrc $ pcalc 0y01001001 + 0y10110111 256 0x100 0y100000000
reader@hacking:/booksrc $
Сначала мы видим, что двоичное значение 01001001 – это положитель- ное число 73. Затем инвертируем все разряды и прибавляем 1, чтобы получить дополнительный код для –73 (10110111). Сложив эти величи- ны, получим 0 в исходных 8 разрядах. Программа pcalc показывает ре- зультат 256, потому что не знает, что мы оперируем всего лишь с 8-бит- ными числами. В двоичном сумматоре этот бит переноса будет отбро- шен, поскольку выходит за границы памяти, отведенной переменной.
Этот пример может немного прояснить, как действует механизм допол- нительного кода.
0x260 Возвращаемся к основам
57
В C можно определить переменную как беззнаковую с помощью ключе- вого слова unsigned, помещаемого в объявлении перед именем перемен- ной. Беззнаковое целое объявляется как unsigned int.
Кроме того, размер памяти, выделяемой числовой переменной, мож- но увеличить или уменьшить с помощью ключевых слов long и short.
Факти ческий размер зависит от архитектуры, для которой компили- руется код. В языке C есть оператор sizeof(), умеющий определять раз- мер некоторых типов данных. Он действует как функция, принима- ющая на входе тип данных и возвращающая размер в целевой архитек- туре переменной, объявляемой с таким типом. Следующая програм- ма datatype_sizes.c исследует размер разных типов данных с помощью функции sizeof().
datatype_sizes.c
#include
int main() {
printf(“The ‘int’ data type is\t\t %d bytes\n”, sizeof(int));
printf(“The ‘unsigned int’ data type is\t %d bytes\n”, sizeof(unsigned int));
printf(“The ‘short int’ data type is\t %d bytes\n”, sizeof(short int));
printf(“The ‘long int’ data type is\t %d bytes\n”, sizeof(long int));
printf(“The ‘long long int’ data type is %d bytes\n”, sizeof(long long int));
printf(“The ‘float’ data type is\t %d bytes\n”, sizeof(float));
printf(“The ‘char’ data type is\t\t %d bytes\n”, sizeof(char));
}
В этом коде функция printf() используется несколько по-иному, чем прежде. Здесь применен так называемый спецификатор формата, что- бы показать значения, возвращаемые вызовами sizeof(). Подробнее о спецификаторах формата мы поговорим ниже, а здесь рассмотрим ре- зультат работы программы.
reader@hacking:/booksrc $ gcc datatype_sizes.c reader@hacking:/booksrc $ ./a.out
The ‘int’ data type is 4 bytes
The ‘unsigned int’ data type is 4 bytes
The ‘short int’ data type is 2 bytes
The ‘long int’ data type is 4 bytes
The ‘long long int’ data type is 8 bytes
The ‘float’ data type is 4 bytes
The ‘char’ data type is 1 bytes reader@hacking:/booksrc $
Как уже отмечалось, в архитектуре x86 знаковые и беззнаковые це- лые числа занимают 4 байта. Число с плавающей точкой тоже зани- мает 4 байта, а символьному типу нужен всего 1 байт. Ключевые сло-
58
0x200 Программирование ва long и short можно
1
использовать и с переменными с плавающей точ- кой, чтобы увеличить или сократить их размер.
0x263 Указатели
Регистр EIP представляет собой указатель следующей команды, по- скольку содержит ее адрес. Идея указателей используется и в языке
C. Поскольку физическую память реально перемещать нельзя, при- ходится копировать содержащуюся в ней информацию. Копирование больших блоков памяти, чтобы с ними могли работать разные функ- ции и в разных местах, требует очень больших накладных расходов.
Это дорого обходится и с точки зрения памяти, потому что перед копи- рованием нужно сохранить или выделить память для нового экземпля- ра данных. Решение данной проблемы дают указатели. Гораздо проще не копировать большой участок памяти, а передать адрес его начала.
Указатели объявляются и используются в C так же, как переменные прочих типов. В архитектуре x86 применяется 32-разрядная адреса- ция, поэтому указатели имеют размер 32 бита (4 байта). Указатели объявляются с помощью звездочки (*), помещаемой перед именем пе- ременной. В результате указатель оказывается не переменной неко- торого типа, а средством указания на данные этого типа. Программа
pointer.c демонстрирует применение указателей с типом данных char, имеющих размер 1 байт.
pointer.c
#include
#include
int main() {
char str_a[20]; // Массив из 20 символов char *pointer; // Указатель массива символов char *pointer2; // Другой указатель strcpy(str_a, “Hello, world!\n”);
pointer = str_a; // Установим первый указатель на начало массива.
printf(pointer);
pointer2 = pointer + 2; // Установим второй указатель на 2 байта дальше.
printf(pointer2); // Вывод.
strcpy(pointer2, “y you guys!\n”); // Копируем в то же место.
printf(pointer); // Еще один вывод.
}
Как сказано в комментариях, первый указатель устанавливается на начало массива символов. Такая ссылка на массив фактически пред-
1
Автор ошибается. Можно использовать ключевое слово double (8 байт) или long double
(16 байт, иногда 80 бит). – Прим. перев.
0x260 Возвращаемся к основам
59
ставляет собой указатель. Выше этот буфер передавался функциям printf()
и strcpy() именно как указатель. Второму указателю присва- ивается адрес из первого плюс 2, а затем выполняется печать некото- рых данных.
reader@hacking:/booksrc $ gcc -o pointer pointer.c reader@hacking:/booksrc $ ./pointer
Hello, world!
llo, world!
Hey you guys!
reader@hacking:/booksrc $
Посмотрим эту программу с помощью GDB. Программа компилирует- ся, и задается точка останова в 10-й строке исходного кода. В резуль- тате останов произойдет после того, как строка «Hello, world!\n” будет скопирована в буфер str_a и переменная-указатель будет установлена на его начало.
reader@hacking:/booksrc $ gcc -g -o pointer pointer.c reader@hacking:/booksrc $ gdb -q ./pointer
Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1”.
(gdb) list
1 #include
2 #include
3 4 int main() {
5 char str_a[20]; // Массив символов из 20 элементов
6 char *pointer; // Указатель массива символов
7 char *pointer2; // Другой указатель
8 9 strcpy(str_a, “Hello, world!\n”);
10 pointer = str_a; // Установим первый указатель на начало массива.
11 printf(pointer);
12 13 pointer2 = pointer + 2; // Установим второй указатель на 2 байта дальше.
14 printf(pointer2); // Вывод.
15 strcpy(pointer2, “y you guys!\n”); // Копируем в то же место.
16 printf(pointer); // Еще один вывод.
17 }
(gdb) break 11
Breakpoint 1 at 0x80483dd: file pointer.c, line 11.
(gdb) run
Starting program: /home/reader/booksrc/pointer
Breakpoint 1, main () at pointer.c:11 11 printf(pointer);
(gdb) x/xw pointer
0xbffff7e0: 0x6c6c6548
(gdb) x/s pointer
0xbffff7e0: “Hello, world!\n”
(gdb)
60
0x200 Программирование
Если рассмотреть указатель как строку, то заметно, что это наша стро- ка, находящаяся по адресу 0xbffff7e0. Не забываем, что в переменной- указателе хранится не сама строка, а только ее адрес 0xbffff7e0.
Чтобы увидеть фактические данные, хранящиеся в переменной- указателе, нужно воспользоваться оператором адреса. Оператор адреса является унарным, то есть применяется к единственному аргументу. Он представляет собой всего лишь амперсанд (&), помещаемый перед име- нем переменной. При его использовании возвращается адрес этой пере- менной, а не ее значение. Этот оператор есть и в GDB, и в языке C.
(gdb) x/xw &pointer
0xbffff7dc: 0xbffff7e0
(gdb) print &pointer
$1 = (char **) 0xbffff7dc
(gdb) print pointer
$2 = 0xbffff7e0 “Hello, world!\n”
(gdb)
Мы видим, что переменная pointer находится в памяти по адресу
0xbffff7dc и хранит адрес 0xbffff7e0.
Оператор адреса часто используется вместе с указателями, поскольку указатели хранят адреса памяти. Программа addressof.c демонстриру- ет, как адрес целочисленной переменной помещается в указатель. Со- ответствующая строка кода выделена полужирным.
addressof.c
#include
int main() {
int int_var = 5;
int *int_ptr;
int_ptr = &int_var; // записать адрес int_var в int_ptr
}
Сама программа ничего не выводит, но можно сообразить, что в ней происходит, даже не запуская отладчик.
reader@hacking:/booksrc $ gcc -g addressof.c reader@hacking:/booksrc $ gdb -q ./a.out
Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1”.
(gdb) list
1 #include
2 3 int main() {
4 int int_var = 5;
5 int *int_ptr;
6 7 int_ptr = &int_var; // Записать адрес переменной int_var в int_ptr
8 }
0x260 Возвращаемся к основам
61
(gdb) break 8
Breakpoint 1 at 0x8048361: file addressof.c, line 8.
(gdb) run
Starting program: /home/reader/booksrc/a.out
Breakpoint 1, main () at addressof.c:8 8 }
(gdb) print int_var
$1 = 5
(gdb) print &int_var
$2 = (int *) 0xbffff804
(gdb) print int_ptr
$3 = (int *) 0xbffff804
(gdb) print &int_ptr
$4 = (int **) 0xbffff800
(gdb)
Как обычно, устанавливается точка останова и программа выполняет- ся в отладчике. К этому моменту выполнена большая часть програм- мы. Первая команда print выводит значение переменной int_var, а вто- рая – ее адрес, полученный с помощью оператора адреса. Очередные две команды print показывают, что в int_ptr содержится адрес int_var, а за- одно и адрес int_ptr.
Для работы с указателями есть еще один унарный оператор, называ- емый оператором разыменования (dereference). Он возвращает дан- ные, находящиеся по адресу, содержащемуся в указателе, а не сам этот адрес. Он выглядит как звездочка перед именем переменной, подобно объявлению указателя. Оператор разыменования тоже есть и в GDB, и в C. В GDB с его помощью можно получить целое число, на которое указывает int_ptr.
(gdb) print *int_ptr
$5 = 5
Несколько дополнив код листинга addressof2.c, продемонстрируем все эти понятия. В добавленных функциях printf() используются пара- метры формата, о которых я расскажу в следующем разделе. Здесь же ограничимся рассмотрением результата работы программы.
addressof2.c
#include
int main() {
int int_var = 5;
int *int_ptr;
int_ptr = &int_var; // Записать адрес int_var в int_ptr.
printf(“int_ptr = 0x%08x\n”, int_ptr);
printf(“&int_ptr = 0x%08x\n”, &int_ptr);
printf(“*int_ptr = 0x%08x\n\n”, *int_ptr);
printf(“int_var is located at 0x%08x and contains %d\n”, &int_var, int_var);
62
0x200 Программирование printf(“int_ptr is located at 0x%08x, contains 0x%08x, and points to %d\n\n”, &int_ptr, int_ptr, *int_ptr);
}
В результате компиляции и запуска addressof2.c получим следующий вывод:
reader@hacking:/booksrc $ gcc addressof2.c reader@hacking:/booksrc $ ./a.out int_ptr = 0xbffff834
&int_ptr = 0xbffff830
*int_ptr = 0x00000005
int_var is located at 0xbffff834 and contains 5
int_ptr is located at 0xbffff830, contains 0xbffff834, and points to 5
reader@hacking:/booksrc $
Применяя унарные операторы к указателям, можно представлять себе, что оператор адреса перемещает нас назад, а оператор разымено- вания – вперед по направлению указателя.
0x264 Форматные строки
Функция printf() позволяет не только выводить фиксированные стро- ки. Применяя в ней форматные строки, можно печатать переменные в различных форматах. Форматная строка – это строка символов со специальными управляющими последовательностями, или escape-
последовательностями, указывающими функции на необходимость разместить на месте этих управляющих последовательностей перемен- ные в заданном формате.
Формально в предыдущих программах, вызывавших функцию printf()
, строка «Hello, world!\n” являлась форматной строкой, одна- ко в ней не было специальных управляющих последовательностей.
Escape-последовательности называют также параметрами формати-
рования, и для каждого такого параметра, встретившегося в формат- ной строке, функция должна принять дополнительный аргумент. Каж- дый параметр форматирования начинается с символа процента (%) и со- держит односимвольное сокращение, весьма похожее на символы фор- матирования, применяемые в команде examine отладчика GDB.
Параметр_Тип_вывода_%dДесятичный%uБеззнаковый_десятичный%xШестнадцатеричный_0x260_Возвращаемся_к_основам_63'>Параметр
Тип вывода
%d
Десятичный
%u
Беззнаковый десятичный
%x
Шестнадцатеричный
0x260 Возвращаемся к основам
63
Все указанные параметры форматирования получают данные в виде значений, а не указателей. Однако есть параметры форматирования, которые предполагают применение указателей, например:
Параметр
Тип вывода
%s
Строка
%n
Количество выведенных байтов
Параметр форматирования %s предполагает передачу адреса памяти; он выводит данные начиная с этого адреса, пока не встретится нулевой байт. Параметр форматирования %n является уникальным, поскольку он записывает данные. Он также предполагает передачу адреса памя- ти, по которому записывает количество выведенных к данному момен- ту байтов.
Сейчас нас интересуют только параметры форматирования, применя- емые для вывода данных. Программа fmt_strings.c содержит примеры использования различных параметров форматирования.
fmt_strings.c
#include
int main() {
char string[10];
int A = -73;
unsigned int B = 31337;
strcpy(string, “sample”);
// Пример вывода с разными форматными строками printf(“[A] Dec: %d, Hex: %x, Unsigned: %u\n”, A, A, A);
printf(“[B] Dec: %d, Hex: %x, Unsigned: %u\n”, B, B, B);
printf(“[field width on B] 3: ‘%3u’, 10: ‘%10u’, ‘%08u’\n”, B, B, B);
printf(“[string] %s Address %08x\n”, string, string);
// Пример унарного оператора адреса (разыменования) и форматной строки %x printf(“variable A is at address: %08x\n”, &A);
}
В этом коде для каждого параметра в форматной строке функции printf()
передается в качестве аргумента дополнительная переменная.
В последнем вызове printf() участвует аргумент &A, который передает адрес переменной A. Ниже приведен результат компиляции и выполне- ния программы.
reader@hacking:/booksrc $ gcc -o fmt_strings fmt_strings.c reader@hacking:/booksrc $ ./fmt_strings
[A] Dec: -73, Hex: ffffffb7, Unsigned: 4294967223
[B] Dec: 31337, Hex: 7a69, Unsigned: 31337