Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
#include int main() { int i; char char_array[5] = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’}; int int_array[5] = {1, 2, 3, 4, 5}; unsigned int hacky_nonpointer; hacky_nonpointer = (unsigned int) char_array; for(i=0; i < 5; i++) { // Обход массива char. printf(“[hacky_nonpointer] points to %p, which contains the char ‘%c’\n”, hacky_nonpointer, *((char *) hacky_nonpointer)); hacky_nonpointer = hacky_nonpointer + sizeof(char); } 0x260 Возвращаемся к основам 73 hacky_nonpointer = (unsigned int) int_array; for(i=0; i < 5; i++) { // Обход массива int. printf(“[hacky_nonpointer] points to %p, which contains the integer %d\n”, hacky_nonpointer, *((int *) hacky_nonpointer)); hacky_nonpointer = hacky_nonpointer + sizeof(int); } } Это уже почти хакинг, но поскольку при присваивании и разыменова- нии целочисленное значение приводится к нужному типу, конечный результат остается тем же самым. Обратите внимание: вместо многократного приведения типа беззнако- вого целого для выполнения арифметических действий над указателя- ми используется функция sizeof() и обычная арифметика, что прино- сит тот же самый результат. reader@hacking:/booksrc $ gcc pointer_types5.c reader@hacking:/booksrc $ ./a.out [hacky_nonpointer] points to 0xbffff810, which contains the char ‘a’ [hacky_nonpointer] points to 0xbffff811, which contains the char ‘b’ [hacky_nonpointer] points to 0xbffff812, which contains the char ‘c’ [hacky_nonpointer] points to 0xbffff813, which contains the char ‘d’ [hacky_nonpointer] points to 0xbffff814, which contains the char ‘e’ [hacky_nonpointer] points to 0xbffff7f0, which contains the integer 1 [hacky_nonpointer] points to 0xbffff7f4, which contains the integer 2 [hacky_nonpointer] points to 0xbffff7f8, which contains the integer 3 [hacky_nonpointer] points to 0xbffff7fc, which contains the integer 4 [hacky_nonpointer] points to 0xbffff800, which contains the integer 5 reader@hacking:/booksrc $ Работая с переменными в C, нужно помнить, что их тип интересен толь- ко компилятору. После того как программа скомпилирована, перемен- ные представляют собой всего лишь адреса памяти. Это означает, что переменные одного типа можно заставить вести себя как переменные другого типа, заставив компилятор выполнить соответствующее при- ведение типа. 0x266 Аргументы командной строки Программы, работающие в текстовом режиме, часто получают вход- ные данные в виде аргументов командной строки. В отличие от ввода с помощью scanf(), аргументы командной строки не требуют взаимо- действия с пользователем после запуска программы. Часто такой метод ввода оказывается эффективнее и удобнее. В C доступ к аргументам командной строки можно получить через функцию main(), указав для нее два дополнительных аргумента: целое число и указатель на массив строк. Целое число задает количество ар- 74 0x200 Программирование гументов командной строки, а массив строк хранит все эти аргументы. Это иллюстрирует программа commandline.c. commandline.c #include int main(int arg_count, char *arg_list[]) { int i; printf(“There were %d arguments provided:\n”, arg_count); for(i=0; i < arg_count; i++) printf(“argument #%d\t-\t%s\n”, i, arg_list[i]); } reader@hacking:/booksrc $ gcc -o commandline commandline.c reader@hacking:/booksrc $ ./commandline There were 1 arguments provided: argument #0 - ./commandline reader@hacking:/booksrc $ ./commandline this is a test There were 5 arguments provided: argument #0 - ./commandline argument #1 - this argument #2 - is argument #3 - a argument #4 - test reader@hacking:/booksrc $ Нулевой аргумент массива всегда содержит имя исполняемой програм- мы, а остальные аргументы из массива (часто называемого вектором аргументов) представляют собой строки с аргументами, переданными программе. Иногда программе нужно использовать аргумент командной строки как число, а не как строку. При этом аргументы все равно передаются как строки, но есть стандартные функции преобразования. В отличие от простого приведения типа, эти функции могут действительно преоб- разовать массив символов, которым записано число, в настоящее число. Чаще всего используется функция atoi() (от ASCII to integer), преобра- зующая ASCII-код в целое. Она принимает в качестве аргумента указа- тель на строку, а возвращает целое число, представленное этой строкой. Ее работу можно изучить на примере программы convert.c. convert.c #include void usage(char *program_name) { printf(“Usage: %s exit(1); } int main(int argc, char *argv[]) { 0x260 Возвращаемся к основам 75 int i, count; if(argc < 3) // Если аргументов меньше 3, вывести сообщение usage(argv[0]); // о том, как вызывать программу, и завершить работу. count = atoi(argv[2]); // Преобразовать 2-й аргумент в целое число. printf(“Repeating %d times..\n”, count); for(i=0; i < count; i++) printf(“%3d - %s\n”, i, argv[1]); // Вывести 1-й аргумент. } Ниже приведен результат компиляции и выполнения программы convert.c. reader@hacking:/booksrc $ gcc convert.c reader@hacking:/booksrc $ ./a.out Usage: ./a.out reader@hacking:/booksrc $ ./a.out ‘Hello, world!’ 3 Repeating 3 times.. 0 - Hello, world! 1 - Hello, world! 2 - Hello, world! reader@hacking:/booksrc $ Прежде чем этот код будет работать со строками, оператор if проверя- ет наличие не менее трех аргументов. Если программа попытается об- ратиться к несуществующему или запрещенному для доступа адресу памяти, она аварийно завершится. В программах на C необходимо про- верять возникновение таких ситуаций и обрабатывать их. Если заком- ментировать оператор if, в котором проверяется ошибка, можно иссле- довать, что происходит при нарушении правильного доступа к памяти. Это показано в программе convert2.c. convert2.c #include void usage(char *program_name) { printf(“Usage: %s exit(1); } int main(int argc, char *argv[]) { int i, count; // if(argc < 3) // Если аргументов меньше 3, вывести сообщение // usage(argv[0]); // о том, как вызывать программу, // и завершить работу. count = atoi(argv[2]); // Преобразовать 2-й аргумент в целое число. printf(“Repeating %d times..\n”, count); 76 0x200 Программирование for(i=0; i < count; i++) printf(“%3d - %s\n”, i, argv[1]); // Вывести 1-й аргумент. } Ниже приведен результат компиляции и выполнения convert2.c. reader@hacking:/booksrc $ gcc convert2.c reader@hacking:/booksrc $ ./a.out test Segmentation fault (core dumped) reader@hacking:/booksrc $ Если не передать программе достаточное количество аргументов, она все равно попытается обратиться к элементам массива аргументов, даже если их нет. В результате программа аварийно завершится из-за ошибки сегментирования. Память разбита на сегменты (о которых будет говориться ниже), и неко- торые адреса памяти выходят за границы сегментов, доступ к которым разрешен программе. При попытке обращения по адресу, выходящему за границы, программа аварийно завершается с сообщением об ошиб- ке сегментации (segmentation fault). Это событие можно дополнитель- но исследовать с помощью GDB. reader@hacking:/booksrc $ gcc -g convert2.c reader@hacking:/booksrc $ gdb -q ./a.out Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1”. (gdb) run test Starting program: /home/reader/booksrc/a.out test Program received signal SIGSEGV, Segmentation fault. 0xb7ec819b in ?? () from /lib/tls/i686/cmov/libc.so.6 (gdb) where #0 0xb7ec819b in ?? () from /lib/tls/i686/cmov/libc.so.6 #1 0xb800183c in ?? () #2 0x00000000 in ?? () (gdb) break main Breakpoint 1 at 0x8048419: file convert2.c, line 14. (gdb) run test The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/reader/booksrc/a.out test Breakpoint 1, main (argc=2, argv=0xbffff894) at convert2.c:14 14 count = atoi(argv[2]); // Преобразовать 2-й аргумент // в целое число. (gdb) cont Continuing. Program received signal SIGSEGV, Segmentation fault. 0xb7ec819b in ?? () from /lib/tls/i686/cmov/libc.so.6 (gdb) x/3xw 0xbffff894 0xbffff894: 0xbffff9b3 0xbffff9ce 0x00000000 0x260 Возвращаемся к основам 77 (gdb) x/s 0xbffff9b3 0xbffff9b3: «/home/reader/booksrc/a.out» (gdb) x/s 0xbffff9ce 0xbffff9ce: “test” (gdb) x/s 0x00000000 0x0: (gdb) quit The program is running. Exit anyway? (y or n) y reader@hacking:/booksrc $ Программа запускается в GDB с одним аргументом командной строки test , что приводит к ее аварийному завершению. Команда where ино- гда отображает полезную обратную трассировку стека, однако в дан- ном случае авария слишком повредила стек. Мы поставили точку оста- нова на main и заново запустили программу, чтобы узнать значение вектора аргументов (выделено полужирным). Поскольку вектор аргу- ментов является указателем на список строк, фактически он является указателем на список указателей. Проверив с помощью команды x/3xw первые три адреса памяти, хранящиеся по адресу вектора аргументов, мы видим, что они сами являются указателями на строки. Первый из них – это нулевой аргумент, второй – аргумент test, а третий – ноль, который выходит за границы разрешенной памяти. При попытке обра- титься по этому адресу программа аварийно завершается с сообщением об ошибке сегментации. 0x267 Область видимости переменных Еще одно интересное понятие, касающееся памяти в C, это область ви- димости, или контекст – а именно контекст переменных внутри функ- ций. У каждой функции есть собственный набор локальных перемен- ных, независимых от всего остального. Фактически, если одна функ- ция вызывается многократно, то каждый вызов происходит в отдель- ном контексте. Быстро убедиться в этом можно на примере функции printf() с форматными строками в программе scope.c. scope.c #include void func3() { int i = 11; printf(“\t\t\t[in func3] i = %d\n”, i); } void func2() { int i = 7; printf(“\t\t[in func2] i = %d\n”, i); func3(); printf(“\t\t[back in func2] i = %d\n”, i); } 78 0x200 Программирование void func1() { int i = 5; printf(“\t[in func1] i = %d\n”, i); func2(); printf(“\t[back in func1] i = %d\n”, i); } int main() { int i = 3; printf(“[in main] i = %d\n”, i); func1(); printf(“[back in main] i = %d\n”, i); } Работа этой простой программы демонстрирует выполнение вложен- ных вызовов функций. reader@hacking:/booksrc $ gcc scope.c reader@hacking:/booksrc $ ./a.out [in main] i = 3 [in func1] i = 5 [in func2] i = 7 [in func3] i = 11 [back in func2] i = 7 [back in func1] i = 5 [back in main] i = 3 reader@hacking:/booksrc $ В каждой функции переменной i присваивается новое значение и вы- водится на экран. Обратите внимание: внутри функции main() значе- ние переменной i равно 3 даже после вызова func1(), в которой значе- ние i равно 5. Аналогично в функции func1() переменная i сохраня- ет значение 5 даже после вызова func2(), где она получает значение 7, и так далее. Правильным будет считать, что в каждом вызове функции действует своя версия переменной i. Переменные могут также иметь глобальную область видимости, что означает их действие во всех функциях. Глобальными становятся пе- ременные, объявленные в начале кода вне тела какой-либо функции. В программе scope2.c переменная j объявлена как глобальная и полу- чает значение 42. Чтение и запись этой переменной можно осущест- влять в любой функции, и эти изменения отражаются на всех функ- циях. scope2.c #include int j = 42; // j объявлена как глобальная переменная void func3() { int i = 11, j = 999; // Здесь j - локальная переменная функции func3(). 0x260 Возвращаемся к основам 79 printf(“\t\t\t[in func3] i = %d, j = %d\n”, i, j); } void func2() { int i = 7; printf(“\t\t[in func2] i = %d, j = %d\n”, i, j); printf(“\t\t[in func2] setting j = 1337\n”); j = 1337; // Запись в j func3(); printf(“\t\t[back in func2] i = %d, j = %d\n”, i, j); } void func1() { int i = 5; printf(“\t[in func1] i = %d, j = %d\n”, i, j); func2(); printf(“\t[back in func1] i = %d, j = %d\n”, i, j); } int main() { int i = 3; printf(“[in main] i = %d, j = %d\n”, i, j); func1(); printf(“[back in main] i = %d, j = %d\n”, i, j); } Результат компиляции и выполнения scope2.c: reader@hacking:/booksrc $ gcc scope2.c reader@hacking:/booksrc $ ./a.out [in main] i = 3, j = 42 [in func1] i = 5, j = 42 [in func2] i = 7, j = 42 [in func2] setting j = 1337 [in func3] i = 11, j = 999 [back in func2] i = 7, j = 1337 [back in func1] i = 5, j = 1337 [back in main] i = 3, j = 1337 reader@hacking:/booksrc $ Мы видим, что глобальной переменной j в функции func2() присваива- ется значение, которое используется во всех функциях, кроме func3(), где есть собственная переменная с именем j. В подобном случае ком- пилятор предпочитает использовать локальную переменную. Если пе- ременные имеют одинаковые имена, может возникнуть путаница, но нужно помнить, что все это связано с памятью. Глобальная перемен- ная j хранится в памяти, доступ к которой есть у каждой функции. Ло- кальные переменные отдельной функции хранятся в особом участке памяти, несмотря на совпадение их имен с именами глобальных пере- менных. Если вывести адреса этих переменных, можно лучше понять, 80 0x200 Программирование что происходит. Например, в коде программы scope3.c адреса перемен- ных выводятся с помощью унарного оператора адреса. scope3.c #include int j = 42; // j – глобальная переменная. void func3() { int i = 11, j = 999; // Здесь j – локальная переменная func3() printf(“\t\t\t[in func3] i @ 0x%08x = %d\n”, &i, i); printf(“\t\t\t[in func3] j @ 0x%08x = %d\n”, &j, j); } void func2() { int i = 7; printf(“\t\t[in func2] i @ 0x%08x = %d\n”, &i, i); printf(“\t\t[in func2] j @ 0x%08x = %d\n”, &j, j); printf(“\t\t[in func2] setting j = 1337\n”); j = 1337; // Writing to j func3(); printf(“\t\t[back in func2] i @ 0x%08x = %d\n”, &i, i); printf(“\t\t[back in func2] j @ 0x%08x = %d\n”, &j, j); } void func1() { int i = 5; printf(“\t[in func1] i @ 0x%08x = %d\n”, &i, i); printf(“\t[in func1] j @ 0x%08x = %d\n”, &j, j); func2(); printf(“\t[back in func1] i @ 0x%08x = %d\n”, &i, i); printf(“\t[back in func1] j @ 0x%08x = %d\n”, &j, j); } int main() { int i = 3; printf(“[in main] i @ 0x%08x = %d\n”, &i, i); printf(“[in main] j @ 0x%08x = %d\n”, &j, j); func1(); printf(“[back in main] i @ 0x%08x = %d\n”, &i, i); printf(“[back in main] j @ 0x%08x = %d\n”, &j, j); } Результат компиляции и выполнения scope3.c: reader@hacking:/booksrc $ gcc scope3.c reader@hacking:/booksrc $ ./a.out [in main] i @ 0xbffff834 = 3 [in main] j @ 0x08049988 = 42 [in func1] i @ 0xbffff814 = 5 [in func1] j @ 0x08049988 = 42 0x260 Возвращаемся к основам 81 [in func2] i @ 0xbffff7f4 = 7 [in func2] j @ 0x08049988 = 42 [in func2] setting j = 1337 [in func3] i @ 0xbffff7d4 = 11 [in func3] j @ 0xbffff7d0 = 999 [back in func2] i @ 0xbffff7f4 = 7 [back in func2] j @ 0x08049988 = 1337 [back in func1] i @ 0xbffff814 = 5 [back in func1] j @ 0x08049988 = 1337 [back in main] i @ 0xbffff834 = 3 [back in main] j @ 0x08049988 = 1337 reader@hacking:/booksrc $ Очевидно, что переменная j, используемая в func3(), отличается от j в других функциях. Переменная j из функции func3() хранится по адресу 0xbffff7d0, а переменная j из остальных функций – по адресу 0x08049988 . Обратите также внимание на то, что у каждой функции есть своя переменная i, – адреса этих переменных разные. В следующем листинге видно, что GDB останавливает выполнение в точке останова на функции func3(). После этого команда backtrace по- казывает все вызовы функции, записанные в стеке. reader@hacking:/booksrc $ gcc -g scope3.c reader@hacking:/booksrc $ gdb -q ./a.out Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1”. (gdb) list 1 1 #include 2 3 int j = 42; // j – глобальная переменная. 4 5 void func3() { 6 int i = 11, j = 999; // Здесь j – локальная переменная func3(). 7 printf(“\t\t\t[in func3] i @ 0x%08x = %d\n”, &i, i); 8 printf(“\t\t\t[in func3] j @ 0x%08x = %d\n”, &j, j); 9 } 10 (gdb) break 7 Breakpoint 1 at 0x8048388: file scope3.c, line 7. (gdb) run Starting program: /home/reader/booksrc/a.out [in main] i @ 0xbffff804 = 3 [in main] j @ 0x08049988 = 42 [in func1] i @ 0xbffff7e4 = 5 [in func1] j @ 0x08049988 = 42 [in func2] i @ 0xbffff7c4 = 7 [in func2] j @ 0x08049988 = 42 [in func2] setting j = 1337 Breakpoint 1, func3 () at scope3.c:7 7 printf(“\t\t\t[in func3] i @ 0x%08x = %d\n”, &i, i); (gdb) bt 82 0x200 Программирование #0 func3 () at scope3.c:7 #1 0x0804841d in func2 () at scope3.c:17 #2 0x0804849f in func1 () at scope3.c:26 #3 0x0804852b in main () at scope3.c:35 (gdb) Обратная трассировка показывает также вложенные вызовы функций по записям, хранящимся на стеке. При каждом обращении к функ- ции в стек помещается запись, называемая кадром стека (stack frame). Каждая строка в обратной трассировке соответствует кадру стека. Каж- дый кадр стека также хранит локальные переменные для данного кон- текста. Локальные переменные кадров стека можно показать в GDB, если добавить в команду backtrace слово full (полная). (gdb) bt full #0 func3 () at scope3.c:7 i = 11 j = 999 #1 0x0804841d in func2 () at scope3.c:17 i = 7 #2 0x0804849f in func1 () at scope3.c:26 i = 5 #3 0x0804852b in main () at scope3.c:35 i = 3 (gdb) Полная обратная трассировка подтверждает, что локальная перемен- ная j есть только в контексте func3(). В контекстах других функций ис- пользуется глобальная версия переменной j. Помимо глобальных, есть статические переменные, определяемые до- бавлением в объявление переменной ключевого слова static. Подобно глобальным, статические переменные сохраняют свое значение меж- ду вызовами функции; однако они похожи и на локальные, поскольку остаются локальными внутри контекста конкретной функции. Особым и уникальным свойством статических переменных является то, что они инициализируются только один раз. Все сказанное иллюстри рует код static.c. |