一、ICMP报文格式
ICMP全名为(INTERNET CONTROL MESSAGE PROTOCOL)网络控制消息协议。ICMP 大致分成两种功能:差错通知和信息查询。ICMP 的内容是放在IP 数据包的数据部分里来互相交流的。也就是,从ICMP的报文格式来说,ICMP 是IP 的上层协议。
-
报文格式
ICMP报文格式 -
查询报文格式
ICMP查询报文 -
查询报文字段
ICMP查询报文字段 -
ICMP消息列表
ICMP消息列表
二、实现Ping程序
2.1 Python Struct模块介绍
-
用处
- 按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能 + 传输int,此时先将int转化为字节流,然后再发送;
- 按照指定格式将字节流转换为Python指定的数据类型;
- 处理二进制数据,如果用struct来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
-
内置函数
- struct.pack(fmt,v1,v2,…)
按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回. - struct.unpack(format, buffer)
从buffer中按照format解析对象
- struct.pack(fmt,v1,v2,…)
- 举例
- 将int转换为bytes
将256这个数字通过pack函数转化成了bytes类型,其中pack的第一个参数i表示buf1为int类型的数据。
buf1 = 256
bin_buf1 = struct.pack('i', buf1) # 'i'代表'integer'
ret1 = struct.unpack('i', bin_buf1)
print(bin_buf1, ' <====> ', ret1)
- 将浮点数转化为bytes
buf2 = 3.1415
bin_buf2 = struct.pack('d', buf2) # 'd'代表'double'
ret2 = struct.unpack('d', bin_buf2)
print(bin_buf2, ' <====> ', ret2)
- 将字符串转换为bytes
11s表示buf3为长度11个字符的数组。同理,也可以使用“11i”表示11个int类型的数据,还可以使用“iiiiiiiiiii”表示11i
buf3 = 'Hello World'
bin_buf3 = struct.pack('11s', bytes(buf3,encoding='ascii')) # '11s'代表长度为11的'string'字符数组
ret3 = struct.unpack('11s', bin_buf3)
print(bin_buf3, ' <====> ', ret3)
2.2 Python具体实现
- 具体实现脚本
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
import time,sys,random,re,struct
def ping_one(dst,id_no,seq_no,ttl_no):
'''定义ICMP Echo request数据包'''
send_time = time.time() #获取发包时间
time_in_bytes = struct.pack('>d',send_time)#把时间转为2进制,'>'为网络字节序,d为8个字节浮点/
ping_one_reply = sr1(IP(dst=dst, ttl=ttl_no)/ICMP(id=id_no,seq=seq_no)/time_in_bytes, timeout = 1, verbose=False)
#产生一个ICMP echo request数据包,scapy默认类型为echo request,数据部分为2进制编码后的时间
try:
if ping_one_reply.getlayer(ICMP).type == 0 and ping_one_reply.getlayer(ICMP).code == 0 and ping_one_reply.getlayer(ICMP).id == id_no:
#收到的数据包,要为echo reply(type为0,code为0),并且id位还要匹配。
reply_source_ip = ping_one_reply.getlayer(IP).src
#提取echo reply的源IP地址
reply_seq = ping_one_reply.getlayer(ICMP).seq
#提取echo reply的序列号
reply_ttl = ping_one_reply.getlayer(IP).ttl
#提取echo reply的TTL
reply_data_length = len(ping_one_reply.getlayer(Raw).load) + len(ping_one_reply.getlayer(Padding).load) + 8
#数据长度等于 数据长度(Raw) + 垫片长度(Padding) + 8字节(ICMP头部长度)
reply_data = ping_one_reply.getlayer(Raw).load
#提取数据(Raw)
receive_time = time.time()
#计算接收的时间
echo_request_sendtime = struct.unpack('>d',reply_data)
#把二进制的数据转换为发送时间
time_to_pass_ms = (receive_time-echo_request_sendtime[0]) * 1000
#(接收时间 - 发送时间) * 1000为毫秒数为消耗时间的毫秒数
return reply_data_length, reply_source_ip, reply_seq, reply_ttl, time_to_pass_ms
#返回echo reply中的数据总长度,源IP,序列号,TTL,和消耗的时间
except Exception as e:
if re.match('.*NoneType.*',str(e)):
#如果没有回应,就返回None
return None
def my_ping(dst):
'''发送ICMP Echo request,并格式化输出结果'''
id_no = random.randint(1,65535)#随机产生ID号
for i in range(1,6): #ping 五个包
ping_result = ping_one(dst,id_no,i,64)
if ping_result:#把返回的值打印出来
print('%d bytes from %s: icmp_seq=%d ttl=%d time=%4.2f ms' % (ping_result[0], ping_result[1], ping_result[2], ping_result[3], ping_result[4]))
else:
print('.',end='',flush=True)#如果没有回应就打印'.',注意flush选项,默认会缓存并不直接打印
time.sleep(1)#间隔一秒发送数据包
if __name__ == '__main__':
ip=input("ip>>>")
my_ping(ip)
-
执行效果
使用Pycharm运行结果如下
Pycharm执行效果
在linux接口下使用Tcpdump抓包如下,可以看到主机向外发送大量ICMP Request包,并收到Reply包。
image.png
三、实现Traceroute程序
3.1 Traceroute原理
Traceroute程序发送IP数据包给目标主机,其中第一个数据包中TTL值为1,当中间设备收到这个数据包时将TTL减1,此时TTL值变成0,会向源主机发送一份ICMP"超时"信息,Traceroute程序收到信息后发送TTL值加1的数据包,以此类推,直到目标主机收到数据包,由于数据包内包含的UDP端口号(大于3000,端口号没发送一个数据包后加1)为一个不可能值,因此目标主机将发送一份"端口不可达”的ICMP差错报文给源主机,Tracertroute程序收到端口不可达的ICMP差错报文后,则判定已到达目标不在发送报文。
3.2 实验环境
通过Ensp搭建一个模拟网路环境,执行脚本的Linux主机桥接到网络中。
Traceroute实验环境
3.3 Python实现
- 具体实现脚本
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
import time
import re
def Tracert_one(dst,dport,ttl_no):
'''发一个Traceroute包,参数需要目的地址,目的端口,TTL'''
send_time = time.time()#记录发送时间
Tracert_one_reply = sr1(IP(dst=dst, ttl=ttl_no)/UDP(sport=6600,dport=dport)/b'my traceroute!!!', timeout = 1, verbose=False)
#Scapy中UDP默认源目端口53,需要将源端口也改掉,否则中间设备将不回应
try:
if Tracert_one_reply.getlayer(ICMP).type == 11 and Tracert_one_reply.getlayer(ICMP).code == 0:
#如果收到TTL超时
hop_ip = Tracert_one_reply.getlayer(IP).src
received_time = time.time()
time_to_passed = (received_time - send_time) * 1000
return 1, hop_ip, time_to_passed #返回1表示并未抵达目的地
elif Tracert_one_reply.getlayer(ICMP).type == 3 and Tracert_one_reply.getlayer(ICMP).code == 3:
#如果收到端口不可达
hop_ip = Tracert_one_reply.getlayer(IP).src
received_time = time.time()
time_to_passed = (received_time - send_time) * 1000
return 2, hop_ip, time_to_passed #返回2表示抵达目的地
except Exception as e:
if re.match('.*NoneType.*',str(e)):
return None #测试失败返回None,没有回包
def MY_Tracert(dst,hops):
dport = 33434 #Traceroute的目的端口从33434开始计算
hop = 0
while hop < hops:
dport = dport + hop
hop += 1
Result = Tracert_one(dst,dport,hop)
if Result == None:#如果测试失败就打印‘*’
print(str(hop) + ' *',flush=True)
elif Result[0] == 1:#如果未抵达目的,就打印这一跳和消耗的时间
time_to_pass_result = '%4.2f' % Result[2]
print(str(hop) + ' ' + str(Result[1]) + ' ' + time_to_pass_result + 'ms')
elif Result[0] == 2:#如果抵达目的,就打印这一跳和消耗的时间,并且跳出循环!
time_to_pass_result = '%4.2f' % Result[2]
print(str(hop) + ' ' + str(Result[1]) + ' ' + time_to_pass_result + 'ms')
break
time.sleep(1)
if __name__ == '__main__':
conf.route.add(net='172.16.10.0/24',gw='192.168.10.115')#为Scapy添加路由
destIP=input("目标IP>>>")
hops=input("最大跳数>>>")
MY_Tracert(destIP, int(hops))
-
执行效果
通过Pycharm执行脚本
执行效果 -
在路由器入接口上抓包
R1收到TTL为1的UDP数据包,R1回应ICPM(TTL超时)
TTL 1 UDP报文
ICMP TTL超时
在次发送TTL值为2 的UDP数据包,到达目的R2,发送ICPM(端口不可达)
TTL 2 UDP报文
ICMP 端口不可达