一学期一次的课程设计终于开始了(停课两周,马上放寒假了,哈哈哈哈哈哈。。。)这次我们课程设计的科目是计算机协议,我们小组抽到的题目是利用ICMP模仿ping命令写一个主机存活探测的工具。具体描述和需求如下:
【实验目的】
1. 加深对ICMP协议的理解
2. 掌握原始套接字进行网络程序设计的方法
【案例描述】
Ping工具是使用ICMP协议进行网络连通性检测的工具,在日常生活中使用广泛。请根据ICMP协议的相关原理,使用开发工具为同学开发一个Ping工具。
【需求分析】
根据案例描述,可以总结出用户有以下需求:
1. 使用该工具可以测试目标主机的状态
根据ICMP回显请求和回显应答报文,使用该工具测试目标主机的状态。
2. 程序应该提供帮助信息
为了方便用户使用,该工具应该提供帮助信息,当用户需要帮助时可以进行查询。
【总体设计】
1. 使用原始套接字
由于该工具需要发送和接收ICMP报文,所以应该使用原始套接字。
2. 发送ICMP回显请求报文
可以发送ICMP回显请求报文给目的主机
3. 接收ICMP回显应答报文
可以接收来自目的主机的ICMP回显应答报文。
4. 程序应该提供帮助信息
为了方便用户使用,该工具应该提供帮助信息。可以在程序中设计一个用于提供帮助的函数,当用户需要帮助时调用该函数。
题目不是很难,在正式开始之前要首先弄清楚一个概念什么是原始套接字和标准套接字?它们有什么不同?
弄清楚这个概念后,我们主要在学习怎样构造ICMP Request报文。先学习ICMP Request报文的格式如下图:
下表是各字段的含义:
字段 | 长度 | 含义 |
---|---|---|
Type | 1字节 | 消息类型: - 0:回显应答报文 - 8:请求回显报文 |
Code | 1字节 | 消息代码,此处值为0。 |
Checksum | 2字节 | 检验和。 |
Identifier | 2字节 | 标识符,发送端标示此发送的报文 |
Sequence Number | 2字节 | 序列号,发送端发送的报文的顺序号。每发送一次顺序号就加1。 |
Data | 可变 | 选项数据,是一个可变长的字段,其中包含要返回给发送者的数据。回显应答通常返回与所收到的数据完全相同的数据。 |
main.py用来实现各个代码的调用和命令行化,networkscan实现单个主机的探测,networkscans实现网段探测主机存活数,readlogs实现读取日志文件,logs是日志文件,下图是主要功能代码的各个方法。
首先是代码使用到的模块。
import os
import struct
import array
import time
import socket
import logging
from queue import Queue
代码主要用两个类来实现其中SendPing类中的run()方法主要用来实现对自定义ICMP数据包的发送。
class SendPing():
'''
发送ICMP请求报文的线程。
参数:
ipPool -- IP地址
icmpPacket -- 构造的icmp报文
icmpSocket -- icmp套字接
timeout -- 设置发送超时
'''
def __init__(self, ipPool, icmpPacket, icmpSocket, timeout=3):
self.Sock = icmpSocket
self.ipPool = ipPool
self.packet = icmpPacket
self.timeout = timeout
self.Sock.settimeout( timeout + 3 )
def run(self):
try:
self.Sock.sendto(self.packet, (self.ipPool, 0))
except OSError:
print("你不能输入一个内部保留地址!")
exit()
def makeIpPool(self, startIP, lastIP):
'''生产 IP 地址池'''
IPver = 6 if self.IPv6 else 4
intIP = lambda ip: IPy.IP(ip).int() #将IP地址转换为整型格式
ipPool = {IPy.intToIp(ip, IPver) for ip in range(intIP(startIP), intIP(lastIP)+1)}
return {ip for ip in ipPool if self.isUnIP(ip)}
NetworkScan()方法用来实现探测过程, isUnIP()主要用来判断IP地址的合法性,mPing方法中又调用了__icmpSocket和__icmpPacket方法来建立和构造套接字和ICMP数据包,再通过调SendPing类实现发送给目标主机。
# -*- coding: utf-8 -*-
import os
import struct
import array #array模块是python中实现的一种高效的数组存储类型。
import time
import socket
import logging
from queue import Queue
'''
Queue类实现了一个基本的先进先出(FIFO)容器,使用put()将元素添加到序列尾端,get()从队列尾部移除元素。
'''
'''
filename:指定日志文件名;
filemode:和file函数意义相同,指定日志文件的打开模式,'w'或者'a';
format:指定输出的格式和内容,format可以输出很多有用的信息,
'''
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S",
filename="logs", filemode="a")
class SendPing():
'''
发送ICMP请求报文的线程。
参数:
ipPool -- 可迭代的IP地址池
icmpPacket -- 构造的icmp报文
icmpSocket -- icmp套字接
timeout -- 设置发送超时
'''
def __init__(self, ip, icmpPacket, icmpSocket, timeout=3):
self.Sock = icmpSocket
self.ip = ip
self.packet = icmpPacket
self.timeout = timeout
self.Sock.settimeout( timeout + 3 )
def run(self):
try:
self.Sock.sendto(self.packet, (self.ip, 0))
except OSError:
print("你不能输入一个内部保留地址!")
exit()
class NetworkScan():
'''
参数:
timeout -- Socket超时,默认3秒
IPv6 -- 是否是IPv6,默认为False
'''
def __init__(self, timeout=3, IPv6=False):
self.timeout = timeout
self.IPv6 = IPv6
self._LOGS = Queue()
'''
按照给定的格式(fmt),把数据转换成字符串(字节流)
,并将该字符串返回.
'''
self.__data = struct.pack('d', time.time()) #用于ICMP报文的负荷字节(8bit)
self.__id = os.getpid() #构造ICMP报文的ID字段,无实际意义
@property #属性装饰器
def __icmpSocket(self):
'''socket.getprotobyname('icmp')创建ICMP Socket'''
if not self.IPv6:
#socket.SOCK_RAW 原始套接字
#作用:获得网络协议名(如:'icmp')对应的值
Sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
else:
Sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.getprotobyname("ipv6-icmp"))
return Sock
def __inCksum(self, packet):
'''ICMP 报文效验和计算方法
& 按位与运算符:参与运算的两个值,如果两个相应位都为1,则该位的结果为1,否则为0
'''
if len(packet) & 1:
packet = packet + '\\0'
words = array.array('h', packet)
sum = 0
for word in words:
sum += (word & 0xffff)
'''
右移动运算符:把">>"左边的运算数的各二进位全部右移若干位,>> 右边的数字指定了移动的位数
'''
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
#按位取反运算符:对数据的每个二进制位取反,即把1变为0,把0变为1 。~x 类似于 -x-1
return (~sum) & 0xffff
@property #负责把一个方法变成属性调用
def __icmpPacket(self):
'''构造 ICMP 报文'''
if not self.IPv6:
header = struct.pack('bbHHh', 8, 0, 0, self.__id, 0) # TYPE、CODE、CHKSUM、ID、SEQ
else:
header = struct.pack('BbHHh', 128, 0, 0, self.__id, 0)
packet = header + self.__data # packet without checksum
chkSum = self.__inCksum(packet) # make checksum
if not self.IPv6:
header = struct.pack('bbHHh', 8, 0, chkSum, self.__id, 0)
else:
header = struct.pack('BbHHh', 128, 0, chkSum, self.__id, 0)
return header + self.__data # packet *with* checksum
def isUnIP(self, IP):
'''判断IP是否是一个合法的单播地址'''
IP = [int(x) for x in IP.split('.') if x.isdigit()]
if len(IP) == 4:
if (0 < IP[0] < 223 and IP[1] < 256 and IP[2] < 256 and 0 < IP[3] < 255):
return True
return False
def mPing(self, ip):
'''利用ICMP报文探测网络主机存活
参数:
ipPool -- 可迭代的IP地址池
'''
Sock = self.__icmpSocket
Sock.settimeout(self.timeout)
packet = self.__icmpPacket
recvFroms = ''
sendThr = SendPing(ip, packet, Sock, self.timeout)
sendThr.run()
try:
recvFroms = Sock.recvfrom(1024)[1][0]
except Exception:
pass
return recvFroms
def NetworkScan(self, network): #设置要扫描的网段
self.print_logs(" 等待中。。。。。。")
if self.isUnIP(network):
alive_ip = self.mPing(network)
if alive_ip != '':
self.print_logs("%s is alive." % network)
else:
self.print_logs("%s is die." % network)
else:
self.print_logs("输入的IP地址有误!")
def print_logs(self, msg):
print(time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime()) + msg)
logging.info(msg)
self._LOGS.put(time.strftime("[%Y-%m-%d %H:%M:%S] ", time.localtime()) + msg)
主要看一下这个语句 header = struct.pack(‘bbHHh’, 8, 0, 0, self.__id, 0) 其中bbHHh是用来控制网络传输的数据格式,其与的参数分别对应ICMP报文的TYPE、CODE、CHKSUM、ID、SEQ字段。然后使用packet = header + self.__data,自定义完整的ICMP数据包,再使用 self.__inCksum(packet)语句,调用计算检验和的方法计算出检验后和,最后在重新封装ICMP数据包。
SendPingThr类中主要构建了run()方法用来实现发送ICMP数据包和建立多线程扫描提高代码运行速度。
class SendPingThr(threading.Thread):
def __init__(self, ipPool, icmpPacket, icmpSocket, timeout=3):
threading.Thread.__init__(self)
self.Sock = icmpSocket
self.ipPool = ipPool
self.packet = icmpPacket
self.timeout = timeout
self.Sock.settimeout( timeout + 3 )
def run(self):
time.sleep(0.01) #等待接收线程启动
for ip in self.ipPool:
try:
self.Sock.sendto(self.packet, (ip, 0))
except socket.timeout:
break
time.sleep(self.timeout)
NetworkScan类中添加了makeIpPool()方法生成地址池,修改了NetworkScan()方法用来实现扫描过程,首先它有设置网段的功能,然后通过设置的网段调用makeIpPool()方法生成地址池,再将地址池中的每个地址通过for循环传给mPing()方法实现发送数据包的过程。
def makeIpPool(self, startIP, lastIP):
'''生产 IP 地址池'''
IPver = 6 if self.IPv6 else 4
intIP = lambda ip: IPy.IP(ip).int() #将IP地址转换为整型格式
ipPool = {IPy.intToIp(ip, IPver) for ip in range(intIP(startIP), intIP(lastIP)+1)}
return {ip for ip in ipPool if self.isUnIP(ip)}
def NetworkScan(self, network): #设置要扫描的网段
args = "".join(network)
ip_prefix = '.'.join(args.split('.')[:-1])
ip_start = ip_prefix + ".1"
ip_end = ip_prefix + ".255"
self.print_logs(" [*] 开始内网主机扫描")
ipPool = self.makeIpPool(ip_start, ip_end)
alive_ip = self.mPing(ipPool)
for i in alive_ip:
self.print_logs(" [+] %s is alive." % i)
实现命令行化主要使用argparse模块,想具体学习argparse模块的可以查看官方文档。
import networkscans
import networkscan
import argparse
from readlogs import read
if __name__ == '__main__':
#定义一个容器
parser = argparse.ArgumentParser(description="这是一个探测工具!", formatter_class=argparse.RawTextHelpFormatter,
epilog='''use examples:
python main.py -i 192.168.1.1
python main.py -s 192.168.1.0
python main.py -r logs
''')
#设置需要的参数
parser.add_argument('-i', metavar = '', help = '探测主机存活,-i参数后面输入主机IP')
parser.add_argument('-s', metavar = '',help = '探测内网存活的主机,-s参数后面输入一个内网网段')
parser.add_argument('-r', metavar='',help = '-r参数后加logs,查看日志文件')
args = parser.parse_args()
#实现参数的各个功能
if args.i:
s = networkscan.NetworkScan()
s.NetworkScan(args.i)
elif args.s:
s = networkscans.NetworkScan()
s.NetworkScan(args.s)
elif args.r:
read(args.r)
else:
print("输入的参数有误,请使用-h参数查看帮助信息!")
打包main.py成exe文件,具体方法参考我的这篇博客将自己的python代码打包成exe的可执行文件,然后将生成的exe文件放入C:\Windows\System32打开命令行是用如下图。
main -h 显示帮助信息
main -s 172.22.188.0 探测网段主机
main -i 172.22.188.25 探测具体主机状态
main -r logs 打印日志文件