Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
Эта команда ассемблера поместит значение 0 в ячейку памяти, адрес которой на 4 меньше того, что содержится в регистре EBP. Это адрес, по которому хранится переменная i кода C; i была объявлена как це- лое число, использующее 4 байта памяти в архитектуре x86. По суще- ству, данная команда обнулит переменную i для цикла for. Если по- смотреть сейчас содержимое памяти по этому адресу, там обнаружит- ся случайный мусор. Отобразить содержимое памяти можно разными способами. (gdb) i r ebp ebp 0xbffff808 0xbffff808 (gdb) x/4xb $ebp - 4 0xbffff804: 0xc0 0x83 0x04 0x08 (gdb) x/4xb 0xbffff804 0xbffff804: 0xc0 0x83 0x04 0x08 (gdb) print $ebp - 4 $1 = (void *) 0xbffff804 (gdb) x/4xb $1 0xbffff804: 0xc0 0x83 0x04 0x08 (gdb) x/xw $1 0xbffff804: 0x080483c0 (gdb) Мы видим, что регистр EBP содержит адрес 0xbffff808, а команда ас- семблера будет выполнять запись в ячейку с адресом, смещенным от- носительно него на 4 байта, то есть 0xbffff804. Можно прямо дать ко- манде examine этот адрес памяти, а можно заставить ее саму заниматься арифметикой. Простые математические действия можно выполнить с помощью команды print, результат которой также записывается от- ладчиком во временную переменную. Имя этой переменной – $1, поз- же она позволит снова быстро обратиться к определенному адресу па- мяти. Все приведенные выше способы делают одно и то же: отобража- ют 4 байта мусора в том месте памяти, которое будет очищено в резуль- тате выполнения текущей команды. Давайте выполним текущую команду с помощью команды отладчика nexti (от next instruction). Процессор прочтет команду по адресу, нахо- дящемуся в EIP, выполнит ее и переведет EIP на следующую команду. (gdb) nexti 0x0804838b 6 for(i=0; i < 10; i++) (gdb) x/4xb $1 0xbffff804: 0x00 0x00 0x00 0x00 (gdb) x/dw $1 0xbffff804: 0 (gdb) i r eip eip 0x804838b 0x804838b (gdb) x/i $eip 0x804838b (gdb) 46 0x200 Программирование Как мы и ожидали, предыдущая команда обнулила 4 байта по адресу EBP минус 4, представляющему собой память, отведенную для пере- менной i. После этого EIP перешел к следующей команде. Несколько следующих команд лучше обсуждать как единую группу. (gdb) x/10i $eip 0x804838b 0x8048391 0x8048393 0x804839a 0x80483a2 0x80483a4 0x80483a6 0x80483a7 (gdb) Первая команда cmp (от compare) выполняет сравнение, в данном случае содержимого памяти, отведенной для переменной i языка C, со значе- нием 9. Следующая команда jle выполняет переход при условии «мень- ше или равно». При этом проверяется результат предыдущей коман- ды сравнения (хранящийся в регистре EFLAGS), и если он совпадает с «меньше или равно», то EIP перенацеливается на совсем другой уча- сток кода. В данном случае инструкция говорит, что нужно перейти по адресу 0x8048393, если хранящееся в памяти значение переменной i мень- ше или равно 9. Если это не так, EIP укажет на следующую за ней ко- манду, выполняющую безусловный переход. Она переведет EIP на адрес 0x80483a6 . Эти три команды вместе образуют управляющую структуру if-then-else : если значение i меньше или равно 9, то перейти к коман- де по адресу 0x8048393; в противном случае перейти к команде по адре- су 0x80483a6. Первый адрес 0x8048393 (выделен полужирным) – это просто команда, идущая после команды безусловного перехода, а второй адрес 0x80483a6 (выделен курсивом) находится в конце функции. Мы знаем, что по адресу памяти, который сравнивается с 9, находится 0, и знаем, что 0 меньше или равен 9, поэтому после выполнения следу- ющих двух команд в EIP будет находиться 0x8048393. (gdb) nexti 0x0804838f 6 for(i=0; i < 10; i++) (gdb) x/i $eip 0x804838f (gdb) nexti 8 printf(“Hello, world!\n”); (gdb) i r eip eip 0x8048393 0x8048393 (gdb) x/2i $eip 0x8048393 (gdb) 0x250 Практическая работа 47 Как и ожидалось, две предыдущие команды продолжили выполнение программы с адреса 0x8048393, где мы встречаем следующие две коман- ды. Первая – mov, которая запишет значение 0x8048484 в ячейку памя- ти, адрес которой содержится в регистре ESP. Но куда указывает ре- гистр ESP? (gdb) i r esp esp 0xbffff800 0xbffff800 (gdb) В данный момент ESP содержит адрес 0xbffff800, а после выполнения команды mov по этому адресу будет записан адрес 0x8048484. Но зачем? Что такого интересного находится по адресу 0x8048484? Это можно вы- яснить. (gdb) x/2xw 0x8048484 0x8048484: 0x6c6c6548 0x6f57206f (gdb) x/6xb 0x8048484 0x8048484: 0x48 0x65 0x6c 0x6c 0x6f 0x20 (gdb) x/6ub 0x8048484 0x8048484: 72 101 108 108 111 32 (gdb) Опытный читатель может кое-что заметить в этих данных, в частности диапазон значений байтов. Поизучав некоторое время память, вы ста- нете быстро определять такого рода систему. Дело в том, что эти байты соответствуют диапазону отображаемых символов ASCII. ASCII – это стандарт отображения всех символов, которые вы видите на своей кла- виатуре (и некоторых других), в фиксированные числа. Байты 0x48, 0x65, 0x6c и 0x6f соответствуют буквам алфавита в таблице ASCII, ко- торая приведена ниже. Таблицу ASCII можно вывести командой man ascii , которая есть в большинстве систем UNIX. ASCII Table Oct Dec Hex Char Oct Dec Hex Char ----------------------------------------------------------- 000 0 00 NUL ‘\0’ 100 64 40 @ 001 1 01 SOH 101 65 41 A 002 2 02 STX 102 66 42 B 003 3 03 ETX 103 67 43 C 004 4 04 EOT 104 68 44 D 005 5 05 ENQ 105 69 45 E 006 6 06 ACK 106 70 46 F 007 7 07 BEL ‘\a’ 107 71 47 G 010 8 08 BS ‘\b’ 110 72 48 H 011 9 09 HT ‘\t’ 111 73 49 I 012 10 0A LF ‘\n’ 112 74 4A J 013 11 0B VT ‘\v’ 113 75 4B K 014 12 0C FF ‘\f’ 114 76 4C L 015 13 0D CR ‘\r’ 115 77 4D M 48 0x200 Программирование 016 14 0E SO 116 78 4E N 017 15 0F SI 117 79 4F O 020 16 10 DLE 120 80 50 P 021 17 11 DC1 121 81 51 Q 022 18 12 DC2 122 82 52 R 023 19 13 DC3 123 83 53 S 024 20 14 DC4 124 84 54 T 025 21 15 NAK 125 85 55 U 026 22 16 SYN 126 86 56 V 027 23 17 ETB 127 87 57 W 030 24 18 CAN 130 88 58 X 031 25 19 EM 131 89 59 Y 032 26 1A SUB 132 90 5A Z 033 27 1B ESC 133 91 5B [ 034 28 1C FS 134 92 5C \ ‘\\’ 035 29 1D GS 135 93 5D ] 036 30 1E RS 136 94 5E ^ 037 31 1F US 137 95 5F _ 040 32 20 SPACE 140 96 60 ` 041 33 21 ! 141 97 61 a 042 34 22 “ 142 98 62 b 043 35 23 # 143 99 63 c 044 36 24 $ 144 100 64 d 045 37 25 % 145 101 65 e 046 38 26 & 146 102 66 f 047 39 27 ‘ 147 103 67 g 050 40 28 ( 150 104 68 h 051 41 29 ) 151 105 69 i 052 42 2A * 152 106 6A j 053 43 2B + 153 107 6B k 054 44 2C , 154 108 6C l 055 45 2D - 155 109 6D m 056 46 2E . 156 110 6E n 057 47 2F / 157 111 6F o 060 48 30 0 160 112 70 p 061 49 31 1 161 113 71 q 062 50 32 2 162 114 72 r 063 51 33 3 163 115 73 s 064 52 34 4 164 116 74 t 065 53 35 5 165 117 75 u 066 54 36 6 166 118 76 v 067 55 37 7 167 119 77 w 070 56 38 8 170 120 78 x 071 57 39 9 171 121 79 y 072 58 3A : 172 122 7A z 073 59 3B ; 173 123 7B { 074 60 3C < 174 124 7C | 075 61 3D = 175 125 7D } 076 62 3E > 176 126 7E 077 63 3F ? 177 127 7F DEL 0x250 Практическая работа 49 К счастью, команда GDB examine позволяет отображать и такое содер- жимое памяти. Формат c автоматически покажет байты согласно та- блице ASCII, а формат s покажет всю строку символьных данных. (gdb) x/6cb 0x8048484 0x8048484: 72 ‘H’ 101 ‘e’ 108 ‘l’ 108 ‘l’ 111 ‘o’ 32 ‘ ‘ (gdb) x/s 0x8048484 0x8048484: “Hello, world!\n” (gdb) Эти команды показывают, что по адресу 0x8048484 хранится строка «Hello, world!\n» . Эта строка передается в качестве аргумента функ- ции printf(), указывающего, что запись адреса этой строки (0x8048484) в ячейку, адрес которой содержит ESP, как-то связана с этой функци- ей. Следующий листинг показывает запись адреса этой строки данных по адресу, на который указывает ESP. (gdb) x/2i $eip 0x8048393 (gdb) x/xw $esp 0xbffff800: 0xb8000ce0 (gdb) nexti 0x0804839a 8 printf(“Hello, world!\n”); (gdb) x/xw $esp 0xbffff800: 0x08048484 (gdb) Следующая команда является вызовом функции printf(), которая вы- водит строку данных. Предыдущая команда служила подготовкой к вызову функции, а результат вызова функции выделен ниже полу- жирным. (gdb) x/i $eip 0x804839a (gdb) nexti Hello, world! 6 for(i=0; i < 10; i++) (gdb) Продолжая отладку с помощью GDB, посмотрим на следующие две ко- манды. Их тоже лучше рассмотреть совместно. (gdb) x/2i $eip 0x804839f 0x80483a2 (gdb) По сути, эти две инструкции просто увеличивают переменную i на 1. Команда lea (от Load Effective Address – загрузка эффективного адре- са) загружает уже знакомый адрес EBP минус 4 в регистр EAX. Ниже показан результат ее выполнения. 50 0x200 Программирование (gdb) x/i $eip 0x804839f (gdb) print $ebp - 4 $2 = (void *) 0xbffff804 (gdb) x/x $2 0xbffff804: 0x00000000 (gdb) i r eax eax 0xd 13 (gdb) nexti 0x080483a2 6 for(i=0; i < 10; i++) (gdb) i r eax eax 0xbffff804 -1073743868 (gdb) x/xw $eax 0xbffff804: 0x00000000 (gdb) x/dw $eax 0xbffff804: 0 (gdb) Следующая команда inc увеличит на 1 значение, находящееся по это- му адресу (теперь записанному в регистр EAX). Выполнение этой ко- манды показано ниже. (gdb) x/i $eip 0x80483a2 (gdb) x/dw $eax 0xbffff804: 0 (gdb) nexti 0x080483a4 6 for(i=0; i < 10; i++) (gdb) x/dw $eax 0xbffff804: 1 (gdb) В результате значение, хранящееся по адресу EBP минус 4 (0xbffff804), увеличивается на 1. Это соответствует той части кода на C, где в цикле for увеличивается значение переменной i. Следующая команда – безусловный переход. (gdb) x/i $eip 0x80483a4 (gdb) В результате выполнения этой команды программа снова возвращает- ся к инструкции по адресу 0x804838b. Для этого данный адрес просто за- писывается в EIP. Теперь, глядя на весь дизассемблированный код, вы можете сказать, ка- кие части кода C в какие машинные команды были скомпилированы. (gdb) disass main Dump of assembler code for function main: 0x08048374 0x08048375 0x08048377 0x260 Возвращаемся к основам 51 0x0804837a 0x08048384 0x0804838b 0x0804838f 0x08048391 0x08048393 0x0804839a 0x0804839f 0x080483a2 0x080483a4 0x080483a6 0x080483a7 End of assembler dump. (gdb) list 1 #include 2 3 int main() 4 { 5 int i; 6 for(i=0; i < 10; i++) 7 { 8 printf(“Hello, world!\n”); 9 } 10 } (gdb) Команды, выделенные полужирным, образуют цикл for, а те, что кур- сивом – находящийся внутри цикла вызов printf(). Выполнение про- граммы переходит обратно к команде сравнения, затем происходит вы- зов printf() и переменная счетчика увеличивается, пока ее значение не достигнет 10. В этот момент условный переход jle не произойдет, вме- сто этого указатель команды перейдет на команду безусловного пере- хода, которая выполнит выход из цикла и завершит программу. 0x260 Возвращаемся к основам Теперь, когда ваше представление о программировании стало более конкретным, следует познакомиться с некоторыми другими важными аспектами языка C. Ассемблер и процессоры появились раньше язы- ков программирования высокого уровня, и за это время в программи- ровании возникло много новых идей. Подобно тому как некоторое зна- комство с латынью может заметно помочь более глубокому пониманию английского языка, представление о низкоуровневом программирова- нии может облегчить понимание программирования на языках более высокого уровня. Переходя к следующему разделу, не забывайте, что код C может что-либо сделать лишь после того, как будет скомпилиро- ван в машинные команды. 52 0x200 Программирование 0x261 Строки Значение «Hello, world!\n», передаваемое функции printf() в предыду- щей программе, является строкой – формально, массивом символов. Массив в C это просто список из n элементов определенного типа. Мас- сив из 20 символов это 20 символов, размещенных в памяти один за другим. Массивы также называют буферами. Программа char_array.c содержит пример массива символов. char_array.c #include int main() { char str_a[20]; str_a[0] = ‘H’; str_a[1] = ‘e’; str_a[2] = ‘l’; str_a[3] = ‘l’; str_a[4] = ‘o’; str_a[5] = ‘,’; str_a[6] = ‘ ‘; str_a[7] = ‘w’; str_a[8] = ‘o’; str_a[9] = ‘r’; str_a[10] = ‘l’; str_a[11] = ‘d’; str_a[12] = ‘!’; str_a[13] = ‘\n’; str_a[14] = 0; printf(str_a); } Компилятор GCC принимает ключ -o, с помощью которого можно за- дать имя выходного файла, содержащего результат компиляции. Ниже показано, как скомпилировать эту программу в исполняемый двоичный файл char_array. reader@hacking:/booksrc $ gcc -o char_array char_array.c reader@hacking:/booksrc $ ./char_array Hello, world! reader@hacking:/booksrc $ В данной программе определен массив str_a из 20 символов, и во все элементы этого массива поочередно выполняется запись. Обратите внимание: нумерация элементов начинается с 0, а не с 1. Кроме того, последним символом является 0 (null, или нулевой байт). Поскольку массив определен как символьный, ему было выделено 20 байт памяти, но фактически использовано из них только 12. Нуле- вой байт в конце играет роль разделителя, сообщая функциям, рабо- 0x260 Возвращаемся к основам 53 тающим с этой строкой, что в этом месте они должны остановиться. Оставшиеся байты будут проигнорированы. Если ввести нулевой байт в качестве пятого элемента, то функция printf() выведет только сим- волы Hello. Задавать каждый символ в массиве утомительно, а строки использу- ются достаточно часто, поэтому для обработки строк был создан набор стандартных функций. Например, функция strcpy() копирует стро- ку из источника в приемник, последовательно просматривая строку- источник и копируя каждый байт в строку-приемник (останавливаясь при обнаружении нулевого байта – конца строки). Порядок аргумен- тов функции такой же, как в асемблере Intel: сначала приемник, за- тем источник. Программу char_array.c можно переписать с использованием функции strcpy() , так что она будет выполнять ту же задачу с применением би- блиотеки строковых функций. В следующей версии программы char_ array включается заголовочный файл string.h, поскольку в ней исполь- зуется строковая функция. char_array2.c #include #include int main() { char str_a[20]; strcpy(str_a, “Hello, world!\n”); printf(str_a); } Изучим эту программу с помощью GDB. В приведенном ниже листин- ге скомпилированная программа открывается в отладчике, и устанав- ливаются точки останова до функции strcpy(), на ней и после нее, что отмечено полужирным. Отладчик остановит выполнение программы в каждой из этих точек, что позволит просмотреть содержимое реги- стров и памяти. Код функции strcpy() берется из библиотеки общего доступа, поэтому задать точку останова внутри этой функции можно только после начала выполнения программы. reader@hacking:/booksrc $ gcc -g -o char_array2 char_array2.c reader@hacking:/booksrc $ gdb -q ./char_array2 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]; 6 7 strcpy(str_a, «Hello, world!\n»); 54 0x200 Программирование 8 printf(str_a); 9 } (gdb) break 6 Breakpoint 1 at 0x80483c4: file char_array2.c, line 6. (gdb) break strcpy Function “strcpy” not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 2 (strcpy) pending. (gdb) break 8 Breakpoint 3 at 0x80483d7: file char_array2.c, line 8. (gdb) При запуске программы обрабатывается точка останова strcpy(). В каждой точке останова мы просмотрим содержимое EIP и команды, на которые он указывает. Обратите внимание: в средней точке адрес памяти, находящийся в EIP, не такой, как в других. (gdb) run Starting program: /home/reader/booksrc/char_array2 Breakpoint 4 at 0xb7f076f4 Pending breakpoint “strcpy” resolved Breakpoint 1, main () at char_array2.c:7 7 strcpy(str_a, “Hello, world!\n”); (gdb) i r eip eip 0x80483c4 0x80483c4 (gdb) x/5i $eip 0x80483c4 0x80483cf 0x80483d2 0x80483d7 (gdb) continue Continuing. Breakpoint 4, 0xb7f076f4 in strcpy () from /lib/tls/i686/cmov/libc.so.6 (gdb) i r eip |