Статьи

Создайте сканер портов в Ruby

zoll, schmugel, сканер, Иногда мы помещаем себя в наш мир только для HTTP. Иногда Rails затмевает все другие аспекты Ruby.

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

Сканеры портов обычно не то, что мы ожидаем от Rubyists; это то, с чем люди C имеют дело. Многие люди удивляются, когда узнают, что Metasploit , удивительный набор для проникновения, на самом деле написан на Ruby.

Правда в том, что Ruby превратился в невероятно универсальный язык, что делает его инструментом, который можно использовать для самых разных целей, в том числе для систем, таких как сканер портов.

Давайте погрузимся прямо в!

Основы

Прежде всего, что такое сканер портов? Это именно то, на что это похоже; он принимает IP-адрес и сообщает, какие порты «открыты», т. е. по каким портам компьютер готов к обмену данными.

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

Наконец, как мы узнаем, открыты порты или нет? На самом деле существует масса способов сделать это, потому что TCP / IP (протокол, который компьютеры обычно используют для общения друг с другом) предоставляет множество различных способов (все с различными преимуществами и недостатками) для проверки порта. Самый простой (и тот, который мы будем использовать) называется «сканирование TCP-соединения».

Идея состоит в том, чтобы попытаться подключиться к хосту по указанному порту, если хост отвечает, порт открыт, в противном случае это не так. Этот подход имеет недостатки. Если вы злоумышленник, хост, вероятно, зарегистрирует запрос. Во-вторых, он гораздо менее эффективен, чем некоторые другие методы (такие как сканирование TCP SYN).

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

Давайте углубимся в код.

Простой метод

Мы собираемся использовать библиотеку сокетов, предоставляемую Ruby, для достижения наших целей. Вот код:

require 'socket' #socket library def open_port(host, port) sock = Socket.new(:INET, :STREAM) raw = Socket.sockaddr_in(port, host) puts "#{port} open." if sock.connect(raw) rescue (Errno::ECONNREFUSED) rescue(Errno::ETIMEDOUT) end def main(host, start_port, end_port) until start_port == end_port do open_port(host, start_port) start_port += 1 end end main ARGV[0], ARGV[1].to_i, ARGV[2].to_i 

Как вы можете сказать, код очень прост. Прежде чем мы углубимся в детали, вы можете запустить сканер на своем компьютере:

 ruby scanner.rb localhost 1 100 

Это будет запускать сканер на локальном хосте (то есть на машине, на которой вы запускали код), проверяя все порты от 1 до 100 включительно.

Код принимает адрес для сканирования в качестве первого аргумента, начальный порт в качестве второго аргумента и конечный порт в качестве третьего и печатает открытые порты. Давайте разберем код, чтобы увидеть, как он это делает.

Прежде всего, используйте массив ARGV для чтения аргументов из командной строки. Передайте эти аргументы в метод main, который затем просто перебирает необходимые порты и проверяет, открыты ли они с open_port метода open_port (который также печатает выходные данные).

open_port функционирует, как обсуждалось, открывая соединение с машиной, которую мы сканируем на указанном порту, чтобы увидеть, отвечает ли он. Но эта реализация имеет несколько проблем. Он ожидает сканирования порта, прежде чем перейти к следующему. Тем не менее, это явно не нужно; порядок, в котором мы проверяем порты, не имеет отношения к результатам. Итак, давайте попробуем написать версию сканера портов, которая не ждет.

Сделать еще один шаг

как нам это сделать? Очевидно, нам нужен какой-то способ делегирования задач, чтобы они могли выполняться вне «нормального» хода вещей, т.е. нам не нужно ждать их. Обычно это называется параллелизмом.

В Ruby есть несколько проблем с параллелизмом. Во-первых, «официальная» (то есть MRI) реализация Ruby реализует систему, в которой отдельные «потоки» (вроде как процессы, за исключением более легких) не могут работать одновременно; все они работают под одной нитью. Это означает, что две вещи не могут происходить в один и тот же момент. Таким образом, мы не можем сканировать два порта одновременно. Однако это не означает, что в этом случае Ruby бесполезен — если мы разделим наши порты на разные потоки, эти потоки все равно можно запланировать для запуска, пока мы ожидаем других сокетов.

Как мы будем иметь дело со всеми этими потоками? Я лично являюсь поклонником библиотеки Celluloid для Ruby, которая использует модель Actor, чтобы упростить работу с потоками. На самом деле, мне так нравится, что я даже написал об этом статью , которую вы, вероятно, должны прочитать, так как я буду использовать Celluloid в оставшейся части этой статьи.

Вот очень быстрая и грязная реализация, которую мы собираемся улучшить:

 require 'celluloid' require 'socket' class ScanPort include Celluloid def initialize(port, host) @port = port @host = host end def run begin sock = Socket.new(:INET, :STREAM) raw = Socket.sockaddr_in(@port, @host) puts "#{@port} open." if sock.connect(raw) rescue if sock != nil sock.close end end end end def main host = ARGV[0] start_port = ARGV[1].to_i end_port = ARGV[2].to_i until start_port == end_port do sp = ScanPort.new start_port, host sp.async.run start_port += 1 end end main 

Ничего себе, это кажется довольно сложным на первый взгляд. На самом деле, мы использовали много кода из нашей предыдущей версии. Прежде всего, мы определяем класс ScanPort который будет использоваться для порождения «актеров» (в основном, объектов в потоках). Обратите внимание на строку include в классе; это по существу делает класс актером.

У нас есть метод run внутри класса ScanPort (я просто решил вызвать его как run ; мы могли бы вызвать его как угодно). Он выполняет практически то же самое, что и метод openport в более ранней версии, за исключением того, что он закрывает сокет сразу после проверка порта. Мы делаем это так, чтобы количество открытых сокетов не превышало системный предел.

main метод довольно прост. Самая важная строка — строка 36 — обратите внимание, что мы не вызываем sp.run , вместо этого это sp.async.run . Это вызывает метод run асинхронно, то есть мы движемся дальше, не дожидаясь его завершения.

Еще один важный момент, который следует отметить, это то, что в примере кода для краткости и простоты пропущено множество обработчиков ошибок — в нем обязательно должна использоваться какая-то группа контроля (это концепция целлулоида).

При запуске это дает примерно тот же результат, что и раньше. Вы можете заметить, что он работает примерно в то же или большее время, чем синхронная версия! Дело в том, что мы запускаем поток для каждого порта в предоставленном нами диапазоне — это отнимает время и ресурсы! Что если вместо этого мы предоставим каждому субъекту только диапазон портов для проверки? Это будет означать меньше потоков, поэтому меньше времени потребуется для запуска и закрытия потоков! Давайте попробуем:

 require 'celluloid' require 'socket' class ScanPort include Celluloid def initialize(start_port, end_port, host) @start_port = start_port @end_port = end_port @host = host end def run until @start_port == @end_port do scan @start_port @start_port += 1 end end def scan(port) begin sock = Socket.new(:INET, :STREAM) #build the socket raw = Socket.sockaddr_in(port, @host) puts "#{port} open." if sock.connect(raw) rescue if sock != nil sock.close end end end end def main host = ARGV[0] start_port = ARGV[1].to_i end_port = ARGV[2].to_i segment_size = 100 until start_port >= end_port do sp = ScanPort.new start_port, start_port + segment_size, host sp.async.run start_port += segment_size end end main 

Код в основном такой же, однако мы добавили переменную segment_size , которую вы можете изменить в зависимости от того, сколько портов вы хотите обработать для каждого участника. Слишком большое значение этого числа будет означать, что параллелизма будет мало, а слишком низкое значение будет означать, что много времени уходит на создание и уничтожение потоков. Я запустил от 1 до 2000 портов на моей машине, и получилось примерно 50% реального времени синхронной версии. Очевидно, что на это влияет множество факторов, но это общая идея.

Если вы хотите, чтобы ваш код выполнялся с «реальным» параллелизмом, то есть с потоками, работающими в то же время, что и другие, вам придется выбрать реализацию Ruby, в которой нет проблемы «все в одном потоке». JRuby — популярный выбор.

Завершение

Портовые сканеры являются невероятно интересным программным обеспечением. Надеюсь, вам понравилось создание версии сканера портов (хотя и довольно простой) в Ruby.