【技术好文】使用python自制专属于自己的端口扫描工具

317 阅读10分钟

端口扫描的代码实现

1、相关包和函数简介

Socket

socket即套接字,是操作系统提供的独立于具体协议的网络编程接口,使用socket可以很方便地编写出数据传输程序,实现计算机之间的通信,而无需考虑其背后的原理。

socket 的一个典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。

socket起源于Unix,而Unix类的操作系统有一个很好的设计理念,即一切皆文件,对文件使用【打开+读写+关闭】的模式进行操作。而socket即是一种特殊的文件,当我们通过socket()函数创建一个网络连接(或者说打开一个网络文件)时,socket()函数的返回值就是文件描述符,有了这个文件描述符,我们就可以使用普通的文件操作来传输数据了。

基于Python的TCP Socket

编程思路:

  1. TCP服务端
    • 创建套接字,并绑定到本地IP与端口
    • 开始监听客户端的连接
    • 进入循环,不断接受客户端的连接请求
    • 然后接收客户端传来的数据,并发送给客户端数据
    • 传输完毕后关闭套接字
  2. TCP客户端
    • 创建套接字,根据服务器的IP地址和端口号连接上服务器
    • 成功连接后发送数据并接收服务器传来的数据
    • 传输完毕后主动关闭套接字

基本语法:

socket.socket([family[, type[, proto]]])

参数:

  • family: 套接字家族可以使 AF_UNIX 或者 AF_INET。
  • type: 套接字类型可以根据是面向连接的还是非连接分为 SOCK_STREAM 或 SOCK_DGRAM。SOCK_STREAM 代表的是TCP协议,而 SOCK_DGRAM 代表的则是UDP协议。
  • protocol: 一般不填默认为 0。

Socket 对象(内建)方法:

服务器端套接字

s.bind(): 绑定地址(host,port)到套接字, 在 AF_INET下,以元组(host,port)的形式表示地址。

s.listen(): 开始 TCP 监听。backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为 1,大部分应用程序设为 5 就可以了。

s.accept(): 被动接受TCP客户端连接,(阻塞式)等待连接的到来。

客户端套接字:

s.connect(): 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

s.connect_ex(): connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数:

s.recv(): 接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。

s.send(): 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小。

s.sendall(): 完整发送 TCP 数据。将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。

s.recvfrom(): 接收 UDP 数据,与 recv() 类似,但返回值是(data,address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。

s.sendto() : 发送 UDP 数据,将数据发送到套接字,address 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。

s.close(): 关闭套接字

s.getpeername(): 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

s.getsockname(): 返回套接字自己的地址。通常是一个元组(ipaddr,port)

s.setsockopt(level,optname,value): 设置给定套接字选项的值。

s.getsockopt(level,optname[.buflen]): 返回套接字选项的值。

s.settimeout(timeout): 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())

s.gettimeout(): 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。

s.fileno(): 返回套接字的文件描述符。

s.setblocking(flag): 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。

s.makefile(): 创建一个与该套接字相关连的文件

示例:

服务端:

import socket

host = '127.0.0.1'
# host = socket.gethostname() #获取本地的主机名 
port = 9999

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind((host,port))
s.listen(5)
#开始等待客户端的连接
client_s,addr = s.accept()
print("连接上的客户端的ip地址和端口号:",addr)

while True:
	recvmsg = client_s.recv(1024)
	data = recvmsg.decode("utf-8")
	print("收到来自客户端的消息:",data)
	if data == "exit":
		break
	msg = input("请输入要发送的数据:")
	client_s.send(msg.encode("utf-8"))
s.close()

客户端:

import socket

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#设置服务器的ip地址和端口号
#本地回环地址,范例为:同一台机器上两个进程间的通信
host = '127.0.0.1'
port = 9999

s.connect((host,port))

while True:
	sendmsg = input("请输入要发送的数据:")
	s.send(sendmsg.encode("utf-8"))	
	msg = s.recv(1024)
	if sendmsg == "exit":
		break
	print("收到来自服务器的消息:",msg.decode("utf-8"))
s.close()

ipaddress

因为 ipaddress 是一个用于检查和操作 IP 地址的模块,使用ipaddress模块可以创建 Address/Network/Interface 对象以进行IP操作。您可以使用 ipaddress 从字符串和整数创建对象。

IP主机地址

通常称为“主机地址”的地址是使用IP寻址时最基本的单元。 创建地址的最简单方法是使用 ipaddress.ip_address 工厂函数,该函数根据传入的值自动确定是创建 IPv4 还是 IPv6 地址:

ipaddress.ip_address('192.0.2.1')
### IPv4Address('192.0.2.1')

ipaddress.ip_address('2001:DB8::1')
### IPv6Address('2001:db8::1')

地址也可以直接从整数创建,适配32位的值并假定为IPv4地址:

ipaddress.ip_address(3221225985)
### IPv4Address('192.0.2.1')

ipaddress.ip_address(42540766411282592856903984951653826561)
### IPv6Address('2001:db8::1')

要强制使用IPv4或IPv6地址,可以直接调用相关的类。 这对于强制为小整数创建IPv6地址特别有用:

ipaddress.ip_address(1)
### IPv4Address('0.0.0.1')

ipaddress.IPv4Address(1)
### IPv4Address('0.0.0.1')

ipaddress.IPv6Address(1)
### IPv6Address('::1')

定义网络

主机地址通常组合在一起形成IP网络,因此 ipaddress 提供了一种创建、检查和操作网络定义的方法。 IP网络对象由字符串构成,这些字符串定义作为该网络一部分的主机地址范围。 该信息的最简单形式是“网络地址/网络前缀”对,其中前缀定义了比较的前导比特数,以确定地址是否是网络的一部分,并且网络地址定义了那些位的预期值。

对于地址,提供了一个自动确定正确IP版本的工厂函数:

ipaddress.ip_network('192.0.2.0/24')
### IPv4Network('192.0.2.0/24')

ipaddress.ip_network('2001:db8::0/96')
### IPv6Network('2001:db8::/96')

网络对象不能设置任何主机位。 这样做的实际效果是192.0.2.1/24没有描述网络。 这种定义被称为**接口对象,**因为网络上IP表示法通常用于描述给定网络上的计算机的网络接口。

默认情况下,尝试创建一个设置了主机位的网络对象将导致  ValueError 被引发。 要请求将附加位强制为零,可以将标志strict=False 传递给构造函数:

ipaddress.ip_network('192.0.2.1/24')
### Traceback (most recent call last):
###    ...
### ValueError: 192.0.2.1/24 has host bits set

ipaddress.ip_network('192.0.2.1/24', strict=False)
### IPv4Network('192.0.2.0/24')

虽然字符串形式提供了更大的灵活性,但网络也可以用整数定义,就像主机地址一样。 在这种情况下,网络被认为只包含由整数标识的单个地址,因此网络前缀包括整个网络地址:

ipaddress.ip_network(3221225984)
### IPv4Network('192.0.2.0/32')

ipaddress.ip_network(42540766411282592856903984951653826560)
### IPv6Network('2001:db8::/128')

与地址一样,可以通过直接调用类构造函数而不是使用工厂函数来强制创建特定类型的网络。

主机接口

如上所述,如果您需要描述特定网络上的地址,则地址和网络类都不够。 像 192.0.2.1/24 这样的表示法通常被网络工程师和为防火墙和路由器编写工具的人用作“ 192.0.2.0/24 网络上的主机 192.0.2.1 ”的简写。因此,ipaddress 提供了一组将地址与特定网络相关联的混合类。用于创建的接口与用于定义网络对象的接口相同,除了地址部分不限于是网络地址。

ipaddress.ip_interface('192.0.2.1/24')
### IPv4Interface('192.0.2.1/24')

ipaddress.ip_interface('2001:db8::1/96')
### IPv6Interface('2001:db8::1/96')

接受整数输入(与网络一样),并且可以通过直接调用相关构造函数来强制使用特定IP版本。

2、简易扫描器的代码实现

本扫描器通过使用socket模块发起连接的方式,来确认特定ip上的特定端口是否开启了,如果成功连接上,则表示端口是开放的。

注意:本扫描器并不会区分端口是过滤掉了扫描请求还是本身就是关闭状态

### 使用socket模块,是用于进行网络连接,进行网络测试时使用
import socket
### 引入ipaddress模块是想使用ip_address方法来实例化有效的ip地址
import ipaddress
### 使用re正则模块来确认,输入的是正确的格式
import re

# Regular Expression Pattern to extract the number of ports you want to scan. 
# You have to specify <lowest_port_number>-<highest_port_number> (ex 10-100)
### 正则表达模板是用于提取你想要扫描的端口范围
### 端口范围从较小的端口开始,一直到范围内最大的端口
port_range_pattern = re.compile("([0-9]+)-([0-9]+)")
### 初始化中端口的默认范围大小
port_min = 0
port_max = 65535


### 列表用户搜集维护开放的端口编号
open_ports = []

### 输入想要扫描的ip地址
while True:
    ip_add_entered = input("\nPlease enter the ip address that you want to scan: ")
    # 如果输入无效地址,则会报错并跳转至except处理分支上去
    try:
        ip_address_obj = ipaddress.ip_address(ip_add_entered)
        print("You entered a valid ip address.")
        break
    except:
        print("You entered an invalid ip address")
    
### 输入想要扫描的端口范围
while True:
    print("Please enter the range of ports you want to scan in format: <int>-<int> (ex would be 60-120)")
    port_range = input("Enter port range: ")
		### 使用字符串中的replace函数,过滤掉人们输入时常自带的空格
    port_range_valid = port_range_pattern.search(port_range.replace(" ",""))
		### port_range_valid是列表,其中包含有为正则表达式筛选出来的两个数值对象
    if port_range_valid:
        port_min = int(port_range_valid.group(1))
        port_max = int(port_range_valid.group(2))
        break

# 开始扫描
for port in range(port_min, port_max + 1):
		### 向想要扫描的ip上的端口的socket发起连接
    try:
				### 设置成SOCK_STREAM,是使用TCP协议进行连接
				### 使用with的上下文机制,可以不对s手动使用close()关闭连接
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
						### 设置成超时实践
            s.settimeout(0.5)
            ### 使用connect发起连接,如果连接成功,则将端口号加入到队列中
						### 如果失败,则会抛出异常,后续的加入操作也不会执行 
            s.connect((ip_add_entered, port))
            open_ports.append(port)

    except:
        pass

# 打印出开放的端口号
for port in open_ports:
    print(f"Port {port} is open on {ip_add_entered}.")

加入社群

康创护网研习社