ARP原理概述——基於WinPcap傳送ARP請求資料包獲取遠端MAC地址

NKU丨陽發表於2020-12-04

ARP原理概述——基於WinPcap傳送ARP請求資料包獲取遠端MAC地址

ARP協議

ARP概述

網際網路中,IP地址的作用是遮蔽物理網路地址的差異,方便訪問和資料傳輸,為上層使用者提供一種統一的地址格式。但是地層的物理網路還是得通過實體地址傳送和接收資訊。那麼當兩臺主機進行通訊的時候其實還是需要知道MAC地址的。ARP(Address Resolution Protocol)是乙太網中常用的經IP地址與MAC地址進行對映的方法。

ARP工作原理

乙太網的一個特點就是支援廣播而且MAC地址長度是固定的,ARP協議的設計與實現很好的利用了這個特點。在一個乙太網中,主機A想要獲取主機B的MAC地址的話,工作過程如圖所示:
在這裡插入圖片描述
(1)主機A檢查自己的快取記憶體區(圖中的cache表)中的ARP表,看是否能找到B的IP與MAC對映關係,如果能找到就解析結束,找不到進入步驟(2).
(2)主機A廣播一個帶有B的IP地址的請求包,請求B的IP地址與MAC地址對映關係。這個包中還包含A自身的IP地址與MAC地址。
(3)乙太網上所有主機都會收到這個請求包,然後將A的IP地址與MAC地址的對映關係存到各自的快取記憶體區
(4)主機B收到以後回送帶有自身IP地址與MAC地址對映關係的包
(5)A收到主機B的回覆後將B的IP地址與MAC地址對映關係存入自己的快取記憶體區
注意,這個過程中的“主機”也可以是路由器。

ARP資料包格式

乙太網中的ARP報文格式

編寫程式傳送ARP請求獲取本機和遠端IP的MAC

這個程式的整個過程可以大體分為兩步:
(1)向本機傳送ARP請求獲取本機MAC地址
(2)通過本機的IP與MAC地址,向遠端主機傳送ARP請求,然後獲取對方MAC地址
需要用到的標頭檔案有:

#include <stdio.h>
#include <tchar.h>
#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <conio.h>
#ifndef  HAVE_REMOTE
#define HAVE_REMOTE
#endif
#include <pcap.h>
#include <remote-ext.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "packet.lib")
#pragma comment(lib, "wpcap.lib")
#define _AFXDLL
using namespace std;

注意,#define HAVE_REMOTE 這一句一定要寫到#include<pcap.h>之前

接下來是用到的一些資料結構和功能函式

//乙太網資料包結構
typedef struct Ethernet_head
{
	u_char DestMAC[6];    //目的MAC地址 6位元組
	u_char SourMAC[6];   //源MAC地址 6位元組
	u_short EthType;
};

//ARP資料包頭結構
typedef struct ARPFrame_t
{
	unsigned short HardwareType; //硬體型別
	unsigned short ProtocolType; //協議型別
	unsigned char HardwareAddLen; //硬體地址長度
	unsigned char ProtocolAddLen; //協議地址長度
	unsigned short OperationField; //操作欄位
	unsigned char SourceMacAdd[6]; //源mac地址
	unsigned long SourceIpAdd; //源ip地址
	unsigned char DestMacAdd[6]; //目的mac地址
	unsigned long DestIpAdd; //目的ip地址
};
//arp包結構
struct ArpPacket {
	Ethernet_head ed;
	ARPFrame_t ah;
};
//執行緒引數
struct sparam {
	pcap_t *adhandle;
	char *ip;
	unsigned char *mac;
	char *netmask;
};
struct gparam {
	pcap_t *adhandle;
};

struct sparam sp;//傳送執行緒引數
struct gparam gp;//接收執行緒引數
char *myBroad;//本機廣播地址
unsigned char *m_MAC=new unsigned char[6];//本機MAC地址
char *m_IP;//本機IP地址
char *m_mask;//本機網路掩碼
#define IPTOSBUFFERS    12
void ifprint(pcap_if_t *d);//列印網路卡資訊
char *iptos(u_long in);//主機序到網路序的轉換函式
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac);//獲取自身MAC
DWORD WINAPI SendArpPacket(LPVOID lpParameter);//傳送ARP執行緒
DWORD WINAPI GetLivePC(LPVOID lpParameter);//接收回複執行緒

主函式內容如下:

int _tmain(int argc, _TCHAR* argv[])
{
	pcap_if_t *alldevs;
	pcap_if_t *d;
	pcap_addr_t *a;
	int num = 0;
	char errbuf[PCAP_ERRBUF_SIZE];
	struct tm *ltime;
	time_t local_tv_sec;
	char timestr[16];
	struct pcap_pkthdr *header = new pcap_pkthdr;
	const u_char *pkt_data = new u_char;
	int res;

	//獲取本機裝置列表
	if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
	{
		//錯誤處理
		fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
	}
	//顯示介面列表
	for (d = alldevs; d != NULL; d = d->next)
	{
		/* 裝置名(Name) */
		printf("%s\n", d->name);

		/* 裝置描述(Description) */
		if (d->description)
			printf("\tDescription: %s\n", d->description);

		printf("\n");
	}
	//選擇裝置
	d = alldevs;
	cout << "輸入選擇的裝置號:" << endl;
	cin >> num;
	for (int i = 0; i < num - 1; i++)
		d = d->next;
	ifprint(d);
	//開啟指定的網路介面
	pcap_t *adhandle;
	if ((adhandle = pcap_open_live(d->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, errbuf)) == NULL)
	{
		fprintf(stderr, "\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
		pcap_freealldevs(alldevs);
		return -1;
	}

	printf("\nlistening on %s...\n", d->description);
	GetSelfMac(adhandle, m_IP, m_MAC);
	printf("MyMAC: %02x:%02x:%02x:%02x:%02x:%02x\n", 
		*m_MAC,
		*(m_MAC+1),
		*(m_MAC+2),
		*(m_MAC+3),
		*(m_MAC+4),
		*(m_MAC+5));

	HANDLE sendthread;      //傳送ARP包執行緒
	HANDLE recvthread;       //接受ARP包執行緒
	sp.adhandle = adhandle;
	sp.ip = m_IP;
	sp.netmask = m_mask;
	sp.mac = m_MAC;
	gp.adhandle = adhandle;
	
		sendthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)SendArpPacket,
			&sp, 0, NULL);
		recvthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)GetLivePC, &gp,
			0, NULL);
		
	pcap_freealldevs(alldevs);
	CloseHandle(sendthread);
	CloseHandle(recvthread);
	while (1);
	return 0;
}

列印本機資訊函式

/* 列印所有可用資訊 */
void ifprint(pcap_if_t *d)
{
	pcap_addr_t *a;

	/* IP addresses */
	for (a = d->addresses; a; a = a->next) 
	{
		printf("\tAddress Family: #%d\n", a->addr->sa_family);
		switch (a->addr->sa_family)
		{
		case AF_INET:
			printf("\tAddress Family Name: AF_INET\n");
			if (a->addr)
			{
				m_IP = iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr);
				printf("\tIP Address: %s\n", m_IP);
			}
			if (a->netmask)
			{
				m_mask = iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr);
				printf("\tNetmask: %s\n", m_mask);
			}
				
			if (a->broadaddr)
			{
				myBroad = iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr);
				printf("\tBroadcast Address: %s\n", myBroad);
			}
			if (a->dstaddr)
				printf("\tDestination Address: %s\n", iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
			break;
		default:
			//printf("\tAddress Family Name: Unknown\n");
			break;
		}
	}
	printf("\n");
}

網路序和主機序轉換函式
這部分我是在WinPcap的官方指導文件裡找到的,對具體理解IP地址格式很有幫助

char *iptos(u_long in)
{
	static char output[IPTOSBUFFERS][3 * 4 + 3 + 1];
	static short which;
	u_char *p;

	p = (u_char *)&in;
	which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
	sprintf(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
	return output[which];
}

獲取自己的MAC地址

#define ETH_ARP         0x0806  //乙太網幀型別表示後面資料的型別,對於ARP請求或應答來說,該欄位的值為x0806
#define ARP_HARDWARE    1  //硬體型別欄位值為表示乙太網地址
#define ETH_IP          0x0800  //協議型別欄位表示要對映的協議地址型別值為x0800表示IP地址
#define ARP_REQUEST     1   //ARP請求
#define ARP_REPLY       2      //ARP應答
#define HOSTNUM         255   //主機數量
// 獲取自己主機的MAC地址
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac) {
	unsigned char sendbuf[42]; //arp包結構大小
	int i = -1;
	int res;
	Ethernet_head eh; //乙太網幀頭
	ARPFrame_t ah;  //ARP幀頭
	struct pcap_pkthdr * pkt_header;
	const u_char * pkt_data;
	//將已開闢記憶體空間 eh.dest_mac_add 的首 6個位元組的值設為值 0xff。
	memset(eh.DestMAC, 0xff, 6); //目的地址為全為廣播地址
	memset(eh.SourMAC, 0x0f, 6);
	memset(ah.DestMacAdd, 0x0f, 6);
	memset(ah.SourceMacAdd, 0x00, 6);
	//htons將一個無符號短整型的主機數值轉換為網路位元組順序
	eh.EthType = htons(ETH_ARP);
	ah.HardwareType = htons(ARP_HARDWARE);
	ah.ProtocolType = htons(ETH_IP);
	ah.HardwareAddLen = 6;
	ah.ProtocolAddLen = 4;
	ah.SourceIpAdd = inet_addr("100.100.100.100"); //隨便設的請求方ip
	ah.OperationField = htons(ARP_REQUEST);
	ah.DestIpAdd = inet_addr(ip_addr);
	memset(sendbuf, 0, sizeof(sendbuf));
	memcpy(sendbuf, &eh, sizeof(eh));
	memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah));
	printf("%s", sendbuf);
	if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) {
		printf("\nPacketSend succeed\n");
	}
	else {
		printf("PacketSendPacket in getmine Error: %d\n", GetLastError());
		return 0;
	}
	
	while ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) {
		if (*(unsigned short *)(pkt_data + 12) == htons(ETH_ARP)
			&& *(unsigned short*)(pkt_data + 20) == htons(ARP_REPLY)
			&& *(unsigned long*)(pkt_data + 38)
			== inet_addr("100.100.100.100")) {
			for (i = 0; i < 6; i++) {
				ip_mac[i] = *(unsigned char *)(pkt_data + 22 + i);
			}
			printf("獲取自己主機的MAC地址成功!\n");
			break;
		}
	}
	if (i == 6) {
		return 1;
	}
	else {
		return 0;
	}
}

傳送與監聽執行緒

bool flag;
DWORD WINAPI SendArpPacket(LPVOID lpParameter)
{
	sparam *spara = (sparam *)lpParameter;
	pcap_t *adhandle = spara->adhandle;
	char *ip = spara->ip;
	unsigned char *mac = spara->mac;
	char *netmask = spara->netmask;
	printf("ip_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", mac[0], mac[1], mac[2],
		mac[3], mac[4], mac[5]);
	printf("自身的IP地址為:%s\n", ip);
	printf("地址掩碼NETMASK為:%s\n", netmask);
	printf("\n");
	unsigned char sendbuf[42]; //arp包結構大小
	Ethernet_head eh;
	ARPFrame_t ah;
	//賦值MAC地址
	memset(eh.DestMAC, 0xff, 6);       //目的地址為全為廣播地址
	memcpy(eh.SourMAC, mac, 6);
	memcpy(ah.SourceMacAdd, mac, 6);
	memset(ah.DestMacAdd, 0x00, 6);
	eh.EthType = htons(ETH_ARP);//幀型別為ARP3
	ah.HardwareType = htons(ARP_HARDWARE);
	ah.ProtocolType = htons(ETH_IP);
	ah.HardwareAddLen = 6;
	ah.ProtocolAddLen = 4;
	ah.SourceIpAdd = inet_addr(ip); //請求方的IP地址為自身的IP地址
	ah.OperationField = htons(ARP_REQUEST);
	//向區域網內廣播傳送arp包
	unsigned long myip = inet_addr(ip);
	unsigned long mynetmask = inet_addr(netmask);
	unsigned long hisip = htonl((myip & mynetmask));
	//向指定IP主機傳送
	char desIP[16];
	printf("輸入目標IP:");
	scanf("%s", &desIP);
	//char* desIP = "192.168.43.55";
	ah.DestIpAdd = htonl(inet_addr(desIP));
		//構造一個ARP請求
		memset(sendbuf, 0, sizeof(sendbuf));
		memcpy(sendbuf, &eh, sizeof(eh));
		memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah));
		//如果傳送成功
		if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) {
			printf("\nPacketSend succeed\n");
		}
		else {
			printf("PacketSendPacket in getmine Error: %d\n", GetLastError());
		}
	flag = TRUE;
	return 0;
}
DWORD WINAPI GetLivePC(LPVOID lpParameter) //(pcap_t *adhandle)
{
	
	gparam *gpara = (gparam *)lpParameter;
	pcap_t *adhandle = gpara->adhandle;
	int res;
	unsigned char Mac[6];
	struct pcap_pkthdr * pkt_header;
	const u_char * pkt_data;
	while (true) {
		if ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) {
			if (*(unsigned short *)(pkt_data + 12) == htons(ETH_ARP)) {
				ArpPacket *recv = (ArpPacket *)pkt_data;
				if (*(unsigned short *)(pkt_data + 20) == htons(ARP_REPLY)) {
					printf("-------------------------------------------\n");
					printf("IP地址:%d.%d.%d.%d   MAC地址:",
						recv->ah.SourceIpAdd & 255,
						recv->ah.SourceIpAdd >> 8 & 255,
						recv->ah.SourceIpAdd >> 16 & 255,
						recv->ah.SourceIpAdd >> 24 & 255);
					for (int i = 0; i < 6; i++) {
						Mac[i] = *(unsigned char *)(pkt_data + 22 + i);
						printf("%02x ", Mac[i]);
					}
					printf("\n");
				}
			}
		}
		Sleep(10);
	}
	return 0;
}

注意:

程式碼邏輯整體應該沒啥問題,但是在實際執行的時候,由於我的接收執行緒是在while迴圈裡不停的接收,所以有可能收到很多相同的來自本機的回覆,也就是在獲取本機MAC地址的時候的回覆資訊。你輸入的目標IP的回覆可能需要一段時間才能顯示出來,甚至有可能有時候你等半天也看不到你想要的那個回覆,如果這樣的話建議關閉程式重新執行多試幾次。
出現這個問題的原因目前不明,其實我自己也覺得很奇怪為什麼會收到那麼多相同的回覆,而且我該發現如果我在接收執行緒的迴圈最後不加上那個Sleep(10),那麼程式執行以後就會在pkt_data那裡發生溢位,貌似是由於瞬間接收到太多包導致寫入溢位。但如果我不迴圈接收的話那麼一般是還沒有收到ARP回覆資訊執行緒就關閉了。
我到最後也沒想到為什麼會這樣,也不知道如何解決,如果評論區有大佬看出來問題了的話可以指點一下!

相關文章