安小琪's blog

少年有梦,不应止于心动

主机存活探测程序编写

通过ICMP协议进行主机存货探测程序编写

任务标题: 主机存活探测程序

任务目标

编写程序使用 ICMP 协议探测主机是否存活

任务描述

在实际的渗透中,对主机进行端口扫描时需要先探测主机是否存活,通常ICMP协议是用来探测主机是否存活,还能根据TTL值判断主机版本信息。

ping 程序就是用来探测主机是否存活的,通常情况服务器未开启防火墙时是允许被ICMP探测的。

报告要求

1、理解ICMP协议的原理

2、实现代码,尽可能多的实现探测主机是否存活的功能

扩展任务

使用多线程技术提升探测速度


在上一节学习了TCP/IP、UDP协议并利用socket实现客户机和服务机之间的通信。这次要学习的是一个新的网络协议ICMP,以及如何开发相应的python脚本,同时学习如何使用多线程来提高效率.

ICMP协议

ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。 [1]

ICMP使用IP的基本支持,就像它是一个更高级别的协议,但是,ICMP实际上是IP的一个组成部分,必须由每个IP模块实现。

「ICMP的报文数据是通过封装在IP数据报中进行数据传输的」。ICMP的报文分为两个部分:ICMP报文首部和ICMP报文数据

ICMP报文首部

  • 「类型」:主要指的是ICMP报文的种类(主要有两大类,后边会进行说明)
  • 「代码」:主要是指,不同的ICMP报文种类具体有哪些错误
  • 「校验和」:主要是校验报文在整个传输中,是否存在错误

在IP协议首部中,有一个8位协议,它表明的是IP数据所携带的具体数据是什么协议的。ICMP的字段值为1

如果IP协议传输的数据是ICMP数据的话,那么,将会在协议中写入1

ICMP协议报文的两个种类

差错报告报文

  • 「网络不可达」:IP地址可以表示一个网络,当主机号全为0时就表示的是某一个网络,如果整个网络不可达,就会报告一个类型为3,具体代码为0的ICMP协议报文

  • 「主机不可达」:如果计算机A要和计算机B进行通信,而计算机B是关机的状态,就会出现主机不可达的情况

  • 「网络重定向」:传输给某一个网络的数据,可能不能走该网络了,需要进行重定向

  • 「主机重定向」:如果发送的报文,主机告知不能处理,请发送到另外一个主机

询问报文

  • 「回送请求或应答」:主要是验证网络是否通。假设计算机A要和计算机B进行通信,A会发送一个空的数据给B,如果B收到,就给一个回应
  • 「时间戳请求或应答」:当需要进行时间同步时,会用到这个

回送请求(0/8)用于进行通信的主机或路由器之间,判断所发送的数据包是否已经成功到达对端的一种消息,ping 命令就是利用这个消息实现的。

可以向对端主机发送回送请求的消息(ICMP Echo Request Message,类型 8),也可以接收对端主机发回来的回送应答消息(ICMP Echo Reply Message,类型 0)。

相比原生的 ICMP,这里多了两个字段:

  • 标识符:用以区分是哪个应用程序发 ICMP 包,比如用进程 PID 作为标识符;
  • 序号:序列号从 0 开始,每发送一次新的回送请求就会加 1, 可以用来确认网络包是否有丢失。

选项数据中,ping 还会存放发送请求的时间值,来计算往返时间,说明路程的长短。

ICMP工作流程

ICMP 主要的功能包括:

  • 确认 IP 包是否成功送达目标地址
  • 报告发送过程中 IP 包被废弃的原因和改善网络设置等

IP 通信中如果某个 IP 包因为某种原因未能达到目标地址,那么这个具体的原因将由 ICMP 负责通知

如上图例子,主机 A 向主机 B 发送了数据包,由于某种原因,途中的路由器 2 未能发现主机 B 的存在,这时,路由器 2 就会向主机 A 发送一个 ICMP 目标不可达数据包,说明发往主机 B 的包未能成功。

ICMP 的这种通知消息会使用 IP 进行发送

因此,从路由器 2 返回的 ICMP 包会按照往常的路由控制先经过路由器 1 再转发给主机 A 。收到该 ICMP 包的主机 A 则分解 ICMP 的首部和数据域以后得知具体发生问题的原因。

ping — 查询报文类型的使用

接下来,我们重点来看 ping发送和接收过程

同个子网下的主机 A 和 主机 B,主机 A 执行ping 主机 B 后,我们来看看其间发送了什么?

  1. ping 命令执行的时候,源主机首先会构建一个 ICMP 回送请求消息数据包。
    ICMP 数据包内包含多个字段,最重要的是两个:
  • 第一个是类型,对于回送请求消息而言该字段为 8
  • 另外一个是序号,主要用于区分连续 ping 的时候发出的多个数据包。
    每出一个请求数据包,序号会自动加 1。为了能够计算往返时间 RTT,它会在报文的数据部分插入发送时间。
  1. 然后,由 ICMP 协议将这个数据包连同地址 192.168.1.2 一起交给 IP 层。IP 层将以 192.168.1.2 作为目的地址,本机 IP 地址作为源地址协议字段设置为 1 表示是 ICMP 协议,再加上一些其他控制信息,构建一个 IP 数据包。
  1. 接下来,需要加入 MAC 头。如果在本地 ARP 映射表中查找出 IP 地址 192.168.1.2 所对应的 MAC 地址,则可以直接使用;如果没有,则需要发送 ARP 协议查询 MAC 地址,获得 MAC 地址后,由数据链路层构建一个数据帧,目的地址是 IP 层传过来的 MAC 地址,源地址则是本机的 MAC 地址;还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。
  1. 主机 B 收到这个数据帧后,先检查它的目的 MAC 地址,并和本机的 MAC 地址对比,如符合,则接收,否则就丢弃。
    接收后检查该数据帧,将 IP 数据包从帧中提取出来,交给本机的 IP 层。同样,IP 层检查后,将有用的信息提取后交给 ICMP 协议。

  2. 主机 B 会构建一个 ICMP 回送响应消息数据包,回送响应数据包的类型字段为 0序号为接收到的请求数据包中的序号,然后再发送出去给主机 A。

在规定的时候间内,源主机如果没有接到 ICMP 的应答包,则说明目标主机不可达;如果接收到了 ICMP 回送响应消息,则说明目标主机可达。

此时,源主机会检查,用当前时刻减去该数据包最初从源主机上发出的时刻,就是 ICMP 数据包的时间延迟。

针对上面发送的事情,总结成了如下图:

当然这只是最简单的,同一个局域网里面的情况。如果跨网段的话,还会涉及网关的转发、路由器的转发等等。

但是对于 ICMP 的头来讲,是没什么影响的。会影响的是根据目标 IP 地址,选择路由的下一跳,还有每经过一个路由器到达一个新的局域网,需要换 MAC 头里面的 MAC 地址

说了这么多,可以看出 ping 这个程序是使用了 ICMP 里面的 ECHO REQUEST(类型为 8 ) 和 ECHO REPLY (类型为 0)

主机存活探测程序编写(python)

scapy模块

scapy是python中一个可用于网络嗅探的非常强大的第三方库,可以用它来做 packet 嗅探和伪造 packet。 scapy已经在内部实现了大量的网络协议。如DNS、ARP、IP、TCP、UDP等等,可以用它来编写非常灵活实用的工具。

换言之,Scapy 是一个强大的操纵报文的交互程序。它可以伪造或者解析多种协议的报文,还具有发送、捕获、匹配请求和响应这些报文以及更多的功能。Scapy 可以轻松地做到像扫描(scanning)、路由跟踪(tracerouting)、探测(probing)、单元测试(unit tests)、攻击(attacks)和发现网络(network discorvery)这样的传统任务。它可以代替 hping 、arpspoof 、arp-sk、arping,p0f 甚至是部分的Namp、tcpdump 和 tshark 的功能。

命令 效果
str(pkt) 组装数据包
hexdump(pkt) 十六进制转储
ls(pkt) 显示出字段值的列表
pkt.summary() 一行摘要
pkt.show() 针对数据包的展开试图
pkt.show2() 显示聚合的数据包(例如,计算好了校验和)
pkt.sprintf() 用数据包字段填充格式字符串
pkt.decode_payload_as() 改变payload的decode方式
pkt.psdump() 绘制一个解释说明的PostScript图表
pkt.pdfdump() 绘制一个解释说明的PDF
pkt.command() 返回可以生成数据包的Scapy命令

scapy的安装和使用

scapy默认是不安装的,安装命令:pip install scapy

Scapy采用分层的形式来构造数据包,通常最下面的一个协议为Ether,然后是IP,在之后是TCP或者UDP。例如:

数据包 例子
HTTP Ether()/IP(dst=“www.baidu.com”)/TCP()/“GET /index.html HTTP/1.0 \n\n”
ARP Ether()/ARP(pdst=“192.168.8.12”)
ICMP IP(dst=“192.168.8.12”)/ICMP()
TCP tcp=IP(dst=“192.168.8.12”)/TCP(dport=80,flags=“S”)

主机存货探测脚本(v1.0)

成功探测到192.168.43.1为存货主机

ipaddress模块

在IP地址规划中,涉及到计算大量的IP地址,包括网段、网络掩码、广播地址、子网数、IP类型等

别担心,Ipy模块拯救你。Ipy模块可以很好的辅助我们高效的完成IP的规划工作。

ipaddress模块包括用于处理 IPv4 和 IPv6 网络地址的类。这些类支持验证,查找网络上的地址和主机以及其他常见操作。

注:此库支持ipv4和ipv6

ip_network.hosts迭代获取可用的主机地址(没有广播和0地址)

1
2
3
4
5
6
7
8
9
10
11
>>> net4 = ipaddress.ip_network('192.0.2.0/24')
>>> for x in net4.hosts():
... print(x)
192.0.2.1
192.0.2.2
192.0.2.3
192.0.2.4
...
192.0.2.252
192.0.2.253
192.0.2.254

这样列举出所有主机地址之后就可以进行ICMP发包了

主机存货探测脚本(v2.0)

在该版本中增加了批量探测同一网段下所有主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from scapy.all import *
import ipaddress
import sys

def icmp_request(ip_dst):
pocket = Ether()/IP(dst=ip_dst)/ICMP(type=8)/b'Hello'
req = srp1(pocket,timeout=2,verbose=False)
if req:
print('[+]',ip_dst,':', type,'Host is up')

def main():
print("----------------scan begin----------------")
network = list(ipaddress.ip_network(sys.argv[1]))
for ip in network:
icmp_request(str(ip))
if __name__ == '__main__':
main()

虽然可以成功扫描,但是耗时非常久,因此决定加入多线程

threading 模块

进程和线程

进程是资源分配的最小单位,一个程序至少有一个进程。

线程是程序执行的最小单位,一个进程至少有一个线程。

进程都有自己独立的地址空间,内存,数据栈等,所以进程占用资源多。由于进程的资源独立,所以通讯不方便,只能使用进程间通讯(IPC)。

线程共享进程中的数据,他们使用相同的地址空间,使用线程创建快捷,创建开销比进程小。同一进程下的线程共享全局变量、静态变量等数据,所以线程通讯非常方便,但会存在数据同步与互斥的问题,如何处理好同步与互斥是编写多线程程序的难点。

一个进程中可以存在多个线程,在单核CPU中每个进程中同时刻只能运行一个线程,只有在多核CPU中才能存在线程并发的情况。

当线程需要运行但没有运行空间时,会对线程的优先级进行判断,高优先级先运行,低优先级进程让行。

可调用对象(函数,类的实例方法)使用多线程

Python 常用的多线程模块有threading 和 Queue,在这里我们将 threading 模块。

threading 模块的Thread 类是主要的执行对象。使用Thread 类,可以有很多方法来创建线程。最常用的有下面两种:

创建Thread 的实例,传给它一个可调用对象(函数或者类的实例方法)。
派生Thread 的子类,并创建子类的实例。

这里用的主要是第一种可调用对象(函数,类的实例方法)使用多线程

步骤如下:

示例:创建Thread实例,传递给他一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from threading import Thread
from time import sleep, ctime

def func(name, sec):
print('---开始---', name, '时间', ctime())
sleep(sec)
print('***结束***', name, '时间', ctime())

# 创建 Thread 实例
t1 = Thread(target=func, args=('第一个线程', 1))
t2 = Thread(target=func, args=('第二个线程', 2))

# 启动线程运行
t1.start()
t2.start()

# 等待所有线程执行完毕
t1.join() # join() 等待线程终止,要不然一直挂起
t2.join()

示例:创建Thread实例,传递给他一个类的实例方法

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
from threading import Thread
from time import sleep, ctime


class MyClass(object):

def func(self,name,sec):
print('---开始---', name, '时间', ctime())
sleep(sec)
print('***结束***', name, '时间', ctime())

def main():
# 创建 Thread 实例
t1 = Thread(target=MyClass().func, args=(1, 1))
t2 = Thread(target=MyClass().func, args=(2, 2))

# 启动线程运行
t1.start()
t2.start()

# 等待所有线程执行完毕
t1.join() # join() 等待线程终止,要不然一直挂起
t2.join()

if __name__=="__main__":
main()

运行结果:

—开始— 一 时间 Fri Nov 29 11:34:31 2019
—开始— 二 时间 Fri Nov 29 11:34:31 2019
***结束*** 一 时间 Fri Nov 29 11:34:32 2019
**结束\** 二 时间 Fri Nov 29 11:34:33 2019

程序总共运行两秒,如果程序按照线性运行需要3秒,节约1秒钟。

Thread 实例化时需要接收 target,args(kwargs)两个参数。

target 用于接收需要使用多线程调用的对象。

args 或 kwargs 用于接收调用对象的需要用到的参数,args接收tuple,kwargs接收dict。

start() 是方法用来启动线程的执行。

join() 方法是一种自旋锁,它用来等待线程终止。也可以提供超时的时间,当线程运行达到超时时间后结束线程,如join(500),500毫秒后结束线程运行。

注意:如果当你的主线程还有其他事情要做,而不是等待这些线程完成,就可以不调用join()。join()方法只有在你需要等待线程完成然后在做其他事情的时候才是有用的。

主机存货探测脚本(v3.0)

增加了多线程扫描

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
29
30
31
32
33
34
from scapy.all import *
import ipaddress
import sys


def icmp_request(ip_dst):
global a # call a
pocket = Ether()/IP(dst=ip_dst)/ICMP(type=8)/b'Hello'
req = srp1(pocket, timeout=2, verbose=False)
if req:
print('[+]', ip_dst, ' Host is up')
a += 1

def icmp_speed(network):
threads = []
length = len(network)
for ip in network:
t = threading.Thread(target=icmp_request, args=(str(ip),))
threads.append(t)
for i in range(length):
threads[i].start()
for i in range(length):
threads[i].join()

def main():
print("----------------scan begin----------------")
network = list(ipaddress.ip_network(sys.argv[1]))
icmp_speed(network)
print("----------------scan end------------------")
print("Scan finished: ", len(network), "IP addresses (", a, "hosts up) scanned")
if __name__ == '__main__':
a = 0
main()

argparse模块

argsparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以让我们直接在命令行中就可以向程序中传入参数并让程序运行。

来看一个最简单的argsparse库的使用的例子。

1
2
3
4
5
6
7
8
9
10
11
#demo.py
import argparse

parser = argparse.ArgumentParser(description='命令行中传入一个数字')
#type是要传入的参数的数据类型 help是该参数的提示信息
parser.add_argument('integers', type=str, help='传入的数字')

args = parser.parse_args()

#获得传入的参数
print(args)

在命令行中输入python demo.py -h或者python demo.py --help, 这里我输入的是

1
python demo.py -h

在命令行中看到demo.py的运行结果如下

1
2
3
4
5
6
7
8
9
usage: demo.py [-h] integers

命令行中传入数字

positional arguments:
integers 传入的数字

optional arguments:
-h, --help show this help message and exit

现在我们在命令行中给demo.py 传入一个参数5,

1
python demo.py 5

运行,得到的运行结果是

1
Namespace(integers='5')

主机存货探测脚本(最终版)

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
29
30
31
32
33
34
35
36
37
38
39
from scapy.all import *
import ipaddress
import argparse


def icmp_request(ip_dst):
global a # call a
pocket = Ether()/IP(dst=ip_dst)/ICMP(type=8)/b'Hello'
req = srp1(pocket, timeout=2, verbose=False)
if req:
print('[+]', ip_dst, ' Host is up')
a += 1

def icmp_speed(network):
threads = []
length = len(network)
for ip in network:
t = threading.Thread(target=icmp_request, args=(str(ip),))
threads.append(t)
for i in range(length):
threads[i].start()
for i in range(length):
threads[i].join()

def main():
parser = argparse.ArgumentParser(description='imformation')
# type是要传入的参数的数据类型 help是该参数的提示信息
parser.add_argument('network', type=str, help='eg: 192.168.1.0/24')
args = parser.parse_args()
print("----------------scan begin(%s)----------------"%time.ctime())
network = list(ipaddress.ip_network(args.network))
icmp_speed(network)
print("----------------scan end(%s)------------------"%time.ctime())
print("Scan finished: ", len(network), "IP addresses (", a, "hosts up) scanned")

if __name__ == '__main__':
a = 0
main()

后记

通过此次的主机存活探测程序的编写,不仅让我对icmp协议有了更深刻的理解,同时也学到的python的一些有趣的模块。更是提高了我的代码编写能力,努力逃离脚本小子称号!

参考链接

https://blog.csdn.net/qq_42551635/article/details/119507187

https://zhuanlan.zhihu.com/p/91601448

https://juejin.cn/post/6854573211598716941

https://blog.csdn.net/zhizhengguan/article/details/109206015

https://zhuanlan.zhihu.com/p/56922793