ICMP(Internet Control Message
Protocol,Internet 报文协议) 是 TCP/IP 的一种子协议,位于 OSI7 层网络模型中的网络层,其目的是用于在 IP 主机、路由器之前传递控制信息
OS17 层网络模型

图片来自 https://www.cnblogs.com/yunjisuanchengzhanglu/p/16502307.html

ICMP 工作流程

ICMP 中提供了多种报文,这些报文又可以分成两大类:"差错通知" 和 "信息查询"

差错通知

当 IP 数据包在对方计算机处理过程中出现未知的发送错误时,ICMP 会向发送者传送错误事实以及错误原因等,示意图如下

信息查询

信息查询由一个请求和一个应答构成的。只需要向目标发送一个请求数据包,如果收到了来自目标的回应,就可以判断目标是活跃主机,否则可以判断目标是非活跃主机,示意图如下

ICMP 主机探测过程

Ping 命令是 ICMP 中较为常见的一种应用,经常使用这个命令来测试本地与目标之前的连通性,发送一个 ICMP 请求消息给目标主机,若源主机收到目标主机的应答响应消息,则表示目标可达,主机存在。例如想要判断某一台主机是否为存活主机,那么 ping 一下该主机,查看有无回应即可判断是否存活

现在来编写一个利用 ICMP 实现探测活跃主机的代码程序。程序有很多种可实现的方法,这里借助 Scapy 库来完成。它是 python 中的一个第三方库,在 Scapy 库中已经实现了大量的网络协议。如 TCP、UDP、IP、ARP 等,使用该库可以灵活编写各种网络工具。

首先安装 Scapy

1
pip install scapy

接下来就可以进行实现了
1. 导入程序代码所应用到的模块:scapy、random、optparse,其中 scapy 用于发送 ping 请求和接受目标主机的应答数据

1
2
3
from scapy.all import *
from random import randint
from optparse import OptionParser

对用户输入的参数进行接受和批量处理,并将处理后的 IP 地址传入 Scan 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def main():
parser = OptionParser("Usage:%prog -i <target host>") #输出帮助信息
parser.add_option('-i',type='string',dest='IP',help='specify target host')
#获取IP参数
options,args = parser.parse_args()
print("Scan report for"+options.IP+"\n")
#判断是单台主机还是多台主机
#IP中存在-,说明是要扫描多台主机
if '-' in options.IP:
#代码举例:198.168.1.1-120
#通过”-“进行分隔,把192.168.1.1和120分开
#把192.168.1.1通过”,“进行分隔,取最后一个属作为range函数的start,然后把120+1作为range函数的stop
#这样循环遍历出需要扫描的IP地址
for i in range(int(options.IP.split('-')[0].split('-')[3]),int(options.IP.split('-')[1])+1):
Scan(
options.IP.split('.')[0]+'.' +options.IP.split('.')[1]+'.'+options.IP.split('.')[2]+'.'+str(i)
)
time.sleep(0.2)
else:
Scan(options.IP)

print("\nScan finished!...\n")

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("interrupted by user,killing all threads...")

Scan 函数通过调用 ICMP,将构造好的请求包发送到目的地址,并根据目的地址的应答判断目标主机是否存活。存活的 IP 地址会打印出 "xx.xx.xx.xx->Host
is up",对于不存活的主机打印出"xx.xx.xx.xx->Host is down":

1
2
3
4
5
6
7
8
9
10
11
12
def Scan(ip):
ip_id = randint(1,65535)
icmp_id = randint(1,65535)
icmp_seq = randint(1,65535)
packet=IP(dst=ip,ttl=64,id=ip_id)/ICMP(id=icmp_id,seq=icmp_seq)/b'rootkit'
result = sr1(packet,timeout=1,verbose=False)
if result:
for rcv in result:
scan_ip = rcv[IP].src
print(scan_ip + '--->' 'Host is up')
else:
print(ip + '--->' 'Host is down')

Nmap

此处,也可以导入 Nmap 库函数,实现探测主机存活工具的编写。这里使用 Nmap 函数的 - sn 与 - PE 参数,-PE 表示使用 ICMP,-sn 表示只测试该主机的状态

首先导入代码所应用到的模块:nmap、optparse。nmap 模块用于产生 ICMP 的请求数据包,optparse 用于生成命令行参数

1
2
import nmap
import optparse

接着利用 optparse 模块生成命令行参数化形式,对用户输入的参数进行接收和批量处理,最后将处理后的 IP 地址传入 NmapScan 函数

1
2
3
4
5
6
7
8
9
10
11
if __name__ == '__main__':
parser = optparse.Optparse('usage:python %prog -i ip \n\n')
parser.add_option('-i','--ip',dest='targetIP',default='192.168.1.1',type='string',help='target ip address')
options,args = parser.parse_args()
if '-' in options.targetIP:
for i in range(int(options.IP.split('-')[0].split('-')[3]), int(options.IP.split('-')[1]) + 1):
NmapScan(
options.IP.split('.')[0] + '.' + options.IP.split('.')[1] + '.' + options.IP.split('.')[2] + '.' + str(i)
)
else:
NmapScan(options.IP)

接下来就是处理 NmapScan 这个扫描判断函数了,因为要传入 - sn
-PE 参数,所以需要通过调用 nm.scan () 函数来实现,发起 ping 扫描,并打印出扫描之后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
def NmapScan(targetIP):
#实例化PortScanner对象
nm = nmap.PortScanner()
try:
#hosts为目标IP地址,arguments为Nmap的扫描参数
#-sn:使用ping进行扫描
#-PE:使用ICMP的echo请求包(-pp:使用timestamp请求包 -PM:netmask请求包)
result = nm.scan(host=targetIP,argument='-sn -PE')
#对结果进行切片,提取主机状态信息
state = result['scan'][targetIP]['status']['state']
print("[{}] is [{}]".format(targetIP,state))
except Exception as e:
pass

最后,基于 ICMP 协议的主机探测、判断主机存活是一种很常见的方法,无论是以太网还是互联网都能使用。因此,当网络设备,如路由器、防火墙之类的对 ICMP 采取了限制策略,那就会影响到扫描结果,导致结果不准确。
Nmap 模块实现的大体上的流程与使用 Scapy 差不多,相当于是把接受用户参数调用了 optparse 模块来实现,然后同样的遍历传入 NmapScan,再在 NmapScan 函数里面调用一个 nm.scan () 函数来传入 - sn
-PE 参数,就可以实现 ICMP 协议的实现和主机状态的探测功能