Программирование на C++ глазами хакера

         

Самый быстрый сканер портов


Потоки — это очень мощная и удобная вещь, позволяющая создать многозадачность даже внутри отдельного приложения. Но у них есть один большой недостаток — программисты, познакомившись с потоками, начинают использовать их везде, где это надо и не надо.

Я видел много сканеров, которые используют по 20—50 потоков для одновременного сканирования большого количества портов. Я понимаю, что пример, который мы рассмотрели в главе 4, был очень медленным, и его надо ускорять, но не таким же методом. Попробуйте на досуге реализовать сканирование с помощью потоков. Вы увидите, что это не так уж и просто. Ну и, конечно же, вы уже знаете, что потоки излишне нагружают систему.

Сейчас вам предстоит увидеть, как можно реализовать быстрое сканирование портов без использования потоков. А тогда как? Конечно, с помощью асинхронной работы с сетью. Можно создать несколько асинхронных сокетов и запустить ожидание соединения. Потом собрать все сокеты в набор fd_set и выполнить функцию select в ожидании события соединения с сервером. По завершении ее выполнения необходимо проверить все сокеты на удачное соединение и вывести результат.

Давайте попробуем реализовать это на примере. Для иллюстрации сказанного создайте новое приложение MFC Application на основе диалогового окна. При этом не включайте опцию поддержки WinSock в разделе Advanced Features. В данном случае мы будем использовать некоторые функции WinSock2. Поэтому подключите заголовочный файл winsock2.h вручную и укажите в свойствах проекта необходимость использования библиотеки ws2_32.lib. Все это мы уже не раз делали, и это не должно вызвать затруднений.

Теперь откройте в редакторе ресурсов главное окно программы. Оформите его в соответствии с 6.1. Здесь необходимо добавить три поля ввода Edit Box, список List Box и кнопку, по нажатии которой будет происходить сканирование. Для всех полей ввода нужно создать следующие переменные:

chHostName — имя или IP-адрес сканируемого компьютера;

chStartPort — порт, с которого надо начать сканирование;


chEndPort — порт, до которого нужно сканировать.

Портов очень много, и даже наш быстрый сканер затратит на это немало времени.



6.1. Окно будущей программы FastScan

Теперь перейдем к программированию. Создайте обработчик события BN_CLICKED для кнопки, по нажатии которой должно начинаться сканирование. Код, который здесь нужно написать, достаточно большой (см. листинг 6.2), но несмотря на то, что он есть на компакт-диске, я советую набрать его вручную. Только в этом случае вы сможете разобраться в предназначении каждой строчки. Я же постараюсь дать вам всю необходимую информацию.



Листинг 6.2. Быстрое сканирование портов
void CFastScanDlg::OnBnClickedButton1() { char tStr[255]; SOCKET sock[MAX_SOCKETS]; int busy[MAX_SOCKETS], port[MAX_SOCKETS]; int iStartPort, iEndPort, iBusySocks = 0; struct sockaddr_in addr; fd_set fdWaitSet;

WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { SetDlgItemText(IDC_STATUSTEXT, "Can't load WinSock"); return; }

SetDlgItemText(IDC_STATUSTEXT, "Resolving host");

chStartPort.GetWindowText(tStr, 255); iStartPort = atoi(tStr); chEndPort.GetWindowText(tStr, 255); iEndPort = atoi(tStr);

chHostName.GetWindowText(tStr, 255);

struct hostent *host=NULL; host = gethostbyname(tStr); if (host == NULL) { SetDlgItemText(IDC_STATUSTEXT, "Unable to resolve host"); return; }

for (int i = 0; i MAX_SOCKETS; i++) busy[i] = 0;

SetDlgItemText(IDC_STATUSTEXT, "Scanning");

while (((iBusySocks) || (iStartPort = iEndPort))) { for (int i = 0; i MAX_SOCKETS; i++) { if (busy[i] == 0 iStartPort = iEndPort) { sock[i] = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock[i] 0) { SetDlgItemText(IDC_STATUSTEXT, "Socket filed"); return; } iBusySocks++; addr.sin_family = AF_INET; addr.sin_port = htons (iStartPort); CopyMemory(addr.sin_addr, host-h_addr_list[0], host-h_length);

ULONG ulBlock; ulBlock = 1; if (ioctlsocket(sock[i], FIONBIO, ulBlock) == SOCKET_ERROR) { return; }



connect(sock[i], (struct sockaddr *) addr, sizeof (addr)); if (WSAGetLastError() == WSAEINPROGRESS) { closesocket (sock[i]); iBusySocks--; } else { busy[i] = 1; port[i] = iStartPort; } iStartPort++; } } FD_ZERO (fdWaitSet); for (int i = 0; i MAX_SOCKETS; i++) { if (busy[i] == 1) FD_SET (sock[i], fdWaitSet); }

struct timeval tv; tv.tv_sec = 1; tv.tv_usec = 0;

if (select (1, NULL, fdWaitSet, NULL, tv) == SOCKET_ERROR) { SetDlgItemText(IDC_STATUSTEXT, "Select error"); return; }

for (int i = 0; i MAX_SOCKETS; i++) { if (busy[i] == 1) { if (FD_ISSET (sock[i], fdWaitSet)) { int opt; int Len = sizeof(opt); if (getsockopt(sock[i], SOL_SOCKET, SO_ERROR, (char*)opt, Len) == SOCKET_ERROR) SetDlgItemText(IDC_STATUSTEXT, "getsockopt error");

if (opt == 0) { struct servent *tec; itoa(port[i],tStr, 10); strcat(tStr, " ("); tec = getservbyport(htons (port[i]), "tcp"); if (tec==NULL) strcat(tStr, "Unknown"); else strcat(tStr, tec-s_name);

strcat(tStr, ") - open"); m_PortList.AddString(tStr); busy[i] = 0; shutdown(sock[i], SD_BOTH); closesocket(sock[i]); } busy[i] = 0; shutdown (sock[i], SD_BOTH); closesocket (sock[i]); iBusySocks--; } else { busy[i] = 0; closesocket(sock[i]); iBusySocks--; } } } ProcessMessages(); } WSACleanup(); SetDlgItemText(IDC_STATUSTEXT, "Scaning complete"); return; }

В данном примере для сканирования используются три массива:

sock — массив дескрипторов сокетов, которые ожидают соединения;

busy — состояние сканируемых портов. Любой из них может быть занят и вызвать ошибку. В файле помощи по WinSock написано, что не каждый порт можно использовать. Поэтому элемент массива, номер которого соответствует такому занятому (зарезервированному) порту, делается равным 1, в противном случае — присваивается 0;

port — массив сканируемых портов. В принципе, можно было бы обойтись и без этого массива, но для упрощения кода я его ввел.

В этом примере есть одна новая функция, которую мы не рассматривали, — getservbyport. Она выглядит следующим образом:



struct servent FAR * getservbyport ( int port, const char FAR * proto );

Функция возвращает информацию о сервисе, работающем на порту, указанном первым параметром. Второй параметр определяет протокол. В качестве результата возвращается структура типа servent, в которой поле s_name содержит символьное описание сервиса. Если функция вернет нулевое значение, то невозможно определить по номеру порта параметры работающего сервиса.

Данные, которые возвращает функция getservbyport, не являются точными, и ее легко обмануть. Например, для порта с номером 21 функция будет всегда возвращать информацию о протоколе FTP (File Transfer Protocol), но никто вам не мешает запустить на этом порту Web-сервер, и функция getservbyport не сможет этого определить.

Все остальное вам уже должно быть знакомо, но я подведу итоги, описав используемый алгоритм:

Загрузить сетевую библиотеку.

Определить адрес сканируемого компьютера до начала цикла. Этот адрес будет использоваться внутри цикла перебора портов в структуре sockaddr_in. Сама структура будет заполняться в цикле, потому что каждый раз будет новый порт, а адрес изменяться не будет, поэтому его определение вынесено за пределы цикла. Нет смысла на каждом этапе цикла делать одну и ту же операцию, тем более, что определение IP-адреса может занять время, если указано имя сканируемого компьютера.

Запустить цикл, который будет выполняться, пока начальный порт не превысит конечный. Внутри этого большого цикла выполняются следующие действия:


    запустить цикл от 0 до значения MAX_SOCKETS. В этом цикле создается сокет, переводится в асинхронный режим и запускается функция connect. Так как сокеты находятся в асинхронном режиме, то не будет происходить ожидания соединения и замораживания программы, но при этом и неизвестно, произошло соединение или нет;

    обнулить переменную fdWaitSet типа fd_set;

    запустить цикл от 0 до значения MAX_SOCKETS. В этом цикле все сокеты помещаются в набор fd_set;

    ожидать события от сокета с помощью функции select;



    запустить цикл от 0 до значения MAX_SOCKETS. В этом цикле проверяется, какие сокеты удачно соединились с сервером. Если соединение прошло успешно, то получить символьное имя порта с помощью функции getsockopt. После этого сокет закрыть, чтобы разорвать соединение с сервером;

    Выгрузить сетевую библиотеку.

    Что такое MAX_SOCKETS? Это константа, которая определяет количество сканируемых сокетов. В данном примере она равна 40, и это оптимальное значение для различных сред. Чем больше количество сокетов, сканируемых за один проход, тем быстрее оно будет проходить.

    Еще один недостаток — сканирование блокирует работу программы, поэтому открытые порты вы сможете увидеть только после окончания сканирования, когда программа освободится и перерисует окно. Чтобы избежать заморозки можно написать следующую процедуру:

    void ProcessMessages() { MSG msg; while (PeekMessage(msg,NULL,0,0,PM_NOREMOVE)) { if (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } else return; } }

    Эта функция содержит простой цикл — обработчик сообщений, который вы уже не раз видели в Win32-приложениях. В данном случае он не бесконечный, и обрабатывает все сообщения, накопившиеся в очереди. А когда они заканчиваются, цикл прерывается, и программа будет продолжать сканирование.



    6.2. Результат сканирования моего компьютера

    Напишите саму функцию где-нибудь в начале модуля и вставьте вызов ProcessMessages() в конце цикла поиска портов. В этом случае вы избавитесь от заморозки и сможете увидеть открытые порты сразу.

    Стоит еще заметить, что в данном случае использовался протокол, который отображает открытые TCP-порты. Он никак не связан с UDP-портами. Чтобы сканировать UPD, необходимо создавать сокет (функция socket), ориентированный на сообщения.

    Примечание
    Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\FastScan.

    Содержание раздела