Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
Переменные часто используются в условных операторах описанных выше управляющих структур. Условные операторы используют тот или иной тип сравнения. Операторы сравнения в C имеют сокращен- ный синтаксис, довольно распространенный среди других языков про- граммирования. Условие Обозначение Пример Меньше < (a < b) Больше > (a > b) 0x240 Основные понятия программирования 27 Условие Обозначение Пример Меньше или равно <= (a <= b) Больше или равно >= (a >= b) Равно == (a == b) Не равно != (a != b) Почти все эти операторы самоочевидны, однако обратите внимание, что равенство записывается с помощью двух знаков «равно». Это суще- ственно: двойной знак «равно» служит для проверки равенства, а оди- ночный – для присваивания значения переменной. Выражение a = 7 означает «записать число 7 в переменную a», а выражение a == 7 озна- чает «проверить, что переменная a равна 7». (В некоторых языках про- граммирования, например в Pascal, чтобы избежать путаницы, при- сваивание обозначают символами :=.) Заметим также, что восклица- тельный знак обычно означает отрицание. С его помощью значение лю- бого выражения можно изменить на противоположное. !(a < b) равносильно (a >= b) Операторы сравнения можно соединить в цепочку с помощью логиче- ских операций ИЛИ и И. Логическая операция Обозначение Пример ИЛИ || ((a < b) || (a < c)) И && ((a < b) && !(a < c)) В этом примере выражение, образованное из двух условий, соединен- ных оператором ИЛИ, будет иметь истинное значение, если a меньше b ИЛИ если a меньше c. Аналогично выражение, образованное из двух условий, соединенных оператором И, будет иметь истинное значение, если a меньше b И если a не меньше c. Эти выражения могут иметь раз- личный вид и содержать круглые скобки. С помощью переменных, операторов сравнения и управляющих струк- тур можно описать очень многое. Если вернуться к примеру с голодной мышкой, то можно представить голод как логическую (булеву) пере- менную, которая может принимать значения истина/ложь. Естествен- но, 1 означает истину, а 0 – ложь. while (голодна == 1) { ищи что-то съедобное; съешь то, что нашла; } 28 0x200 Программирование Вот еще одно сокращение, которым часто пользуются программисты и хакеры. На самом деле в C нет булевых операторов, и любое ненулевое значение считается истинным, а равное нулю – ложным. Фактически операторы сравнения возвращают значение 1, если сравнение выполне- но, и 0 в противном случае. Проверка значения переменной голодна на равенство 1 вернет 1, если оно равно 1, и 0, если оно равно 0. Поскольку в программе могут встретиться только эти два случая, оператор сравне- ния можно вообще опустить. while (голодна) { ищи что-то съедобное; съешь то, что нашла; } Вот более сложная программа мыши с дополнительными входными данными, которая показывает, как можно сочетать операторы сравне- ния с переменными. while ((голодна) && !(рядом_кошка)) { ищи что-то съедобное; If(!(еда - приманка_в_мышеловке)) съешь то, что нашла; } В этом примере вводятся переменные, описывающие присутствие кош- ки и местонахождение пищи, принимающие значения 1 или 0 в соот- ветствии с истинностью или ложностью условия. Помните, что всякое ненулевое значение считается истинным, а нулевое – ложным. 0x244 Функции Иногда программист знает, что некоторый набор инструкций он бу- дет использовать несколько раз. Такие инструкции можно объединить в небольшую подпрограмму, называемую функцией (function). В дру- гих языках кроме понятия функции, есть подпрограмма (subroutine), или процедура (procedure). Например, чтобы выполнить поворот авто- мобиля, нужно выполнить несколько более мелких инструкций: вклю- чить указатель поворота, замедлить ход, пропустить приближающий- ся транспорт, повернуть рулевое колесо и так далее. Схема проезда, опи- санная в начале главы, содержит несколько поворотов, и перечислять для каждого из них все отдельные инструкции было бы скучно (а чи- тать – утомительно). В функцию можно передавать в качестве аргумен- тов переменные, тем самым изменяя результат ее выполнения. Напри- мер, передадим функции направление поворота. Function повернуть(направление_поворота) { Включить мигающий сигнал направление_поворота; 0x240 Основные понятия программирования 29 Замедлить ход; Проверить, нет ли приближающегося транспорта; while(есть приближающийся транспорт) { Остановиться; Пропустить приближающийся транспорт; } Повернуть руль в сторону направление_поворота; while(поворот не завершен) { if(скорость < 5 км в час) Прибавить газ; } Повернуть рулевое колесо в исходное положение; Выключить мигающий сигнал направление_поворота; } Эта функция описывает все инструкции, которые нужно выполнить, чтобы осуществить поворот. Когда программе, которой известно об этой функции, нужно выполнить поворот, она может просто вызвать эту функцию. При вызове функции выполняются находящиеся в ней инструкции с теми аргументами, которые переданы в функцию; затем выполнение программы возобновляется с той инструкции, перед которой была вы- звана функция. Функции можно передать аргумент налево или направо, чтобы она выполнила поворот в нужном направлении. В языке C по умолчанию функция после ее вызова может вернуть зна- чение. Те, кто знаком с математическими функциями, понимают, на- сколько это разумно. Например для функции, вычисляющей фактори- ал числа, естественно вернуть результат вычислений. Функция в C не помечается ключевым словом function, а объявляет- ся указанием типа данных переменной, которую она возвращает. Это очень похоже на объявление переменной. Если функция должна вер- нуть целое число (например функция, вычисляющая факториал чис- ла x), она может иметь следующий вид: int factorial(int x) { int i; for(i=1; i < x; i++) x *= i; return x; } Эта функция объявлена как целое число, потому что она перемножа- ет все числа от 1 до x и возвращает результат, который тоже будет це- лым числом. Оператор return в конце функции возвращает содержи- мое переменной x и завершает работу функции. Эту функцию вычисле- ния факториала можно использовать как целую переменную в основ- 30 0x200 Программирование ной части любой программы, которой известно о существовании такой функции. int a=5, b; b = factorial(a); После выполнения этой короткой программы переменная b будет иметь значение 120, потому что функция factorial будет вызвана с аргумен- том 5 и вернет значение 120. Кроме того, в C компилятор должен «знать» о функциях, чтобы ис- пользовать их. Для этого можно просто написать всю функцию до того, как она будет использована в программе, или воспользоваться прото- типом функции. Прототип функции сообщает компилятору, что он встретит функцию с указанным именем и с указанными типами воз- вращаемого значения и аргументов. Сама функция может распола- гаться где-нибудь в конце программы, но использовать ее можно в лю- бом другом месте, потому что компилятор уже знает о ней. Вот пример прототипа для функции factorial(): int factorial(int); Обычно прототипы функций размещаются в начале программы. Объ- являть какие-либо переменные в прототипе не нужно, потому что это делается в фактической функции. Единственное, что нужно компиля- тору, это имя функции, тип возвращаемых ею данных и типы данных ее аргументов. Если функция не должна возвращать никакого значения, как, скажем, функция повернуть() в предшествующем примере, ее следует объявить с типом void. Однако функция повернуть() еще не охватывает все дей- ствия, необходимые для проезда по маршруту. Для каждого поворота на маршруте указаны направление поворота и название улицы. Следо- вательно, у функции поворота должны быть две переменные: направ- ление и название улицы, на которую нужно свернуть. Это осложняет функцию, поскольку прежде чем поворачивать, нужно найти нужную улицу. Ниже приведен C-подобный псевдокод более полной функции поворота. void повернуть(направление_поворота, название_нужной_улицы) { Найти табличку с названием улицы; название_следующей_улицы = прочитать название улицы; while(название_следующей_улицы != название_нужной_улицы) { Найти следующую табличку с названием улицы; название_следующей_улицы = прочитать название улицы; } Включить мигающий сигнал направление_поворота; Замедлить ход; Проверить, нет ли приближающегося транспорта; while(есть приближающийся транспорт) 0x240 Основные понятия программирования 31 { Остановиться; Пропустить приближающийся транспорт; } Повернуть руль в сторону направление_поворота; while(поворот не завершен) { if(скорость < 5 км в час) Прибавить газ; } Повернуть рулевое колесо в исходное положение; Выключить мигающий сигнал направления_поворота } В этой функции есть участок, ответственный за поиск пересечения с нужной улицей путем поиска таблички с названием улицы, чтения с нее названия улицы и записи его в переменную название_следующей_ улицы . Поиск и чтение табличек продолжаются, пока не будет найдена нужная улица, после чего выполняются остальные инструкции для по- ворота. Теперь можно изменить псевдокод схемы проезда, введя в него эту новую функцию поворота. Начать движение по Главной улице в восточном направлении; while (справа нет церкви) Двигаться по Главной улице; if (движение перекрыто) { Повернуть(направо, 15-я улица); Повернуть(налево, Сосновая улица); Повернуть(направо, 16-я улица); } else Повернуть(направо, 16-я улица); Повернуть (налево, Дорога к цели); for (i=0; i<5; i++) Проехать 1 км; Остановиться у дома 743 по Дороге к цели; Обычно в псевдокоде не используют функции, потому что псевдокод служит в основном для создания эскиза программы перед тем, как пи- сать код, который можно компилировать. Поскольку псевдокод реаль- но не будет работать, полностью писать функции не требуется – доста- точно отметить что-то вроде «Сделать здесь какую-то сложную вещь». Но в языках программирования, таких как C, функции используются весьма интенсивно. Немалую долю реальной полезности C составляют наборы готовых функций, называемые библиотеками. 32 0x200 Программирование 0x250 Практическая работа Теперь, после некоторого знакомства с синтаксисом C и базовыми поня- тиями программирования, довольно легко приступить к практическо- му программированию на C. Компиляторы C есть практически для всех имеющихся операционных систем и типов процессоров, но в этой книге речь идет только о Linux и процессорах семейства x86. Linux – это бес- платная операционная система, доступная всем желающим, а процес- соры архитектуры x86 наиболее широко используются во всем мире. Поскольку хакинг неразрывно связан с экспериментированием, лучше всего работать с этой книгой, если в вашем распоряжении есть компи- лятор C. Материалы для этого издания организованы в виде загрузочного диска, которым можно воспользоваться, если у вас процессор x86 1 . Диск нуж- но вставить в привод и перезагрузить компьютер. Вы окажетесь в сре- де Linux, причем установленная у вас операционная система не будет затронута. В этой Linux-среде вы сможете выполнять примеры из кни- ги и проводить собственные эксперименты. Закончив работу, просто из- влеките диск и снова перезагрузите компьютер. Займемся делом. Программа firstprog.c – простой C-код, который 10 раз выводит на экран строку «Hello, world!». firstprog.c #include int main() { int i; for(i=0; i < 10; i++) // Цикл повторяется 10 раз. { printf(“Hello, world!\n”); // Вывести строку. } return 0; // Сообщить ОС, что программа завершилась без ошибок. } Выполнение C-программы начинается с главной функции, которую так и называют – main(). Текст после двух косых линий (//) является комментарием, компилятор его игнорирует. Первая строка программы может озадачить, но это всего лишь синтак- сис C, с помощью которого компилятору сообщают о необходимости включить заголовки для библиотеки стандартного ввода/вывода (I/O), 1 Образ диска можно скачать с сайта издательства по адресу www.symbol.ru/ library/hacking-2ed. – Прим. ред. 0x250 Практическая работа 33 имя которой – stdio. Этот заголовочный файл будет добавлен в про- грамму во время компиляции. Его полное имя /usr/include/stdio.h, и в нем определены некоторые константы и прототипы функций, нахо- дящихся в стандартной библиотеке ввода/вывода. Поскольку в функ- ции main() используется функция printf() из стандартной библиотеки ввода/вывода, сначала нужно описать прототип printf(). Этот и мно- гие другие прототипы находятся в заголовочном файле stdio.h. Мощь C в значительной мере определяется его возможностями расширения и библиотеками. Оставшийся код во многом похож на псевдокод, ко- торый мы видели выше, и должен быть интуитивно понятен. Заметим также, что часть фигурных скобок можно опустить. Достаточно оче- видно, что должна делать эта программа, но на всякий случай скомпи- лируем ее с помощью GCC и запустим на выполнение. GNU Compiler Collection (GCC) – это бесплатный компилятор C, кото- рый транслирует C в машинный язык, понятный процессору. В резуль- тате трансляции появляется исполняемый двоичный файл, имя кото- рого по умолчанию a.out. Делает ли скомпилированная программа то, что предполагалось? reader@hacking:/booksrc $ gcc firstprog.c reader@hacking:/booksrc $ ls -l a.out -rwxr-xr-x 1 reader reader 6621 2007-09-06 22:16 a.out reader@hacking:/booksrc $ ./a.out Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! reader@hacking:/booksrc $ 0x251 Общая картина Хорошо, все эти важные основы на уровне начального курса програм- мирования. В большинстве вводных курсов по программированию учат, как читать и писать на C. Но поймите меня правильно: свобод- но владеть C очень полезно и этого достаточно, чтобы считать себя про- граммистом, но это лишь часть более общей картины. Большинство программистов, даже выучив язык от корки до корки, никогда не до- стигнут большего. Преимущество хакеров в том, что они понимают, как взаимодействуют между собой все части общей картины. Чтобы смотреть на программирование шире, нужно просто понять, что код C предназначен для компиляции. Этот код не может ничего осуществить, пока он не будет скомпилирован в исполняемый двоичный файл. Вос- 34 0x200 Программирование приятие исходного кода C как программы – обычное заблуждение, на котором систематически играют хакеры. Двоичные инструкции в a.out написаны на машинном языке – простом языке, понятном централь- ному процессору (ЦП). Компиляторы нужны для того, чтобы перево- дить код на языке C на машинные языки различных процессорных ар- хитектур. В нашем случае процессор принадлежит к семейству архи- тектуры x86. Существуют также архитектуры процессоров Sparc (ис- пользуется в рабочих станциях Sun) и PowerPC (используется в доин- теловских Маках). У каждой архитектуры свой машинный язык, поэ- тому компилятор выступает как промежуточное звено, переводя код C на машинный язык для целевой архитектуры. Обычному программисту, которому нужно только, чтобы скомпилиро- ванная программа работала, интересен собственно исходный код. Но хакер понимает, что в реальности выполняется именно скомпилиро- ванная программа. Хорошо разбираясь в работе ЦП, хакер может ма- нипулировать программами, которые на нем выполняются. Мы видим исходный код своей первой программы и компилируем его в исполня- емый двоичный файл для архитектуры x86. Но как выглядит этот дво- ичный файл? Среди инструментов разработчика GNU есть программа под названием objdump, с помощью которой можно изучать скомпили- рованные двоичные файлы. Начнем с того, что посмотрим с ее помо- щью машинный код, в который была транслирована функция main(). reader@hacking:/booksrc $ objdump -D a.out | grep -A20 main.: 08048374 8048374: 55 push %ebp 8048375: 89 e5 mov %esp,%ebp 8048377: 83 ec 08 sub $0x8,%esp 804837a: 83 e4 f0 and $0xfffffff0,%esp 804837d: b8 00 00 00 00 mov $0x0,%eax 8048382: 29 c4 sub %eax,%esp 8048384: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp) 804838b: 83 7d fc 09 cmpl $0x9,0xfffffffc(%ebp) 804838f: 7e 02 jle 8048393 8048391: eb 13 jmp 80483a6 8048393: c7 04 24 84 84 04 08 movl $0x8048484,(%esp) 804839a: e8 01 ff ff ff call 80482a0 804839f: 8d 45 fc lea 0xfffffffc(%ebp),%eax 80483a2: ff 00 incl (%eax) 80483a4: eb e5 jmp 804838b 80483a6: c9 leave 80483a7: c3 ret 80483a8: 90 nop 80483a9: 90 nop 80483aa: 90 nop reader@hacking:/booksrc $ Программа objdump выводит слишком много строк, чтобы можно было в них разобраться, поэтому в командной строке ее вывод направлен на 0x250 Практическая работа 35 вход grep с параметром, задающим вывести только 20 строк после ре- гулярного выражения main.:. Байты представлены в шестнадцатерич- ной системе счисления, основание которой – 16. Нам более привыч- на десятичная система, поэтому начиная с 10 нужны дополнительные символы. В шестнадцатеричной системе цифры от 0 до 9 представлены теми же цифрами от 0 до 9, а числа от 10 до 15 – буквами от A до F. Это удобная система обозначений, потому что в байте 8 бит, каждый из ко- торых может иметь значение 1 (истина) или 0 (ложь). То есть один байт имеет 256 (2 8 ) возможных значений и может быть описан двумя шест- надцатеричными цифрами. Шестнадцатеричные числа слева (начиная с 0x8048374) – это адреса па- мяти. Биты команд машинного языка должны где-то размещаться, и это «где-то» называется памятью. Память – это набор байтов для вре- менного хранения данных; каждый байт имеет числовой адрес. Как улица состоит из домов, имеющих адреса, так и память можно представить как ряд байтов, у каждого из которых есть свой адрес. К каждому байту памяти можно обратиться по его адресу, и ЦП обра- щается к этим участкам памяти, чтобы извлечь из них машинные ко- манды, из которых состоит скомпилированная программа. В старых процессорах Intel x86 применялась 32-разрядная система адресации, а в новых используется 64-разрядная. В 32-разрядных процессорах мо- жет быть 2 32 (или 4 294 967 296) разных адресов, а в 64-разрядных – 2 64 (1,84467441 × 10 19 ) адресов. 64-разрядные процессоры могут работать в режиме, совместимом с 32-разрядными, что позволяет им быстро вы- полнять 32-разрядный код. Шестнадцатеричные числа в середине приведенного листинга пред- ставляют собой машинные команды для процессора x86. Разумеется, они описывают байты, состоящие из двоичных нолей и единиц, кото- рые только и может понимать ЦП. Но поскольку работать с последова- тельностью вроде 01010101100010011110010110000011111011001111000 01 . . . неприятно никому, кроме самого процессора, код отображает- ся в виде шестнадцатеричных байтов, а каждая инструкция размеща- ется на отдельной строке, подобно тому как абзац разделяется на пред- ложения. Если подумать, то и с шестнадцатеричными байтами не очень удобно работать, и тут появляется язык ассемблера. На нем записаны коман- ды, расположенные справа. Он представляет собой набор мнемоник для соответствующих машинных команд. Команду ret гораздо легче понять и запомнить, чем 0xc3 или 11000011. В отличие от C и других ком- пилируемых языков, команды ассемблера однозначно соответствуют командам машинного языка. Из этого следует, что поскольку в каж- дой процессорной архитектуре свой язык машинных команд, языки ассемблера для них отличаются. Ассемблер для программистов лишь способ представления машинных команд, передаваемых процессору. Конкретный вид машинных команд определяется договоренностью |