Einen Port-Scanner entwickeln

CybersicherheitBeginner
Jetzt üben

Einführung

Willkommen zum Lab „Einen Port-Scanner entwickeln“.

Ein Port-Scanner ist ein grundlegendes Werkzeug in der Netzwerksicherheit, mit dem offene Ports und Dienste auf einem Zielsystem identifiziert werden können. Tools wie Nmap sind unglaublich leistungsfähig, aber für jeden angehenden Cybersicherheitsexperten oder Netzwerktechniker ist es unerlässlich, deren Funktionsweise im Detail zu verstehen.

In diesem Lab bauen Sie von Grund auf Ihren eigenen benutzerdefinierten TCP-Port-Scanner mit Python. Sie beginnen mit dem Parsen von Befehlszeilenargumenten und implementieren anschließend die grundlegende Socket-Verbindungslogik, um zu prüfen, ob ein Port offen ist. Danach fügen Sie eine Fehlerbehandlung hinzu, um Ihr Tool robust zu machen, und implementieren schließlich Multithreading, um den Scanvorgang drastisch zu beschleunigen. Am Ende dieses Labs verfügen Sie über einen funktionalen, schnellen Port-Scanner und ein fundiertes Verständnis der Netzwerkprogrammierung in Python.

Das pädagogische Ziel ist es nicht, ausgereifte Tools wie Nmap zu ersetzen. Es geht darum, die beweglichen Teile offenzulegen, die diese Tools automatisieren: Eingabeverarbeitung, Verbindungsversuche, Timeouts, Ergebnisinterpretation und Leistungsabwägungen. Sobald Sie selbst einen einfachen Scanner gebaut haben, verlieren professionelle Scanner ihren geheimnisvollen Charakter.

Befehlszeilenargumente parsen

In diesem Schritt legen Sie das Fundament für Ihren Port-Scanner, indem Sie ein Python-Skript erstellen, das Befehlszeilenargumente akzeptiert. Befehlszeilenargumente ermöglichen es Benutzern, die Ziel-IP-Adresse und den zu scannenden Portbereich festzulegen, ohne den Code ändern zu müssen.

Dies ist ein Beispiel für die Trennung von Tool-Logik und Benutzereingabe. Gute Sicherheitstools sind wiederverwendbar, da der Code gleich bleibt, während der Anwender die Parameter über die Befehlszeile ändert.

Wir verwenden das in Python integrierte Modul argparse, das das Schreiben benutzerfreundlicher Befehlszeilenschnittstellen erleichtert.

Stellen Sie zunächst sicher, dass Sie sich in Ihrem Projektverzeichnis befinden. Hier werden Sie Ihr Python-Skript erstellen und ausführen.

cd ~/project

Erstellen Sie eine neue Datei namens scanner.py mit dem Texteditor nano:

nano scanner.py

Kopieren Sie den folgenden Code und fügen Sie ihn in den Editor ein. Dieser Code importiert das Modul argparse und definiert eine Funktion zur Verarbeitung der Eingaben für die Ziel-IP und den Portbereich.

import argparse
import sys

def get_arguments():
    parser = argparse.ArgumentParser(description="A simple TCP port scanner")
    parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
    parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")

    options = parser.parse_args()
    return options

if __name__ == "__main__":
    options = get_arguments()

    ## Extract the target and ports from the user's input
    target_ip = options.target
    ports_input = options.ports

    print(f"[*] Target set to: {target_ip}")
    print(f"[*] Port range set to: {ports_input}")

Speichern Sie die Datei mit Ctrl+O und drücken Sie anschließend Enter. Wenn Sie direkt mit dem nächsten Schritt fortfahren möchten, können Sie in nano bleiben und die Datei weiter bearbeiten. Verwenden Sie Ctrl+X nur, wenn Sie zum Terminal zurückkehren möchten.

Testen Sie nun Ihr Skript, indem Sie es mit Python ausführen. Wir übergeben die Localhost-IP (127.0.0.1) und einen kleinen Portbereich (7990-8010).

python3 scanner.py -t 127.0.0.1 -p 7990-8010

Sie sollten eine Ausgabe sehen, die in etwa so aussieht:

[*] Target set to: 127.0.0.1
[*] Port range set to: 7990-8010

Wenn Sie versuchen, das Skript ohne das Argument -t auszuführen, generiert argparse automatisch eine Fehlermeldung und zeigt die korrekte Verwendung an, was die Bedienung Ihres Tools über das Terminal erleichtert.

Socket-Verbindungslogik implementieren

In diesem Schritt implementieren Sie die Kernfunktionalität des Port-Scanners: die Socket-Verbindung.

Auf Protokollebene ist ein einfacher TCP-Connect-Scan simpel: Versuchen Sie, eine TCP-Verbindung zu einem Zielport aufzubauen und beobachten Sie, ob dies gelingt. Diese eine Idee reicht aus, um das Verhalten einer großen Klasse von Port-Scannern zu erklären.

Ein Socket ist ein Endpunkt zum Senden oder Empfangen von Daten über ein Computernetzwerk. Um zu prüfen, ob ein Port offen ist, versucht Ihr Scanner, eine TCP-Verbindung zur Ziel-IP-Adresse auf einem bestimmten Port herzustellen. Wenn die Verbindung erfolgreich ist, ist der Port offen; wenn sie fehlschlägt oder ein Timeout auftritt, ist der Port geschlossen oder gefiltert.

Wenn scanner.py noch in nano geöffnet ist, bearbeiten Sie es weiter. Andernfalls öffnen Sie es erneut:

nano ~/project/scanner.py

Aktualisieren Sie Ihren Code, um das Modul socket einzubinden und die Scan-Logik hinzuzufügen. Ersetzen Sie den gesamten Inhalt der Datei durch den folgenden Code:

import argparse
import sys
import socket

def get_arguments():
    parser = argparse.ArgumentParser(description="A simple TCP port scanner")
    parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
    parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
    return parser.parse_args()

def scan_port(ip, port):
    ## Create a new socket using the IPv4 address family and TCP protocol
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ## Set a timeout of 1 second so the scanner doesn't hang on closed ports
    sock.settimeout(1)

    ## connect_ex returns 0 if the connection was successful
    result = sock.connect_ex((ip, port))
    if result == 0:
        print(f"[+] Port {port} is OPEN")

    ## Always close the socket after using it
    sock.close()

if __name__ == "__main__":
    options = get_arguments()
    target_ip = options.target

    ## Parse the start and end ports from the input string
    start_port, end_port = map(int, options.ports.split('-'))

    print(f"[*] Scanning target {target_ip} from port {start_port} to {end_port}...\n")

    ## Loop through the specified range and scan each port sequentially
    for port in range(start_port, end_port + 1):
        scan_port(target_ip, port)

    print("\n[*] Scan completed.")

Speichern Sie die Datei mit Ctrl+O und Enter. Sie können in nano bleiben und mit dem nächsten Schritt fortfahren, da das Skript noch nicht getestet werden muss, es sei denn, Sie möchten Ihren Fortschritt überprüfen.

In diesem Code erstellt socket.socket(socket.AF_INET, socket.SOCK_STREAM) einen TCP-Socket. Die Methode connect_ex() ist besonders nützlich für Scanner, da sie einen Fehlerindikator (wie 0 für Erfolg) zurückgibt, anstatt eine Exception auszulösen, wenn eine Verbindung fehlschlägt. Das hält unseren Code sauber.

Führen Sie Ihr aktualisiertes Skript gegen Ihre lokale Maschine aus. Die Lab-Umgebung hat einen versteckten Webserver, der auf Port 8000 läuft. Scannen wir also einen Bereich, der diesen einschließt:

python3 scanner.py -t 127.0.0.1 -p 7990-8010

Sie sollten eine Ausgabe ähnlich der folgenden sehen, die Port 8000 erfolgreich als offen identifiziert:

[*] Scanning target 127.0.0.1 from port 7990 to 8010...

[+] Port 8000 is OPEN

[*] Scan completed.

Je nach Lab-Umgebung sehen Sie möglicherweise auch andere offene Ports in der Ausgabe. Das wichtige Ergebnis für diesen Schritt ist, dass Port 8000 als offen angezeigt wird.

Verbindungs-Exceptions behandeln

In diesem Schritt machen Sie Ihren Scanner robuster, indem Sie eine Fehlerbehandlung (Exception Handling) hinzufügen.

Sicherheitstools verbringen einen Großteil ihrer Zeit unter unvollkommenen Bedingungen: fehlerhafte Eingaben, nicht erreichbare Hosts, langsame Netzwerke und unterbrochene Abläufe. Fehlerbehandlung ist keine Dekoration. Sie ist Teil dessen, was das Tool im realen Betrieb einsetzbar macht.

Netzwerkoperationen sind unvorhersehbar. Der Benutzer könnte einen ungültigen Hostnamen angeben, das Netzwerk könnte abbrechen oder der Benutzer möchte den Scan mitten im Prozess durch Drücken von Ctrl+C abbrechen. Ohne eine ordnungsgemäße Fehlerbehandlung würde Ihr Python-Skript abstürzen und dem Benutzer einen unschönen Traceback-Fehler anzeigen.

Wir verwenden try-except-Blöcke, um diese spezifischen Fehler abzufangen und sie elegant zu behandeln.

Wenn Sie scanner.py noch bearbeiten, arbeiten Sie in derselben nano-Sitzung weiter. Andernfalls öffnen Sie es erneut:

nano ~/project/scanner.py

Aktualisieren Sie Ihren Code, um die Fehlerbehandlung einzubinden. Ersetzen Sie den Inhalt durch die folgende aktualisierte Version:

import argparse
import sys
import socket

def get_arguments():
    parser = argparse.ArgumentParser(description="A simple TCP port scanner")
    parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
    parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
    return parser.parse_args()

def scan_port(ip, port):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1)
        result = sock.connect_ex((ip, port))
        if result == 0:
            print(f"[+] Port {port} is OPEN")
        sock.close()
    except socket.gaierror:
        ## This exception is raised for address-related errors, like an invalid hostname
        print("\n[-] Hostname could not be resolved.")
        sys.exit(1)
    except socket.error:
        ## This catches general socket errors
        print("\n[-] Could not connect to server.")
        sys.exit(1)

if __name__ == "__main__":
    options = get_arguments()
    target_ip = options.target

    try:
        start_port, end_port = map(int, options.ports.split('-'))
    except ValueError:
        print("[-] Invalid port format. Please use start-end (e.g., 1-1024)")
        sys.exit(1)

    print(f"[*] Scanning target {target_ip} from port {start_port} to {end_port}...\n")

    try:
        for port in range(start_port, end_port + 1):
            scan_port(target_ip, port)
    except KeyboardInterrupt:
        ## This catches the Ctrl+C signal from the user
        print("\n[-] Scan canceled by user. Exiting...")
        sys.exit(1)

    print("\n[*] Scan completed.")

Speichern Sie die Datei mit Ctrl+O und Enter. Wenn Sie die Fehlerbehandlung sofort testen möchten, beenden Sie nano mit Ctrl+X und führen Sie die unten stehenden Befehle aus. Wenn nicht, können Sie direkt mit dem letzten Schritt fortfahren und nano erst verlassen, wenn Sie bereit sind, den fertigen Scanner auszuführen.

Testen wir nun die Fehlerbehandlung. Versuchen Sie, das Skript mit einem ungültigen Portformat auszuführen, um den ValueError auszulösen:

python3 scanner.py -t 127.0.0.1 -p 8000,8001

Sie sollten eine saubere Fehlermeldung anstelle eines Python-Absturzes sehen:

[-] Invalid port format. Please use start-end (e.g., 1-1024)

Starten Sie als Nächstes einen Scan über einen großen Portbereich und drücken Sie während des Laufs Ctrl+C auf Ihrer Tastatur, um ihn abzubrechen:

python3 scanner.py -t 127.0.0.1 -p 1-5000

Wenn Sie Ctrl+C drücken, fängt das Skript den KeyboardInterrupt ab und beendet sich ordnungsgemäß:

[*] Scanning target 127.0.0.1 from port 1 to 5000...

[-] Scan canceled by user. Exiting...

Falls Ihr Scan abgeschlossen ist, bevor Sie ihn unterbrechen können, führen Sie ihn erneut mit einem größeren Bereich wie 1-9000 aus und drücken Sie dann Ctrl+C, während er noch läuft.

Multithreading für Geschwindigkeit hinzufügen

In diesem letzten Schritt lösen Sie das größte Problem Ihres aktuellen Scanners: die Geschwindigkeit.

Im Moment prüft Ihr Scanner einen Port nach dem anderen. Wenn ein Port gefiltert ist und 1 Sekunde für das Timeout benötigt, dauert das Scannen von 1.000 Ports 1.000 Sekunden. Um dies zu beheben, verwenden wir Multithreading. Multithreading ermöglicht es Ihrem Programm, mehrere Aufgaben gleichzeitig auszuführen. Anstatt darauf zu warten, dass eine Port-Prüfung abgeschlossen ist, bevor die nächste beginnt, starten wir für jeden Port einen neuen Thread.

Wenn Sie scanner.py geöffnet gelassen haben, bearbeiten Sie es weiter. Andernfalls öffnen Sie es erneut:

nano ~/project/scanner.py

Importieren Sie das Modul threading und aktualisieren Sie die Hauptschleife, um Threads zu verwenden. Ersetzen Sie den Inhalt durch diese finale Version:

import argparse
import sys
import socket
import threading

def get_arguments():
    parser = argparse.ArgumentParser(description="A simple TCP port scanner")
    parser.add_argument("-t", "--target", dest="target", help="Target IP address", required=True)
    parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024)", default="1-1024")
    return parser.parse_args()

def scan_port(ip, port):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1)
        result = sock.connect_ex((ip, port))
        if result == 0:
            print(f"[+] Port {port} is OPEN")
        sock.close()
    except (socket.gaierror, socket.error):
        pass

if __name__ == "__main__":
    options = get_arguments()
    target_ip = options.target

    try:
        start_port, end_port = map(int, options.ports.split('-'))
    except ValueError:
        print("[-] Invalid port format. Please use start-end (e.g., 1-1024)")
        sys.exit(1)

    print(f"[*] Scanning target {target_ip} from port {start_port} to {end_port}...\n")

    try:
        ## Create a list to keep track of active threads
        threads = []

        for port in range(start_port, end_port + 1):
            ## Create a new thread for each port scan
            thread = threading.Thread(target=scan_port, args=(target_ip, port))
            threads.append(thread)
            thread.start()

        ## Wait for all threads to complete before exiting
        for thread in threads:
            thread.join()

    except KeyboardInterrupt:
        print("\n[-] Scan canceled by user. Exiting...")
        sys.exit(1)

    print("\n[*] Scan completed.")

Speichern Sie die Datei (Ctrl+O, Enter) und beenden Sie (Ctrl+X).

Beachten Sie, wie wir threading.Thread(target=scan_port, args=(target_ip, port)) hinzugefügt haben. Dies weist Python an, die Funktion scan_port im Hintergrund auszuführen. Die thread.join()-Schleife stellt sicher, dass das Hauptprogramm darauf wartet, dass alle Hintergrund-Scans abgeschlossen sind, bevor "Scan completed." ausgegeben wird.

Führen Sie nun einen Scan über einen großen Portbereich aus. Sie werden feststellen, dass er viel schneller abgeschlossen ist als die vorherige sequentielle Version.

python3 scanner.py -t 127.0.0.1 -p 1-9000

Die Ausgabe sieht in etwa so aus:

[*] Scanning target 127.0.0.1 from port 1 to 9000...

[+] Port 8000 is OPEN

[*] Scan completed.

Je nachdem, welche Dienste in der Lab-Umgebung laufen, sehen Sie möglicherweise neben 8000 noch weitere offene Ports. Das ist normal.

Sie haben erfolgreich einen schnellen, multithreaded TCP-Port-Scanner in Python erstellt.

Zusammenfassung

In diesem Lab haben Sie erfolgreich einen benutzerdefinierten, multithreaded TCP-Port-Scanner von Grund auf mit Python erstellt. Sie haben damit begonnen, zu lernen, wie man Befehlszeilenargumente mit dem Modul argparse parst, um Ihr Tool flexibel zu gestalten. Anschließend sind Sie in die Netzwerkprogrammierung eingestiegen, indem Sie Socket-Verbindungen implementiert haben, um offene Ports zu erkennen. Um sicherzustellen, dass Ihr Tool robust ist, haben Sie eine Fehlerbehandlung hinzugefügt, um Netzwerkfehler und Benutzerunterbrechungen elegant zu verwalten. Schließlich haben Sie Multithreading implementiert und ein langsames, sequentielles Skript in einen Hochgeschwindigkeits-Scanner umgewandelt.

Diese praktische Erfahrung lüftet das Geheimnis, wie professionelle Netzwerk-Scan-Tools im Hintergrund funktionieren, und bietet eine solide Grundlage für die weitere Erkundung der Netzwerksicherheit und der Python-Entwicklung.