|
Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
64 0x200 Программирование [field width on B] 3: ‘31337’, 10: ‘ 31337’, ‘00031337’ [string] sample Address bffff870 variable A is at address: bffff86c reader@hacking:/booksrc $ Первые два вызова printf() демонстрируют вывод переменных A и B с помощью разных параметров форматирования. Поскольку в каждой строке три параметра форматирования, переменные A и B нужно пере- дать по три раза каждую. Формат %d позволяет выводить отрицатель- ные значения, а формат %u – нет, поскольку предполагает беззнаковые величины. Когда переменная A выводится с параметром форматирования %u, ее значение оказывается очень большим. Это происходит потому, что от- рицательное число A хранится в дополнительном коде, а параметр фор- матирования пытается вывести его так, как если бы это было беззнако- вое значение. Поскольку дополнительный код образуется инвертиро- ванием всех битов и прибавлением единицы, прежние нулевые стар- шие биты превращаются в единицы. Третья строка примера, начинающаяся с метки [field width on B], по- казывает, как в параметре форматирования задается ширина поля. Это просто целое число, задающее минимальную ширину поля для данно- го параметра. Однако максимальную ширину поля оно не определяет: если выводимое значение больше, чем допускает ширина поля, она бу- дет увеличена. Так происходит, например, когда задана ширина поля 3, а для вывода данных нужно 5 байт. Если задать ширину поля 10, то перед данными будет выведено 5 пробелов. Кроме того, если ширина поля начинается с 0, поле будет дополнено нулями. Например, при за- данной ширине поля 08 выводится 00031337. Четвертая строка, начинающаяся с метки [string], показывает приме- нение параметра форматирования %s. Напомню, что переменная стро- ка фактически является указателем, содержащим адрес строки, что прекрасно, поскольку параметр форматирования %s предполагает пе- редачу данных по ссылке. Последняя строка показывает адрес переменной A, полученный с помо- щью унарного оператора адреса. Он выводится в виде восьми шестнад- цатеричных цифр с дописанными нолями. Из этих примеров видно, что для десятичных чисел нужно применять формат %d, для целых без знака – %u, а для шестнадцатеричных – %x. Минимальную ширину поля можно задать с помощью числа, следу- ющего сразу за знаком процента, а если размер поля начинается с 0, оно будет дополнено незначащими нулями. С помощью параметра %s можно вывести строку, для чего в качестве аргумента передается адрес этой строки. Пока все ясно. Строки форматирования применяются в целом семействе стандартных функций ввода/вывода, включая scanf(), которая похожа на printf(),
0x260 Возвращаемся к основам 65 но служит для ввода, а не вывода. Важное отличие scanf() состоит в том, что все ее аргументы являются указателями, поэтому ей нужно передавать не сами переменные, а их адреса. Это можно сделать с по- мощью переменных-указателей или применения унарного оператора адреса к обычным переменным. Программа input.c иллюстрирует это. input.c #include #include int main() { char message[10]; int count, i; strcpy(message, “Hello, world!”); printf(“Repeat how many times? “); scanf(“%d”, &count); for(i=0; i < count; i++) printf(“%3d - %s\n”, i, message); } В программе input.c функция scanf() служит для установки значения переменной count. Ниже демонстрируется работа этой программы. reader@hacking:/booksrc $ gcc -o input input.c reader@hacking:/booksrc $ ./input Repeat how many times? 3 0 - Hello, world! 1 - Hello, world! 2 - Hello, world! reader@hacking:/booksrc $ ./input Repeat how many times? 12 0 - Hello, world! 1 - Hello, world! 2 - Hello, world! 3 - Hello, world! 4 - Hello, world! 5 - Hello, world! 6 - Hello, world! 7 - Hello, world! 8 - Hello, world! 9 - Hello, world! 10 - Hello, world! 11 - Hello, world! reader@hacking:/booksrc $ Форматные строки используются весьма часто, поэтому полезно хо- рошо их изучить. Кроме того, умея выводить значения переменных, можно отлаживать программу, не прибегая к отладчику. Возможность
660x200 Программирование мгновенно видеть реакцию на свои действия очень важна в процессе обучения хакера, а такие простые вещи, как вывод значения перемен- ной, открывают большие возможности для исследования программ. 0x265 Приведение типаПриведение типа ( typecasting) – это способ временно изменить тип дан- ных, хранящихся в переменной, на другой, отличный от ее первона- чального объявления. Приведение типа переменной есть указание ком- пилятору обрабатывать ее так, как если бы она имела заданный новый тип, но только на время выполнения текущей операции. Синтаксис приведения типа имеет следующий вид: ( новый_тип_данных) переменнаяПриведением типа можно воспользоваться, когда нужно обработать целые числа и числа с плавающей точкой, что демонстрирует програм- ма typecasting.c. typecasting.c#include int main() { int a, b; float c, d; a = 13; b = 5; c = a / b; // Деление целых чисел. d = (float) a / (float) b; // Деление целых чисел, // приведенных к типу float. printf(“[integers]\t a = %d\t b = %d\n”, a, b); printf(“[floats]\t c = %f\t d = %f\n”, c, d); } Ниже приведен результат компилирования и прогона программы typecasting.c. reader@hacking:/booksrc $ gcc typecasting.c reader@hacking:/booksrc $ ./a.out [integers] a = 13 b = 5 [floats] c = 2.000000 d = 2.600000 reader@hacking:/booksrc $ Как уже говорилось, деление целого числа 13 на 5 даст неверный окру- гленный результат 2, даже если записывать его в переменную с плава- ющей точкой. Однако если привести эти целочисленные переменные к типу с плавающей точкой, они будут обрабатываться как действи- тельные числа, и мы получим правильный результат 2,6.
0x260 Возвращаемся к основам 67 Это наглядный пример, но еще более ярко приведение типа проявля- ет себя при работе с переменными-указателями. Несмотря на то что указатель – это всего лишь адрес памяти, компилятор C требует ука- зывать тип данных для всех указателей. Отчасти это вызвано стремле- нием предотвратить ошибки программистов. Указатель на целый тип должен указывать на целочисленные данные, а указатель на символь- ный тип – только на символьные. Другая причина – арифметика ука- зателей. Целое число занимает четыре байта, а символ – всего один. Программа pointer_types.c иллюстрирует и объясняет эти понятия. В ее коде используется параметр форматирования %p, задающий вывод адреса памяти. Это сокращенное обозначение формата для вывода ука- зателей, практически равносильного формату 0x%08x. pointer_types.c #include int main() { int i; char char_array[5] = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’}; int int_array[5] = {1, 2, 3, 4, 5}; char *char_pointer; int *int_pointer; char_pointer = char_array; int_pointer = int_array; for(i=0; i < 5; i++) { // Обойти массив целых с помощью int_pointer. printf(“[integer pointer] points to %p, which contains the integer %d\n”, int_pointer, *int_pointer); int_pointer = int_pointer + 1; } for(i=0; i < 5; i++) { // Обойти массив символов с помощью char_pointer. printf(“[char pointer] points to %p, which contains the char ‘%c’\n”, char_pointer, *char_pointer); char_pointer = char_pointer + 1; } } Этот код определяет в памяти два массива: один с целочисленными данными, а другой – с символьными. Определены также два указате- ля – на данные целого типа и на символьные, – каждый из них содер- жит адрес начала соответствующего массива. Два отдельных цикла for осуществляют обход массивов с применением арифметики указа- телей для перенацеливания указателей на очередные элементы масси- вов. Обратите внимание: когда целые числа и символы выводятся в ци-
68 0x200 Программирование клах с помощью параметров форматирования %d и %c, соответствующие аргументы printf() должны разыменовывать переменные-указатели. Для этого применяется унарный оператор разыменования *. reader@hacking:/booksrc $ gcc pointer_types.c reader@hacking:/booksrc $ ./a.out [integer pointer] points to 0xbffff7f0, which contains the integer 1 [integer pointer] points to 0xbffff7f4, which contains the integer 2 [integer pointer] points to 0xbffff7f8, which contains the integer 3 [integer pointer] points to 0xbffff7fc, which contains the integer 4 [integer pointer] points to 0xbffff800, which contains the integer 5 [char pointer] points to 0xbffff810, which contains the char ‘a’ [char pointer] points to 0xbffff811, which contains the char ‘b’ [char pointer] points to 0xbffff812, which contains the char ‘c’ [char pointer] points to 0xbffff813, which contains the char ‘d’ [char pointer] points to 0xbffff814, which contains the char ‘e’ reader@hacking:/booksrc $ Несмотря на то что к int_pointer и char_pointer добавляется в циклах одно и то же число 1, компилятор увеличивает хранящиеся в указате- лях адреса по-разному. Тип char занимает 1 байт, поэтому указатель на следующий символ будет больше на 1 байт. А тип integer занимает 4 байта, поэтому указатель на следующее целое число будет больше на 4 байта. В программе pointer_types2.c указатели преобразованы так, чтобы ука- затель на целый тип int_pointer указывал на символьные данные и на- оборот. pointer_types2.c #include int main() { int i; char char_array[5] = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’}; int int_array[5] = {1, 2, 3, 4, 5}; char *char_pointer; int *int_pointer; char_pointer = int_array; // Теперь char_pointer и int_pointer указывают int_pointer = char_array; // на несовместимые типы данных for(i=0; i < 5; i++) { // Обойти массив целых с помощью int_pointer. printf(“[integer pointer] points to %p, which contains the integer %d\n”, int_pointer, *int_pointer); int_pointer = int_pointer + 1; }
0x260 Возвращаемся к основам 69 for(i=0; i < 5; i++) { // Обойти массив символов с помощью char_pointer. printf(“[char pointer] points to %p, which contains the integer %d\n”, char_pointer, *char_pointer); char_pointer = char_pointer + 1; } } Ниже показаны предупредительные сообщения, выдаваемые компи- лятором. reader@hacking:/booksrc $ gcc pointer_types2.c pointer_types2.c: In function `main’: pointer_types2.c:12: warning: assignment from incompatible pointer type pointer_types2.c:13: warning: assignment from incompatible pointer type reader@hacking:/booksrc $ Пытаясь помешать появлению ошибки в программе, компилятор преду преждает, что указатели указывают на несовместимые типы дан- ных. Но тип указателя волнует только компилятор и, возможно, про- граммиста. В скомпилированном коде указатель – всего лишь адрес памяти, поэтому компилятор скомпилирует код, даже если указатель указывает на несовместимый тип данных, – он лишь предупреждает программиста, что результат работы программы может оказаться не- ожиданным. reader@hacking:/booksrc $ ./a.out [integer pointer] points to 0xbffff810, which contains the char ‘a’ [integer pointer] points to 0xbffff814, which contains the char ‘e’ [integer pointer] points to 0xbffff818, which contains the char ‘8’ [integer pointer] points to 0xbffff81c, which contains the char ‘ [integer pointer] points to 0xbffff820, which contains the char ‘?’ [char pointer] points to 0xbffff7f0, which contains the integer 1 [char pointer] points to 0xbffff7f1, which contains the integer 0 [char pointer] points to 0xbffff7f2, which contains the integer 0 [char pointer] points to 0xbffff7f3, which contains the integer 0 [char pointer] points to 0xbffff7f4, which contains the integer 2 reader@hacking:/booksrc $ Несмотря на то что int_pointer указывает на символьные данные, со- держащие 5 байт данных, он сохраняет тип указателя на целое. Это означает, что прибавление к указателю 1 будет увеличивать адрес на 4. Аналогично адрес в char_pointer будет каждый раз увеличиваться все- го на 1, и проход через 20 байт целочисленных данных (пять 4-байт- ных чисел) будет осуществляться по одному байту. Просматривая 4-байтные целые числа по одному байту, мы снова убеждаемся, что данные хранятся в порядке «сначала младший байт». 4-байтное число 0x00000001 хранится в памяти как байты 0x01, 0x00, 0x00, 0x00. Мы будем сталкиваться с подобными ситуациями, когда указатель указывает на данные несовместимого с ним типа. Поскольку тип ука- зателя определяет размер данных, на которые он указывает, нужно следить за правильностью типа. Как видно из приведенного ниже ли- 70 0x200 Программирование стинга pointer_types3.c, приведение типа позволяет изменить тип пере- менной «на лету». pointer_types3.c #include int main() { int i; char char_array[5] = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’}; int int_array[5] = {1, 2, 3, 4, 5}; char *char_pointer; int *int_pointer; char_pointer = (char *) int_array; // Приведение к типу int_pointer = (int *) char_array; // данных указателя. for(i=0; i < 5; i++) { // Обойти массив целых с помощью int_pointer. printf(“[integer pointer] points to %p, which contains the integer %d\n”, int_pointer, *int_pointer); int_pointer = (int *) ((char *) int_pointer + 1); } for(i=0; i < 5; i++) { // Обойти массив символов с помощью char_pointer. printf(“[char pointer] points to %p, which contains the integer %d\n”, char_pointer, *char_pointer); char_pointer = (char *) ((int *) char_pointer + 1); } } В этом коде при начальном задании значений указателей происходит приведение типа данных к типу данных указателя. В результате ком- пилятор C перестанет сообщать о конфликте типов данных, но ариф- метика указателей все равно будет действовать некорректно. Чтобы исправить положение, при добавлении 1 к указателю нужно сначала привести его к нужному типу данных, чтобы адрес увеличился на пра- вильную величину. После этого нужно снова вернуть первоначальный тип данных указателя. Не очень изящно, но действует. reader@hacking:/booksrc $ gcc pointer_types3.c reader@hacking:/booksrc $ ./a.out [integer pointer] points to 0xbffff810, which contains the char ‘a’ [integer pointer] points to 0xbffff811, which contains the char ‘b’ [integer pointer] points to 0xbffff812, which contains the char ‘c’ [integer pointer] points to 0xbffff813, which contains the char ‘d’ [integer pointer] points to 0xbffff814, which contains the char ‘e’ [char pointer] points to 0xbffff7f0, which contains the integer 1 [char pointer] points to 0xbffff7f4, which contains the integer 2 [char pointer] points to 0xbffff7f8, which contains the integer 3
0x260 Возвращаемся к основам 71[char pointer] points to 0xbffff7fc, which contains the integer 4 [char pointer] points to 0xbffff800, which contains the integer 5 reader@hacking:/booksrc $ Конечно, гораздо проще сразу выбрать для указателя правильный тип данных, однако иногда желательно иметь некий общий указатель без типа. В C такой указатель без типа определяется с помощью ключево- го слова void. В ходе экспериментов с такими указателями быстро выясняются не- которые их особенности. Во-первых, нельзя разыменовать указатель, если у него нет типа. Чтобы извлечь из памяти данные, адрес которых содержится в указателе, компилятору нужно знать тип этих данных. Во-вторых, указатель void нужно привести к какому-нибудь типу, что- бы выполнять с ним арифметические действия. Это достаточно очевид- ные ограничения, из которых следует, что основная задача указателя без типа – просто хранить адрес памяти. Программу pointer_types3.c можно модифицировать так, чтобы в ней использовался один указатель void, приводимый к нужному типу при каждом его использовании. Компилятор знает, что у указателя void нет типа, и позволяет присвоить ему любой указатель без приведения типа. Однако при разыменовании указателя void необходимо выпол- нить приведение его типа. Все это показано в листинге pointer_types4.c, где используется указатель void. pointer_types4.c#include int main() { int i; char char_array[5] = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’}; int int_array[5] = {1, 2, 3, 4, 5}; void *void_pointer; void_pointer = (void *) char_array; for(i=0; i < 5; i++) { // Обход массива char. printf(“[char pointer] points to %p, which contains the char ‘%c’\n”, void_pointer, *((char *) void_pointer)); void_pointer = (void *) ((char *) void_pointer + 1); } void_pointer = (void *) int_array; for(i=0; i < 5; i++) { // Обход массива int. printf(“[integer pointer] points to %p, which contains the integer %d\n”, void_pointer, *((int *) void_pointer));
720x200 Программирование void_pointer = (void *) ((int *) void_pointer + 1); } } В результате компиляции и выполнения программы pointer_types4.c получаем: reader@hacking:/booksrc $ gcc pointer_types4.c reader@hacking:/booksrc $ ./a.out [char pointer] points to 0xbffff810, which contains the char ‘a’ [char pointer] points to 0xbffff811, which contains the char ‘b’ [char pointer] points to 0xbffff812, which contains the char ‘c’ [char pointer] points to 0xbffff813, which contains the char ‘d’ [char pointer] points to 0xbffff814, which contains the char ‘e’ [integer pointer] points to 0xbffff7f0, which contains the integer 1 [integer pointer] points to 0xbffff7f4, which contains the integer 2 [integer pointer] points to 0xbffff7f8, which contains the integer 3 [integer pointer] points to 0xbffff7fc, which contains the integer 4 [integer pointer] points to 0xbffff800, which contains the integer 5 reader@hacking:/booksrc $ Компиляция и вывод программ pointer_types4.c и pointer_types3.c вы- глядят одинаково. Указатель void всего лишь хранит адрес памяти, а приведение типа сообщает компилятору, какой тип указателя дол- жен использоваться в каждом случае. Поскольку нужный тип устанавливается путем приведения типа, ука- затель void – это всего лишь адрес памяти. Если тип данных задается с помощью приведения типа, значит в роли указателя void можно ис- пользовать любой тип, размер которого позволяет хранить четыре бай- та данных. В программе pointer_types5.c для хранения адреса исполь- зуется беззнаковое целое. |
|
|