Сканирование портов выполняет законную роль в системном администрировании / владении. Точно подтверждая, к каким портам компьютер принимает подключения, можно убедиться, что операционная система не открыла ненужные порты для всего мира в процессе установки по умолчанию. Это также позволяет нам проверить, что наша система не была скомпрометирована и у нее открыты порты, позволяющие взломщику отправлять удаленные команды на нашу машину через Интернет.
Хотя сканирование портов TCP собственных портов является обычным явлением, многие недооценивают потенциальную опасность открытых портов UDP — это может привести к компрометации или указанию на то, что компромисс произошел. Как говорится на странице руководства nmap:
«Существует также программа-бэкдор cDc Back Orifice, которая скрывает настраиваемый порт UDP на компьютерах с Windows. Не говоря уже о многих обычно уязвимых службах, использующих UDP, таких как snmp, tftp, NFS и т. Д.»
Единственным серьезным препятствием для обычного домашнего пользователя, выполняющего сканирование портов своих собственных компьютеров, является отсутствие простого программного обеспечения для проведения сканирования для них. Не поймите меня неправильно — nmap — один из лучших доступных инструментов для сканирования портов — но насколько удобно Joe Average User использовать инструмент командной строки * nix? Мне кажется, что инструмент, который требует, чтобы г-н Средний пользователь только переходил по определенному URL-адресу и ждал, пока PHP-скрипт запускает сканирование TCP и UDP на своем компьютере, а затем возвращает результаты сканирования им, — вот что доктор приказал.
Имея это в виду, я начал искать в Интернете и нашел реализацию сканера портов TCP, который Джим Барселона кодировал и сделал доступным в мастере php
Большой! Половина проделанной работы позволила бы мне мгновенно извлечь сканер UDP-порта. Я имею в виду, насколько сложнее может быть кодирование сканера портов UDP? Значительно сложнее, как оказалось.
TCP-портсканирование в PHP
Когда сокет TCP создается с fsockopen
функции fsockopen
, вы указываете IP-адрес удаленного компьютера и номер порта, к которому хотите подключиться. Используя основную функциональность сокетов, PHP затем попытается создать виртуальный канал для удаленной машины на указанном порту, чтобы обеспечить дальнейшее взаимодействие. Если порт назначения недоступен, то поставщик TCP на удаленном компьютере отклонит запрос на подключение, а функция fsockopen
вернет логическое значение fsockopen
.
Итак, теперь у нас есть простой для понимания и реализации TCP-порт сканер. Мы устанавливаем цикл, который задает минимальные и максимальные номера портов, которые мы хотим сканировать, и внутри цикла пытаемся открыть соединение с текущим значением индекса цикла. Если попытка открыть порт не удалась, на этом порту нет службы. Если мы можем успешно открыть сокет для этого порта, то на этом порту есть служба, которую мы регистрируем в нашем массиве открытых портов в качестве ключа. Затем мы вставляем значение для этого ключа с помощью вызова getservbyport
, который возвращает сервис unix, который обычно регистрируется на этом порту. Наконец, мы закрываем открытый сокет и переходим к следующей итерации нашего цикла.
UDP портсканирование в PHP
Когда приложение желает отправить данные по сети, используя протокол UDP, оно передает данные UDP через назначенный номер порта, также сообщая UDP, на какой порт в системе назначения следует отправлять данные. Затем UDP создает сообщение UDP, помечая номера портов источника и назначения, которые затем передаются IP для доставки.
Когда UDP-пакет получен машиной назначения, программное обеспечение UDP просматривает заголовок пакета для номера порта назначения и передает полезную нагрузку любому зарегистрированному приложению, которое использует этот номер порта. Если ни одно приложение не зарегистрировано для указанного номера порта, то сообщение об ошибке «ICMP Destination Unreachable: Port Unreachable» возвращается на удаленный компьютер, и полезная нагрузка отбрасывается.
Создание сокета UDP в PHP аналогично созданию сокета TCP с несколькими отличиями. Вы по-прежнему вызываете функцию fsockopen
, но вы должны указать:
- что вы хотите использовать протокол UDP,
- IP-адрес удаленного компьютера и
- номер порта, к которому вы хотите подключиться.
В отличие от сокета TCP, на данный момент нет соединения с удаленным компьютером.
function _scanPort ($portNumber) { $handle = fsockopen($this-> targetIP, $portNumber, &$errno, &$errstr, 2); if (!$handle) { echo "$errno : $errstr <br/>"; } socket_set_timeout ($handle, $this-> timeout); $write = fwrite($handle,"x00"); if (!$write) { echo "error writing to port: $index.<br/>"; next; } $startTime = time(); $header = fread($handle, 1); $endTime = time(); $timeDiff = $endTime - $startTime; if ($timeDiff >= $this-> timeout) { fclose($handle); return 1; } else { fclose($handle); return 0; } }
Поскольку вызов для чтения из нашего сокета UDP будет блокироваться (дождаться получения данных), мы установили значение для функции set_socket_timeout
. Это говорит сокету прослушивать (блокировать) сокет только для ответа от удаленного компьютера в течение определенного периода времени. Если это время превышено, сокет прекратит прослушивание, и код продолжит работу. Скоро мы увидим, как мы используем это для распознавания открытого порта.
Поскольку UDP не подключен, на этом этапе виртуальный канал не настроен на удаленном компьютере. Вместо этого вы должны передать данные, которые вы хотите отправить, на удаленный компьютер через сокет, используя функцию fwrite
. Затем мы немедленно записываем время начала прослушивания ответа от удаленного компьютера и используем fread
для прослушивания сокета. На этом этапе может произойти одно из двух:
- удаленный сервер возвращает нам сообщение об ошибке «ICMP Destination Unreachable: Port Unreachable», и
fread
заканчивается, или - время ожидания сокета в ожидании ответа.
В любом случае мы теперь записываем время окончания прослушивания и выводим общее время, потраченное на прослушивание, вычитая время окончания из времени начала. Мы сравниваем это число с установленным нами значением времени ожидания сокета. Если оно меньше значения тайм-аута, то удаленный сервер вернул нам сообщение об ошибке «ICMP Destination Unreachable: Port Unreachable», и мы знаем, что порт закрыт. Если время ожидания сокета истекло, то мы знаем, что произошла одна из двух других вещей: приложение на этом порту ожидало получения действительной команды, или пакет был потерян при передаче (UDP не предлагает гарантированную доставку, и если пакет потерянный в пути, мы не собираемся получать никакой информации на этот счет).
Итак, мы создали средство, чтобы определить, есть ли что-нибудь в порту (то есть сокет, возвращенный до истечения времени ожидания), и теперь нам нужен способ определить, было ли время ожидания сокета результатом приложения, ожидающего законного данные, или пакет был потерян в пути. Самый простой способ сделать это — отправить несколько пакетов на этот порт, и если мы получим один ответ обратно без тайм-аута, то мы знаем, что там нет приложения, и порт закрыт. Но как мы можем сделать это таким образом, чтобы не слишком расточать пропускную способность и минимизировать время выполнения приложения?
Перед нашим полным сканированием портов UDP, которые класс получил для сканирования, мы собираемся провести сканирование портов меньшего размера очень высоко в диапазоне портов, чтобы минимизировать обнаружение законных открытых портов и проверить состояние сети между машины. Это начальное сканирование выполняется в
наш метод _networkProbe
. Так как большинство открытых портов UDP, которые открыты, находятся на порте 1024 или ниже, мы проведем сканирование в диапазоне портов 55000. Мы можем предположить, что любые порты, которые истекают так высоко в диапазоне портов, делали это из-за потерянных дейтаграмм, а не потому, что мы обнаружили открытый порт.
function _networkProbe ($noTrials=100, $startPortNumber=55000) { $endPortNumber = $startPortNumber + $noTrials; // temporarily set timeout to 2 seconds. we'll modify this with the // data that we get from this method $this-> timeout = 2; // setup a for loop to scan the ports for ($portNumber = $startPortNumber; $portNumber < $endPortNumber; $portNumber++) { $startTime = $this-> _getmicrotime(); $result = $this-> _scanPort($portNumber); $endTime = $this-> _getmicrotime(); $timeDiff = $endTime - $startTime; if (!$result) { $responsesArray[] = $timeDiff; $totalTime += $timeDiff; } } $noResponses = count($responsesArray); // if more than 40% of the datagrams timed out, abort the scan if ($noResponses < (.6 * $noTrial)) { echo "The connection is losing too many packets. Scan aborted. <br/>"; exit; } $averageResponseTime = $this-> _calcAvgResponseTime ($noResponses, $totalTime); $standardDeviation = $this-> _calcStdrDeviation ($responsesArray); // calculate the timeout value $timeoutValue = ceil($averageResponseTime + 4 * $standardDeviation); // calculate number of cleanup iterations we'll need // percentFalsePositive is the % of datagrams that we sent in // the trial that timed out $percentFalsePositives = ($noTrials - $noResponses)/$noTrials; // percentResponses is the % of datagrams that we sent in the trial // that returned (eg -- didn't timeout) $percentResponses = $noResponses/$noTrials; // calculate the total number of ports to be scanned in the // real scan $portRange = $this-> maxPort - $this-> minPort + 1; // estFalsePositives is the estimated number of false positives we // anticipate getting from the real scan $estFalsePositives = $portRange * $percentFalsePositives; $this-> cleanupIterations = $this-> _calcNoIterations ($estFalsePositives, $percentResponses, $portRange); if ($this-> debug == 1) { echo "<br/>"; echo "total time $totalTime<br/>"; echo "timeout value: " . $this-> timeout . "<br/>"; echo "cleanup iterations: " . $this-> cleanupIterations . "<br/>"; echo "<br/>"; flush(); } }
Итак, теперь мы знаем:
- количество пакетов, которые были потеряны из нашего образца,
- общее количество отправленных пакетов и
- среднее время, которое мы должны были прослушивать для каждого пакета, который отвечал без блокировки.
С учетом количества пакетов, потерянных из нашей выборки, общего количества пакетов, отправленных при нашем первоначальном сканировании, и диапазона количества портов, которые мы будем сканировать при основном сканировании, мы можем рассчитать количество итераций, которые нам понадобятся запустить и повторно протестировать обнаруженные открытые порты, чтобы устранить ложные срабатывания. Формула, которую мы используем для этого, представляет собой логарифм экспоненциального затухания, и его функциональность может быть найдена в методе _calcNoIterations
класса.
Мы также собираемся использовать среднее время ответа вызовов fread
которые вернули сообщение об ошибке «ICMP Destination Unreachable: Port Unreachable» и не заблокировали, чтобы вычислить стандартное отклонение этих отдельных времен. Мы умножаем стандартное отклонение в четыре раза (четыре сигмы) и добавляем его к нашему среднему времени отклика. Это позволяет нам минимизировать значение тайм-аута и при этом быть достаточно уверенным в том, что мы не исключаем слишком много сканирований, которые могли бы быть возвращены, если бы не истекло время ожидания. На данный момент эта проверка на самом деле является излишней, учитывая, что значение set_socket_timeout
не может быть установлено равным менее чем одной секунде, и именно там будет находиться большинство полученных значений времени ожидания. Однако, если значение времени ожидания сокета когда-либо изменяется, чтобы принимать значения менее одной секунды, мы можем ожидать уменьшения времени выполнения до пяти раз при устранении портов, на которых нет службы.
Использование класса UDP для сканирования портов
Класс сканирования портов UDP отлично справляется со сложностью сканирования портов UDP. Использование класса — это сама простота: включите класс и создайте новый объект, основанный на классе, который передает ему целевой IP-адрес, и, опционально, порт, с которого нужно начать сканирование, порт, с которого нужно завершить сканирование, и указание, является ли объект должен выводить информацию во время сканирования. По умолчанию начальный порт установлен на 1, конечный порт на 1024, и выход включен.
Затем вызовите метод doScan
объектов и присвойте его выходные данные переменной, которая будет содержать результаты сканирования в виде массива. Вот пример:
include ('classes/udpPortScanner.inc'); $udpScanner = new udpPortScanner("$REMOTE_ADDR"); $ports = $udpScanner-> doScan(); if (count($ports) == 0) { echo "no open udp ports detected.<br/>"; } else { echo "open udp ports:<br/>"; foreach ($ports as $portNumber => $service) { echo "$portNumber ($service)<br/>"; } }
закрытие
Есть две модификации, которые я должен сделать, и другая, которую я хотел бы сделать. Во-первых, вычисленное значение числа итераций для устранения ложных срабатываний привело к тому, что число было слишком низким, если условия сети хорошие, и я получал возвращенные порты, которые на самом деле не были открыты. Я изменил код, чтобы увеличить минимальное количество итераций очистки до значения пять, чтобы компенсировать это.
Сканер портов UDP сильно зависит от условий сети, существующих между двумя компьютерами. Во время тестирования у меня были друзья в Австралии и Южной Африке (я нахожусь в США), которые добровольно проводили сканирование, и сканеру было действительно трудно обнаружить закрытые порты. Это было связано с тем, что условия сети приводили к очень большому количеству пакетов, теряющихся при передаче, и возникали очень длительные периоды работы. В защиту сканера сканирование nmap первых 1024 портов UDP на хосте в Южной Африке заняло почти час, что указывает на то, что это характерно для сканирования портов UDP, а не для реализации этого сканера. Учитывая это, стало очевидно, что некоторые сетевые подключения были слишком плохими, чтобы выполнить сканирование за разумное время. Из-за этого я изменил код для прерывания, если во время начального сканирования сетевого тестирования было потеряно более 40% пакетов. Не стесняйтесь устранять или изменять это значение, но имейте в виду, что сканирование по поврежденному сетевому промежутку может занять значительное время.
Наконец, я также хотел бы собрать информацию о том, на какие порты Microsoft размещает свои различные сервисы, чтобы дополнить функцию getservbyport
, которая будет возвращать только те сервисы, которые сопоставлены с традиционными портами на * nix. Затем я мог бы указать, какая служба Microsoft работает на порте, и в более поздней версии указать, насколько это полезно, и описать способы отключения службы и закрытия порта, если он не используется.
Я хотел бы поблагодарить Дэвида Лакруа (David LaCroix) из внезапного сайта decelaration.com за помощь в настройке tcpdump для меня, чтобы проанализировать сканирование портов UDP, которое я проводил на одной из его добровольных машин. Его помощь сыграла важную роль в завершении отладки этого программного обеспечения. Кроме того, благодаря Дэниелу Богану из waferbaby.com и Заку МакГрегору из carfolio.com , оба из которых помогли определить, что транс-океаническое сканирование UDP-портов не является хорошей идеей.
Для получения дополнительной информации об интернет-протоколах я настоятельно рекомендую О’Рейли и Партнеры «Интернет-протоколы ядра: полное руководство» Эрика А. Холла. Вы можете найти больше информации на сайте O’Reilly .
Официальный дом для классов tcpPortScan и udpPortScan находится здесь . Вы можете проверить последние обновления классов и сообщить о любых проблемах, проблемах или предложениях, которые у вас есть.