ARP PING 实现

1、首先以太网封装格式:

ARP PING 实现_第1张图片

以太网头部(用的是系统自带的  #include ):

struct ether_header
{
u_int8_t ether_dhost[ETH_ALEN];      // 目的MAC地址 
u_int8_t ether_shost[ETH_ALEN];      // 源MAC地址
u_int16_t ether_type;                 // 以太网帧类型 //ARP包协议类型是0x0806,宏ETHERTYPE_ARP
} __attribute__ ((__packed__));

目的地址:目的MAC地址。

源地址:源MAC地址。

 类型:0x0800 :IP数据包

            0x0806 :ARP请求应答

            0x8035 :RARP请求应答

 CRC:用于对帧内部数据进行校验,保证数据传输的正确性,通常由硬件实现,例如在网卡设备中实现网络数据的CRC校验。一般在组包时可以忽略。

2、ARP包PING原理

A只知道B的IP地址192.168.100.28,所以A发送ARP请求包,在局域网内广播,问everybody谁的ip是192.168.100.28。这个时候局域网里所有的活动主机收到包后,会分析目的IP是否是本机,不是则丢弃。故此时只会有B机器响应。B将自己的MAC地址填充进去,然后交换交换两个目的地址和两个发送端地址,以构建ARP应答并返回。A收到响应包判断B机器存活。

故我们要做的就是构造ARP请求包(广播),解析ARP响应包。

3、构建ARP请求包(发包sendto)

ARP包封装格式

ARP PING 实现_第2张图片

以太网首部在第一部分已经介绍过

硬件类型字段:1 表示硬件地址的类型。

协议类型字段:0x800,表示要映射的协议地址类型,它的值为0x800,表示IP地址。

硬件地址长度:0x06(对MAC地址来说),对应硬件地址的长度

协议地址长度:0x04(对IP地址来说),对应协议地址。

操作(op):ARP请求(值为1),ARP应答(值为2),RARP请求(值为3),RARP应答(值为4)。

最后四个字段指定通信双方的以太网地址和IP地址。

注意:ARP请求/应答报文的长度为28字节。

一个完整的携带ARP请求/应答报文的以太帧长度为46字节,但是有的实现要求以太帧数据部分长度至少46字节,这样ARP会增加一些填充字节,这样一个携带ARP请求/应答报文的以太帧长度变为64字节。(6B目的MAC、6B源MAC、2B类型、46字节内容,4BCRC校验)

ARP结构体定义:

struct arphdr

在头文件中,对ARP首部进行了封装:

struct ether_arp {
    struct arphdr ea_hdr; /* fixed-size 8 bytes header */
    u_int8_t arp_sha[ETH_ALEN]; /* sender hardware address */
    u_int8_t arp_spa[4]; /* sender protocol address */
    u_int8_t arp_tha[ETH_ALEN]; /* target hardware address */
    u_int8_t arp_tpa[4]; /* target protocol address */
};
#define arp_hrd ea_hdr.ar_hrd
#define arp_pro ea_hdr.ar_pro
#define arp_hln ea_hdr.ar_hln
#define arp_pln ea_hdr.ar_pln
#define arp_op ea_hdr.ar_op 

typedef struct arphdr  

{

    u_short ar_hrd;  

    u_short ar_pro;  

    u_char ar_hln;  

    u_char ar_pln;  

    u_short ar_op;  

}ARP_HEADER;  

构包要点:

以太网目的MAC填FF:FF:FF:FF:FF:FF,表示广播地址。操作码填充1,表示ARP请求。目的MAC地址不填。源MAC地址和源IP地址通过ioctl获得,后面会介绍。其他填充字段看上文。

4、创建原始套接字,绑定接口,获取各种接口信息。

    创建原始套接字
    int rawsock = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ARP));
    if(rawsock == -1)
        return err_sys("rawsock error");

    创建好套接字后,就可以通过与UDP一样的recvfromsendto函数进行数据的收发,其目的地址结构为sockaddr_ll,这与传送层的地址结构定义是不一样的,其长度为20字节(在TCP/IP的链路层地址中使用了18字节),而传送层的地址结构长度为16字节。sockaddr_ll需要绑定网卡接口号。其中需要用到struct ifreq结构体,用来获取ip地址,掩码,接口号、MAC地址等接口信息。关于struct ifreq具体参考https://blog.csdn.net/gujintong1110/article/details/45530911

    struct ifreq ifr;
    memset(&ifr,0,sizeof(ifr));

    绑定接口
    strcpy(ifr.ifr_name,"eth2");

    获取网卡接口号
    if(-1 == ioctl(rawsock,SIOCGIFINDEX,&ifr))
        return err_sys("ioctl siocgiifindex error");

    将网卡接口号绑定地址结构中
    struct sockaddr_ll dstaddr;
    bzero(&dstaddr,sizeof(dstaddr));
    dstaddr.sll_family = PF_PACKET;
    dstaddr.sll_ifindex = ifr.ifr_ifindex;

    获取网卡MAC地址
    if(-1 == ioctl(rawsock,SIOCGIFHWADDR,ifr))
        err_sys("ioctl siocgifhwaddr error");
    memcpy(hd->ether_shost,ifr->ifr_hwaddr.sa_data,ETH_ALEN);

    获取网卡IP
    if(-1 == ioctl(rawsock,SIOCGIFADDR,ifr))
        err_sys("ioctl siocgifaddr error");
 

5、sendto发包

       ret = sendto(rawsock,(char*)sendbuf,60,0,(struct sockaddr*)&dstaddr,sizeof(dstaddr)); 

        if(ret < 0)
            return err_sys("sendto error");

6、recvfrom收包

            int len = recvfrom(rawsock,(char*)&recvbuf,sizeof(recvbuf),0,NULL,NULL); //收到的是以太网数据包
            if(ret < 0)
            {
                return err_sys("recvfrom error");
            }
7、 解包
      取出以太网帧类型是ARP包,ARP包源ip是请求ip,操作码是2。即为响应包。8、

8、代码实例如下

  运用select超时机制,若在超时时间内得不到回应的话,就等待一段时间继续发送。最大探测m次。

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct parameter
{
	struct timeval wait_time;  //发包超时时间
	int probe_count;  //探测次数
	useconds_t  interval;  //发包间隔
}parameter;

int err_sys(char *s)
{
	perror(s);
	return -1;
}

static void packet(char *sendbuf,struct ifreq *ifr,uint32_t ip,int rawsock)
{
	//填充以太网头 目的MAC,源MAC,协议类型
	struct ether_header *hd = (struct ether_header*)sendbuf;
	unsigned char dst_mac[] = {
		0xff,0xff,0xff,0xff,0xff,0xff
	};
	memcpy(hd->ether_dhost,dst_mac,sizeof(dst_mac));


	//获取网卡MAC地址
	if(-1 == ioctl(rawsock,SIOCGIFHWADDR,ifr))
		err_sys("ioctl siocgifhwaddr error");
	memcpy(hd->ether_shost,ifr->ifr_hwaddr.sa_data,ETH_ALEN);

	hd->ether_type = htons(ETHERTYPE_ARP);

	//填充ARP请求数据包
	struct ether_arp *arp = (struct ether_arp*)(sendbuf+sizeof(struct ether_header));
	//硬件类型
	arp->arp_hrd = htons(ARPHRD_ETHER);
	//协议类型
	arp->arp_pro = htons(ETHERTYPE_IP);
	//硬件类型长度
	arp->arp_hln = ETH_ALEN;
	//协议类型长度
	arp->arp_pln = 4;
	//操作码 ARP请求
	arp->arp_op = htons(ARPOP_REQUEST);
	//发送端以太网地址
	memcpy(arp->arp_sha,ifr->ifr_hwaddr.sa_data,ETH_ALEN);

	//获取网卡IP
	if(-1 == ioctl(rawsock,SIOCGIFADDR,ifr))
		err_sys("ioctl siocgifaddr error");
	struct sockaddr_in *addr = (struct sockaddr_in*)(&(ifr->ifr_addr));

	char sipi[20];
	printf("sip:%s\n",inet_ntop(AF_INET,&(addr->sin_addr.s_addr),sipi,sizeof(sipi)));

	//发送端IP地址
	memcpy(arp->arp_spa,&(addr->sin_addr.s_addr),4);

	//目的端ip地址
	memcpy(arp->arp_tpa,&ip,4);
}

int arp_ping(uint32_t ip,parameter *para)
{
	unsigned char sendbuf[64];
	unsigned char recvbuf[1500];

	//创建原始套接字
	int rawsock = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ARP));
	if(rawsock == -1)
		return err_sys("rawsock error");


	struct ifreq ifr;
	memset(&ifr,0,sizeof(ifr));
	strcpy(ifr.ifr_name,"eth2");

	//获取网卡接口号
	if(-1 == ioctl(rawsock,SIOCGIFINDEX,&ifr))
		return err_sys("ioctl siocgiifindex error");

	//将网卡接口号绑定地址结构中
	struct sockaddr_ll dstaddr;
	bzero(&dstaddr,sizeof(dstaddr));
	dstaddr.sll_family = PF_PACKET;
	dstaddr.sll_ifindex = ifr.ifr_ifindex;

	//构造arp请求包
	bzero(sendbuf,sizeof(sendbuf));
	packet(sendbuf,&ifr,ip,rawsock);
	
	fd_set readfds;
	FD_ZERO(&readfds);
	int ret = 0;
	while(para->probe_count--)
	{
		printf("%d probe\n",para->probe_count+1);
		//发包
		ret = sendto(rawsock,(char*)sendbuf,60,0,(struct sockaddr*)&dstaddr,sizeof(dstaddr));

		if(ret < 0)
			return err_sys("sendto error");

		FD_SET(rawsock,&readfds);
		//成功返回就绪的文件描述符个数,失败返回-1(设置errno),超时返回0
		int readn = select(rawsock+1,&readfds,NULL,NULL,&(para->wait_time));
		if(readn == -1)
		{
			if(errno == EINTR)
				continue;
			else
				return err_sys("select error");

		}
		else if(readn == 0)
		{//超时
			printf("outtime\n");
			usleep(para->interval);
			continue;
		}
		else
		{
			int len = recvfrom(rawsock,(char*)&recvbuf,sizeof(recvbuf),0,NULL,NULL);
			if(ret < 0)
			{
				return err_sys("recvfrom error");
			}
			//unpacket
			//以太网类型是ARP包,源ip是请求ip操作码是2
			struct ether_header *ethhdr = (struct ether_header*)recvbuf;
			struct ether_arp *arp = (struct ether_arp*)(recvbuf+sizeof(struct ether_header));
			if(ntohs(ethhdr->ether_type) == ETHERTYPE_ARP)
			{
				uint32_t sip;
				memcpy(&sip,arp->arp_spa,4);
				if(ntohs(arp->arp_op) == 2 && sip == ip)
				{
					close(rawsock);
					return 0;

				}
			}
			else
			{
				usleep(para->interval);
				continue;
			}
		}
	}
	close(rawsock);
	return 1;

}

int main(int argc,char *argv[])
{
	parameter para;
	para.wait_time.tv_sec = 6;
	para.wait_time.tv_usec = 0;
	para.probe_count = 3;
	para.interval = 1*1000*1000; 

	uint32_t ip;
	inet_pton(AF_INET,argv[1],&ip);

	int ret = arp_ping(ip,¶);

	if(ret == 0)
		printf("open\n");
	else if(ret == 1)
		printf("down");
    else
        printf("scan error");


	return 0;

}

9、关于socket原始套接字

    socketfd=socket(PF_PACKET,int socket_type,int protocol);(需要用户有root权限,即程序需以root身份运行)

    其中socket_type有两种类型,一种为SOCK_RAW,它是包含了MAC层头部信息的原始分组,当然这种类型的套接字在发送的时候需要自己加上一个MAC头部(其类型定义在linux/if_ether.h中,ethhdr),另一种是SOCK_DGRAM类型,它是已经进行了MAC层头部处理的,即收上的帧已经去掉了头部,而发送时也无须用户添加头部字段。

  Protocol是指其送交的上层的协议号,类型如下:

   ETH_P_IP,IP协议数据包

   ETH_P_ARP  ARP协议数据包

   ETH_P_ALL   收发所有协议数据包

   如IP为0x0800,当其为htons(ETH_P_ALL)  (其宏定义为0)时表示收发所有的协议。

   参考用法:

   socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP) 发送接收ip数据包,不能用IPPROTO_IP,因为如果是用了IPPROTO_IP,系统根本就不知道该用什么协议。
   socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))发送接收以太网数据帧
说明:AF_INET 作用于ip层以上,即收发的都是IP数据包。PF_PACKET:作用于数据链路层,收到的是以太网帧。

  注意事项:

  使用原始套接字时应该注意的问题

(1)对于UDP/TCP产生的IP数据包,内核不将它传递给任何原始套接字,而只是将这些数据交给对应的UDP/TCP数据处理句柄(所以,如果你想要通过原始套接字来访问TCP/UDP或者其它类型的数据,调用socket函数创建原始套接字第三个参数应该指定为htons(ETH_P_IP),也就是通过直接访问数据链路层来实现.(我们后面的密码窃取器就是基于这种类型的).

(2)对于ICMP和EGP等使用IP数据包承载数据但又在传输层之下的协议类型的IP数据包,内核不管是否已经有注册了的句柄来处理这些数据,都会将这些IP数据包复制一份传递给协议类型匹配的原始套接字.

(3)对于不能识别协议类型的数据包,内核进行必要的校验,然后会查看是否有类型匹配的原始套接字负责处理这些数据,如果有的话,就会将这些IP数据包复制一份传递给匹配的原始套接字,否则,内核将会丢弃这个IP数据包,并返回一个ICMP主机不可达的消息给源主机.

(4)如果原始套接字bind绑定了一个地址,核心只将目的地址为本机IP地址的数包传递给原始套接字,如果某个原始套接字没有bind地址,核心就会把收到的所有IP数据包发给这个原始套接字.

(5) 如果原始套接字调用了connect函数,则核心只将源地址为connect连接的IP地址的IP数据包传递给这个原始套接字.

(6)如果原始套接字没有调用bind和connect函数,则核心会将所有协议匹配的IP数据包传递给这个原始套接字.

你可能感兴趣的:(ARP PING 实现)