1. ICMP设备在线探测
系统读入用户输入的IP地址,通过构造ICMP请求报文并发送给目标IP地址的方式对目标设备是否在线进行探测,若目标设备返回了ICMP回显应答,则表示目标设备在线;否则代表目标设备已下线,若设备已下线则将不会继续进行后续的端口扫描操作。
该功能通过Scapy进行实现,Scapy是一个功能强大的交互式数据包操作程序,能够构造数据包并发送。
本模块中调用其IP()、ICMP()函数实现ICMP请求报文的构造,调用sr()函数将ICMP请求报文发送给目标IP地址并接收其返回的数据包。
关键实现代码如下:
if ping:
print()
show_info('Initiating ICMP Ping Scan')
# sr 是发送并接收(send, receive)数据包的意思
# 构建一个 ICMP Ping 包
ans, uans = sr(IP(dst=dst)/ICMP(id=RandShort()), verbose=0, retry=2, timeout=timeout)
if not ans:
elapsed = time.time() - start_time
print("Note: Host seems down.")
print(f"scanner done: 1 IP address(0 hosts up) scanned in {
round(elapsed)} seconds")
return
else:
print("Note: Host is up.")
for snd, rcv in ans:
ip = rcv.src
elasped = time.time() - start_time
print(f'Get the response from {
rcv.src} in {
round(elasped * 1000)} ms')
ans, uans = sr(IP(dst=dst)/ICMP(id=RandShort()), verbose=0, retry=2, timeout=timeout)
ans, uans为两个列表,ans保存已收到应答的数据包和对应的应答报文,uans保存未收到应答的包,通过下标访问其中的数据。
IP(dst=dst)/ICMP(id=RandShort()) 为我们根据用户输入的IP地址构造的ICMP数据包,其中‘/’用于区分网络层次,一般从左到右由底层到高层编写。
verbose参数用来开启或关闭收发包详情信息,默认开启。
retry=2参数会对无应答的数据包重复发送2次,若该参数设置为负数则会一直发送无应答的数据包,直至timeout。
timeout参数设置最后一个包发送后的超时时间,默认单位为秒。
扫描结果如下图所示,IP地址为192.168.1.108的设备已下线,www.hikvision.com的设备在线并返回了ICMP回显报文,系统接收到了来自223.111.205.246的回复。
2. SYN半连接扫描
在TCP/IP网络中,端口号是主机上提供的服务的标识。
常用的端口扫描技术有TCP全连接扫描、SYN半连接扫描、TCP FIN扫描、NULL扫描、UDP扫描等。
鉴于SYN扫描只有前两次握手过程且不同端口状态下的目标行为区别较为明显,该部分使用SYN扫描的方式实现对目标IP地址的端口扫描功能。
目标行为与端口状态对照情况如下表所示:
行为 | 状态 |
---|---|
数次重发未响应 | filtered |
收到ICMP不可达错误 | filtered |
SYN/ACK | open |
RST | closed |
本模块中首先要对用户输入的端口号范围进行处理,由于已规定端口范围输入格式为“1-1024”或“22”,因此使用split(‘-‘)的方式将开始与结束的端口号进行分离,并对分离出来的开始端口号与结束端口号进行判断,开始端口号应大于等于1,结束端口号应小于65535,且开始端口号应小于结束端口号,否则将提示用户输入的端口号无效。
if '-' in port:
start, end = list(map(int, port.split('-')))
# 用于判断一个表达式,在表达式条件为 false 的时候触发异常
assert start >= 1 and end <= 65535 and start <= end, "Invalid port range"
if verbose:
show_info(f"Scanning {
dst}[{
end - start + 1} ports]")
else:
start = end = int(port) # only one port to scan
if verbose:
show_info(f"Scanning {
dst}[{
1} ports]")
SYN半连接扫描同样使用scapy中的sr()函数完成数据包的构造、发送及应答的接收工作。
ans,uans= sr(IP(dst=ip)/TCP(sport=sport, dport=ports, flags="S"),timeout=timeout,verbose=0)
其中ports为待检测的端口号列表,以此实现一组数据包的构造与发送,TCP标志位设为"S",即SYN(0x02),表示构造仅设置了SYN标志位的空TCP数据包。
如果用户输入的端口号范围较大,扫描的速度将会变慢且消耗较多的系统资源,因此设计了多线程的方法完成端口扫描,根据用户输入的起始和结束端口分配多个线程同时进行扫描并返回扫描结果。
# 将要扫描的端口分成若干个组,一个线程扫描一组端口
def syn_scan_range(dst, start, end, timeout=timeout, verbose=False, size=size):
"""Scan a range of ports
Arguments:
dst {str} -- target IP address
start {int} -- start port
end {int} -- end port
Keyword Arguments:
timeout {number} -- time wait for a response packet (default: {timeout})
verbose {bool} -- verbose or not (default: {False})
size {int} -- how many ports assign to a thread (default: {size})
"""
if start == end:
synscan(dst, start, timeout, verbose)
return
# 线程列表,用例存放线程
thread_list = []
#print(size)
for i in range(start, end + 1, size):
if i + size > 65535:
ports = list(range(i, 65536))
else:
if i + size > end + 1:
ports = list(range(i, end + 1))
else:
ports = list(range(i, i + size))
# 产生线程的实例
t1 = threading.Thread(target=synscan,args=(dst, ports, timeout, verbose)) #target是要执行的函数名,args是函数对应的参数,以元组的形式
thread_list.append(t1)
# 循环列表,依次执行各个子线程
[x.start() for x in thread_list]
# 将最后一个子线程阻塞主线程,只有当该子线程完成后主线程才能往下执行
[x.join() for x in thread_list]
threading.Thread() 用以创建线程实例,target参数对应线程要执行的函数名,args以元组的方式传入synscan函数所需要的参数。
t.start() 启动子线程,t.join() 将最后一个子线程阻塞主线程,只有当该子线程完成后主线程才能往下执行。
扫描结束后,系统将扫描结果展示给用户: