ВЛАДИМИР МЕШКОВ
Сканер портов: пример реализации
Подавляющее большинство сетевых служб использует при работе протокол TCP. Согласно модели OSI, ТСР является протоколом транспортного уровня. Он обеспечивает надежное двунаправленное соединение между двумя процессами; данные передаются в обоих направлениях без ошибок, пакеты не теряются и не дублируются, последовательность передачи данных не нарушается.
Процесс, получающий или отправляющий данные, идентифицируется на этом уровне номером, который называется номером порта, или просто портом. Другими словами, порт определяет сетевую службу, которой предназначен пакет.
Для возможности установления соединения с какой-либо сетевой службой соответствующий ей порт должен быть открыт, или, выражаясь терминологией TCP-соединения, находится в состоянии LISTEN (прослушиваться). Быстро определить состояние порта позволяет специальное программное обеспечение – сканер портов.
Сканирование портов выполняется, как правило, для того, чтобы найти на узле службу, уязвимую с точки зрения безопасности сети. Это своего рода разведка, которая может осуществляться как администратором сети, так и злоумышленником.
Целью данной статьи является описание принципов функционирования и внутреннего устройства простого сканера портов TCP-протокола.
Порядок установления TCP-соединения
Для понимания принципа работы сканера необходимо знать, каким образом устанавливается TCP-соединение.
При установлении соединения задействуются поля «Порядковый номер» (SEQ), «Номер подтверждения» (ACK-SEQ), флаги SYN, ACK и RST заголовка TCP-пакета. Флаг SYN является флагом синхронизации. Он используется при установлении соединения и устанавливает начальный порядковый номер, используемый для последующей передачи данных. Флаг ACK указывает на то, что поле номера подтверждения содержит достоверные данные. Флаг RST используется для сброса соединения.
Соединение устанавливается в 3 этапа:
- инициатор соединения (клиент) формирует SYN-пакет (TCP-пакет с установленным флагом SYN), заполняет поле SEQ и передает пакет серверу;
- если порт, на который пришел запрос на соединение, открыт, сервер формирует SYN|ACK-пакет, заполняет поля SEQ, ACK-SEQ и передает пакет клиенту. Значение поля ACK-SEQ равно значению поля SEQ из пакета клиента, увеличенному на 1 (т.е. ACK-SEQ-сервера = SEQ-клиента + 1). Если порт закрыт, клиенту отправляется RST-пакет;
- получив SYN|ACK-пакет, клиент проверяет поле ACK-SEQ и высылает серверу ACK-пакет. После этого соединение считается установленным и переходит в фазу обмена данными между клиентом и сервером.
Методы сканирования
Существует достаточно большое число методов сканирования, каждый из которых имеет свои преимущества и, соответственно, недостатки. Подробнее о каждом из них можно прочитать в статье «NESSUS – современный анализ безопасности, методы сканирования»(http://www.hackzone.ru/articles/nessus.htm#scan), а также в документации на сканер Nmap (http://www.insecure.org/nmap).
Мы рассмотрим один из методов – SYN-сканирование. Этот метод часто называют half-open (полуоткрытым) сканированием, т.к. полное TCP-соединение с портом сканируемой машины не устанавливается. Суть данного метода заключается в следующем. Вы посылаете TCP-пакет с установленным флагом SYN, как если бы собирались установить реальное соединение с выбранным портом, и ожидаете ответ. Принятый пакет с установленными флагами SYN и ACK указывает на то, что выбранный порт открыт и ожидает соединения. Флаг RST означает обратное. Если получен SYN|ACK-пакет, следует немедленно отправить пакет с флагом RST для сброса соединения, хотя реально за вас это сделает ядро. Преимуществом данной технологии является отсутствие в log-файлах сканируемой машины записей о попытках установления соединения с ней. Недостаток – необходимость наличия прав root для формирования SYN-пакета.
Пример реализации сканера
Приведенный ниже код был разработан и протестирован в ОС GNU/Linux, дистрибутив Slackware 7.1, компилятор gcc-2.95.2.
Алгоритм реализации следующий:
- определить необходимые переменные и заголовочные файлы;
- создать сокеты для приема и передачи пакетов;
- сформировать SYN-пакет и отправить его сканируемому хосту;
- принять ответный пакет и проанализировать состояние флагов SYN, ACK, RST;
- сделать вывод о статусе проверяемого порта.
Заголовочные файлы и переменные
Заголовочные файлы и переменные разместим в файле, который назовем scan.h. Для работы нам понадобятся следующие header-файлы:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/if.h>
#include <linux/if_ether.h>
структуры:
struct ifreq *ifr – структура для хранения параметров сетевого интерфейса
struct iphdr *ih – структура, содержащая заголовок IP-пакета
sturct tcphdr *th – структура, содержащая заголовок TCP-пакета
struct sockaddr_in local – структура, содержащая адресную информацию о локальной системе
struct sockaddr_in dest – структура, содержащая адресную информацию об удаленной системе
struct p_header {
u_long s_addr;
u_long d_addr;
u_char zer0;
u_char protocol;
u_int lenght;
}
*pseudo – псевдозаголовок. Необходим при расчете контрольной суммы TCP-пакета (взят из исходных текстов сканера Nmap)
и переменные:
int fd – дескриптор вспомогательного сокета
e0_s – дескриптор сокета для передачи
e0_r – дескриптор сокета для приема
sent – число переданных байт
rec – число принятых байт
port – номер сканируемого порта
index – индекс интерфейса, через который осуществляется сканирование.
u_char *packet – пакет, передаваемый в сеть.
Сокет для передачи
Дескриптор сокета для передачи получим при помощи функции getsock_send, приведенной ниже.
Необходимые заголовочные файлы:
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <linux/if.h>
#include <linux/if_ether.h>
int getsock_send ( char *intf) – вызов функции
Функция getsock_send принимает строковое значение (имя интерфейса) и возвращает дескриптор сокета в случае положительного завершения или -1, если произошла ошибка.
Переменные:
int fd – дескриптор сокета
const int on=1 – флаг включения заголовка (см. ниже)
struct ifreq ifr – структура для хранения параметров сетевого интерфейса.
Сокет создадим следующим системным вызовом:
if (( fd = socket ( AF_INET, SOCK_RAW, htons(ETH_P_IP) )) < 0 ) {
perror ( «socket» );
return (-1);
}
В данном и последующих вызовах мы будем включать код, обрабатывающий возможные ошибки. Комментировать его особого смысла нет, и так все понятно, надеюсь.
Сокеты типа SOCK_RAW (RAW-сокеты) домена AF_INET удобны тем, что позволяют получить непосредственный доступ к служебным полям протокола TCP/IP, в отличии от типов SOCK_STREAM и SOCK_DGRAM (для TCP и UDP соединений, соответственно). Существуют также пакетные сокеты, о которых я рассказывал в статье «Анализатор сетевого траффика», на них мы сейчас останавливаться не будем.
Так как формировать пакеты мы будем вручную, то необходимо в опциях сокета указать данный факт. Делается это при помощи системного вызова setsockopt следующим образом:
if ( setsockopt ( fd, IPPROTO_IP, IP_HDRINCL, ( const void *) &on, sizeof ( on ) ) < 0 ) {
perror ( «setsockopt IP_HDRINCL» );
close ( fd );
return ( -1 );
}
Опция IP_HDRINCL является опцией протокола IP, поэтому параметр level вызова setsockopt равен IPPROTO_IP. Если опция IP_HDRINCL установлена, приложение строит и вставляет в исходящий пакет полный IP заголовок. Данная опция включается ненулевым значением (const int on=1).
Поскольку мы собираемся работать через определенный интерфейс, необходимо осуществить привязку сокета к выбранному интерфейсу. Для этой цели также используется системный вызов setsockopt:
sprintf ( ifr.ifr_name, «%s», intf );
if ( setsockopt ( fd, SOL_SOCKET, SO_BINDTODEVICE, (void *)&ifr, sizeof (ifr)) <0 ) {
perror ( « SO_BINDTODEVICE» );
close ( fd );
return ( -1 );
}
Опция SO_BINDTODEVICE является опцией сокета, параметр level вызова setsockopt принимает значение SOL_SOCKET.
Опция SO_BINDTODEVICE использует экземпляр структуры ifreq. Вызов setsockopt считывает из буфера данной структуры имя интерфейса, при помощи которого будут обслуживаться все доступы к рассматриваемому сокету. Поэтому вначале мы заполняем соответствующее поле структуры ifreq именем интерфейса, который был передан как параметр функции getsock_send.
Если все успешно, возвращаем в главную функцию дескриптор сокета: return ( fd ).
Сокет для приема
Дескриптор сокета для приема получим при помощи функции getsock_recv (пример данной функции был рассмотрен ранее в статье «Анализатор сетевого трафика»).
Заголовочные файлы:
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h >
#include <linux/if_packet.h>
#include <linux/if_ether.h>
int getsock_recv (int index) – вызов функции
Функция getsock_recv принимает в качестве параметра индекс интерфейса и возвращает дескриптор сокета.
Дескриптор сокета:
int fd;
Структура для хранения адресной информации об интерфейсе (см. файл <linux/if_packet.h>):
struct sockaddr_ll s_ll;
Создадим пакетный сокет:
if (( fd= socket (SOCK_PACKET, SOCK_DGRAM, htons (ETH_P_ALL) )) <0 )
{
perror ( «socket» );
return ( - 1 );
}
Пакетный сокет имеет тип SOCK_DGRAM. Это означает, что заголовок физического уровня (MAC-адрес в случае Ethernet) будет отброшен при приеме.
Выделим память для структуры struct sockaddr_ll s_ll:
memset (&s_ll, 0, sizeof (struct sockaddr_ll));
Заполним поля структуры s_ll необходимыми значениями:
s_ll.sll_family = PF_PACKET; – тип сокета
s_ll.sll_protocol = htons (ETH_P_ALL); – тип принимаемого протокола
s_ll.sll_ifindex = index; – номер интерфейса
s_ll.sll_pkttype = PACKET_HOST; – тип пакета (для локальной машины)
Для получения пакетов только с определенного интерфейса используется функция bind: таким образом мы соединяем пакетный сокет с интерфейсом, номер которого указан в структуре struct sockaddr_ll s_ll.
Привяжем сокет к интерфейсу:
if ((bind (fd, (struct sockaddr *) &s_ll, sizeof (struct sockaddr_ll)) <0 ) {
perror («bind»);
close (fd);
return (-1);
}
Возвратим дескриптор сокета в вызывающую функцию:
return (fd)
Главная функция
#include «scan.h» – файл с переменными и header-файлами
int main ( int argc, char *argv [ ] ) – вызов главной функции
Главной функции мы передаем два параметра: IP-адрес сканируемого хоста (аргумент argv[1]) и номер порта (аргумент argv[2]). Обработкой ошибочного ввода параметров будет заниматься функция usage ():
void usage () {
printf ( « \n scan [ dest_IP ] [ dest_port ] \n» );
return;
}
Первое, что необходимо сделать при запуске программы, это проверить, все ли необходимые параметры указаны:
if ( argc != 3 ) {
usage ();
exit ( 1 );
}
Преобразуем введенные строковые значения адреса и порта в сетевой формат и заполним адресную структуру удаленной системы:
port = atoi ( argv [2] );
memset ( &dest, 0, sizeof ( struct sockaddr_in ));
dest.sin_addr.s_addr = inet_ntoa ( argv [1] );
dest.sin_port = htons ( port );
Тоже самое проделаем для локального хоста.
memset ( &local, 0, sizeof ( struct sockaddr_in ));
Получим IP-адрес интерфейса и занесем его в адресную структуру local:
fd = socket ( AF_INET, SOCK_DGRAM, 0 );
sprintf ( ifr -> ifr_name, «%s», «eth0»);
octl ( fd, SIOCGIFADDR, ifr );
memcpy (( char *) &local, ( char *)&( ifr -> ifr_addr ), sizeof ( struct sockaddr ));
local.sin_port = htons (53);
Получим индекс интерфейса:
ioctl ( fd, SIOCGIFINDEX, ifr );
index = ifr -> ifindex;
Выделим память для хранения данных, передаваемых и принимаемых из сети. Пакет будет состоять только из заголовков IP и TCP протоколов.
packet = ( u_char * )malloc( sizeof ( struct iphdr ) + sizeof ( struct tcphdr ));
Внутри общего пакета разместим служебные заголовки IP и TCP протоколов, а также псевдозаголовок.
in = ( struct iphdr * ) packet;
th = ( struct tcphdr * ) (packet + sizeof ( struct iphdr ));
pseudo = (struct p_header *) ( packet + sizeof ( struct iphdr) - sizeof ( struct p_header ));
Заполним поля псевдозаголовка необходимыми значениями.
memset ( pseudo, 0, sizeof ( struct p_header )); – обнулим структуру
pseudo -> s_addr = local.sin_addr.s_addr; – адрес локального хоста
pseudo -> d_addr = dest.sin_addr.s_addr; – адрес удаленного хоста
pseudo -> protocol = 6; – протокол (TCP)
pseudo -> lenght = htons ( sizeof ( struct tcphdr )); – длина псевдозаголовка
Сформируем TCP-заголовок.
memset ( th, 0, sizeof ( struct tcphdr )); – обнулим структуру
th -> source = local.sin_port; – локальный порт
th -> dest = dest.sin_port; – удаленный порт
th -> seq = htonl ( 1156270349 ); – начальный порядковый номер
th -> ack_seq = 0; – номер подтверждения
th -> doff = 5; – длина заголовка (в 32-х разрядных словах)
th -> syn = 1; – установить флаг SYN
th -> window = htons ( 3072 ); – размер окна
th -> check = 0; – обнулить поле контрольной суммы
th -> check = in_cksum (( u_short *)pseudo, sizeof ( struct tcphdr) + sizeof ( struct p_header));
Алгоритм расчета контрольной суммы для заголовков TCP и IP одинаковый и будет изложен ниже.
Сформируем IP-заголовок.
memset ( ih, 0, sizeof ( struct iphdr )); – обнулим структуру
ih -> version = 4; – версия протокола
ih -> ihl = 5; – длина заголовка (число 32-х битных слов)
ih -> tot_len = htons (sizeof (struct iphdr)+sizeof(struct tcphdr)); – длина пакета
ih -> id = 3290; – порядковый номер пакета (идентификация)
ih -> ttl = 42; – время жизни
ih -> protocol = 6; – транспортный протокол (TCP)
ih -> saddr = local.sin_addr.s_addr; – локальный адрес
ih -> daddr = dest.sin_addr.s_addr; – удаленный адрес
ih -> check = in_cksum (( u_short *) ih, sizeof (struct iphdr )); – контрольная сумма
Некоторые поля заголовков (например, поле «Начальный порядковый номер» TCP заголовка и поле «Идентификация» заголовка IP) были выбраны совершенно произвольно, т.к. в данном случае эти значения ни на что не влияют.
Отобразим для контроля имеющуюся адресную информацию:
printf ( «IP-адрес назначения \t -\t %s \n «, inet_ntoa ( ih -> daddr ));
printf ( «IP-адрес источника \t - \t %s \n «, inet_ntoa ( ih -> saddr ));
printf ( «Порт назначения \t \t - \t %d \n «, ntohs ( th -> dest ));
printf ( «Порт источника \t\t - \t %d \n «, ntohs ( th -> source ));
Создадим сокеты для передачи и приема пакетов.
if (( e0_s = getsock_send ( «eth0» )) < 0 ) {
perror ( «getsock_send» );
exit ( 1 );
}
if (( e0_r = getsock_recv ( index )) < 0 ) {
perror ( «getsock_recv» );
exit ( 1 );
}
Передадим сформированный SYN-пакет хосту назначения.
dest.sin_family = AF_INET;
sent = sendto ( e0_s, (char *) packet, ntohs ( ih -> tot_len), 0, ( struct sockaddr *)&dest,sizeof (struct sockaddr_in));
if ( sent <= 0 ) {
perror ( «sendto» );
exit ( 1 );
}
printf ( «\n Передано %d байт \n», sent );
Примем ответ на наш запрос. Прием будем осуществлять в бесконечном цикле, каждый раз обнуляя приемный буфер.
for ( ; ; ) {
bzero ( packet, sizeof (packet));
rec = 0;
rec = recvfrom ( e0_r, (char *) packet, sizeof ( struct iphdr ) + sizeof ( struct tcphdr ),0, NULL, NULL );
if ( rec <0 || rec > 1500 ) {
perror ( «recvfrom» );
exit ( 1 );
}
Число 1500 определяет максимальный размер MTU для сети Ethernet. Больше этого значения в одном пакете принять мы не можем, и любое превышение данного предела трактуется как ошибка.
Теперь займемся анализом принятого пакета.
Для начала проверим соответствие версии протокола IP. Поле «Версия» должно содержать 4. Если это не так (к нам мог поступить ARP-запрос, который мы не собираемся обрабатывать), то принятый пакет отбрасывается и продолжается ожидание:
if (( ih -> version ) != 4 ) continue;
Также мы не будем обрабатывать IP-пакеты, отправителем которых не является сканируемый хост:
if (( ih -> saddr != dest.sin_addr.s_addr ) continue;
и если транспортный протокол не есть TCP:
if (( ih -> protocol != 6 ) continue;
Если принятый пакет соответствует всем условиям, то нам останется только отобразить результаты:
printf ( «Принято %d байт \n \n «, rec );
printf ( «%s \t -> \t « , inet_ntoa ( ih ->saddr ));
printf ( «%s \t \n \n «, inet_ntoa ( ih -> daddr ));
printf ( «Версия \t \t \t = %d \n», ih -> version );
printf ( «Длина заголовка \t \t = %d \n», ih -> ihl );
printf ( «Длина пакета \t \t= %d \n», ntohs (ih -> tot_len ));
printf ( «Идентификатор \t \t = %d \n», ih -> id );
printf ( «Время жизни \t \t = %d \n», ih -> ttl );
printf ( «Протокол \t \t = %d \n», ih -> protocol );
printf ( «Контрольная сумма IP \t= %d \n», ih -> check );
printf ( «Порт источник \t \t = %d \n», ntohs ( th -> source ));
printf ( «Порт назначения \t \t = %d \n», ntohs ( th -> dest ));
printf ( «Контрольная сумма TCP \t = %d \n», th->check);
printf ( «SEQ \t \t \t = %lu \n», ntohl ( th -> seq ));
printf ( «ACK-SEQ \t \t \t = %lu \n», ntohl ( th -> ack_seq ));
if ( th -> syn == 1 ) printf ( «Флаг SYN установлен \n» );
if ( th -> ack == 1 ) printf ( «Флаг ACK установлен \n» );
if ( th -> fin == 1 ) printf ( «Флаг FIN установлен \n» );
if ( th -> rst == 1 ) printf ( «Флаг RST установлен \n» );
if ( th -> psh == 1 ) printf ( «Флаг PUSH установлен \n» );
if ( th -> urg == 1 ) printf ( «Флаг URG установлен \n» );
if (( th -> syn == 1 )&&(th->ack==1)) printf («Порт %d открыт \n», ntohs (th -> source ));
Здесь все предельно ясно. После этого мы прерываем цикл приема пакетов и выходим из программы.
break;
}
return (1); }
Сброс соединения возложим на ядро.
Контрольная сумма
Расчет контрольной суммы выполняет следующая функция:
#include < linux/types.h >
__u16 in_cksum ( __u16 *ptr, int nbytes )
{
register __u32 sum;
__u16 oddbyte;
register __u16 answer;
sum = 0;
while ( nbytes > 1 ) {
sum += *ptr ++;
nbytes -= 2;
}
if ( nbytes == 1 ) {
oddbytes = 0;
* (( unsigned char *) &oddbyte ) = * (unsigned char *) ptr;
sum += oddbyte;
}
sum = ( sum >> 16 ) + ( sum & 0xFFFF);
sum += (sum >> 16 );
answer=~sum;
return (answer);
}
Код для расчета контрольной суммы взят из исходных текстов сканера Nmap, поэтому приводится без комментариев. Порядок расчета контрольной суммы изложен в RFC 1071.
Makefile
Для сборки выполняемого модуля создадим Makefile следующего содержания:
CC = gcc
name = scan
SCAN = scan.o checksum.o getsock_send.o getsock_recv.o
$( name ) : $( SCAN )
$( CC ) -g -o $( name ) $( NAME )
scan.o : scan.c
$( CC ) -c scan.c
checksum.o : checksum.c
$( CC ) -c checksum.c
getsock_send.o : getsock_send.c
$( CC ) -c getsock_send.c
getsock_recv.o : getsock_recv.c
$( CC ) -c getsock_recv.c
clean:
rm -f *.o
Для получения исполняемого модуля достаточно ввести команду make. После этого в каталоге, где размещены файлы программы, появиться файл scan. При запуске в командной строке модуля необходимо указать IP-адрес сканируемого хоста и номер проверяемого порта.
Вот в принципе и все.
Обо всех замечаниях и пожеланиях пишите на ubob@mail.ru.