Самый быстрый сканер портов
Потоки — это очень мощная и удобная вещь, позволяющая создать многозадачность даже внутри отдельного приложения. Но у них есть один большой недостаток — программисты, познакомившись с потоками, начинают использовать их везде, где это надо и не надо.
Я видел много сканеров, которые используют по 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. Быстрое сканирование портов |
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. |
![]() |
![]() |