Статья Создание сканера портов на 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.
 
Последнее редактирование:
@Revoltage Скажу Богу что ты зарегистрировался в Codeby.

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


Не пишите бред
 
  • Нравится
Реакции: Muxtar
@Revoltage Скажу Богу что ты зарегистрировался в Codeby.

Статья просто топ. А NMAP ерунда
Иронично что написанный выше код использует этот самый Nmap.

Ну есть же

Не пишите бред
По поводу argparse, есть какая-то разница в результате?
 
Последнее редактирование модератором:
  • Нравится
Реакции: Muxtar
Есть такой вопрос вот здесь:
Python:
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
Если мой диапазон будет (1, 65535) как будут работать потоки в цикле?
 
  • Нравится
Реакции: Muxtar
Есть такой вопрос вот здесь:
Python:
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
Если мой диапазон будет (1, 65535) как будут работать потоки в цикле?
Немного не понял вопроса.
Если вы поставите порты от 1 до 65535, то кол-во потоков будет ровняться значению конечного порта, они запустятся и каждый начнет сканирование своего порта, на моей системе от 1 до 65000 все успешно просканировалось за 24 секунды с учётом запуска nmap .

Есть такой вопрос вот здесь:
Python:
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
Если мой диапазон будет (1, 65535) как будут работать потоки в цикле?
Тесты с док-во скорости и работоспособности можете оценить в конце статьи.
 
  • Нравится
Реакции: Muxtar
Немного не понял вопроса.
Если вы поставите порты от 1 до 65535, то кол-во потоков будет ровняться значению конечного порта, они запустятся и каждый начнет сканирование своего порта, на моей системе от 1 до 65000 все успешно просканировалось за 24 секунды с учётом запуска nmap .
65k потоков?)
 
  • Нравится
Реакции: Muxtar
Да, получается так.
На моей системе 65к потоков работают очень шустро и по тестам были самым быстрым вариантом, в следующей части планируется дать возможность настроить потоки юзеру, чтобы не возникло сюрпризов.
 
  • Нравится
Реакции: ace911 и Muxtar
Да, получается так.
На моей системе 65к потоков работают очень шустро и по тестам были самым быстрым вариантом, в следующей части планируется дать возможность настроить потоки юзеру, чтобы не возникло сюрпризов.
Круто.
Только поаккуратнее, а то ведь юзверь может разогнать потоки до сверхсветовой :)
 
  • Нравится
Реакции: ace911 и Muxtar
Круто.
Только поаккуратнее, а то ведь юзверь может разогнать потоки до сверхсветовой :)
Насколько помню, у ОС стоят ограничения не позволяющие создать потоков больше чем определенное кол-во.
В Linux ulimit отвечает за изменения ограничения, в Windows не знаю

Насколько помню, у ОС стоят ограничения не позволяющие создать потоков больше чем определенное кол-во.
В Linux ulimit отвечает за изменения ограничения, в Windows не знаю
Чтобы юзвери не разогнали до сверхсветовой скорость вращения кулера вместе со скоростью сканирования.
 
  • Нравится
Реакции: ace911 и Muxtar
По поводу argparse, есть какая-то разница в результате?
В результате - нет. Вы получите содержимое аргументов.
В удобстве - очень даже. И проще, и, например, там есть встроенная проверка типов.
 
  • Нравится
Реакции: ace911 и Muxtar
В результате - нет. Вы получите содержимое аргументов.
В удобстве - очень даже. И проще, и, например, там есть встроенная проверка типов.
Да, в удобстве это действительно так.
Но по сути это просто автоматизация того, что мы сделали с if в начале, мне показалось что для обучения будет неплохо написать больше самому?
Гораздо удобнее использовать готовый MasScan который использует свой TCP стэк, который может сделать все за секунды, чем писать сканер самому, однако при изучении второе полезнее, согласитесь?
 
  • Нравится
Реакции: ace911 и Muxtar
Да, в удобстве это действительно так.
Но по сути это просто автоматизация того, что мы сделали с if в начале, мне показалось что для обучения будет неплохо написать больше самому?
Обучение - это замечательно, и я сам не люблю тянуть внешние (не из core) либы, но тут важно соблюдать баланс велосипедописания. Если есть возможность использовать уже готовый и специально для этого написанный код, который облегчит в дальнейшем поддержку и расширение продукта, то почему нет? Тем более либа уже в комплекте.
 
  • Нравится
Реакции: Muxtar
Обучение - это замечательно, и я сам не люблю тянуть внешние (не из core) либы, но тут важно соблюдать баланс велосипедописания. Если есть возможность использовать уже готовый и специально для этого написанный код, который облегчит в дальнейшем поддержку и расширение продукта, то почему нет? Тем более либа уже в комплекте.
Возможно действительно было бы лучше использовать argparser, но не велика потеря
 
Последнее редактирование:
  • Нравится
Реакции: ace911, Muxtar и Pernat1y
Дичайший изврат запускать подпроцессы в потоках. Почему бы сразу не использовать ProcessPoolExecutor?
И 65к потоков... Серьезно? GIL: "ну да, ну да... пошел я нахер"
 
Постыдились бы это говно выкладывать!
Быстрее чем на С не будет! Хотите быстрее чем Python, используйте Golang или Rust.
Хочешь показать пример, используй стандартную библиотеку, а не костыли.
 
Постыдились бы это говно выкладывать!
Быстрее чем на С не будет! Хотите быстрее чем Python, используйте Golang или Rust.
Хочешь показать пример, используй стандартную библиотеку, а не костыли.
При всем уважении, я в конце статьи привел тесты, где использовался обычный nmap -sV и скрипт из статьи. Там видно что скрипт работал быстрее, можно потестить nmap с разными параметрами, не думаю что что-то сильно измениться. По поводу библиотеки, вы имеете ввиду argparser вместо sys? Я вроде уже признал этот недочёт в комментариях. Не вижу причины стыдиться выкладывать статью.
 
Мы в соцсетях:

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