Статья Создание сканера портов на Python. Часть 1: Быстрее чем Nmap

Приветствую, Codeby!

Большинство из вас слышали, использовали и продолжают активно использовать Nmap, как основной инструмент для сканирования портов. Nmap — проверенная временем утилита с огромным количеством скриптов, но можно ли лучше?

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

Теория:

sensors-20-04423-g002-550.jpg


Сканер портов — программа для проверки открытых портов на удаленном компьютере.
Мы рассмотрим два типа сканирования:

SYN сканирование — наиболее популярный способ и самый быстрый. Обычный TCP сканер использует сетевые функции операционной системы и делает трехэтапное соединение, в отличии от него SYN сканер сам создаёт IP пакеты, он делает так называемые полуоткрытые соеденения, эта техника быстрее, но иногда менее надёжная.

TCP Сканер, как я упомянул выше, использует сетевые функции ОС, делает трёхэтапное рукопожатие. Требует меньше прав для выполнения, медленный и используется когда SYN не даёт по какой либо причине результатов. Так как наша цель — скорость, то наш сканер будет работать на SYN.

Основная часть:

Подготовка:

Будем работать с библиотеками:

threading - для реализации мультипоточности.
socket - для самого сканирования.
os - для полной остановки все потоков.
subprocess - для выполнения команд.
sys - для получения аргументов.
psutil - для проверки процессов.
time - для того чтобы засечь время.
colorama - чтобы было красивее.

Импортируем нужные нам библиотеки:

Python:
import threading
import socket
import os
import subprocess
import sys
import psutil
import time
from colorama import Fore, Back, Style
Чтобы использовать инструмент на постоянной основе нужно сделать его удобным, добавим первые аргументы и получим их значения:
if("--target" in sys.argv):
    indexoftarget=sys.argv.index("--target")
    target=sys.argv[indexoftarget+1]
else:
    print("Target is not specified, see --help")
    exit()

Как видно из кода выше в аргументах(sys.argv) мы будем искать "--target", после чего назначаем переменной indexoftarget индекс аргумента который содержит данную строку.
Так как после "--target" юзер будет вводить цель, то нужно получить ее индекс, а он будет равняться индексу "--target" + 1 т. к. юзер всегда должен вводить значение после "--target"
Не важно в каком порядке юзер поставит аргументы, мы все равно найдем нужные.
Если же у нас нет цели для сканирования, то нужно завершить программу и назвать причину.
Так как юзер может быть незнаком с аргументами, нужно сделать команду "--help":

Python:
if("--help" in sys.argv):
    print("Usage: python3 rollerscan.py --target [target]")
    print("Additional flags:")
    print("--virtual-hosts (-vh) — try to find virtual hosts")
    print("--vuln (-v) — find possible exploits")
    print("--censys (-c) — use censys to search for additional info.")
    print("--port (-p) — specify port range for scan, by default 1-60 000")
    exit()

1, 2 и 3 флаг мы реализуем в следуещей части, а пока займемся флагом для определения портов.
Пока что мы реализуем только ввод промежутка портов, т.е через -.

Python:
if("--port" in sys.argv):
    indexofport=sys.argv.index("--port")
    port=sys.argv[indexofport+1]
    if("-" in port):
        port=port.split("-")
        end=int(port[1])
        start=int(port[0])
elif("-p" in sys.argv):
    indexofport=sys.argv.index("-p")
    port=sys.argv[indexofport+1]
    if("-" in port):
        port=port.split("-")
        end=int(port[1])
        start=int(port[0])
else:
    start=1
    end=60000

В первом if ситуация такая-же как и с target, поэтому ее обсуждать мы не будем.
Посмотрим на второй if, там мы проверим есть ли в значении которое ввел юзер "-", если есть, то с помощью "split" мы создадим из строки лист, используя разделитель "-".
Конечным портом для сканирования назначим port[1], а начальным port[0]
В elif реализуем тоже самое, но искать будем "-p", это сделано чтобы юзеру не приходилось каждый раз печатать "--port"
В else мы рассмотрим ситуацию когда ни "--port" ни "-p" найдены не были и зададим значения по умолчанию.
Перед началом сканирования хорошо бы убедиться что цель вообще доступна.

response=os.system("ping -c 1 " + target)
Зададим нужные нам в будущем переменные:
processes=[]
nmapdone={}

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

Python:
if (response==0):
    print("[", Fore.LIGHTCYAN_EX+"^"+Style.RESET_ALL, "]", Fore.YELLOW+target+Style.RESET_ALL, Fore.GREEN+"is UP"+Style.RESET_ALL)
if (response!=0):
    print("[", Fore.LIGHTCYAN_EX+"^"+Style.RESET_ALL, "]", Fore.LIGHTYELLOW_EX+target+Style.RESET_ALL, Fore.RED+"is DOWN"+Style.RESET_ALL)
    choise=input("Do you want to continue considiring that target is marked as DOWN? Y/N: ")
    if(choise=="Y" or choise=="y"):
        pass
    else:
        print(Fore.RED+"Shutting down")
        exit()

Само сканирование:

Проинформируем о начале сканирования и узнаем точное время запуска:

Python:
print("[", Fore.LIGHTCYAN_EX+"&"+Style.RESET_ALL, "]", Fore.BLUE+"Starting Scan!"+Style.RESET_ALL)
start_time=time.time()

Определим socket.socket(socket.AF_INET, socket.SOCK_STREAM) как s:
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Чтобы сканирование было быстрее, поставим лимит времени на один порт, после которого он определиться как закрытый:
s.settimeout(0.5)
Теперь сама функция сканирования портов:

Python:
def portscan(port):
    try:
        con = s.connect((target,port))
        print('[', Fore.LIGHTCYAN_EX+'*'+Style.RESET_ALL,']',Fore.YELLOW+f'''Port: {port}'''+Style.RESET_ALL, Fore.GREEN+"is opened."+Style.RESET_ALL)
        process=subprocess.Popen(f'''nmap -sV {target} -p {port}''', shell=True)
        processes.append(process.pid)
        con.close()
    except Exception as e:
        pass

Мы будем брать "port" и пробовать подключаться к порту, написать что он открыт, после чего автоматически вызовем nmap для определения сервиса и его версии на порте, записав процесс с ним в переменную "processes", после чего закроем подключение.
Если возникнет какая либо ошибка то порт пропускается как закрытый.

Теперь мультипоточность:

Python:
r = start
for r in range(start, end):
    try:
        t = threading.Thread(target=portscan,kwargs={'port':r})
        r += 1
        t.start()
    except KeyboardInterrupt:
        os._exit(0)
    except Exception as e:
        portscan(r)
        r += 1

"r" мы присвоем значение "start", т.е начального порта, после чего для каждой r в промежутке от начального до конечного порта мы будем создавать свой поток, где будет выполняться функции "portscan", где "port" будет равняться "r".
except KeyboardInterrupt нужен чтобы словить Ctrl+C и выполнить свою команду вместо exit(), т.к последняя закроет только один поток, а не всю прогрмму, но os._exit(0) справиться с этим отлично.
В случае если по какой либо причине поток не создался вызываем функцию по обычному.

Завершение программы:

Теперь перед завершением нужно удостовериться, что nmap просканировал порты, помните с помощью subprocess мы сохраняли в "processes" id процессов в которых запущен nmap?
Они нам нужны сейчас, с помощью psutil мы будем проверять не завершил ли работу процесс, если не завершил, то в переменную "nmapdone" запишем False, в других случаях True

Python:
def checkprocess():
    for proc in processes:
        if psutil.pid_exists(proc):
            nmapdone[proc]='False'
        else:
            nmapdone[proc]='True'

Запустим функцию:

checkprocess()
Если кто-то не понял почему мы используем словарь, а не лист, то мы это делаем, чтобы у нас не создалось бесконечное кол-во True И False, ведь при новой проверке старые значения из листа мы не удаеляем, а добавляем новые, в словаре же мы меняем значение трех процессов не добавляя новые.
Теперь пока там будет False мы будем проверять все снова и снова.
Python:
while 'False' in nmapdone.values():
    checkprocess()

Но это еще не все, нам нужно проверить завершились ли все потоки:

Python:
threadslist=int(threading.active_count())
while threadslist>1:
    threadslist=int(threading.active_count())
    time.sleep(0.000001)

Мы получим кол-во работающих потоков и будем проверять их значение до тех пор пока они больше чем один.
И финальная строка где мы получим время работы:

print(Fore.BLUE+"Scan of ports Ended in:"+Style.RESET_ALL, Fore.GREEN+str(round(time.time()-start_time))+Style.RESET_ALL, "s")

Заключение:

Мы написали быстрый сканер на Python, у меня он работает быстрее чем сам nmap:


В следующей части (которая совсем не за горами) напишем пару дополнительных скриптов для RollerScanner.
 
Последнее редактирование:

Сергей Попов

Кодебай
30.12.2015
4 718
6 705
BIT
369
При всем уважении, я в конце статьи привел тесты, где использовался обычный nmap -sV и скрипт из статьи. Там видно что скрипт работал быстрее, можно потестить nmap с разными параметрами, не думаю что что-то сильно измениться. По поводу библиотеки, вы имеете ввиду argparser вместо sys? Я вроде уже признал этот недочёт в комментариях. Не вижу причины стыдиться выкладывать статью.
У Вас появились поклонники. Это здорово :)
 
  • Нравится
Реакции: Revoltage

Revoltage

Grey Team
07.10.2020
42
94
BIT
7
Дичайший изврат запускать подпроцессы в потоках. Почему бы сразу не использовать ProcessPoolExecutor?
И 65к потоков... Серьезно? GIL: "ну да, ну да... пошел я нахер"
Я пишу вторую часть, спасибочки за указание на недочёты. Исправлю.
 
  • Нравится
Реакции: ace911 и anonym-s

breadrock1

Member
04.07.2019
6
0
BIT
0
Неужели твой код так быстро работает? Быстрее, чем существующие популярные аналоги?)
Как я понял, основной подход, описанный в данной статье, в ускорении работы программы, заключается в распараллеливании. Честно говоря, это очень сомнительная идея, учитывая, что Python - плохое решение для использования многопоточности с целью решение задач по ускорению выполнения. Причем многие комментарии, а также огромное количество статей в интернете от мастодонтов программирования, говорят об обратном эффекте использования многопоточности в Python проектах)

В основном, если опускаться до привитивизма, многопоточность используется для решения двух задач:
  1. Ускорение вычислений путём распараллеливания выполнения, задействуя вычислительные ресурсы ЭВМ;
  2. Абстрагирование действий пользователя и внутренних процессов программы. Примером решения подобной задачи является любое GUI-приложение.
Многопоточность в Python годится только для выполнения второй задачи, но никак не для ускорения работы выполнения (вычислений, в том числе сканирования). Конечно, есть хитрости, к примеру, некоторые библиотеки для Python вроде PyTorch (если не ошибаюсь с примером) скомпилированна JIT'ом, что позволяет ей работать, не обращая внимания на GIL. Однако в твоём коде никаких подобных хитростей я не заметил)

И так подведу вывод! При всем уважении к стараниям автора, мне статья совсем не понравилась. Вообще не понятно для чего она написана? Какую задачу/проблему решает или освещает? В чем реальные плюсы твоего подхода? Где результаты сравнений с другими сканерами? И самое главное - а почему пользователи должны скачивать именно твою программу, а не тот же самый NMAP?)

На эти вопросы я не нашел ответов в статье, более того у меня еще больше вопросов по самому коду :D
Автору удачи в начинаниях!
 
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!