ARP 协议(地址解析协议)属于数据链路层的协议,主要负责根据网络层地址(IP)来获取数据链路层地址(MAC)
以太网协议规定,同一局域网中的一台主机要和另一台主机进行直接通信,必须知道目标主机的 MAC 地址。而在 TCP/IP 中,网络层只关注目标主机的 IP 地址。这就导致在以太网中使用 IP 协议时,数据链路层的以太网协议接收到的网络层 IP 协议提供的数据中,只包含目标主机的 IP 地址。于是需要 ARP 协议来完成 IP 地址到 MAC 地址的转换

这里假设一个以太网的结构图,如下图所示

在这里假设 PC1 想和 PC3 进行通信,且知道了 PC3 的 IP 地址为 192.168.1.3,但是根据网络层的 IP 无法进行通信,根据以太网的协议规定,两台主机想要进行直接通信必须知道对方的 MAC 地址

步骤如下:

1.PC1 会检查自己的 ARP 缓存表中该 IP 是否有对应的 MAC 地址,若有可以直接进行通信

2. 若无则 PC1 就会使用以太网广播包来给网络上的每一台主机发送 ARP 请求,询问 192.168.1.3 的 MAC 地址,同时 ARP 请求中也包含了 PC1 自己的 IP 地址和 MAC 地址,以太网内的所有主机都会接收到 ARP 请求,并检查是否与自己的 IP 地址相符合。如果不符合则丢弃该 ARP 请求

3.PC3 确定 ARP 请求中的 IP 与自己的 IP 相符合后,就将 ARP 请求中的 PC1 的 IP 地址与 MAC 地址添加到本地的 ARP 缓存中

4.PC3 将自己的 MAC 地址发送给 PC1

5.PC1 收到 PC3 的 ARP 相应,将 PC3 的 IP 地址和 MAC 地址都更新到本地的 ARP 缓存中

这样就可以进行直接通信了,用一个通俗点的方式来概括说明一下就是:

你(PC1)知道了网友(PC3)的网名或者是 QQ 号(IP 地址),你们打算参加一个网友见面会来面基,在这个活动上你就拿着该网名(IP 地址)去挨个询问(发送 ARP 请求)参加的网友,看看谁是你想要面基的人,不认识的人就否认你的询问,直到问道了网友(PC3),他按照自己的网名知道了自己就是你想寻找的那个人,于是他就把你的网名和名字(MAC 地址)都对应记了下来,并且告诉你了他的名字,你也记下了他的名字,更新了备注
本地 ARP 缓存表是有生存周期的,生存周期结束后,就会再重复上面的过程

当目标主机与我们共同处于一个以太网时,利用 ARP 进行主机发现是一个最好的选择,这种方式快且精准。同样的我们借助 Scapy 来编写 ARP 主机发现的脚本:

通过脚本对以太网内的每个主机都进行 ARP 请求。若主机存活,则会响应我们的 ARP 请求,否则不会响应。因为 ARP 涉及到网络层和数据链路层,因此需要用到 Scapy 中的 Ether 和 ARP
库中的 Eher 和 ARP 参数如下所示

Ether 中的 dst 代表目标 MAC 地址,src 代表源 MAC 地址。ARP 中的 op 代表消息类型,1 为 ARP 请求,2 为 ARP 响应,hwsrc 和 psrc 分别代表源 MAC 地址和源 ip 地址,hwdst 和 pdst 分别代表的是目标 MAC 地址和目标 ip 地址

导入模块

1
2
3
4
import os
import re
import optparse
from scapy.all import *

编写本机 IP 地址和 MAC 地址获取函数,通过正则表达式来进行获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 取IP地址和MAC地址函数
def HostAddress(iface):
# os.open执行后返回执行结果
ipData = os.popen('ifconfig' + iface)
# 对ipData进行类型转换,再用正则进行匹配
dataLine = ipData.readlines()
# re.search利用正则匹配返回第一个成功匹配的结果,存在结果则为true
# 取MAC地址
if re.search('\w\w:\w\w:\w\w:\w\w:\w\w:\w\w', str(dataline)):
# 取出匹配结果
MAC = re.search('\w\w:\w\w:\w\w:\w\w:\w\w:\w\w', str(dataLine)).group(0)
# 取ip地址
if re.search(r'((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d)', str(dataLine)):
IP = re.search(r'((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d)', str(dataLine)).group(0)
# 将IP和MAC通过元组的形式返回
addressInfo = (IP, MAC)
return addressInfo

接着编写 ARP 探测函数,根据本机的 IP 地址和 MAC 地址信息,自动生成目标进行探测并把结果写入文件

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
40
41
42
#ARP扫描函数
def ArpScan(iface='eth0'):
#通过HostAddress返回的元组取出MAC地址
mac = HostAddress(iface)[1]
#取出本机IP地址
ip = HostAddress(iface)[0]
#对本机IP地址进行分隔并作为一句元素,用于生成需要扫描的IP地址
ipSplit = ip.split('.')
#需要扫描的IP地址列表
ipList = []
#根据本机IP生成IP扫描范围
for i in range(1,255):
ipItem = ipSplit[0] + '.' + ipSplit[1] + '.' + ipSplit[2] + '.' + str(i)
ipList.append(ipItem)
'''
发送ARP包
因为要用到OSI的二层和三层,所以要写成Rther/ARP
因为最底层用到了二层,所以要用srp()发包
'''
result = srp(Ether(src=mac,dst='FF:FF:FF:FF:FF:FF')/ARP(op=1,hwsrc=mac,hwdst='00:00:00:00:00:00',pdst=ipList),iface=iface,timeout=2,verbose=False)
#读取result中的应答包和应答包内容
resultAns = result[0].res
#存活主机列表
liveHost = []
#number 为接收到应答包的总数
number = len(resultAns)
print("=========================")
print("ARP探测结果")
print("本机ip地址:"+ip)
print("本机mac地址:"+mac)
print("=========================")
for x in range(number):
IP = resultAns[x][1][1].fields['psrc']
MAC = resultAns[x][1][1].fields['hwsrc']
liveHost.append([IP,MAC])
print("IP:"+IP+"\n\n"+"MAC:"+MAC)
print("============================")
#把存活主机IP写入文件
resultFile = open("result","w")
for i in range(len(liveHost)):
resultFile.write(liveHost[i][0]+"\n")
resultFile.close()

编写 main 函数,利用 optparse 模块生成命令行参数化形式

1
2
3
4
5
6
if __name__ == '__main__':
parser = optparse.OptionParser('usage:python %prog -i interfaces \n\n''Example:python %prog -i eth0\n')
#添加网卡参数
parser.add_option('-i','--iface',dest = 'iface',default='eth0',type='string',help='interfaces name')
(options,args) = parser.parse_args()
ArpScan(options.iface)

提示:普通用户运行时需要 sudo,不然会出现 Operation not permitted 的提醒