python开发端口扫描

1. 前言

在很多时候编写程序工具的时候,能够熟练的使用库中的函数是很好的,但是也并非需要背下来,那么多的参数背下来也不容易,而且正常情况下我们不是主要搞开发的,背下来你不经常使用也会忘记,我想需要做到的应该是不懂后能够通过百度搜索函数来获取知识,看懂即可。

2. 端口扫描方式

端口扫描是网络安全领域中的一种技术,用于检测目标主机上哪些端口是开放的,从而了解目标主机上可能运行的服务和潜在的安全风险。端口扫描的方式多种多样,主要包括以下几种:

全文源码获取:端口扫描源码

2.1. TCP扫描(全连接扫描)

原理:利用TCP协议的三次握手过程,扫描器尝试与目标主机的每个端口建立完整的TCP连接。如果连接成功,则认为该端口是开放的;如果连接失败(如被拒绝或超时),则认为该端口是关闭的。

优点:实现简单,不需要特殊权限,扫描速度快。

缺点:容易被目标主机上的防火墙或入侵检测系统(IDS)发现并记录,从而暴露扫描行为。

2.2. SYN扫描(半开放扫描)

原理:扫描器向目标端口发送一个SYN数据包(TCP三次握手中的第一个数据包),但不完成后续的三次握手过程。如果目标端口开放,会回应一个SYN-ACK数据包;如果端口关闭,会回应一个RST数据包。扫描器根据收到的回应来判断端口状态。

优点:隐蔽性较高,因为不建立完整的TCP连接,所以不会在目标主机的日志中留下记录。

缺点:需要攻击者拥有客户机的root权限,且可能被一些高级防火墙或IDS检测出来。

2.3. UDP扫描

原理:由于UDP是无连接的协议,扫描器向目标端口发送UDP数据包,并根据目标主机的回应来判断端口状态。如果目标端口未开放,通常会返回一个ICMP端口不可达消息;如果端口开放但没有相应服务监听,则可能不会有任何回应。

优点:可以扫描UDP端口,补充TCP扫描的不足。

缺点:UDP扫描的可靠性较低,因为UDP是无连接的协议,数据包可能会丢失或不被处理。此外,一些网络设备或防火墙可能会过滤或修改ICMP消息,导致扫描结果不准确。

2.4. ICMP扫描

原理:通过发送ICMP Echo请求(Ping)来检测目标主机是否在线。虽然这不是直接的端口扫描方式,但可以用来确定哪些IP地址是活动的,从而缩小后续端口扫描的范围。

优点:实现简单,可以快速确定目标网络中的活动主机。

缺点:容易被防火墙或路由器过滤掉ICMP请求和回应。

3. python编写端口扫描

现在有很多的工具都可以实现端口扫描,而很多情况下我们默认使用别人提供的工具来使用,而很少知道原理或者底层,而这次希望能够用最简单的方式来实现端口扫描,跨出脚本小子,虽然该用工具还是用工具,至少我们要懂得底层的原理是什么。

3.1. socket库

socket库是 Python 标准库中的一个重要部分,它提供了对底层网络接口的访问。通过使用 socket库,Python 程序可以创建网络连接、发送和接收数据。这使得 Python 能够实现各种网络应用,包括客户端和服务器程序、网络爬虫、聊天应用等。

3.1.1. socket库常用参数

3.1.1.1. socket.socket() 参数

family(地址族): 指定地址族,最常用的有 socket.AF_INET(IPv4 地址)和 socket.AF_INET6(IPv6 地址)。

type(套接字类型): 指定套接字类型,常用的有 socket.SOCK_STREAM(TCP 套接字,面向连接的)和 socket.SOCK_DGRAM(UDP 套接字,无连接的)。

proto(协议号): 大多数情况下,该参数为 0,因为协议是由地址族和套接字类型自动选择的。但在某些特殊情况下,可能需要显式指定协议号。

3.1.1.2. socket.bind() 参数

address(地址): 一个包含主机名和端口号的元组,用于将套接字绑定到一个特定的地址和端口上。对于 IPv4 地址,主机名可以是一个点分十进制的 IP 地址(如 '192.168.1.1')或特殊值 '0.0.0.0'(表示绑定到所有可用的网络接口)。对于 IPv6 地址,主机名应该是一个包含 IPv6 地址的字符串,并可能包括作用域 ID(对于链路本地地址)。

3.1.1.3. socket.listen() 参数

backlog(监听队列大小): 指定在拒绝连接之前,系统应该为等待接受的连接请求排队的最大数量。大多数情况下,这个值设置为 5 到 10 就足够了,但在高负载情况下可能需要更高的值。有些系统可能会忽略这个参数,并使用默认值。

3.1.1.4. socket.connect() 参数

address(地址): 一个包含服务器地址和端口号的元组,用于建立到服务器的连接。对于客户端程序,这是连接到服务器所必需的。

3.1.1.5. socket.send()socket.recv() 参数

buffer(缓冲区): 要发送或接收的数据的字节对象(bytes)。send() 方法将这个缓冲区中的数据发送到连接的套接字,而 recv() 方法从套接字接收最多 buffersize 字节的数据。

buffersize(缓冲区大小): recv() 方法的一个可选参数,指定要接收的最大字节数。如果省略此参数,则通常使用默认值(通常是 8192 字节或更大)。

3.1.1.6. 其他参数

虽然不直接作为 socket 方法的参数,但在使用 socket 库时还会遇到其他一些与套接字行为相关的参数,例如:

timeout(超时时间): 可以通过 socket.settimeout(timeout) 方法设置套接字的超时时间(以秒为单位)。如果在指定的时间内没有发生 I/O 操作,则 socket 将抛出一个 socket.timeout 异常。如果设置为 None,则表示套接字没有超时限制。

reuse_addr(地址重用): 可以通过 socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 来设置地址重用选项。这允许在同一端口上启动服务器程序的新实例,即使旧实例仍在其超时时间范围内处于 TIME_WAIT 状态。

3.1.2. 常用参数使用

3.1.2.1. 创建 Socket

在 Python 中,使用 socket.socket() 函数来创建一个新的 socket 对象。这个函数接受两个参数:地址族(AF_INET 用于 IPv4 地址,AF_INET6 用于 IPv6 地址)和套接字类型(SOCK_STREAM 表示 TCP 套接字,SOCK_DGRAM 表示 UDP 套接字)。

import socket  
  
# 创建一个 IPv4 TCP 套接字  
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3.1.2.2. 连接到服务器

对于客户端程序,可以使用 connect() 方法连接到服务器。这个方法需要一个包含服务器地址和端口的元组作为参数。

# 连接到服务器  
server_address = ('localhost', 10000)  
s.connect(server_address)
3.1.2.3. 绑定端口

对于服务器程序,需要使用 bind() 方法将套接字绑定到一个特定的地址和端口上。这通常是在服务器开始监听连接之前进行的。

# 绑定到地址和端口  
host = 'localhost'  
port = 12345  
s.bind((host, port))
3.1.2.4. 监听连接

服务器程序还需要调用 listen() 方法来开始监听传入的连接。这个方法可以指定一个参数,表示最大连接数,但大多数情况下可以省略此参数或设置为 0(表示无限制,但受限于系统资源)。

# 开始监听  
s.listen(1)
3.1.2.5. 接受连接

服务器使用 accept() 方法来接受一个连接。这个方法会阻塞,直到一个连接到达。当连接到达时,它返回一个包含新连接的 socket 对象和一个地址的元组。

# 接受连接  
conn, address = s.accept()  
print('Connected by', address)
3.1.2.6. 发送和接收数据

一旦建立了连接,就可以使用 send()recv() 方法来发送和接收数据了。send() 方法发送数据,而 recv() 方法接收数据。这两个方法都可以指定一个缓冲区大小作为参数。

# 发送数据  
message = 'Hello, world'  
conn.sendall(message.encode('utf-8'))  
  
# 接收数据  
data = conn.recv(1024)  
print('Received', repr(data))

3.2. 端口扫描基础案例

这里就编写一个基础的案例,简短的测试一下代码的连通性。

公众号回复"240906"获取所有源代码

import socket   ##引入socket库


def port_scan(ip, port):  ##获取IP地址及端口
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  ##创建一个基于网络并且使用TCP协议的套接字
        s.settimeout(1)  ##设置超时时间
        s.connect((ip, port))  ##用于服务器连接,我们去连接别人,我们是作为客户端,对方为服务端
        print("[+]{}:{} \t open".format(ip, port))  ##拼接输出
    except socket.error as e:
        print("[-]{}:{} \t closed or not responding".format(ip, port))  ##不通的输出
    except Exception as e:
        print(e)  ##报错提示


ip = "10.108.13.51"
port = 33891
port_scan(ip, port)  ##传参

在这里插入图片描述

3.2.1. 手动输入地址端口

这里我们将IP地址以及端口都设置成手动添加的。

import socket


def port_scan(ip, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        s.connect((ip, port))
        print("[+]{}:{} \t open".format(ip, port))
    except socket.error as e:
        print("[-]{}:{} \t closed or not responding".format(ip, port))
    except Exception as e:
        print(e)


ip = input("请输入IP地址:")
port = int(input("请输入端口:"))
port_scan(ip, port)

在这里插入图片描述

3.2.2. 批量端口扫描

在上述中基本上我们已经实现端口扫描了,不过这个端口扫描是单个端口,无法实现多个端口扫描,例如我们要扫描0到65535端口,那么我们该如何实现这类功能,当然我们也可以使用读取文件的功能来实现,不过这类方式你需要手动输入6万多的数字,显然是不现实的,这里我们就可以使用循环以及字符串处理来解决这些问题。

其中在scan_ports 函数这里,它首先检查端口范围字符串是否包含-:作为分隔符,然后使用split方法将字符串拆分为起始和结束端口,并使用map函数将它们转换为整数。之后,它使用一个for循环遍历从起始端口到结束端口的每个端口,并对每个端口调用port_scan函数。

import socket


def port_scan(ip, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)  # 设置超时时间为1秒
        s.connect((ip, port))  # 尝试连接到指定的IP和端口
        print("[+]{}:{} \t open".format(ip, port))  # 如果连接成功,打印端口开放信息
        s.close()  # 关闭socket连接
    except socket.error as e:
        print("[-]{}:{} \t closed or not responding".format(ip, port))  # 如果连接失败,打印端口关闭或未响应信息
    except Exception as e:
        print(e)  # 捕获并打印其他所有类型的异常


def scan_ports(ip, port_range):
    try:
        # 解析端口范围,检查是否使用-或:作为分隔符
        if '-' in port_range:  ##使用split方法将字符串拆分为起始和结束端口,使用map函数将它们转换为整数,
            start, end = map(int, port_range.split('-'))
        elif ':' in port_range:
            start, end = map(int, port_range.split(':'))
        else:
            print("无效的端口范围格式,请使用'start-end'或'start:end'的格式。")
            return
        
        if start > end: ##判断开始是否大于结束
            raise ValueError("起始端口不能大于结束端口。")

            # 循环扫描端口
        for port in range(start, end + 1):
            port_scan(ip, port)  ##传参给port_scan函数使用。

    except ValueError as e:  ##抛出异常
        print(e)


# 用户输入
ip = input("请输入IP地址: ")
port_range = input("请输入端口范围(如1-100或1:100): ")
scan_ports(ip, port_range)

在这里插入图片描述

简单来说,这个整体的代码中还是存在很多错误未处理的,比如没有判断用户是否真实的输入了IP地址,虽然最后确实有一条可以判断的,但是无法在最开始就让用户重新输入,还有比如,全部输错后,整个程序会自动结束,并不会让用户重新输入,而这个也简单,直接做个循环就可以了,这里就自行解决。

3.2.3. 优化扫描

不知道这里有没有发现,我设置的是多个端口扫描,而当输入单个端口后则会报错,那么这样该如何解决,同时解决一些上述提到的一些问题,例如判断用户是否输入了IP地址,程序如何自动重新回到开始执行等等。

这里优化的内容:

端口可以输入三种类型,“3389”、“3389,3390,3391”、“3389-3391”

IP地址可以判断,用户是否未输入。

添加了循环,并且通过输入"q"来进行退出控制。

import socket


def port_scan(ip, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        s.connect((ip, port))
        print(f"[+] {ip}:{port} \t open")
    except socket.error:
        print(f"[-] {ip}:{port} \t closed or not responding")
    finally:
        # 无论连接是否成功,都关闭socket连接
        s.close()


def scan_ports(ip, port_input):
    # 检查是否为单个端口或多个用逗号分隔的端口
    if ',' not in port_input and port_input.isdigit():
        # 如果是单个端口,直接扫描
        port_scan(ip, int(port_input))
    elif ',' in port_input:
        # 如果是多个端口,分割并逐个扫描
        ports = []  # 初始化一个空列表来存储转换后的端口号
        port_strings = port_input.split(',')  # 使用split方法将字符串按逗号分割成列表
        # 遍历分割后的列表
        for port_string in port_strings:
            # 检查当前字符串是否全部由数字组成
            if port_string.isdigit():
                # 如果是,则将其转换为整数并添加到ports列表中
                ports.append(int(port_string))

        for port in ports:
            port_scan(ip, port)
    else:
        # 尝试解析为端口范围(只接受'-'作为分隔符)
        try:
            # 如果 port_input 包含 '-',则使用 '-' 作为分隔符来分割字符串,并且限制分割次数为 1(即只分割第一个出现的 '-')。
            parts = port_input.split('-', 1)
            # 首先检查parts的长度是否为2
            if len(parts) == 2:
                # 然后检查parts中的每个元素是否都是数字
                if all(part.isdigit() for part in parts):
                    # 如果两个条件都满足,使用map函数将parts中的每个字符串转换为整数
                    # 并将转换后的整数解包赋值给start和end
                    start, end = map(int, parts)
                    # 检查起始端口是否小于等于结束端口
                    if start <= end:
                        # 循环扫描端口范围
                        for port in range(start, end + 1):
                            port_scan(ip, port)
                    else:
                        print("起始端口不能大于结束端口。")
            else:
                # 如果不是'-'分隔或分割后不是两个有效的数字,给出错误提示
                print("无效的端口输入,请使用'端口号'、'端口号,端口号'(多个非连续端口)或'起始端口-结束端口'的格式。")
        except ValueError:
            # 如果在转换端口为整数时发生错误(例如,输入了非数字字符),打印错误信息
            print("端口输入包含非数字字符,请确保输入有效的端口号或端口范围。")


def main():
    print(f"""  
程序名称: 端口扫描器  
作者: Yu to 
开发时间: 2024年9月  
版本号: 1.0  
使用说明:
    -IP地址及端口输入时,输入'q'或'quit'即可退出!
    -IP地址仅支持单IP输入。
    -端口支持单个或多个,格式为:'端口号'、'端口号,端口号'或'起始端口-结束端口'
""")
    while True:
        # 获取用户输入的IP地址
        ip = input("请输入IP地址: ").strip().lower()
        if ip in ('q', 'quit'):
            print("已退出程序!")
            break
        if not ip:
            print("IP地址不能为空,请重新输入!")
            continue

            # 获取用户输入的端口信息
        port_input = input(
            "请输入端口: ").strip().lower()
        if port_input in ('q', 'quit'):
            print("已退出程序!")
            break
        if not port_input:
            print("端口不能为空,请重新输入!")
            continue

            # 调用scan_ports函数进行端口扫描
        scan_ports(ip, port_input)


if __name__ == "__main__":
    main()

在这里插入图片描述

3.3. ipaddress库

上述代码中都是使用手动输入IP地址,而我们更多的时候处理的是多个IP或者IP段,这里的话手动输入就不方便了,这里就需要使用到ipaddress库对文件中每一行的IP地址段进行处理。

这里只是简短的说明如何使用,具体的很多参数还需要自行的去学习参考,简单来说,我也说不全,只能到使用的时候,用到常见的。

ipaddress 是 Python 的一个标准库,用于处理 IPv4 和 IPv6 地址以及网络。它提供了丰富的功能来创建、操作、比较和迭代 IP 地址和网络。

3.3.1. ipaddress库常用参数

3.3.1.1. 常用类
ipaddress.IPv4Address类
表示一个 IPv4 地址。可以通过字符串或整数(作为网络字节顺序的整数)来创建实例。

ipaddress.IPv6Address类
表示一个 IPv6 地址。同样可以通过字符串或整数来创建实例。

ipaddress.IPv4Network类
表示一个 IPv4 网络,包括网络地址、广播地址和子网掩码。可以用来确定一个 IP 地址是否属于该网络。

ipaddress.IPv6Network类
表示一个 IPv6 网络,功能与 `IPv4Network` 类似,但用于 IPv6 地址。

ipaddress.ip_interface类
一个工厂函数,用于根据提供的字符串创建 `IPv4Interface` 或 `IPv6Interface` 实例。这些接口对象表示具有特定网络掩码长度的 IP 地址。
3.3.1.2. 常用方法

由于 ipaddress 库主要通过其类来提供功能,所以“方法”通常指的是这些类的方法。以下是一些常用方法:

__init__(构造函数)
用于创建 IPv4Address、IPv6Address、IPv4Network 或 IPv6Network 的实例。
通常通过传递一个表示 IP 地址或网络的字符串来调用。

is_private
返回一个布尔值,指示 IP 地址或网络是否是私有地址。

is_global
返回一个布尔值,指示 IP 地址或网络是否是全局唯一的(非私有)。

is_multicast
返回一个布尔值,指示 IP 地址是否是多播地址。

is_loopback
返回一个布尔值,指示 IP 地址是否是回环地址(如 127.0.0.1)。

is_link_local
返回一个布尔值,指示 IP 地址是否是链路本地地址。

hosts()
在 IPv4Network 或 IPv6Network 上调用时,返回一个迭代器,生成网络中的主机地址(不包括网络地址和广播地址)。

address_exclude(other)
在 IPv4Network 或 IPv6Network 上调用时,如果 other 是另一个网络,则返回一个包含两个网络的元组,这两个网络是原始网络减去 other 网络后的结果。

overlaps(other)
检查当前网络与 other 网络是否重叠。

subnets(prefixlen_diff=1, new_prefix=None)
在 IPv4Network 或 IPv6Network 上调用时,根据指定的子网掩码长度差异或新的子网掩码长度,生成子网的迭代器。

3.3.2. 案例使用

3.3.2.1. 创建 IP 地址

你可以使用 ipaddress.IPv4Addressipaddress.IPv6Address 类来创建 IPv4 和 IPv6 地址对象。

ipv4_addr = ipaddress.IPv4Address('192.168.1.1')  
ipv6_addr = ipaddress.IPv6Address('2001:db8::1')
3.3.2.2. 创建网络

使用 ipaddress.IPv4Networkipaddress.IPv6Network 类可以创建表示 IP 网络的对象。这些对象可以包含地址范围、子网掩码等信息。

ipv4_net = ipaddress.IPv4Network('192.168.1.0/24')  
ipv6_net = ipaddress.IPv6Network('2001:db8::/32')
3.3.2.3. 迭代网络中的地址

你可以迭代网络对象来获取网络中的所有地址。

for addr in ipv4_net:  
    print(addr)
3.3.2.4. 检查地址是否在网络中

使用 in 关键字可以检查一个地址是否属于某个网络。

print(ipv4_addr in ipv4_net)  # 输出: True
3.3.2.5. 主机地址

网络对象有一个 .hosts() 方法,它返回一个迭代器,包含网络中的所有主机地址(排除了网络地址和广播地址)。

for host in ipv4_net.hosts():  
    print(host)
3.3.2.6. 广播地址

对于 IPv4 网络,你可以使用 .broadcast_address 属性来获取广播地址。

print(ipv4_net.broadcast_address)
3.3.2.7. 子网划分

你可以使用 subnets() 方法来将网络划分为更小的子网。

for subnet in ipv4_net.subnets(new_prefix=26):  
    print(subnet)

3.4. 端口扫描进阶案例

这里的进阶扫描,就是实现如何从文件中读取单个IP地址或IP地址段或IP地址子网来进行对端口进行扫描,当然端口的输入方式依旧不变,还是需要手动输入,而不能通过文件,端口是否通过文件输入问题不是太大。因为我们已经解决单端口,多端口以及端口范围的测试了,这里我们主要是对IP地址进行输入,因为IP地址的输入问题会较多,不好使用代码来进行处理,而在python中有现成的库来调用,这里就使用库来实现。

3.4.1. 文件输入端口扫描

想要执行下属代码,需要在该代码文件的同目录创建一个txt文件,至于名词没有固定,我创建了一个IP.txt的文件,里面存放了以下内容:

10.108.13.51
10.108.13.51-10.108.13.52
10.108.13.0/24

在这里插入图片描述

import socket
import ipaddress


def port_scan(ip, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        s.connect((ip, port))
        print(f"[+] {ip}:{port} \t open")
    except socket.error:
        print(f"[-] {ip}:{port} \t closed or not responding")
    finally:
        s.close()


def scan_ports(ip, port_input):
    if ',' not in port_input and port_input.isdigit():
        port_scan(ip, int(port_input))
    elif ',' in port_input:
        ports = []
        port_strings = port_input.split(',')
        for port_string in port_strings:
            if port_string.isdigit():
                ports.append(int(port_string))

        for port in ports:
            port_scan(ip, port)
    else:
        try:
            parts = port_input.split('-', 1)
            if len(parts) == 2:
                if all(part.isdigit() for part in parts):
                    start, end = map(int, parts)

                    if start <= end:
                        for port in range(start, end + 1):
                            port_scan(ip, port)
                    else:
                        print("起始端口不能大于结束端口。")
            else:
                print("无效的端口输入,请使用'端口号'、'端口号,端口号'(多个非连续端口)或'起始端口-结束端口'的格式。")
        except ValueError:
            print("端口输入包含非数字字符,请确保输入有效的端口号或端口范围。")


def process_ip_range(ip_range):
    try:
        # 检查是否是CIDR格式
        """
        首先,函数尝试使用 ipaddress.ip_network(ip_range, strict=False) 来解析输入字符串 ip_range 是否为CIDR(无类别域间路由)格式。
        如果是,它将返回一个 IPv4Network 或 IPv6Network 对象,该对象代表了这个网络。
        然后,它for循环来获取这个网络内除了网络地址和广播地址之外的所有IP地址(即主机地址),并将它们转换为字符串形式,最后返回这个列表。
        """
        network = ipaddress.ip_network(ip_range, strict=False)
        hosts_list = []
        for ip in network.hosts():
            hosts_list.append(str(ip))
        return hosts_list
    except ValueError:
        # 处理'-'格式的IP地址段
        try:
            # 分割 IP 范围字符串
            start_ip_str, end_ip_str = ip_range.split('-', 1)
            # 去除前后的空格并转换为 IPv4Address 对象
            start_ip = ipaddress.IPv4Address(start_ip_str.strip())
            end_ip = ipaddress.IPv4Address(end_ip_str.strip())
            # 检查起始 IP 是否小于等于结束 IP
            if start_ip > end_ip:
                print("起始IP不能大于结束IP。")
                return []
            # 将 IP 地址转换为整数
            start_int = int(start_ip)
            end_int = int(end_ip)
            # 生成 IP 地址的整数列表
            ip_list = []
            for ip in range(start_int, end_int + 1):
                # 将整数转换回 IP 地址并添加到列表中
                ip_list.append(str(ipaddress.IPv4Address(ip)))
            return ip_list
        except ValueError:
            print("IP范围格式无效。")
            return []


def main():
    print(f"""  
程序名称: 端口扫描器  
作者: Yu to 
开发时间: 2024年9月  
版本号: 1.1  
使用说明:
    -IP地址及端口输入时,输入'q'或'quit'即可退出!
    -IP地址支持单个IP、IP范围(起始IP-结束IP)以及子网掩码(CIDR格式)。
    -端口支持单个或多个,格式为:'端口号'、'端口号,端口号'或'起始端口-结束端口'
""")
    try:
        while True:
            file_path = input("请输入IP地址文件路径: ").strip()
            if file_path.lower() in ('q', 'quit'):
                print("已退出程序!")
                break
            try:
                with open(file_path, 'r') as file:
                    ip_lines = file.readlines()
            except FileNotFoundError:
                print("文件未找到,请重新输入!")
                continue

            port_input = input("请输入端口: ").strip().lower()
            if port_input in ('q', 'quit'):
                print("已退出程序!")
                break
            if not port_input:
                print("端口不能为空,请重新输入!")
                continue

            for line in ip_lines:
                ip_range = line.strip()  # 去除空格
                ips = process_ip_range(ip_range)  # 对获取到的一行内容进行数据处理。
                if ips:
                    for ip in ips:  ##挨个传输IP地址。
                        scan_ports(ip, port_input)

    except KeyboardInterrupt:
        print("\n程序已被用户中断,正在退出...")


if __name__ == "__main__":
    main()

在这里插入图片描述

至于更多的测试方式,还自行测试,同时由于是自己使用,多少代码或程序中存在一些错误或者未测试出来的错误,理论上不影响使用。

3.4.2. 优化文件输入端口扫描

不知道这里有没有人看出来,上述代码中,每次对传过来的IP地址进行端口扫描的时候,端口扫描都需要再次对用户输入的内容进行一次分割。例如一个IP地址段:10.108.13.51-10.108.13.52,当执行10.108.13.51的时候会对端口进行处理,再次执行10.108.13.52的时候还是会对端口进行处理,而如果IP地址多的时候,就会导致程序运行占用内存巨大,当然使用的时候可能感觉不出来,这里涉及到内存优化,我也说不清楚,只是在编写代码的时候,发现了这个问题,就想着优化以下,当然不优化也无所谓。

这里具体优化就是加了一个 parse_ports 函数来解析用户输入的端口字符串,并将其转换为一个整数列表。然后,在主循环中,我调用这个函数一次来获取端口列表,并在处理每个IP地址时重复使用它。这样可以避免在每个IP地址上重复解析端口输入。

import socket
import ipaddress


def port_scan(ip, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        s.connect((ip, port))
        print(f"[+] {ip}:{port} \t open")
    except socket.error:
        print(f"[-] {ip}:{port} \t closed or not responding")
    finally:
        s.close()


def scan_ports(ip, ports):
    for port in ports:
        port_scan(ip, port)


def parse_ports(port_input):
    ports = []
    if ',' in port_input:
        port_strings = port_input.split(',')
        for port_string in port_strings:
            if port_string.isdigit():
                ports.append(int(port_string))
    elif '-' in port_input:
        parts = port_input.split('-', 1)
        if len(parts) == 2 and all(part.isdigit() for part in parts):
            start, end = map(int, parts)
            if start <= end:
                ports.extend(range(start, end + 1))
            else:
                print("起始端口不能大于结束端口。")
    elif port_input.isdigit():
        ports.append(int(port_input))
    else:
        print("无效的端口输入,请使用'端口号'、'端口号,端口号'或'起始端口-结束端口'的格式。")
    return ports


def process_ip_range(ip_range):
    try:
        # 检查是否是CIDR格式
        """
        首先,函数尝试使用 ipaddress.ip_network(ip_range, strict=False) 来解析输入字符串 ip_range 是否为CIDR(无类别域间路由)格式。
        如果是,它将返回一个 IPv4Network 或 IPv6Network 对象,该对象代表了这个网络。
        然后,它for循环来获取这个网络内除了网络地址和广播地址之外的所有IP地址(即主机地址),并将它们转换为字符串形式,最后返回这个列表。
        """
        network = ipaddress.ip_network(ip_range, strict=False)
        hosts_list = []
        for ip in network.hosts():
            hosts_list.append(str(ip))
        return hosts_list
    except ValueError:
        # 处理'-'格式的IP地址段
        try:
            # 分割 IP 范围字符串
            start_ip_str, end_ip_str = ip_range.split('-', 1)
            # 去除前后的空格并转换为 IPv4Address 对象
            start_ip = ipaddress.IPv4Address(start_ip_str.strip())
            end_ip = ipaddress.IPv4Address(end_ip_str.strip())
            # 检查起始 IP 是否小于等于结束 IP
            if start_ip > end_ip:
                print("起始IP不能大于结束IP。")
                return []
            # 将 IP 地址转换为整数
            start_int = int(start_ip)
            end_int = int(end_ip)
            # 生成 IP 地址的整数列表
            ip_list = []
            for ip in range(start_int, end_int + 1):
                # 将整数转换回 IP 地址并添加到列表中
                ip_list.append(str(ipaddress.IPv4Address(ip)))
            return ip_list
        except ValueError:
            print("IP范围格式无效。")
            return []


def main():
    print(f"""  
    程序名称: 端口扫描器  
    作者: Yu to 
    开发时间: 2024年9月  
    版本号: 1.2  
    使用说明:
        -IP地址及端口输入时,输入'q'或'quit'即可退出!
        -IP地址支持单个IP、IP范围(起始IP-结束IP)以及子网掩码(CIDR格式)。
        -端口支持单个或多个,格式为:'端口号'、'端口号,端口号'或'起始端口-结束端口'
    """)
    try:
        while True:
            file_path = input("请输入IP地址文件路径: ").strip()
            if file_path.lower() in ('q', 'quit'):
                print("已退出程序!")
                break
            try:
                with open(file_path, 'r') as file:
                    ip_lines = file.readlines()
            except FileNotFoundError:
                print("文件未找到,请重新输入!")
                continue

            port_input = input("请输入端口: ").strip().lower()
            if port_input in ('q', 'quit'):
                print("已退出程序!")
                break
            if not port_input:
                print("端口不能为空,请重新输入!")
                continue

            ports = parse_ports(port_input)  # 解析端口输入并保存
            if not ports:
                continue  # 如果端口解析失败,则跳过当前循环

            for line in ip_lines:
                ip_range = line.strip()
                ips = process_ip_range(ip_range)
                if ips:
                    for ip in ips:
                        scan_ports(ip, ports)  # 使用解析后的端口列表进行扫描

    except KeyboardInterrupt:
        print("\n程序已被用户中断,正在退出...")


if __name__ == "__main__":
    main()

在这里插入图片描述

3.5. threading库

3.5.1. threading.Thread类的常用参数

  • group:默认值为None。这个参数目前主要用于未来的扩展,保留给ThreadGroup类的实现,通常不需要设置。
  • target:目标函数,即线程启动后要调用的函数。该函数应当是可调用的(如函数名或方法名),且可以接收参数。target参数接收的是函数的地址,由run()方法调用执行函数中的内容。
  • name:线程名,用于标识线程。如果不指定,Python会自动为线程分配一个名字,如“Thread-1”、“Thread-2”等。
  • args:传递给target函数的位置参数元组。如果target函数需要参数,可以通过这个参数来传递。
  • kwargs:传递给target函数的关键字参数字典。如果target函数需要关键字参数,可以通过这个参数来传递。
  • daemon:守护线程标志,默认为None。如果设置为True,则表示该线程为守护线程。守护线程的特点是,当主线程退出时,守护线程也会随之退出,且不会等待守护线程执行完毕。注意,必须在start()方法调用之前设置daemon属性,否则将引发RuntimeError
import threading  
import time  
  
def my_function(name, delay):  
    time.sleep(delay)  
    print(f"Hello from {name}")  
  
# 创建线程  
t1 = threading.Thread(target=my_function, args=("Thread-1", 2), name="Thread1")  
t2 = threading.Thread(target=my_function, args=("Thread-2", 1), name="Thread2", daemon=True)  
  
# 启动线程  
t1.start()  
t2.start()  
  
# 等待t1线程结束  
t1.join()  
  
print("Main thread has ended!")  
# 注意:由于t2是守护线程,主线程结束后,t2也会立即结束,不会等待其执行完毕。

3.6. 端口扫描最终案例

这里就是最终案例了,主要涉及的就是端口扫描如何使用多线程来实现操作,这里需要注意一点,由于只是写工具,不是专业的程序开发,多少可能存在逻辑上的问题,同时也借助AI来实现代码优化。

同时以下功能:

计时器:计算运行时间。

多线程:更快的完成任务

界面问题:优化多线程在同一时间会将多条结果输出到同一行。

import socket
import ipaddress
import time
import threading

# 创建一个锁对象
print_lock = threading.Lock()


def port_scan(ip, port):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基于TCP
        s.settimeout(1)  # 超时时间
        s.connect((ip, port))  # 连接服务端,也就是连接目标的IP地址及端口
        # 使用锁来确保输出是互斥的
        with print_lock:
            print(f"[+] {ip}:{port} \t open")  # 输出成功的结果
    except socket.error:
        with print_lock:
            print(f"[-] {ip}:{port} \t closed or not responding")  # 输出错误的结果
    finally:
        s.close()


def scan_ports(ip, ports, num_threads):
    # 定义了一个内部函数worker,用于执行端口扫描。它接受一个端口号作为参数,并调用另一个未定义的函数port_scan来进行实际的扫描工作。
    def worker(port):
        port_scan(ip, port)

    threads = []  ## 初始化一个空列表,用于存储创建的线程对象。
    for i in range(0, len(ports), num_threads):
        # 使用range函数生成一个序列,从0开始,到ports列表的长度(不包括),步长为num_threads。
        # 这个循环的目的是将端口列表分割成多个子列表,每个子列表包含num_threads个端口(除了最后一个可能少于num_threads)。
        for port in ports[i:i + num_threads]:
            # 在这个内部循环中,遍历当前子列表中的每个端口
            t = threading.Thread(target=worker, args=(port,))
            # 为每个端口创建一个线程,线程的目标函数是worker,传递给worker的参数是当前遍历到的端口号。
            t.start()
            # 启动线程。
            threads.append(t)
            # 将创建的线程对象添加到threads列表中。
        for t in threads:
            # 这个循环尝试等待所有线程完成。
            t.join()


def parse_ports(port_input):
    ports = []  # 空列表
    if ',' in port_input:  # 对输入的端口中如果存在","进行处理
        port_strings = port_input.split(',')  # 以逗号分割
        for port_string in port_strings:  # 传输
            if port_string.isdigit():  # 检查是否为数字
                ports.append(int(port_string))  # 追加到列表中
    elif '-' in port_input:  # 对输入的端口中如果存在"-"进行处理
        parts = port_input.split('-', 1)  # 以-为分隔符,分割成两个
        if len(parts) == 2:  # 首先检查parts的长度是否为2
            if all(part.isdigit() for part in parts):  # 然后检查parts中的每个元素是否都是数字
                # 如果两个条件都满足,使用map函数将parts中的每个字符串转换为整数
                # 并将转换后的整数解包赋值给start和end
                start, end = map(int, parts)
                # 检查起始端口是否小于等于结束端口
                if start <= end:
                    ports.extend(range(start, end + 1))
                else:
                    print("起始端口不能大于结束端口。")
    elif port_input.isdigit():
        # 如果是单个端口,直接扫描
        ports.append(int(port_input))
    else:
        print("无效的端口输入,请使用'端口号'、'端口号,端口号'或'起始端口-结束端口'的格式。")
    return ports


def process_ip_range(ip_range):
    try:
        # 检查是否是CIDR格式
        """
        首先,函数尝试使用 ipaddress.ip_network(ip_range, strict=False) 来解析输入字符串 ip_range 是否为CIDR(无类别域间路由)格式。
        如果是,它将返回一个 IPv4Network 或 IPv6Network 对象,该对象代表了这个网络。
        然后,它for循环来获取这个网络内除了网络地址和广播地址之外的所有IP地址(即主机地址),并将它们转换为字符串形式,最后返回这个列表。
        """
        network = ipaddress.ip_network(ip_range, strict=False)
        hosts_list = []
        for ip in network.hosts():
            hosts_list.append(str(ip))
        return hosts_list
    except ValueError:
        # 处理'-'格式的IP地址段
        try:
            start_ip_str, end_ip_str = ip_range.split('-', 1)
            # 去除前后的空格并转换为 IPv4Address 对象
            start_ip = ipaddress.IPv4Address(start_ip_str.strip())
            end_ip = ipaddress.IPv4Address(end_ip_str.strip())
            # 检查起始 IP 是否小于等于结束 IP
            if start_ip > end_ip:
                print("起始IP不能大于结束IP。")
                return []
            # 将 IP 地址转换为整数
            start_int = int(start_ip)
            end_int = int(end_ip)
            # 生成 IP 地址的整数列表
            ip_list = []
            for ip in range(start_int, end_int + 1):
                # 将整数转换回 IP 地址并添加到列表中
                ip_list.append(str(ipaddress.IPv4Address(ip)))
            return ip_list
        except ValueError:
            print("IP范围格式无效。")
            return []


def main():
    print(f"""  
    程序名称: 端口扫描器  
    作者: Yu to 
    开发时间: 2024年9月  
    版本号: 1.3  
    使用说明:
        -IP地址及端口输入时,输入'q'或'quit'即可退出。
        -IP地址仅支持从文件读取。
        -IP地址支持单个IP、IP范围(起始IP-结束IP)以及子网掩码(CIDR格式)。
        -端口支持单个或多个,格式为:'端口号'、'端口号,端口号'或'起始端口-结束端口'。
        -线程数量支持单个整数,最大值为100,超过100,默认按照100执行,低于端口数量,默认按照端口数量执行。
    """)
    try:
        while True:
            file_path = input("请输入IP地址文件路径: ").strip()
            if file_path.lower() in ('q', 'quit'):
                print("已退出程序!")
                break
            try:
                with open(file_path, 'r') as file:
                    ip_lines = file.readlines()
            except FileNotFoundError:
                print("文件未找到,请重新输入!")
                continue

            port_input = input("请输入端口: ").strip().lower()
            if port_input in ('q', 'quit'):
                print("已退出程序!")
                break
            if not port_input:
                print("端口不能为空,请重新输入!")
                continue

            try:
                num_threads = int(input("请输入线程数量(支持1-100):").strip())
                if port_input in ('q', 'quit'):
                    print("已退出程序!")
                    break
                if num_threads < 1:
                    print("线程数量必须大于0。")
                    continue
                if num_threads > 100:
                    num_threads = 100
            except ValueError:
                print("无效的线程数量输入。")
                continue

            start_time = time.time()  # 计时器,记录开始时间
            ports = parse_ports(port_input)
            if not ports:
                continue

            # 确保线程数量不超过端口数量
            if len(ports) < num_threads:
                num_threads = len(ports)
                print(f"注意:由于端口数量({len(ports)})少于请求的线程数量,线程数已自动调整为({len(ports)})线程。")

            for line in ip_lines:
                ip_range = line.strip()
                ips = process_ip_range(ip_range)
                if ips:
                    for ip in ips:
                        scan_ports(ip, ports, num_threads)

            end_time = time.time()  # 记录结束时间
            total_time = end_time - start_time  # 获取最终时间
            print(f"\n程序运行完毕,总耗时: {total_time:.2f} 秒。")

    except KeyboardInterrupt:
        print("\n程序已被用户中断,正在退出...")


if __name__ == "__main__":
    main()

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

剁椒鱼头没剁椒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值