一.課題概述。
一學期一次的課程設計終於開始了(停課兩週,馬上放寒假了,哈哈哈哈哈哈。。。)這次我們課程設計的科目是計算機協議,我們小組抽到的題目是利用ICMP模仿ping命令寫一個主機存活探測的工具。具體描述和需求如下:
【實驗目的】
1. 加深對ICMP協議的理解
2. 掌握原始套接字進行網絡程序設計的方法
【案例描述】
Ping工具是使用ICMP協議進行網絡連通性檢測的工具,在日常生活中使用廣泛。請根據ICMP協議的相關原理,使用開發工具爲同學開發一個Ping工具。
【需求分析】
根據案例描述,可以總結出用戶有以下需求:
1. 使用該工具可以測試目標主機的狀態
根據ICMP回顯請求和回顯應答報文,使用該工具測試目標主機的狀態。
2. 程序應該提供幫助信息
爲了方便用戶使用,該工具應該提供幫助信息,當用戶需要幫助時可以進行查詢。
【總體設計】
1. 使用原始套接字
由於該工具需要發送和接收ICMP報文,所以應該使用原始套接字。
2. 發送ICMP回顯請求報文
可以發送ICMP回顯請求報文給目的主機
3. 接收ICMP回顯應答報文
可以接收來自目的主機的ICMP回顯應答報文。
4. 程序應該提供幫助信息
爲了方便用戶使用,該工具應該提供幫助信息。可以在程序中設計一個用於提供幫助的函數,當用戶需要幫助時調用該函數。
二、分析。
題目不是很難,在正式開始之前要首先弄清楚一個概念什麼是原始套接字和標準套接字?它們有什麼不同?
- 標準套接字,指在傳輸層下面使用的套接字。流式套接字和數據報套接字這兩種套接字工作在傳輸層,主要爲應用層的應用程序提供服務,並且在接收和發送時只能操作數據部分,而不能對IP首部或TCP和UDP首部進行操作,通常把這兩種套接字稱爲標準套接字。
- 原始套接字,它是一種對原始網絡報文進行處理的套接字。原始套接字的用途主要有:
(1)發送自定義的IP 數據報
(2)發送ICMP 數據報
(3)網卡的偵聽模式,監聽網絡上的數據包。
(4)僞裝IP地址。
(5)自定義協議的實現。
比如發送一個自定義的IP包、UDP包、TCP包或ICMP包,捕獲所有經過本機網卡的數據包,僞裝本機的IP,想要操作IP首部或傳輸層協議首部,等等。
弄清楚這個概念後,我們主要在學習怎樣構造ICMP Request報文。先學習ICMP Request報文的格式如下圖:
下表是各字段的含義:
字段 | 長度 | 含義 |
---|---|---|
Type | 1字節 | 消息類型: - 0:回顯應答報文 - 8:請求回顯報文 |
Code | 1字節 | 消息代碼,此處值爲0。 |
Checksum | 2字節 | 檢驗和。 |
Identifier | 2字節 | 標識符,發送端標示此發送的報文 |
Sequence Number | 2字節 | 序列號,發送端發送的報文的順序號。每發送一次順序號就加1。 |
Data | 可變 | 選項數據,是一個可變長的字段,其中包含要返回給發送者的數據。回顯應答通常返回與所收到的數據完全相同的數據。 |
三、代碼實現。
1.代碼結構。
main.py用來實現各個代碼的調用和命令行化,networkscan實現單個主機的探測,networkscans實現網段探測主機存活數,readlogs實現讀取日誌文件,logs是日誌文件,下圖是主要功能代碼的各個方法。
2.主要功能分析。
首先是代碼使用到的模塊。
import os
import struct
import array
import time
import socket
import logging
from queue import Queue
- struct:生成用於網絡傳輸的而二進制數據。
- array:是python中實現的一種高效的數組存儲類型。
- socket:套接字。
- logging:主要用於輸出運行日誌,可以設置輸出日誌的等級、日誌保存路徑、日誌文件回滾等。
- Queue:實現了一個基本的先進先出(FIFO)容器,使用put()將元素添加到序列尾端,get()從隊列尾部移除元素。
代碼主要用兩個類來實現其中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數據包。
3.多線程實現網段探測主機存活數。
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)
3.命令行化。
實現命令行化主要使用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 打印日誌文件