构建端口扫描器

网络安全Beginner
立即练习

介绍

欢迎来到「构建端口扫描器」实验。

端口扫描器是网络安全中的基础工具,用于识别目标机器上开放的端口及运行的服务。像 Nmap 这样的工具功能极其强大,但对于任何有志于从事网络安全或网络工程的专业人士来说,理解其底层工作原理至关重要。

在本实验中,你将使用 Python 从零开始构建一个自定义的 TCP 端口扫描器。你将首先学习如何解析命令行参数,然后实现核心的套接字(socket)连接逻辑来检测端口是否开放。接着,你将添加异常处理以增强工具的健壮性,最后通过实现多线程技术来大幅提升扫描速度。实验结束时,你将拥有一个功能完备、速度快捷的端口扫描器,并对 Python 网络编程有扎实的理解。

本实验的教育目的并非为了替代 Nmap 等成熟工具,而是为了拆解这些工具所自动化的各个环节:输入处理、连接尝试、超时控制、结果解读以及性能权衡。一旦你亲手构建了一个基础扫描器,专业的扫描工具对你而言将不再神秘。

解析命令行参数

在这一步中,你将通过创建一个能够接收命令行参数的 Python 脚本,为端口扫描器打下基础。命令行参数允许用户在不修改代码的情况下,指定目标 IP 地址和想要扫描的端口范围。

这是将工具逻辑与用户输入分离的一个典型示例。优秀的安全性工具之所以可重用,是因为代码保持不变,而操作员可以通过命令行更改参数。

我们将使用 Python 内置的 argparse 模块,它能让你轻松编写用户友好的命令行界面。

首先,确保你位于项目目录中。你将在此处创建并运行 Python 脚本。

cd ~/project

使用 nano 文本编辑器创建一个名为 scanner.py 的新文件:

nano scanner.py

将以下代码复制并粘贴到编辑器中。这段代码导入了 argparse 模块,并定义了一个函数来处理目标 IP 和端口范围的输入。

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}")

按下 Ctrl+O 然后按 Enter 保存文件。如果你想直接继续下一步,可以保持在 nano 中继续编辑同一个文件。仅当你想要返回终端时才使用 Ctrl+X。

现在,通过 Python 运行脚本来测试它。我们将传入本地回环地址(127.0.0.1)和一个较小的端口范围(7990-8010)。

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

你应该会看到类似以下的输出:

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

如果你尝试在不带 -t 参数的情况下运行脚本,argparse 会自动生成错误消息并显示正确用法,这使得你的工具在终端中更易于使用。

实现套接字连接逻辑

在这一步中,你将实现端口扫描器的核心功能:套接字连接。

在协议层面,基础的 TCP 连接扫描非常简单:尝试与目标端口建立 TCP 连接,并观察是否成功。这一理念足以解释绝大多数端口扫描器的行为。

套接字(socket)是计算机网络中用于发送或接收数据的端点。为了检查端口是否开放,你的扫描器将尝试与目标 IP 地址的特定端口建立 TCP 连接。如果连接成功,则端口开放;如果连接失败或超时,则端口关闭或被过滤。

如果 scanner.py 仍在 nano 中打开,请继续编辑。否则,请重新打开它:

nano ~/project/scanner.py

更新你的代码以包含 socket 模块并添加扫描逻辑。用以下代码替换文件的全部内容:

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.")

使用 Ctrl+O 和 Enter 保存文件。你可以留在 nano 中继续下一步,因为除非你想检查进度,否则无需立即测试脚本。

在这段代码中,socket.socket(socket.AF_INET, socket.SOCK_STREAM) 创建了一个 TCP 套接字。connect_ex() 方法对于扫描器特别有用,因为它返回一个错误指示符(例如 0 表示成功),而不是在连接失败时抛出异常,这使得我们的代码更加简洁。

针对你的本地机器运行更新后的脚本。实验环境中有一个运行在 8000 端口的隐藏 Web 服务器,让我们扫描一个包含该端口的范围:

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

你应该会看到类似以下的输出,成功识别出 8000 端口为开放状态:

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

[+] Port 8000 is OPEN

[*] Scan completed.

根据实验环境的不同,你可能还会看到其他开放端口。此步骤的关键结果是确认 8000 端口显示为开放。

处理连接异常

在这一步中,你将通过添加异常处理来增强扫描器的健壮性。

安全工具的大部分生命周期都在不完美的环境中度过:错误的输入、无法访问的主机、缓慢的网络以及被中断的运行。异常处理绝非装饰,它是确保工具在实际操作中可用的必要组成部分。

网络操作是不可预测的。用户可能会提供无效的主机名、网络可能会断开,或者用户可能想通过按下 Ctrl+C 中途取消扫描。如果没有适当的异常处理,你的 Python 脚本将会崩溃,并向用户显示难看的追踪错误信息。

我们将使用 try-except 块来捕获这些特定错误并优雅地处理它们。

如果你仍在编辑 scanner.py,请继续在同一个 nano 会话中工作。否则,请重新打开它:

nano ~/project/scanner.py

更新你的代码以包含异常处理。用以下更新后的版本替换文件内容:

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.")

使用 Ctrl+O 和 Enter 保存文件。如果你想立即测试异常处理,请使用 Ctrl+X 退出并运行下方的命令。如果不想,你可以直接继续最后一步,仅在准备好运行最终扫描器时才退出 nano。

现在,让我们测试异常处理。尝试使用无效的端口格式运行脚本,以触发 ValueError:

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

你应该会看到一条清晰的错误消息,而不是 Python 崩溃:

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

接下来,开始扫描一个较大的端口范围,并在扫描过程中按下键盘上的 Ctrl+C 来取消它:

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

当你按下 Ctrl+C 时,脚本将捕获 KeyboardInterrupt 并优雅地退出:

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

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

如果扫描在你中断之前就完成了,请使用更大的范围(如 1-9000)再次运行,然后在扫描过程中按下 Ctrl+C。

添加多线程以提升速度

在最后一步中,你将解决当前扫描器最大的问题:速度。

目前,你的扫描器一次只检查一个端口。如果某个端口被过滤且需要 1 秒才能超时,那么扫描 1,000 个端口将耗时 1,000 秒。为了解决这个问题,我们将使用多线程。多线程允许程序同时运行多个任务。我们不再等待一个端口检查完成后才开始下一个,而是为每个端口创建一个新线程。

如果你保持 scanner.py 打开,请继续编辑。否则,请重新打开它:

nano ~/project/scanner.py

导入 threading 模块并更新主循环以使用线程。用以下最终版本替换文件内容:

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.")

保存文件(Ctrl+O, Enter)并退出(Ctrl+X)。

注意我们是如何添加 threading.Thread(target=scan_port, args=(target_ip, port)) 的。这告诉 Python 在后台运行 scan_port 函数。thread.join() 循环确保主程序在打印「Scan completed.」之前等待所有后台扫描任务完成。

现在,运行一个大范围的端口扫描。你会发现它比之前的顺序版本快得多。

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

输出将类似于:

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

[+] Port 8000 is OPEN

[*] Scan completed.

根据实验环境中运行的服务,你可能会看到除 8000 之外的其他开放端口,这是正常的。

你已经成功使用 Python 构建了一个快速的多线程 TCP 端口扫描器。

总结

在本实验中,你成功使用 Python 从零开始构建了一个自定义的多线程 TCP 端口扫描器。你首先学习了如何使用 argparse 模块解析命令行参数,使工具更加灵活。接着,你深入网络编程,通过实现套接字连接来检测开放端口。为了确保工具的健壮性,你添加了异常处理,以优雅地管理网络错误和用户中断。最后,你实现了多线程技术,将一个缓慢的顺序脚本转变为高速并发扫描器。

这种实践经验揭开了专业网络扫描工具底层运作的神秘面纱,并为进一步探索网络安全和 Python 开发打下了坚实的基础。