Section I Problem Specification
本次实验是用C/C++写一个使用Arp协议获得本地局域网内在线主机MAC地址的程序。目前使用广泛的局域网都是基于802.3协议,在该局域网内的主机都是通过网卡间的MAC地址来通信。在此之前,一直认为是利用ip地址来保障通信。通过这节课,我明白了:在一个局域网内,一个ip地址对应一个mac地址,当我想向某一个ip地址发送数据时,其实是提取了本机的mac地址,然后将数据发送到有相应mac地址的网卡。我们可以利用命令行:arp –a查看win7存在本地ip与mac地址的对应的列表。也可以利用arp –d删除。
既然通信是依靠mac地址,那么有一些知道ip,却不知道mac地址的情况下是如何通信的呢。这里就用arp协议,过程是:本机会向局域网内所有发送一个含有自己的mac地址的arp的请求包,这个包的意思就是:谁有ip xxx.xxx.xxx.xxx,请把你的mac地址发送给我。所有本地局域网的主机收到这个之后,就看自己的ip是不是这个,是的话,就发一个含有自己mac地址的arp应答包给请求的mac地址。
Section II Solution Method and Design
具体来来说:我要做的就是自行构造一个arp请求包,然后发送出去,再接住返回给我arp应答包。我再将其解析,打印出mac地址即可。请看下图:
该图清晰的展示了这个frame的具体构造。结合下图的例子,让我能够轻松构建arp请求包:
那么,在此包的构造我想已经讲清楚了。在构造包的时候,会需要自己的mac地址,那么我使用的方法是利用iphlpapi.h这个包里的一个api :GetAdaptersInfo():获取设备的信息,其中就有mac地址。
接着是如何发包和接包,其实发包和接包都是调用了winpcap里面的api。调用api的关键就是要搭配环境的问题,我也是参考了别人的做法,
博客地址:http://blog.csdn.net/zhbzljxw/article/details/6101656
其中需要的头文件,我已经上传到我的云盘了。名字是:WpdPack(vs2010使用的winpcap的api.rar
实际调用api的话,就不那么复杂了,只要认真的读懂了函数。调用起来应该不难,在我的代码中,我主要调用了:
pcap_findalldevs():找到所有设备
pcap_open():打开某个设备
pcap_sendpacket():发包
pcap_next_ex():接包
这里粗略解说,源码内我写了详细的注释。
本次实验的流程图如下页所示:
Section III Test Cases and Results Analysis
查询单个ip:
查询本局域网内全部的mac地址:
Section IV Conclusion
本次实验让我初步掌握了arp协议的原理以及arp包的构造。
成功搭配了使用winpcap的api的环境。成功调用了相关api。
学会使用了wireshark来抓包,分析数据,协助编程。
在实际的编码过程中,还有以下收获:
1. 原来在构造arp请求包的时候,发送方(我的)的ip不填写也可以。
仔细思考也觉得确实可以,因为本来就是依据mac地址来通信。
2. 如果我的机子上有两个网卡,就有两个mac地址,那么请求包内的mac地址填哪个的都无所谓,都可以收到arp回应包。按理:应该是哪个网卡接入局域网,哪个填写哪个网卡的mac地址。
目前这个问题我还没有参悟的特别透。
老师解答:发包时,mac地址随便填一个都行。我实验了一下,只要mac地址把不是广播地址。我测试的时候发出方的mac地址全部填0xaa。
结果还是顺利收到回应包了。我认为这肯定和网络机制有关,会不会对方机子就认为我的mac地址就0xaa了。反正因为是我发出的,所以肯定会还给我,不管我的mac地址是多少。然后我程序一判断,是不是回应包,是不是就打印出来了。
记在这里追问老师。
因为虽然我乱填了了一个mac地址,但是交换机会去查的时候,发现没有这个乱填的。交换机就会用flooding算法,算是广播一个返回的arp。我的网卡又设置成了混杂模式,所以就直接把这个接住了。然后返回给我了。
此外:
主机联外网的的流程:
我想联google,先通过本机的DNS查到google的ip
再利用google的ip,根据本机的ip和mask码可以“与”出一个东西,google的ip和我本机的mask码也可以“与”出一个东西。这两个一比,就知道google的这个ip是外网的。那么就直接把这个包丢给网关(即路由器),然路由器往外发。
3. 第一页的图里显示构造的包应该是一个64个字节的数组,但是我在使用wireshark抓包的时候发现,arp的包都是60字节的数组(如下图 )。少了FCS的后4个字节。
还没参透为什么。计算机网络的基础差,需要看一下基础的书。
老师的解答:这个CRC部分是底层的硬件替我们完成了。发和收的时候硬件都替我们完成添加CRC和验证数据的工作。
有待改进的地方:本次使用结构体使用的不是特别彻底,对c语言的使用的能力不佳,又待加强,很多地方都是有数字的方式来处理。
Section V References
计算机网络高级软件编程技术 第二章Ethernet帧结构解析程序
在vs2010搭配使用winpcap的开发环境:
http://blog.csdn.net/zhbzljxw/article/details/6101656
winpcap的api的使用:
http://blog.chinaunix.net/uid-28698407-id-3843171.html
http://blog.sina.com.cn/s/blog_48fa680e010002xy.html
wireshark使用:
http://wenku.baidu.com/view/15f82868011ca300a6c390cf.html
谢谢几个大哥的帮助!
Section VI Appendix
全部源码,但主意一定要搭配环境#include "winsock2.h"
#include "windows.h" //测试过,可以删,可能因为我之前要获得ip的时候调用了一些函数吧。用处包含了其他Windows头文件 :定义了Windows的所有资料型态、函数调用、资料结构和常数识别字,
#include "iphlpapi.h"
#include "pcap.h"
#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib,"ws2_32.lib")//测试过,可以删,提供了对以下网络相关API的支持,若使用其中的API,则应该将ws2_32.lib加入工程
#pragma comment(lib, "wpcap.lib ")
typedef struct Frame{
unsigned char destinationAddress[6];//疑问:原来我填的本机mac地址无论是无线的还是有线的。只要是本机的就行。为什么
unsigned char sourceAddress[6];
unsigned short ethernetType;
unsigned short hardwareType;
unsigned short protocalType;
unsigned char hardwareSize;
unsigned char protocalSIze;
unsigned short Opcode;
unsigned char senderHardwareAddress[6];
unsigned char senderIP[4];//不用填也可以。
unsigned char targetHardwareAddress[6];
unsigned char targetIP[4];
};
Frame senderFrame;
int count=0;//用于在for循环中计数
//int DeviceCount=0;//设备的个数
void GetLocalMacAndSendArp(pcap_t* curDev){
PIP_ADAPTER_INFO pAdapter = 0;
ULONG uBuf = 0;
DWORD dwRet;
dwRet = GetAdaptersInfo(pAdapter,&uBuf);
if(dwRet == ERROR_BUFFER_OVERFLOW)
{
pAdapter = (PIP_ADAPTER_INFO)GlobalAlloc(GPTR,uBuf);
dwRet = GetAdaptersInfo(pAdapter,&uBuf);
if (dwRet == ERROR_SUCCESS) {
for (count=0;count<6;count++)
{
senderFrame.sourceAddress[count]=pAdapter->Address[count];
senderFrame.senderHardwareAddress[count]=pAdapter->Address[count];
}
unsigned char frame[60];
if (senderFrame.targetIP[3]==255)
{
for (senderFrame.targetIP[3]=1;senderFrame.targetIP[3]<255;senderFrame.targetIP[3]++)
{
memset(&frame,0,sizeof(frame));
memcpy(&frame,&senderFrame,sizeof(senderFrame));
pcap_sendpacket(curDev,frame,sizeof(frame));
}
}else{
memset(&frame,0,sizeof(frame));
memcpy(&frame,&senderFrame,sizeof(senderFrame));
pcap_sendpacket(curDev,frame,sizeof(frame));
}
}
}
//GlobalFree(pAdapter);
}
pcap_t* PrepareAdapterDev()//打开第i个设备
{ printf("请选择您的上网设备:\n");
int c=1;
pcap_if_t* Dev,*allDevs;
pcap_t* curDev;
char errBuf[PCAP_ERRBUF_SIZE]; //存放错误信息的缓冲.下面那个pcap_findalldevs 函数用来存放错误信息。http://blog.chinaunix.net/uid-28698407-id-3843171.html
if(pcap_findalldevs(&allDevs,errBuf)!=-1)//列举所有设备 ,这里用来获取网络适配器信息的函数.pcap_findalldevs() returns 0 on success and -1 on failure.当errBuf满了会返回错误信息。
{ int i=1;//记录选择的设备的号码
for(Dev=allDevs;Dev;Dev=Dev->next)
{
if (Dev->description) {
printf("%d. %s\n", i++, Dev->description);
//DeviceCount++;
}else{
printf(" 抱歉,未能找到详尽的描述\n");
}
}
scanf("%d",&i);
for(Dev=allDevs;Dev!=NULL;Dev=Dev->next,c++) //从这里来看,执行了pcap_findalldevs之后,只是有一个list,然后alldevs会指向第一个。然后我就一个一个向下传。就可以找到第一个了。
{
if(c==i)//我们需要的设备
{ //下一个pcap_open函数,就返打开一个设备。并返回。1参数是打开哪一个。2参数是需要保留的数据包的长度。
//3参数保存一些由于抓包需要的标志。 3参数PCAP_OPENFLAG_PROMISCUOUS表示是否进入混杂模式(是指一台机器能够接收所有经过它的数据流,而不论其目的地址是否是他。)。1就是进入,0就是不进入。其是值还可以为2和4.具体看网页
//4参数被用来设置在遇到一个数据包的时候读操作不必立即返回,而是等待一段时间。单位毫秒
//5参数。假如不是远程抓包,该指针被设置为NULL。
//6参数:errbuf:一个指向用户申请的缓冲区的指针,存放当该函数出错时的错误信息。
if((curDev=pcap_open(Dev->name,65535,PCAP_OPENFLAG_PROMISCUOUS,1000,NULL,errBuf))==NULL)//打开设备 http://blog.sina.com.cn/s/blog_48fa680e010002xy.html
break;
pcap_freealldevs(allDevs);// pcap_freealldevs() is used to free a list allocated by pcap_find_alldevs().看到有些博客介绍说好像不释放掉会出点问题。
return curDev;
}
}
}
pcap_freealldevs(allDevs);
return NULL;
}
void initTargetMACandProtocal()
{
for(count;count<6;count++){
senderFrame.destinationAddress[count]=0xff;
senderFrame.targetHardwareAddress[count]=00;
}
senderFrame.ethernetType=htons(0x0806);
senderFrame.hardwareType=htons(0x0001);
senderFrame.protocalType=htons(0x0800);
senderFrame.hardwareSize=6;
senderFrame.protocalSIze=4;
senderFrame.Opcode=htons(0x0001);
}
void printTargetMAC( pcap_t* curDev )
{
int j=0;//其实只是一个用于超时的量。
pcap_pkthdr* hdr;
const u_char *pkt_datas;
printf("\n结果:\tIP:\t\tMAC:\t\t Ctrl+C 可中止查询\n");
while(true){
j++;
pcap_next_ex(curDev,&hdr,&pkt_datas);
unsigned char *data=NULL;
data=(unsigned char*)pkt_datas;
unsigned char GetFrame[60];
int i=0;
for(i;i<60;i++){
GetFrame[i]=*data;
*data=*data+1;
}
//下面这个方式显得是有点二,不过确实有效,我是看着wireshark一个一个数着对应的。
if(data[12]==0x08&&data[13]==0x06&&data[20]==0x00&&data[21]==0x02&&(data[31]==senderFrame.targetIP[3]||senderFrame.targetIP[3]==255))
{
printf("\t%d.%d.%d.%d\t",data[28],data[29],data[30],data[31]);
printf("%02X-%02X-%02X-%02X-%02X-%02X\n",data[6],data[7],data[8],data[9],data[10],data[11]);
if(senderFrame.targetIP[3]!=255){
printf("-----------------------------------------------------\n\n");
break;
} //是255的就表示对所有在线的ip都发一个。 所以就不能停下来。
}
if (j>1000)
{ if (senderFrame.targetIP[3]!=255){printf("请求包早已发出,至今没收到回应:也许您输入的ip并不在线!!!\n\n");j=0;}//莫名其妙,非要加这个一句count=0
else{printf("查询结束!!!\n\n");}
break;
}
}
}
void main(){
pcap_t* curDev=PrepareAdapterDev();//初始化设备
initTargetMACandProtocal();//初始化目标mac地址以及协议。因为是广播,所以全是FF
while (1)
{
//请用户输入目标ip的地址
printf("查询方式:\n1.直接输入格式为XXX.XXX.XXX.XXX则可查询单个活动主机\n2.直接输入XXX.XXX.XXX.255则可查询本局域网的全部在线主机\n3.只输入0则终止程序\n");
scanf("%d.%d.%d.%d",
&senderFrame.targetIP[0],
&senderFrame.targetIP[1],
&senderFrame.targetIP[2],
&senderFrame.targetIP[3]);
if(senderFrame.targetIP[0]==0){
break;
}
printf("您输入的IP地址为:%d.%d.%d.%d\n",
senderFrame.targetIP[0],
senderFrame.targetIP[1],
senderFrame.targetIP[2],
senderFrame.targetIP[3]);
GetLocalMacAndSendArp(curDev);//初始化发送主机的物理地址和本机IP。
printTargetMAC(curDev);//一定得到返回信息就会打印。
}
}