Статья Создание сканера портов на 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.
 
Последнее редактирование:

Muxtar

Green Team
02.06.2021
132
62
@Revoltage Скажу Богу что ты зарегистрировался в Codeby.

Статья просто топ.
 
Последнее редактирование:

Pernat1y

Red Team
05.04.2018
1 444
132
Как видно из кода выше в аргументах(sys.argv) мы будем искать "--target", после чего назначаем переменной indexoftarget индекс аргумента который содержит данную строку.
Ну есть же


Не пишите бред
 
  • Нравится
Реакции: Muxtar

Revoltage

Grey Team
07.10.2020
42
86
@Revoltage Скажу Богу что ты зарегистрировался в Codeby.

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

Ну есть же

Не пишите бред
По поводу argparse, есть какая-то разница в результате?
 
Последнее редактирование модератором:
  • Нравится
Реакции: Muxtar

Trixxx

Grey Team
04.04.2020
183
145
Есть такой вопрос вот здесь:
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

Revoltage

Grey Team
07.10.2020
42
86
Есть такой вопрос вот здесь:
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

Trixxx

Grey Team
04.04.2020
183
145
Немного не понял вопроса.
Если вы поставите порты от 1 до 65535, то кол-во потоков будет ровняться значению конечного порта, они запустятся и каждый начнет сканирование своего порта, на моей системе от 1 до 65000 все успешно просканировалось за 24 секунды с учётом запуска nmap .
65k потоков?)
 
  • Нравится
Реакции: Muxtar

Revoltage

Grey Team
07.10.2020
42
86
Да, получается так.
На моей системе 65к потоков работают очень шустро и по тестам были самым быстрым вариантом, в следующей части планируется дать возможность настроить потоки юзеру, чтобы не возникло сюрпризов.
 
  • Нравится
Реакции: Muxtar

Trixxx

Grey Team
04.04.2020
183
145
Да, получается так.
На моей системе 65к потоков работают очень шустро и по тестам были самым быстрым вариантом, в следующей части планируется дать возможность настроить потоки юзеру, чтобы не возникло сюрпризов.
Круто.
Только поаккуратнее, а то ведь юзверь может разогнать потоки до сверхсветовой :)
 
  • Нравится
Реакции: Muxtar

Revoltage

Grey Team
07.10.2020
42
86
Круто.
Только поаккуратнее, а то ведь юзверь может разогнать потоки до сверхсветовой :)
Насколько помню, у ОС стоят ограничения не позволяющие создать потоков больше чем определенное кол-во.
В Linux ulimit отвечает за изменения ограничения, в Windows не знаю

Насколько помню, у ОС стоят ограничения не позволяющие создать потоков больше чем определенное кол-во.
В Linux ulimit отвечает за изменения ограничения, в Windows не знаю
Чтобы юзвери не разогнали до сверхсветовой скорость вращения кулера вместе со скоростью сканирования.
 
  • Нравится
Реакции: Muxtar

Revoltage

Grey Team
07.10.2020
42
86
В результате - нет. Вы получите содержимое аргументов.
В удобстве - очень даже. И проще, и, например, там есть встроенная проверка типов.
Да, в удобстве это действительно так.
Но по сути это просто автоматизация того, что мы сделали с if в начале, мне показалось что для обучения будет неплохо написать больше самому?
Гораздо удобнее использовать готовый MasScan который использует свой TCP стэк, который может сделать все за секунды, чем писать сканер самому, однако при изучении второе полезнее, согласитесь?
 
  • Нравится
Реакции: Muxtar

Pernat1y

Red Team
05.04.2018
1 444
132
Да, в удобстве это действительно так.
Но по сути это просто автоматизация того, что мы сделали с if в начале, мне показалось что для обучения будет неплохо написать больше самому?
Обучение - это замечательно, и я сам не люблю тянуть внешние (не из core) либы, но тут важно соблюдать баланс велосипедописания. Если есть возможность использовать уже готовый и специально для этого написанный код, который облегчит в дальнейшем поддержку и расширение продукта, то почему нет? Тем более либа уже в комплекте.
 
  • Нравится
Реакции: Muxtar

Revoltage

Grey Team
07.10.2020
42
86
Обучение - это замечательно, и я сам не люблю тянуть внешние (не из core) либы, но тут важно соблюдать баланс велосипедописания. Если есть возможность использовать уже готовый и специально для этого написанный код, который облегчит в дальнейшем поддержку и расширение продукта, то почему нет? Тем более либа уже в комплекте.
Возможно действительно было бы лучше использовать argparser, но не велика потеря
 
Последнее редактирование:
  • Нравится
Реакции: Muxtar и Pernat1y

Xiao bai

One Level
15.10.2021
6
2
Дичайший изврат запускать подпроцессы в потоках. Почему бы сразу не использовать ProcessPoolExecutor?
И 65к потоков... Серьезно? GIL: "ну да, ну да... пошел я нахер"
 

Shadow User

Green Team
10.07.2017
138
29
Постыдились бы это говно выкладывать!
Быстрее чем на С не будет! Хотите быстрее чем Python, используйте Golang или Rust.
Хочешь показать пример, используй стандартную библиотеку, а не костыли.
 

Revoltage

Grey Team
07.10.2020
42
86
Постыдились бы это говно выкладывать!
Быстрее чем на С не будет! Хотите быстрее чем Python, используйте Golang или Rust.
Хочешь показать пример, используй стандартную библиотеку, а не костыли.
При всем уважении, я в конце статьи привел тесты, где использовался обычный nmap -sV и скрипт из статьи. Там видно что скрипт работал быстрее, можно потестить nmap с разными параметрами, не думаю что что-то сильно измениться. По поводу библиотеки, вы имеете ввиду argparser вместо sys? Я вроде уже признал этот недочёт в комментариях. Не вижу причины стыдиться выкладывать статью.
 
Мы в соцсетях:

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