基于 TCP、UDP 的主机发现属于四层主机发现,是一个位于传输层的协议。可以用来探测远程主机存活、端口开放、服务类型以及系统类型等信息,相比于三层主机发现更为可靠,用途更广
TCP 是一种面向连接的、可靠的传输通信协议,位于 IP 层之上,应用层之下的中间层。它每一次建立连接都需要经过三次握手,终止一次连接也需要四次挥手(四次握手),建立完成之后才能进行传输数据。详解:

https://nnnpc.github.io/2022/11/18 / 关于 TCP 与 UDP 协议的学习 /
TCP 建立的连接可以有效防止丢包和服务器一直在最后的确认状态导致无法关闭的情况发生,因此 TCP 是一个可靠的传输协议

我们可以利用 TCP 的三次握手来进行主机存活的探测。有两种方法:

1
2
1.向目标主机直接发送ACK数据包,如果目标主机存活,就会返回一RST数据包来终止这个不正常的TCP连接
2.发送正常的SYN数据包,如果目标主机返回SYN/ACK或者RST包,也可以证明主机为存活状态

工作原理主要依据响应数据包中 flags 字段,如果该字段有值则说明主机存活,该字段通常包括 SYN、FIN、ACK、PSH、RST、URG 六种类型。发送 SYN 包表示是建立连接,而发送 FIN 包表示的是关闭连接,ACK 是应答,PSH 表示包含 data 数据传输,RST 表示连接重置,URG 表示紧急指针
知道了原理和方法之后就可以来编写脚本了,这里还是使用 scapy 库来完成,先进行一个小测试

首先导入应该用的模块

1
2
3
from telnetlib import IP
from scapy.all import *
from scapy.layers.inet import TCP

接着用 a.display () 函数来查看目标主机的返回数据包信息

1
2
3
4
5
6
7
ip = IP()
tcp = TCP()
r = (ip/tcp)
r[IP].dst = "(需要判断的主机IP)"
r[TCP].flags = "A"
a = sr1(r)
a.display()

结果为:

可以明显的看到返回值为返回数据包的标志位为 R,证明主机存活,表示远程主机给源主机发送了一个 REST

根据以上 TCP 发现主机存活的测试,就可以编写相应的工具来探测主机存活了

先导入程序代码所应用到的模块:time、optparse、random 和 scapy。time 模块主要用于产生延迟时间,optparse 用于生成命令行参数,random 模块用于生成随机的端口,scapy 用于以 TCP 发送请求以及接受应答数据

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def main():
usage = "Usage:%prog -i <ip address>" #输出帮助信息
parse = OptionParser(usage=usage)
parse.add_option("-i",'--ip',type="string",dest="targetIP",help="specify the IP address")
options,args = parse.parse_args()
if '-' in options.targetIP:
for i in range(int(options.tagetIP.split('-')[0].split('-')[3]), int(options.targetIP.split('-')[1]) + 1):
Scan(
options.targetIP.split('.')[0] + '.' + options.targetIP.split('.')[1] + '.' + options.targetIP.split('.')[2] + '.' + str(
i)
)
time.sleep(0.2)
else:
Scan(options.targetIP)

if __name__ == '__main__':
main()

Scan () 函数,通过调用 TCP 将构造好的请求包发送到目的地址,并且根据目标主机返回的数据包中的 flags 字段值判断主机是否存活。若 flags 字段为 R,其整性数值为 4 时表示接收到了目标主机的 REST,目标主机为存活状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def Scan():
try:
dport = random.randint(1.65535)
packeet = IP(dst=ip)/TCP(flags="A",dport=dport)
response = sr1(packet,timeout=11.0,verbose=0)
if response:
if int(response[TCP].flags) == 4:
time.sleep(0.5)
print(ip + ' '+"is up")
else:
print(ip + ' '+"is down")
else:
print(ip + '' + "is down")
except:
pass

UDP 是一种利用 IP 提供提供面向无连接的网络通信服务。UDP 会把应用程序发来的数据,在收到的一刻立即原样发送到网络上。即使在网络传输过程中出现丢包、顺序错乱等情况时,UDP 也不会负责重新发送以及纠错。当向目标发送一个 UDP 数据包之后,目标是不会发回任何 UDP 数据包的。但是如果目标主机处于活跃状态并且目标端口关闭,则会返回一个 ICMP 数据包,这个数据包的含义为 unreachable。如果目标主机不处于活跃状态,这是是收不到任何响应数据的。利用这个原理可以实现探测存活主机

现在来利用该原理浅浅编写一个程序测试一下,同样利用 scapy 库来完成,首先先导入需要用到的库

1
2
3
from telnetlib import IP
from scapy.all import *
from scapy.layers.inet import UDP

接着利用 UDP 协议来判断主机是否存活

1
2
3
4
5
6
7
ip = IP()
udp = UDP()
r = (ip/udp)
r[IP].dst = "(需要判断的主机IP)"
r[UDP].dport = 7345
a = sr1(r)
a.display()

如果目标主机存活,就会接收到目标主机的应答信息。看到返回信息中存在 ICMP 的应答信息,"code=port-unreachable" 表示目标端口不可达,这样就可以验证远程主机存活了,如果不存活则不会收到目标机子的响应数据包
接下来就可以根据测试结果和原理来编写相应的 python 工具了

首先需要导入需要用到的模块:time、optparse、random 和 scapy

time:用于产生延迟时间
optparse:用于生成命令行参数
random:用于生成随机的端口
scapy:用于以 UDP 发送请求以及接收应答数据

1
2
3
4
5
6
import time
from optparse import OptionParser
from random import randint
from scapy.all import *
from telnetlib import IP
from scapy.layers.inet import UDP

接着利用 optparse 模块生成命令行参数化形式,对用户输入的参数进行批量接收和处理,并将结果传入 Scan () 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def main():
usage = "Usage:%prog -i <ip address>" # 输出帮助信息
parse = OptionParser(usage=usage)
parse.add_option("-i", '--ip', type="string", dest="targetIP", help="specify the IP address")
options, args = parse.parse_args()
if '-' in options.targetIP:
for i in range(int(options.tagetIP.split('-')[0].split('-')[3]), int(options.targetIP.split('-')[1]) + 1):
Scan(
options.targetIP.split('.')[0] + '.' + options.targetIP.split('.')[1] + '.' +
options.targetIP.split('.')[2] + '.' + str(
i)
)
time.sleep(0.2)
else:
Scan(options.targetIP)



if __name__ == '__main__':
main()

main () 函数中的内容与利用 TCP 协议判断存活主机的 main () 中的内容一样,不同之处在 Scan 函数的协议与判断方式
通过调用 UDP 将构造好的包发送到目标地址,并根据是否接收到目标的响应包来判断目标机子是否存活,当 proto 字段为 1 时则判断为存活状态,否则不存活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def Scan(ip):
try:
dport = random.randint(1,65535)
packet = IP(dst=ip)/UDP(dport=dport)
response = sr1(packet,timeout=1.0,verbose=0)
if response:
if int(response[IP].proto) == 1:
time.sleep(0.5)
print(ip + ' ' + "is up")
else:
print(ip + ' ' + "is down")
else:
print(ip + ' ' + "is down")
except:
pass

同时也可以使用 nmap 库利用 TCP 和 UDP 协议来进行探测,修改 Scan () 函数中的代码即可(调用函数来使用相应的参数)