腾讯云上Winpcap网络编程三之ARP协议获得MAC地址表

学习文档

这里我们直接进入正题吧,关于Winpcap的基础知识讲解这里就不再赘述了。在这里给大家提供一些学习网址:

Winpcap网络编程及通信编程

Winpcap中文技术文档

学习内容

  • 获取设备列表
  • 获取已安装设备的高级信息
  • 打开适配器并捕获数据包
  • 不用回调方法捕获数据包
  • 过滤数据包
  • 分析数据包
  • 处理脱机堆文件
  • 发送数据包
  • 收集并统计网络流量

这些内容,在上述两个链接中均已经有了比较详细的讲解,希望对大家有帮助。

两台主机通信实战

完成两台主机之间的数据通信(数据链路层)

  • 仿真ARP协议获得网段内主机的MAC表
  • 使用帧完成两台主机的通信(Hello! I’m …)

首先我们要理解ARP是干嘛的,ARP主要作用就是通过IP地址来获取MAC地址。那么怎样获取呢?本机向局域网内主机发送ARP包,ARP包内包含了目的IP,源IP,目的MAC,源MAC,其中目的MAC地址为广播地址,FF-FF-FF-FF-FF-FF,即向局域网内所有主机发送一个ARP请求,那么其他主机收到这个请求之后则会向请求来源返回一个数据包。在这个返回的数据包中包含了自身的MAC地址。那么本机收到这些返回的数据包进行解析之后便会得到局域网内所有主机的MAC地址了..

编程开始:

新建一个C++项目,配好环境,引入Winpcap相关的库,这些不再赘述。

头文件引入

#define HAVE_REMOTE
#define WPCAP
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>

在main函数中首先声明一系列变量如下

char *ip_addr;                                    //IP地址
    char *ip_netmask;                             //子网掩码
    unsigned char *ip_mac;          //本机MAC地址

为这三个变量分配地址空间

ip_addr = (char *) malloc(sizeof(char) * 16); //申请内存存放IP地址
    if (ip_addr == NULL)
    {
        printf("申请内存存放IP地址失败!\n");
        return -1;
    }
    ip_netmask = (char *) malloc(sizeof(char) * 16); //申请内存存放NETMASK地址
    if (ip_netmask == NULL)
    {
        printf("申请内存存放NETMASK地址失败!\n");
        return -1;
    }
    ip_mac = (unsigned char *) malloc(sizeof(unsigned char) * 6); //申请内存存放MAC地址
    if (ip_mac == NULL)
    {
        printf("申请内存存放MAC地址失败!\n");
        return -1;
    }

接下来就是烂大街的程序,获取适配器列表并选中相应的适配器,注释已经在代码中了,如果还有不明白的请参照前几次的讲解。

//获取本地适配器列表
    if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs,errbuf) == -1){
        //结果为-1代表出现获取适配器列表失败
        fprintf(stderr,"Error in pcap_findalldevs_ex:\n",errbuf);
        //exit(0)代表正常退出,exit(other)为非正常退出,这个值会传给操作系统
        exit(1);
    }


    for(d = alldevs;d !=NULL;d = d->next){
        printf("-----------------------------------------------------------------\nnumber:%d\nname:%s\n",++i,d->name);
        if(d->description){
            //打印适配器的描述信息
            printf("description:%s\n",d->description);
        }else{
            //适配器不存在描述信息
            printf("description:%s","no description\n");
        }
        //打印本地环回地址
         printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");
         /**
         pcap_addr *  next     指向下一个地址的指针
         sockaddr *  addr       IP地址
         sockaddr *  netmask  子网掩码
         sockaddr *  broadaddr   广播地址
         sockaddr *  dstaddr        目的地址
         */
         pcap_addr_t *a;       //网络适配器的地址用来存储变量
         for(a = d->addresses;a;a = a->next){
             //sa_family代表了地址的类型,是IPV4地址类型还是IPV6地址类型
             switch (a->addr->sa_family)
             {
                 case AF_INET:  //代表IPV4类型地址
                     printf("Address Family Name:AF_INET\n");
                     if(a->addr){
                         //->的优先级等同于括号,高于强制类型转换,因为addr为sockaddr类型,对其进行操作须转换为sockaddr_in类型
                         printf("Address:%s\n",iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
                     }
                    if (a->netmask){
                         printf("\tNetmask: %s\n",iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
                    }
                    if (a->broadaddr){
                           printf("\tBroadcast Address: %s\n",iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
                     }
                     if (a->dstaddr){
                           printf("\tDestination Address: %s\n",iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
                     }
                     break;
                 case AF_INET6: //代表IPV6类型地址
                     printf("Address Family Name:AF_INET6\n");
                     printf("this is an IPV6 address\n");
                     break;
                 default:
                     break;
             }
         }
    }
    //i为0代表上述循环未进入,即没有找到适配器,可能的原因为Winpcap没有安装导致未扫描到
    if(i == 0){
        printf("interface not found,please check winpcap installation");
    }

    int num;
    printf("Enter the interface number(1-%d):",i);
    //让用户选择选择哪个适配器进行抓包
    scanf_s("%d",&num);
    printf("\n");

    //用户输入的数字超出合理范围
    if(num<1||num>i){
        printf("number out of range\n");
        pcap_freealldevs(alldevs);
        return -1;
    }
    //跳转到选中的适配器
    for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

    //运行到此处说明用户的输入是合法的
    if((adhandle = pcap_open(d->name,        //设备名称
                                                        65535,       //存放数据包的内容长度
                                                        PCAP_OPENFLAG_PROMISCUOUS,  //混杂模式
                                                        1000,           //超时时间
                                                        NULL,          //远程验证
                                                        errbuf         //错误缓冲
                                                        )) == NULL){
        //打开适配器失败,打印错误并释放适配器列表
        fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
        // 释放设备列表 
        pcap_freealldevs(alldevs);
        return -1;
    }

上述代码中需要另外声明的有:

    pcap_if_t  * alldevs;       //所有网络适配器
    pcap_if_t  *d;                    //选中的网络适配器
    char errbuf[PCAP_ERRBUF_SIZE];   //错误缓冲区,大小为256
    pcap_t *adhandle;           //捕捉实例,是pcap_open返回的对象
    int i = 0;                            //适配器计数变量
char *iptos(u_long in);       //u_long即为 unsigned long
/* 将数字类型的IP地址转换成字符串类型的 */
#define IPTOSBUFFERS    12
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_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
    return output[which];
}

到此程序应该会编译通过,可以试着编译一下运行。

GO ON…

接下来我们首先要用ifget方法获取自身的IP和子网掩码

函数声明:

void ifget(pcap_if_t *d, char *ip_addr, char *ip_netmask);
//获取IP和子网掩码赋值为ip_addr和ip_netmask
void ifget(pcap_if_t *d, char *ip_addr, char *ip_netmask) {
    pcap_addr_t *a;
    //遍历所有的地址,a代表一个pcap_addr
    for (a = d->addresses; a; a = a->next) {
        switch (a->addr->sa_family) {
        case AF_INET:  //sa_family :是2字节的地址家族,一般都是“AF_xxx”的形式。通常用的都是AF_INET。代表IPV4
            if (a->addr) {
                char *ipstr;
                //将地址转化为字符串
                ipstr = iptos(((struct sockaddr_in *) a->addr)->sin_addr.s_addr); //*ip_addr
                printf("ipstr:%s\n",ipstr);
                memcpy(ip_addr, ipstr, 16);
            }
            if (a->netmask) {
                char *netmaskstr;
                netmaskstr = iptos(((struct sockaddr_in *) a->netmask)->sin_addr.s_addr);
                printf("netmask:%s\n",netmaskstr);
                memcpy(ip_netmask, netmaskstr, 16);
            }
        case AF_INET6:
            break;
        }
    }
}

main函数继续写,如下调用,之前声明的ip_addrip_netmask就已经被赋值了

ifget(d, ip_addr, ip_netmask); //获取所选网卡的基本信息--掩码--IP地址

到现在我们已经获取到了本机的IP和子网掩码,下一步发送一个ARP请求来获取自身的MAC地址

这个ARP请求的源IP地址就随便指定了,就相当于你构造了一个外来的ARP请求,本机捕获到了请求,然后发送回应给对方的数据包也被本机捕获到了并解析出来了。解析了自己发出去的数据包而已。

那么我们就声明一个函数并实现:

int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac);
// 获取自己主机的MAC地址
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac) {
    unsigned char sendbuf[42]; //arp包结构大小
    int i = -1;
    int res;
    EthernetHeader eh; //以太网帧头
    Arpheader 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;
    }
    //从interface或离线记录文件获取一个报文
    //pcap_next_ex(pcap_t* p,struct pcap_pkthdr** pkt_header,const u_char** pkt_data)
    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;
    }
}

其中我们需要定义一下常量如下

#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   //主机数量

另外发送ARP请求少不了帧头和ARP头的结构,我们需要声明出来,另外我们构建发送包需要再声明两个结构体sparam和gparam

//帧头部结构体,共14字节
struct EthernetHeader
{
    u_char DestMAC[6];    //目的MAC地址 6字节
    u_char SourMAC[6];   //源MAC地址 6字节
    u_short EthType;         //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp  2字节
};

//28字节ARP帧结构
struct Arpheader {
    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 {
    EthernetHeader ed;
    Arpheader ah;
};

struct sparam {
    pcap_t *adhandle;
    char *ip;
    unsigned char *mac;
    char *netmask;
};
struct gparam {
    pcap_t *adhandle;
};

struct sparam sp;
struct gparam gp;

到现在代码也是完整可以运行的,如果有问题请检查上述代码完整性和位置。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

熟悉的Str2-045,不一样的认识

0x01 前言 Struts2漏洞频发的Java主流框架,在利用大佬们的poc或者工具时,我们又是否知道这个漏洞到底谁怎么产生的,那么一大串的POC到底是什么意...

4388
来自专栏Jackson0714

不惧面试:HTTP协议(3) - Cookie

1322
来自专栏Golang语言社区

Golang工程经验(下)

线上服务端系统,必须要有降级机制,也最好能够有开关机制。降级机制在于出现异常情况能够舍弃某部分服务保证其他主线服务正常;开关也有着同样的功效,在某些情况下打开开...

4943
来自专栏阮一峰的网络日志

使用 Make 构建网站

网站开发正变得越来越专业,涉及到各种各样的工具和流程,迫切需要构建自动化。 所谓"构建自动化",就是指使用构建工具,自动实现"从源码到网页"的开发流程。这有利于...

2444
来自专栏IT派

初中级 PHP 面试基础汇总

感觉现在发面试题有些冷门,就跟昨天德国那场似的,不过看看当提前复习了。提前备战。这2个月出门面试的童鞋可注意不要中暑哦。

932
来自专栏java一日一条

同步和异步的区别

答案一: 1.异步传输 通常,异步传输是以字符为传输单位,每个字符都要附加 1 位起始位和 1 位停止位,以标记一个字符的开始和结束,并以此实现数据传输同步...

772
来自专栏Jackson0714

不惧面试:HTTP协议(3) - Cookie

27410
来自专栏枕边书

用C写一个web服务器(一) 基础功能

前言 C 语言是一门很基础的语言,程序员们对它推崇备至,虽然它是我的入门语言,但大学的 C 语言知道早已经还给了老师,C 的使用可以说是从头学起。 之前一直在读...

2459
来自专栏小白安全

反射跨站脚本(XSS)示例

如何利用它? 原来的要求如下: ? 应用程序的回应非常清楚。用户ID为空(空)。我们没有为它指定一个值。 ? 我们有XSS。有效负载未被应用...

3657
来自专栏技术博客

系统上线后WCF服务最近经常死掉的原因分析总结

  最近系统上线完修改完各种bug之后,功能上还算是比较稳定,由于最近用户数的增加,不知为何经常出现无法登录、页面出现错误等异常,后来发现是由于WCF服务时不时...

1183

扫码关注云+社区