Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
Скачать 2.5 Mb.
|
Два компьютера, общающиеся между собой, должны говорить на одном и том же языке. Структура этого языка описывается моделью OSI. Мо- дель OSI предоставляет стандарты, благодаря которым такие устрой- ства, как маршрутизаторы (routers) и межсетевые экраны (firewalls), могут сосредоточиться на одном конкретном аспекте связи, который к ним относится, не обращая внимания на все остальные. Модель OSI организована в виде отдельных уровней, благодаря чему маршрутиза- торы и межсетевые экраны могут заниматься передачей данных на бо- 0x410 Модель OSI 221 лее низком уровне, игнорируя более высокий уровень инкапсуляции данных, используемый работающими приложениями. Уровней OSI семь: Физический уровень. Относится к физическому соединению меж- ду двумя точками. Это самый нижний уровень, и его главная зада- ча – передача битовых потоков. Данный уровень отвечает также за активизацию, поддержку и деактивизацию передачи этих битовых потоков. Канальный уровень. Отвечает за фактическую передачу данных между двумя точками. Если физический уровень занимается про- сто пересылкой битов, то данный уровень обеспечивает функции бо- лее высокого уровня, такие как коррекция ошибок и управление потоком. Этот уровень также предоставляет процедуры для активи- зации, поддержки и деактивизации канальных соединений. Сетевой уровень. Действует как промежуточный, его главное назна- чение – передача информации между нижними и верхними уровня- ми. Он определяет адресацию и маршрутизацию. Транспортный уровень. Обеспечивает прозрачную передачу данных между системами. Предоставляя средства для надежной передачи данных, этот уровень освобождает более высокие уровни от забот по осуществлению надежной или экономичной передачи данных. Сеансовый уровень. Отвечает за установление и последующую под- держку связи между сетевыми приложениями. Уровень представления данных. Отвечает за представление данных для приложений с применением понятного им синтаксиса или язы- ка. Здесь возможны такие функции, как шифрование и сжатие дан- ных. Прикладной уровень. Следит за требованиями приложений. В соответствии с этими протоколами данные пересылаются неболь- шими частями, называемыми пакетами. Каждый пакет содержит ре- ализации этих протоколов по уровням. Начиная с прикладного уров- ня данные последовательно оборачиваются в пакет уровня представле- ния данных, затем в пакет сеансового уровня, транспортного и так да- лее. Этот процесс называется инкапсуляцией. На каждом уровне пакет состоит из заголовка и тела. Заголовок содержит информацию прото- кола, необходимую для этого уровня, а тело – данные для этого уров- ня. Тело какого-либо уровня содержит всю упаковку ранее инкапсули- рованных уровней, подобно луковице или контекстам функций в сте- ке программы. Например, при веб-серфинге кабель Ethernet и сетевая карта состав- ляют физический уровень, занимаясь передачей непреобразованных битов с одного конца кабеля на другой. Выше расположен канальный уровень. В случае веб-броузера этот уровень образует Ethernet, предо- ставляя связь нижнего уровня между портами Ethernet в локальной 222 0x400Сетевое взаимодействие сети. Этот протокол поддерживает связь между портами Ethernet, но у этих портов еще нет IP-адресов. Понятие об IP-адресах возникает на следующем уровне, сетевом. Помимо адресации этот уровень отвечает за пересылку данных с одного адреса на другой. Эти три нижние уров- ня в совокупности позволяют пересылать пакеты данных с одного IP- адреса на другой. Затем идет транспортный уровень, обеспечивающий TCP для веб-трафика; он предоставляет двустороннюю связь между со- кетами. Термин TCP/IP описывает использование TCP на транспорт- ном уровне, а термин IP – на сетевом. На этом уровне есть разные си- стемы адресации, но ваш веб-трафик использует, скорее всего, IP вер- сии 4 (IPv4). Адреса IPv4 имеют знакомый вид XX.XX.XX.XX. На этом уровне возможен также IP версии 6 (IPv6) с совершенно другой систе- мой адресации. Поскольку IPv4 встречается чаще всего, в этой книге мы всегда будем подразумевать под IP версию IPv4. Веб-трафик использует для связи HTTP (Hypertext Transfer Protocol), находящийся на верхнем уровне модели OSI. При вебсерфинге ваш броузер, находящийся в локальной сети, обменивается данными через Интернет с веб-сервером, расположенным в другой частной сети. При этом пакеты данных инкапсулируются вплоть до физического уровня, на котором они передаются маршрутизатору. Поскольку маршрутиза- тору безразлично, какие данные в действительности находятся в па- кетах, он должен реализовывать протоколы не выше сетевого уров- ня. Маршрутизатор отправляет пакеты в Интернет, откуда они попа- дают в маршрутизатор, находящийся в другой сети. Этот маршрутиза- тор инкапсулирует полученный пакет заголовками протоколов нижне- го уровня, необходимыми для доставки его конечному адресату. Этот процесс показан на рис. 4.1. (7) Прикладной уровень (6) Уровень представления данных (5) Сеансовый уровень (4) Транспортный уровень (3) Сетевой уровень (2) Канальный уровень (1) Физический уровень Приложение в сети 1 Интернет Приложение в сети 2 Рис. 4.1. Инкапсуляция пакетов при передаче через Интернет 0x420 Сокеты 223 Такая инкапсуляция пакетов представляет собой сложный язык, с по- мощью которого машины, подключенные к Интернету (и сетям других типов), общаются между собой. Эти протоколы программно реализо- ваны в маршрутизаторах, межсетевых экранах и операционных систе- мах компьютеров, позволяя им обмениваться данными друг с другом. Задействующей сеть программе, такой как веб-броузер или почтовый клиент, нужен интерфейс с операционной системой, которая управля- ет сетевыми соединениями. Поскольку операционная система берет на себя все, что связано с инкапсуляцией сетевых соединений, напи- сание программ для работы в сети сводится к применению сетевых ин- терфейсов ОС. 0x420 Сокеты Сокет – это стандартный способ организации обмена данными в сети с помощью операционной системы. Сокет можно представить в виде конечной точки соединения – вроде гнезда на ручном телефонном ком- мутаторе. При этом для программиста сокеты лишь абстракция всех технических деталей описанной выше модели OSI. Программист поль- зуется сокетами для передачи или приема данных по сети. Эти данные передаются на сеансовом уровне (5) (см. рис. 4.1) над нижними уровня- ми (заботу о которых берет на себя операционная система), обеспечива- ющими маршрутизацию. Есть несколько типов сокетов, которые опре- деляют структуру транспортного уровня (4). Чаще всего применяются сокеты потоков и сокеты дейтаграмм. Сокеты потоков предоставляют надежную двустороннюю связь, кото- рую можно сравнить с телефонным соединением. Одна из сторон ини- циирует соединение с другой, и после того как соединение установле- но, обе стороны могут общаться между собой. При этом вы сразу полу- чаете подтверждение того, что сказанное вами достигло адресата. Со- кеты потоков используют стандартный протокол связи Transmission Control Protocol (TCP), соответствующий транспортному уровню (4) модели OSI. В компьютерных сетях данные обычно передаются блока- ми, которые называют пакетами. TCP спроектирован так, чтобы дан- ные поступали без ошибок и в правильном порядке, подобно тому как в телефонном разговоре слова поступают адресату в том порядке, в ко- тором вы их произносите. Веб-серверы, почтовые серверы и клиенты тех и других используют для связи TCP и сокеты потоков. Другой распространенный тип сокетов – сокеты дейтаграмм. Связь че- рез сокеты дейтаграмм больше напоминает отправку письма, чем те- лефонный разговор. Соединение является односторонним и ненадеж- ным. Отправляя по почте несколько писем, вы не уверены, что они при- дут в том же порядке, да и вообще достигнут адресата. Почтовая служ- ба, однако, достаточно надежна в сравнении с Интернетом. В сокетах дейтаграмм на транспортном уровне (4) используется другой стандарт- 224 0x400Сетевое взаимодействие ный протокол – UDP, а не TCP. UDP (User Datagram Protocol – прото- кол дейтаграмм пользователя) служит для создания индивидуальных пользовательских протоколов. Это очень простой и облегченный про- токол, у него очень мало средств защиты. Он не обеспечивает реальное соединение, а только предоставляет элементарный способ отправить данные из одной точки в другую. Сокеты дейтаграмм требуют очень низких накладных расходов на протокол, но и возможности протокола весьма ограниченны. Если программе требуется подтверждение того, что пакет получен адресатом, в коде принимающей программы долж- на быть предусмотрена отправка пакета-подтверждения. В некоторых ситуациях потеря пакета допустима. Сокеты дейтаграмм и UDP часто используются в сетевых играх и пото- ковом вещании, где разработчики могут организовать связь так, как им нужно, не расходуя дополнительные ресурсы на TCP. 0x421 Функции сокетов В языке C сокеты во многом похожи на файлы, поскольку идентифи- цируются с помощью дескрипторов файла (файловых дескрипторов). Сходство с файлами столь велико, что прием и передачу данных через файловые дескрипторы сокетов можно выполнять с помощью функ- ций read() и write(). Однако есть несколько функций, специально соз- данных для работы с сокетами. Прототипы этих функций находятся в /usr/include/sys/sockets.h. socket(int domain, int type, int protocol) Применяется для создания нового сокета, возвращает файловый де- скриптор сокета или –1 в случае ошибки. connect(int fd, struct sockaddr *remote_host, socklen_t addr_length) Соединяет сокет (описываемый файловым дескриптором fd) с уда- ленным узлом. Возвращает 0 в случае успеха и –1 при ошибке. bind(int fd, struct sockaddr *local_addr, socklen_t addr_length) Привязывает сокет к локальному адресу, после чего можно ожи- дать входящих соединений. Возвращает 0 в случае успеха и –1 при ошибке. listen(int fd, int backlog_queue_size) «Слушает» запросы входящих соединений и организует их очередь длиной не больше backlog_queue_size. Возвращает 0 в случае успеха и –1 при ошибке. accept(int fd, sockaddr *remote_host, socklen_t *addr_length) Принимает входящее соединение на связанном сокете. Адресные дан- ные удаленного узла записываются в структуру remote_host, а фак- тический размер адресной структуры записывается в *addr_length. 0x420 Сокеты 225 Эта функция возвращает файловый дескриптор нового сокета приня- того соединения или –1 в случае ошибки. send(int fd, void *buffer, size_t n, int flags) Посылает n байтов из *buffer в сокет fd; возвращает количество пе- реданных байтов или –1 в случае ошибки. recv(int fd, void *buffer, size_t n, int flags) Принимает n байтов из сокета fd в *buffer; возвращает количество принятых байтов или –1 в случае ошибки. Если сокет создается с помощью функции socket(), нужно указать до- мен, тип и протокол сокета. Домен указывает на семейство протоко- лов, используемое сокетом. Сокет может использовать для связи раз- ные протоколы – от стандартных протоколов Интернета, требуемых для вебсерфинга, до протоколов любительской радиосвязи типа AX.25 (если вы крупный «ботаник»). Семейства протоколов определены в фай- ле bits/socket.h, автоматически включаемом в sys/socket.h. Фрагмент /usr/include/bits/socket.h /* Protocol families. */ #define PF_UNSPEC 0 /* Unspecified. */ #define PF_LOCAL 1 /* Local to host (pipes and file-domain). */ #define PF_UNIX PF_LOCAL /* Old BSD name for PF_LOCAL. */ #define PF_FILE PF_LOCAL /* Another nonstandard name for PF_LOCAL. */ #define PF_INET 2 /* IP protocol family. */ #define PF_AX25 3 /* Amateur Radio AX.25. */ #define PF_IPX 4 /* Novell Internet Protocol. */ #define PF_APPLETALK 5 /* Appletalk DDP. */ #define PF_NETROM 6 /* Amateur radio NetROM. */ #define PF_BRIDGE 7 /* Multiprotocol bridge. */ #define PF_ATMPVC 8 /* ATM PVCs. */ #define PF_X25 9 /* Reserved for X.25 project. */ #define PF_INET6 10 /* IP version 6. */ Как уже говорилось, есть несколько типов сокетов, хотя чаще всего ис- пользуются сокеты потоков и сокеты дейтаграмм. Типы сокетов также определены в bits/socket.h. (В приведенном фрагменте /* комментарии */ оформлены в другом стиле: все, что заключено между звездочками, считается комментарием.) Фрагмент /usr/include/bits/socket.h /* Types of sockets. */ enum __socket_type { SOCK_STREAM = 1, /* Sequenced, reliable, connection-based byte streams. */ #define SOCK_STREAM SOCK_STREAM SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams of fixed maximum length. */ 226 0x400Сетевое взаимодействие #define SOCK_DGRAM SOCK_DGRAM ... Последний аргумент функции socket() задает протокол и почти всег- да равен 0. Спецификация допускает наличие в семействе нескольких протоколов, и этот аргумент служит для выбора одного из протоколов в семействе. На практике большинство семейств содержит единственный прото- кол, поэтому значение этого аргумента обычно равно 0, что указыва- ет на первый и единственный протокол в семействе. Это справедливо для всех рассматриваемых в книге случаев, поэтому в наших примерах этот аргумент всегда будет 0. 0x422 Адреса сокетов Многие функции для работы с сокетами передают адресную информа- цию узлов посредством структуры sockaddr. Эта структура тоже опреде- лена в bits/socket.h, как показано ниже. Фрагмент /usr/include/bits/socket.h /* Get the definition of the macro to define the common sockaddr members. */ #include /* Structure describing a generic socket address. */ struct sockaddr { __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ }; Макроопределение SOCKADDR_COMMON находится во включаемом фай- ле bits/sockaddr.h и обычно сводится к unsigned short int. Эта величи- на определяет семейство, к которому принадлежит адрес, а остальная часть структуры хранит адресные данные. Так как сокеты могут об- мениваться данными, используя различные семейства протоколов, в каждом из которых принят свой способ задания конечных адресов, определение адреса тоже должно меняться в зависимости от семей- ства, к которому принадлежит адрес. Возможные семейства адресов тоже определены в bits/socket.h; обычно они прямо переводятся в соот- ветствующее семейство протоколов. Фрагмент /usr/include/bits/socket.h /* Address families. */ #define AF_UNSPEC PF_UNSPEC #define AF_LOCAL PF_LOCAL #define AF_UNIX PF_UNIX #define AF_FILE PF_FILE #define AF_INET PF_INET 0x420 Сокеты 227 #define AF_AX25 PF_AX25 #define AF_IPX PF_IPX #define AF_APPLETALK PF_APPLETALK #define AF_NETROM PF_NETROM #define AF_BRIDGE PF_BRIDGE #define AF_ATMPVC PF_ATMPVC #define AF_X25 PF_X25 #define AF_INET6 PF_INET6 ... Поскольку адрес может содержать информацию разных типов в зави- симости от семейства адресов, к которому он принадлежит, есть и дру- гие адресные структуры, содержащие в части адресных данных общие элементы из структуры sockaddr, а также специфическую для данно- го семейства информацию. Эти структуры имеют одинаковый размер, поэтому их типы можно приводить один к другому. Это означает, что функция socket() примет указатель на структуру sockaddr, который в действительности может быть указателем на адресную структуру для IPv4, Ipv6 или X.25. Благодаря этому функции для сокетов могут дей- ствовать с различными протоколами. В этой книге мы будем иметь дело с Internet Protocol версии 4, принад- лежащим к семейству протоколов PF_INET и использующим семейство адресов AF_INET. Параллельная структура адресов сокетов для AF_INET определена в файле netinet/in.h. Фрагмент /usr/include/netinet/in.h /* Structure describing an Internet socket address. */ struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of ‘struct sockaddr’. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; Часть SOCKADDR_COMMON вверху этой структуры – это просто беззнаковое короткое целое, уже упоминавшееся выше, которое служит для за- дания семейства адресов. Так как конечный адрес сокета состоит из интернет-адреса и номера порта, эти два значения идут следующими в структуре. Номер порта – это 16-разрядное короткое целое, а структу- ра in_addr для хранения интернет-адреса – 32-разрядное число. Осталь- ная часть структуры – это вставка из 8 байт, чтобы заполнить структу- ру sockaddr до конца. Это место не используется, но оно должно быть 228 0x400Сетевое взаимодействие отведено, чтобы структуры были взаимозаменяемы. Структура адреса сокета приведена на рис. 4.2. Семейство Структуры одинаковы по размеру. Семейство Структура sockaddr (общая структура) sa_data (14 байт) Структура sockaddr_in (в IP версии 4) Порт # IP адрес Заполнение (8 байт) Рис. 4.2. Структура адреса сокета 0x423 Порядок байтов в сети Номер порта и IP-адрес в структуре адреса сокета AF_INET должны под- чиняться принятому в сетях порядку байтов: «сначала старший байт» (big-endian). Это порядок, противоположный принятому в архитекту- ре x86, поэтому данные величины нужно преобразовывать. Для вы- полнения таких преобразований есть несколько специальных функ- ций, прототипы которых находятся в заголовочных файлах netinet/ in.h и arpa/inet.h. Ниже приведены стандартные функции преобразо- вания порядка байтов. htonl(значение_long) , Host-to-Network Long Преобразует 32-разрядное целое из порядка байтов узла в сетевой порядок. htons(значение_short) , Host-to-Network Short Преобразует 16-разрядное целое из порядка байтов узла в сетевой порядок. ntohl(значение_long) , Network-to-Host Long Преобразует 32-разрядное целое из сетевого порядка байтов в поря- док узла. ntohs(значение_long) , Network-to-Host Short Преобразует 16-разрядное целое из сетевого порядка байтов в поря- док узла. Для совместимости с разными архитектурами следует применять эти функции преобразования даже тогда, когда для процессора узла уста- новлен порядок «сначала старший байт». 0x420 Сокеты 229 0x424 Преобразование интернет-адресов Увидев строку 12.110.110.204, вы, скорее всего, узнаете в ней адрес Интернета (IP версии 4). Известная система записи в виде чисел и то- чек – стандартный способ задания интернет-адресов, и существуют функции для преобразования таких записей в 32-разрядные числа с се- тевым порядком байтов и обратно. Эти функции определены в файле arpa/inet.h, а наиболее полезны среди них следующие две: inet_aton(char *ascii_addr, struct in_addr *network_addr) ASCII to Network Преобразует строку ASCII, содержащую IP-адрес в виде чисел с точ- ками, в структуру in_addr, которая, как вы помните, содержит толь- ко 32-разрядное целое, представляющее IP-адрес с сетевым поряд- ком байтов. inet_ntoa(struct in_addr *network_addr) Network to ASCII Выполняет обратное преобразование: получает указатель на струк- туру in_addr, содержащую IP-адрес, а возвращает указатель на сим- вольную строку ASCII, содержащую IP-адрес в формате чисел с точ- ками. Эта строка хранится в статическом буфере памяти в функции и доступна до нового обращения к inet_ntoa(), при котором будет пе- резаписана. 0x425 Пример простого сервера Лучше всего изучить применение этих функций на примере. Приве- денный ниже код создает сервер, слушающий соединения TCP на порте 7890. Когда к нему подключается клиент, сервер посылает сообщение «Hello, world!», после чего принимает данные, пока соединение не бу- дет закрыто. Сервер реализован с помощью функций для работы с соке- тами и структур из включаемых файлов, о которых говорилось выше, поэтому в начале программы выполняется включение этих файлов. В hacking.h добавлена следующая полезная функция дампа памяти. Дополнение к hacking.h // Дамп байтов памяти в шестнадцатеричном виде и с разделителями void dump(const unsigned char *data_buffer, const unsigned int length) { unsigned char byte; unsigned int i, j; for(i=0; i < length; i++) { byte = data_buffer[i]; printf(“%02x “, data_buffer[i]); // Вывести в шестнадцатеричном виде. if(((i%16)==15) || (i==length-1)) { for(j=0; j < 15-(i%16); j++) printf(“ “); 230 0x400Сетевое взаимодействие printf(“| “); for(j=(i-(i%16)); j <= i; j++) { // Показать отображаемые символы. byte = data_buffer[j]; if((byte > 31) && (byte < 127)) // Вне диапазона // отображаемых символов printf(“%c”, byte); else printf(“.”); } printf(“\n”); // Конец строки дампа (в строке 16 байт) } // Конец if } // Конец for } Эта функция используется программой сервера для вывода данных па- кета. Она может оказаться полезной и в других случаях, поэтому ее по- местили в hacking.h. Остальная часть кода программы сервера станет ясной по мере чтения исходного кода. simple_server.c #include #include #include #include #include #include #include “hacking.h” #define PORT 7890 // Порт, к которому будут подключаться пользователи int main(void) { int sockfd, new_sockfd; // Слушать на sock_fd, новое соединение на new_fd struct sockaddr_in host_addr, client_addr; // Адресные данные socklen_t sin_size; int recv_length=1, yes=1; char buffer[1024]; if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal(“in socket”); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal(“setting socket option SO_REUSEADDR”); Сначала программа организует сокет с помощью функции socket(). Нам нужен сокет для TCP/IP, поэтому семейство протоколов задается как PF_INET для IPv4, а тип сокета устанавливается как SOCK_STREAM для сокета потоков. Последний аргумент протокола равен 0, потому что в семействе протоколов PF_INET есть только один протокол. Эта функ- ция возвращает файловый дескриптор сокета, записываемый в sockfd. 0x420 Сокеты 231 Функция setsockopt() применяется для задания параметров сокета. При вызове этой функции SO_REUSEADDR устанавливается в значение «истина», что позволит повторно использовать данный адрес для при- вязки. Если не установить этот параметр, то попытка программы при- вязаться к заданному порту окажется неудачной, если этот порт уже используется. Если не закрыть сокет так, как полагается, может пока- заться, что он используется, поэтому данная опция позволяет связать сокет с портом (и получить контроль над ним), даже если кажется, что он используется. Первый аргумент этой функции – сокет (задаваемый дескриптором файла), второй задает уровень опции, а третий – собственно опцию. Поскольку SO_REUSEADDR – опция уровня сокетов, этот уровень задается как SOL_SOCKET. Опций сокета много, и они определены в /usr/include/ asm/socket.h. Последние два аргумента представляют собой указатель на данные, которые должны быть присвоены опции, и длину этих дан- ных. Указатель на данные и длина этих данных часто используются в качестве аргументов функций, работающих с сокетами. Это позво- ляет функциям обрабатывать любые данные – от отдельных байтов до крупных структур данных. Опции SO_REUSEADDR используют в качестве значений 32-разрядные целые, поэтому чтобы задать истинное значе- ние для этой опции, нужно передать в качестве последних двух аргу- ментов указатель на целое число 1 и размер целого числа (который ра- вен 4 байтам). host_addr.sin_family = AF_INET; // Порядок байтов узла host_addr.sin_port = htons(PORT); // Короткое целое, сетевой порядок байтов host_addr.sin_addr.s_addr = 0; // Автоматически заполнить моим IP. memset(&(host_addr.sin_zero), ‘\0’, 8); // Обнулить остаток структуры. if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal(“binding to socket”); if (listen(sockfd, 5) == -1) fatal(“listening on socket”); Следующие несколько строк настраивают структуру host_addr для ис- пользования в вызове bind. Семейство адресов – это AF_INET, посколь- ку мы используем IPv4 и структуру sockaddr_in. Порту присваивается значение PORT, определенное как 7890. Это короткое целое должно быть преобразовано к сетевому порядку байтов, для чего применяется функ- ция htons(). Адресу присваивается значение 0, что влечет его автома- тическое заполнение текущим IP-адресом узла. Так как значение 0 не зависит от порядка байтов, необходимости в преобразовании нет. Вызов bind() передает файловый дескриптор сокета, структуру адре- са и размер структуры адреса. Этот вызов привяжет сокет к текущему IP-адресу на порте 7890. 232 0x400Сетевое взаимодействие Вызов listen() указывает сокету, что нужно ждать входящие соеди- нения, а последующий вызов accept() фактически принимает входя- щие соединения. Функция listen() помещает все входящие соедине- ния в очередь, пока accept() не примет соединение. Последний аргу- мент listen() задает максимальный размер очереди. while(1) { // Цикл accept. sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1) fatal(“accepting connection”); printf(“server: got connection from %s port %d\n”, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); send(new_sockfd, “Hello, world!\n”, 13, 0); recv_length = recv(new_sockfd, &buffer, 1024, 0); while(recv_length > 0) { printf(“RECV: %d bytes\n”, recv_length); dump(buffer, recv_length); recv_length = recv(new_sockfd, &buffer, 1024, 0); } close(new_sockfd); } return 0; } Далее идет цикл для приема входящих соединений. Первые два аргу- мента функции accept() должны быть понятны сразу; последний ар- гумент – указатель на размер структуры адреса. Дело в том, что функ- ция accept() записывает адресную информацию подключающего- ся клиента в структуру адреса, а размер этой структуры – в sin_size. В нашей задаче этот размер не меняется, но для работы с этой функци- ей нужно соблюдать соглашения по ее вызову. Функция accept() воз- вращает файловый дескриптор нового сокета для принятого соедине- ния. Таким образом, начальный файловый дескриптор сокета можно и дальше использовать для приема новых соединений, а новый фай- ловый дескриптор сокета использовать для обмена связи с подклю- чившимся клиентом. Приняв соединение, программа выводит сообщение, используя при этом inet_ntoa() для преобразования структуры sin_addr к виду IP- адреса из чисел с точками и ntohs() для преобразования порядка бай- тов в номере порта sin_port. Функция send() посылает 13 байт строки Hello, world!\n в сокет нового соединения. Последний аргумент функций send() и recv() – это флаги, которые в нашем случае всегда равны 0. Далее следует цикл для приема данных из соединения и их вывода. Функции recv() передаются указатель на буфер и максимальный раз- мер считываемых из сокета данных. Эта функция записывает данные 0x420 Сокеты 233 в указанный буфер и возвращает количество фактически записанных байт. Цикл продолжается, пока функция recv() получает данные. После компиляции и запуска программа выполняет привязку к порту 7890 и ждет входящих соединений. reader@hacking:/booksrc $ gcc simple_server.c reader@hacking:/booksrc $ ./a.out Клиент telnet может выступать в качестве общего клиента соединений TCP, поэтому с его помощью можно подключиться к нашему простому серверу, задав его IP-адрес и номер порта. С удаленной машины matrix@euclid: $ telnet 192.168.42.248 7890 Trying 192.168.42.248... Connected to 192.168.42.248. Escape character is ‘^]’. Hello, world! this is a test fjsghau;ehg;ihskjfhasdkfjhaskjvhfdkjhvbkjgf После соединения сервер посылает строку «Hello, world!», а то, что мы видим дальше, представляет собой локальное эхо набранных на клави- атуре символов. Так как telnet буферизует строки, каждая из этих двух строк отправлялась на сервер при нажатии клавиши Enter. На стороне сервера мы видим сообщение об установленном соединении и пакеты принятых данных. На локальной машине reader@hacking:/booksrc $ ./a.out server: got connection from 192.168.42.1 port 56971 RECV: 16 bytes 74 68 69 73 20 69 73 20 61 20 74 65 73 74 0d 0a | This is a test... RECV: 45 bytes 66 6a 73 67 68 61 75 3b 65 68 67 3b 69 68 73 6b | fjsghau;ehg;ihsk 6a 66 68 61 73 64 6b 66 6a 68 61 73 6b 6a 76 68 | jfhasdkfjhaskjvh 66 64 6b 6a 68 76 62 6b 6a 67 66 0d 0a | fdkjhvbkjgf... 0x426 Пример веб-клиента Программа telnet хорошо работает в качестве клиента нашего сервера, поэтому нет особой необходимости писать для него специальный кли- ент. Однако есть тысячи разных типов серверов, принимающих стан- дартные соединения TCP/IP. Когда вы работаете с веб-броузером, он каждый раз устанавливает соединение с каким-либо веб-сервером. По этому соединению передаются веб-страницы с использованием про- токола HTTP, определяющего порядок запроса и отправки информа- ции. По умолчанию веб-серверы работают на порте 80, который вме- 234 0x400Сетевое взаимодействие сте с многочисленными другими стандартными портами перечислен в файле /etc/services. Фрагмент /etc/services finger 79/tcp # Finger finger 79/udp http 80/tcp www www-http # World Wide Web HTTP HTTP действует на прикладном уровне (верхний уровень модели OSI). На этом уровне все связанные с сетью вопросы уже решены более низ- кими уровнями, и HTTP организован на базе простого текста. Обыч- ным текстом пользуются многие другие протоколы прикладного уров- ня, например, POP3, SMTP, IMAP и управляющий канал FTP. Это стан- дартные протоколы, они хорошо документированы, и их легко изучать. Зная синтаксис этих протоколов, можно вручную общаться с другими программами, использующими тот же язык. Особой беглости не требу- ется, но знание нескольких ключевых фраз поможет в разговоре с не- знакомым сервером. На языке HTTP запросы выполняются с помощью команды GET, в кото- рой нужно задать путь к ресурсу и версию протокола HTTP. Например, GET / HTTP/1.0 запрашивает корневой документ веб-сервера с помощью HTTP версии 1.0. Запрос обращен к корневому каталогу /, но большин- ство веб-серверов автоматически ищет в этом каталоге HTML-документ с именем index.html по умолчанию. Если сервер находит ресурс, то согласно HTTP он отвечает отправкой нескольких заголовков и только вслед за ними – контента. Если вме- сто GET используется команда HEAD, возвращаются только заголовки без контента. Эти заголовки – обычный текст и могут дать некоторую ин- формацию о сервере. Заголовки можно получить вручную, если под- ключиться с помощью telnet к порту 80 известного веб-сайта, набрать на клавиатуре HEAD / HTTP/1.0 и дважды нажать Enter. Ниже показано, как с помощью telnet открыто соединение TCP-IP с сервером http://www. internic.net. После этого на прикладном уровне HTTP вручную запра- шиваются заголовки для страницы главного индекса. reader@hacking:/booksrc $ telnet www.internic.net 80 Trying 208.77.188.101... Connected to www.internic.net. Escape character is ‘^]’. HEAD / HTTP/1.0 HTTP/1.1 200 OK Date: Fri, 14 Sep 2007 05:34:14 GMT Server: Apache/2.0.52 (CentOS) Accept-Ranges: bytes Content-Length: 6743 Connection: close Content-Type: text/html; charset=UTF-8 0x420 Сокеты 235 Connection closed by foreign host. reader@hacking:/booksrc $ Отсюда видно, что веб-сервер – это Apache версии 2.0.52 и даже что узел работает под CentOS. Эта информация может быть полезной, по- этому напишем программу, которая автоматизирует данный ручной процесс. В нескольких следующих программах потребуется передавать и при- нимать большой объем данных. Поскольку стандартные функции для сокетов не слишком дружественны пользователю, напишем несколь- ко функций для передачи и приема данных. Эти функции с именами send_string() и recv_line() будут помещены в новый включаемый файл hacking-network.h. Обычная функция send() возвращает количество записанных байтов, которое не всегда совпадает с количеством байтов, которое вы пыта- лись передать. Функция send_string() принимает в качестве аргумен- тов сокет и указатель на строку и гарантирует, что строка передана че- рез сокет целиком. Общая длина строки, переданной функции, опреде- ляется с помощью strlen(). Вы могли заметить, что все пакеты, полученные простым сервером, оканчивались байтами 0x0D и 0x0A. Так telnet завершает строки – посы- лая символы возврата каретки и перевода строки. В протоколе HTTP тоже предусмотрено завершение строк этими двумя байтами. Взгля- нув на таблицу ASCII, обнаруживаем, что 0x0D – это возврат каретки (‘\r’), а 0x0A – символ перевода строки (‘\n’). reader@hacking:/booksrc $ man ascii | egrep “Hex|0A|0D” Reformatting ascii(7), please wait... Oct Dec Hex Char Oct Dec Hex Char 012 10 0A LF ‘\n’ (new line) 112 74 4A J 015 13 0D CR ‘\r’ (carriage ret) 115 77 4D M reader@hacking:/booksrc $ Функция recv_line() выполняет чтение целых строк данных. Она чи- тает данные из сокета, переданного в качестве первого аргумента, в бу- фер, на который указывает второй аргумент. Прием данных из сокета продолжается до тех пор, пока не встретятся два байта конца строки в заданной последовательности. После этого записывается конец стро- ки, и функция завершает работу. Эти новые функции гарантируют от- правку всех байтов и прием данных в виде строк, завершаемых ‘\r\n’. Они приведены в листинге нового файла hacking-network.h. hacking-network.h /* Эта функция принимает FD сокета и указатель на строку для отправки, * оканчивающуюся на 0. Функция гарантирует передачу всех байтов строки. * Возвращает 1 в случае успеха и 0 при неудаче. */ int send_string(int sockfd, unsigned char *buffer) { 236 0x400Сетевое взаимодействие int sent_bytes, bytes_to_send; bytes_to_send = strlen(buffer); while(bytes_to_send > 0) { sent_bytes = send(sockfd, buffer, bytes_to_send, 0); if(sent_bytes == -1) return 0; // Вернуть 0 при ошибке передачи. bytes_to_send -= sent_bytes; buffer += sent_bytes; } return 1; // Вернуть 1 при успехе. } /* Эта функция принимает FD сокета и указатель на приемный буфер. * Прием данных из сокета ведется до получения байтов конца строки. * Байты конца строки читаются из сокета, но конец строки в буфере * ставится перед этими байтами. * Возвращает размер прочитанной строки (без байтов EOL). */ int recv_line(int sockfd, unsigned char *dest_buffer) { #define EOL “\r\n” // Байты, завершающие строку #define EOL_SIZE 2 unsigned char *ptr; int eol_matched = 0; ptr = dest_buffer; while(recv(sockfd, ptr, 1, 0) == 1) { // Прочитать один байт. if(*ptr == EOL[eol_matched]) { // Входит ли он в EOL? eol_matched++; if(eol_matched == EOL_SIZE) { // Если все байты входят в EOL, *(ptr+1-EOL_SIZE) = ‘\0’; // записать конец строки. return strlen(dest_buffer); // Вернуть кол-во принятых байтов } } else { eol_matched = 0; } ptr++; // Установить указатель на следующий байт. } return 0; // Признак конца строки не найден. } Соединиться с сокетом по численному IP-адресу довольно легко, но обычно для удобства используются именованные адреса. При руч- ном запросе HTTP HEAD программа telnet автоматически выполняет по- иск в DNS (Domain Name Service) и определяет, что www.internic.net переводится в IP-адрес 192.0.34.161. DNS – это протокол, позволяю- щий найти IP-адрес по имени аналогично поиску номера в телефон- ном справочнике. Разумеется, есть функции и структуры для сокетов, предназначенные для поиска имени в DNS. Эти функции и структуры определены в netdb.h. Функция gethostbyname() принимает указатель на строку, содержащую имя сервера, и возвращает указатель на струк- 0x420 Сокеты 237 туру hostent либо NULL в случае ошибки. Структура hostent содержит результаты поиска, включая IP-адрес в виде 32-разрядного целого с се- тевым порядком байтов. Так же как в функции inet_ntoa(), память для этой структуры статически выделяется в функции. Ниже показана эта структура, как она описана в netdb.h. Фрагмент /usr/include/netdb.h /* Описание записи в базе для одиночного узла. */ struct hostent { char *h_name; /* Официальное имя узла */ char **h_aliases; /* Список псевдонимов */ int h_addrtype; /* Тип адреса машины */ int h_length; /* Длина адреса */ char **h_addr_list; /* Список адресов */ #define h_addr h_addr_list[0] /* Для совместимости с предыдущими версиями */ }; Следующий код демонстрирует применение функции gethostbyname(). host_lookup.c #include #include #include #include #include #include #include #include “hacking.h” int main(int argc, char *argv[]) { struct hostent *host_info; struct in_addr *address; if(argc < 2) { printf(“Usage: %s exit(1); } host_info = gethostbyname(argv[1]); if(host_info == NULL) { printf(“Couldn’t lookup %s\n”, argv[1]); } else { address = (struct in_addr *) (host_info->h_addr); printf(“%s has address %s\n”, argv[1], inet_ntoa(*address)); } } 238 0x400Сетевое взаимодействие Эта программа принимает в качестве единственного аргумента имя узла и выводит его IP-адрес. Функция gethostbyname() возвращает указатель на структуру hostent, которая содержит IP-адрес в элемен- те h_addr. Указатель на этот элемент приводится к типу указателя на in_addr и впоследствии разыменовывается для вызова inet_ntoa(), ко- торая принимает в качестве аргумента структуру in_addr. Пример ра- боты программы: reader@hacking:/booksrc $ gcc -o host_lookup host_lookup.c reader@hacking:/booksrc $ ./host_lookup www.internic.net www.internic.net has address 208.77.188.101 reader@hacking:/booksrc $ ./host_lookup www.google.com www.google.com has address 74.125.19.103 reader@hacking:/booksrc $ Воспользовавшись функциями сокетов и этими наработками, нетруд- но написать программу для идентификации веб-сервера. webserver_id.c #include #include #include #include #include #include #include #include “hacking.h” #include “hacking-network.h” int main(int argc, char *argv[]) { int sockfd; struct hostent *host_info; struct sockaddr_in target_addr; unsigned char buffer[4096]; if(argc < 2) { printf(“Usage: %s exit(1); } if((host_info = gethostbyname(argv[1])) == NULL) fatal(“looking up hostname”); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal(“in socket”); target_addr.sin_family = AF_INET; target_addr.sin_port = htons(80); target_addr.sin_addr = *((struct in_addr *)host_info->h_addr); memset(&(target_addr.sin_zero), ‘\0’, 8); // Обнулить остаток структуры 0x420 Сокеты 239 if (connect(sockfd, (struct sockaddr *)&target_addr, sizeof(struct sockaddr)) == -1) fatal(“connecting to target server”); send_string(sockfd, “HEAD / HTTP/1.0\r\n\r\n”); while(recv_line(sockfd, buffer)) { if(strncasecmp(buffer, “Server:”, 7) == 0) { printf(“The web server for %s is %s\n”, argv[1], buffer+8); exit(0); } } printf(“Server line not found\n”); exit(1); } В основном этот код должен быть понятен. В элемент sin_addr структу- ры target_addr записывается адрес из структуры host_info путем при- ведения типа и разыменования – как и раньше, только в одну строку. Вызывается функция connect(), чтобы подключиться к порту 80 нуж- ного узла, посылается строка команды, и программа циклически счи- тывает каждую строку в буфер. Функция strncasecmp() из string.h вы- полняет сравнение строк. Она сравнивает первые n байт, игнорируя различие между строчными и прописными буквами. Первые два аргу- мента – это указатели на строки, а третий аргумент задает количество сравниваемых байтов. Функция возвращает 0, если строки совпадают, поэтому оператор if ищет строку, начинающуюся с “Server:”. Найдя ее, удаляем первые восемь байт и выводим информацию о версии веб- сервера. Результат компиляции и выполнения программы: reader@hacking:/booksrc $ gcc -o webserver_id webserver_id.c reader@hacking:/booksrc $ ./webserver_id www.internic.net The web server for www.internic.net is Apache/2.0.52 (CentOS) reader@hacking:/booksrc $ ./webserver_id www.microsoft.com The web server for www.microsoft.com is Microsoft-IIS/7.0 reader@hacking:/booksrc $ 0x427 Миниатюрный веб-сервер Веб-сервер не должен быть намного сложнее простого сервера, кото- рый мы создали в предыдущем разделе. Приняв соединение TCP-IP, веб-сервер должен реализовать следующие уровни связи по протоко- лу HTTP. Код сервера в приведенном ниже листинге почти идентичен нашему простому серверу, но код для обработки соединения выделен в отдель- ную функцию. Эта функция обрабатывает запросы HTTP GET и HEAD, ко- торые могут поступать от веб-броузера. Программа ищет запрашивае- мый ресурс в локальном каталоге webroot и посылает его броузеру. Если файл не найден, сервер посылает ответ HTTP 404. Вероятно, вам знаком 240 0x400Сетевое взаимодействие этот ответ, который означает, что файл не найден. Ниже приведен пол- ный листинг программы. tinyweb.c #include #include #include #include #include #include #include #include #include “hacking.h” #include “hacking-network.h” #define PORT 80 // Порт, к которому будут подключаться пользователи #define WEBROOT “./webroot” // Корневой каталог веб-сервера void handle_connection(int, struct sockaddr_in *); // Обработка запросов Сети int get_file_size(int); // Вернуть размер файла, // открытого с заданным дескриптором int main(void) { int sockfd, new_sockfd, yes=1; struct sockaddr_in host_addr, client_addr; // Адресные данные socklen_t sin_size; printf(“Accepting web requests on port %d\n”, PORT); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal(“in socket”); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal(“setting socket option SO_REUSEADDR”); host_addr.sin_family = AF_INET; // Порядок байтов на узле host_addr.sin_port = htons(PORT); // short в сетевом порядке байтов host_addr.sin_addr.s_addr = INADDR_ANY; // Автоматически записать мой IP. memset(&(host_addr.sin_zero), ‘\0’, 8); // Обнулить остаток структуры. if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal(“binding to socket”); if (listen(sockfd, 20) == -1) fatal(“listening on socket”); while(1) { // Цикл приема. sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1) 0x420 Сокеты 241 fatal(“accepting connection”); handle_connection(new_sockfd, &client_addr); } return 0; } /* Эта функция обрабатывает соединение на переданном сокете от переданного * адреса клиента. Соединение обрабатывается как веб-запрос, и эта функция * отвечает через сокет соединения. В конце работы функции этот сокет * закрывается. */ void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr) { unsigned char *ptr, request[500], resource[500]; int fd, length; length = recv_line(sockfd, request); printf(“Got request from %s:%d \”%s\”\n”, inet_ntoa(client_addr_ptr-> sin_addr), ntohs(client_addr_ptr->sin_port), request); ptr = strstr(request, “ HTTP/”); // Поиск корректного запроса. if(ptr == NULL) { // Это некорректный HTTP. printf(“ NOT HTTP!\n”); } else { *ptr = 0; // Поместить в буфер конец строки после URL. ptr = NULL; // Записать NULL в ptr // (сигнализирует о некорректном запросе). if(strncmp(request, “GET “, 4) == 0) // Запрос GET ptr = request+4; // ptr is the URL. if(strncmp(request, “HEAD “, 5) == 0) // Запрос HEAD ptr = request+5; // ptr is the URL. if(ptr == NULL) { // Тип запроса неизвестен. printf(“\tUNKNOWN REQUEST!\n”); } else { // Корректный запрос, ptr указывает на имя ресурса if (ptr[strlen(ptr) - 1] == ‘/’) // Если ресурс оканчивается на ‘/’, strcat(ptr, “index.html”); // добавить в конец ‘index.html’. strcpy(resource, WEBROOT); // Поместить в resource // путь к корню strcat(resource, ptr); // и дописать путь к ресурсу. fd = open(resource, O_RDONLY, 0); // Попытка открыть файл. printf(“\tOpening \’%s\’\t”, resource); if(fd == -1) { // Если файл не найден printf(“ 404 Not Found\n”); send_string(sockfd, “HTTP/1.0 404 NOT FOUND\r\n”); send_string(sockfd, “Server: Tiny webserver\r\n\r\n”); send_string(sockfd, “”); send_string(sockfd, “ |