介绍
欢迎来到「构建端口扫描器」实验。
端口扫描器是网络安全中的基础工具,用于识别目标机器上开放的端口及运行的服务。像 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 开发打下了坚实的基础。



