一、实验环境:
(1)Windows 10 系统平台;
(2)在VS环境下进行C++编程。
二、课程目的:
(1)参照有关书籍,掌握有关网络通信基本原理;
(2)学习网络嗅探器的概念及其工作原理;
(3)掌握嗅探器的程序设计。
三、基本原理:
- 网络嗅探器又称为网络监听器,简称为Sniffer子系统,放置于网络节点处,对网络中的数据帧进行捕获的一种被动监听手段,是一种常用的收集有用数据的方法,这些数据可以是用户的账号和密码,可以是一些商用机密数据等等。Sniffer是利用计算机的网络接口截获目的地为其他计算机的数据报文的一种工具。Sniffer的正当用处主要是分析网络的流量,以便找出所关心的网络中潜在的问题。嗅探器可以理解为一个安装在计算机上的窃听设备它可以用来窃听计算机在网络上所产生的众多的信息。
- 计算机直接所传送的数据,事实上是大量的二进制数据。因此一个网络窃听程序必须也使用特定的网络协议来分解嗅探到的数据,嗅探器也就必须能够识别出那个协议对应于这个数据片断,只有这样才能够进行正确的解码。很多计算机网络采用的是“共享媒体",几乎可以在任何连接着的网络上直接窃听到你同一掩码范围内的计算机网络数据,我们称这种窃听方式为“基于混杂模式的嗅探”。尽管如此,这种“共享”的技术发展的很快,慢慢转向“交换”技术,这种技术会长期内会继续使用下去,它可以实现有目的选择的收发数据。
- 在网络嗅探技术中所使用的包捕获机制的方法,大致可归纳为两类:一类是由操作系统内核提供的捕获机制;另一类是由应用软件或系统开发包通过安装包捕获驱动程序提供的捕获机制,该机制主要用于Win32平台下的开发。操作系统提供的捕获机制主要有四种:BPF,DLPI,NIT, Sock Packet类型套接口。BPF由基于BSD的Unix系统内核所实现;DLPI是Solaris(和其它System V Unix)系统的内嵌子系统。从性能上看,Sock Packet最弱。Windows操作系统没有提供内置的包捕获机制。它只提供了数量很少并且功能有限的API调用。WinPcap是Win32上的第一个用来捕获数据包的开放系统软件包,它是一种新提出的强有力并且可扩展的框架结构。
- 以太网的数据传输是基于“共享”原理的,所有的同一本地网范围内的计算机共同接收到相同的数据包。这意味着计算机直接的通讯都是透明可见的。正是因为这样的原因,以太网卡都构造了硬件的“过滤器”这个过滤器将忽略掉一切和自己无关的网络信息。事实上是忽略掉了与自身MAC地址不符合的信息。嗅探程序正是利用了这个特点,它主动的关闭了这个嗅探器,也就是前面提到的设置网卡“混杂模式”。因此,嗅探程序就能够接收到整个以太网内的网络数据信息。
四、方法概述:
- 嗅探器实质就是从网络上获取数据包的一种工具,它可以捕捉流经本地网卡的所有数据包。抓取网络数据包进行分析有很多用处,如分析网络是否有网络病毒等异常数据,通信协议的分析(数据链路层协议、TCP、IP、UDP,甚至各种应用层协议),敏感数据的捕捉等。
程序在执行过程中有两个核心算法设计,一是调用Winpcap函数库实现下层抓包,二是对抓到的包文进行分析。对于抓包算法过程的实现:
(1)初始化Winpcap开发库;
(2)获取当前的网卡列表,同时要求用户指定要操作的网卡;
(3)获得当前的过滤规则;
(4)调用库函数pcap_loop(),同时指定其回调函数,即为数据包分析过程。 - 本实验需要电脑上已经安装过winpcap,最主要的部分是数据包的解析与信息的获取。
- 首先是配置使用pcap.h这个库,只需要在链接上加入wpcap.lib等几个相关联的动态库即可,其中拥有着功能更加强大的函数,现列举几个本嗅探器设计中所使用的函数于下:
- pcap_findalldevs_ex()函数用于获取当前主机的设备列表,使用者可以根据该函数给出的设备列表选定需要进行嗅探的设备究竟是哪一个,例如是以太网口还是无线网卡之类的设备。最后使用pcap_freealldevs()清空即可,否则会一直占用影响下次使用。
- pcap_open()函数则是另一个较为关键的函数,它主要用于打开与连接设备器,后面跟随的参数分别为:设备名 ;要捕捉的数据包的部分,往往用65535保证能捕获到不同数据链路层上的每个数据包的全部内容 ;‘PCAP_OPENFLAG_PROMISCUOUS’用作将其置于混杂模式 ; 读取超时时间 ;远程机器验证(NULL) ;错误缓冲池这几个参数,这也是确保嗅探器开始正确嗅探的一步。
- 当嗅探器正确收集到数据包的时候则需要对数据包进行进一步的解析,以IP结构头为例,iphVerLen就代表了IP头中版本号和头长度(各占4位)的信息,ipSource与ipDestination则是我们所关心的源地址与目标地址信息,通过对获取到的数据包的结构体中这些信息的解析与输出,例如ip_hd->ipDestination便提取出了投中的目标地址,完成到了基本的嗅探操作,同时也可以获得数据包以数据或ASCII码等多种形式予以输出。
- Winpcap提供了pcap_findalldevs_ex()函数,这个函数返回一个指向pcap_if结构的链表,其中的每一项都包含了一个己经绑定的适配器的全部信息。取得网卡列表后就在屏幕上显示出来,如果网卡没有被发现就显示有关错误。pcap_ findalldevs()同其他的libpcap函数一样有一个errbuf参数,当有异常情况发生时,这个参数会被pcap填充为某个特定错误字串。随后,获得网卡的信息后就可以按数据捕获的要求打开网卡,打开网卡的功能是通过pcap_open_live()来实现的。在正常情况下网卡只接受去往它的包而去往其他主机的数据包则被忽略,相反当网卡处于混杂模式时它将接收所有的流经它的数据包。
- 之后,使用pcap_next_ex()从网络接口中读取一个数据包,该函数第一个参数是接口句柄,后两个参数由函数返回,分别为数据包的相关信息和数据包本身。函数返回1表示正常接收一个数据包,每捕获到一个数据包就调用PacketHandler()函数对数据包进行后续解析处理。之后对捕获的数据包按照数据链路层、网络层、传输层和应用层的层次结构自底向上进行解析,最后将解析结果显示输出。
五、实验设计流程:
六、实验结果:
该实验使用了pcap.h这个库文件,实现了更加强大的监听功能,不仅能对TCP进行监听,亦可以对网络中流传的UDP协议通讯进行监听。运行程序后,首先检测2号网卡是否打开,经过监听一段时间发现并没有任何回应,即可说明该网卡端口并没有连接,详见下图:
随后输入网卡号3,同时输出获取的报文,且不对其输出长度做任何限制。这次成功监听到端口信息,详见下图:
七、代码:
#define HAVE_REMOTE
#define LINE_LEN 16
#include "winsock.h"
#include <string.h>
#include "pcap.h"
///
typedef struct ip_address
{ //ip地址
u_char b1;
u_char b2;
u_char b3;
u_char b4;
} ip_address;
typedef struct mac_address
{//mac地址
u_char b1;
u_char b2;
u_char b3;
u_char b4;
u_char b5;
u_char b6;
} mac_address;
typedef struct ethe_header
{ //mac帧首部
mac_address mac_dest_address;
mac_address mac_source_address;
u_short ether_type;
} ethe_header;
typedef struct ip_header
{ //ip地址首部
u_char ver_ihl;
u_char tos;
u_short tlen;
u_short identification;
u_short flags_fo;
u_char ttl;
u_char proto;
u_short crc;
ip_address saddr;
ip_address daddr;
u_int op_pad;
} ip_header;
typedef struct udp_header
{ //UPD首部
u_short sport;
u_short dport;
u_short len;
u_short crc;
} udp_header;
typedef struct tcp_header
{ //TCP首部
u_short sport;
u_short dport;
u_int num;
u_int ack;
u_short sum;
u_short windonw;
u_short crc;
u_short ugr;
} tcp_header;
void packet_handler(u_char * param, const struct pcap_pkthdr * header, const u_char *pkt_data);
char judge;
int length;
int main()
{
pcap_if_t * alldevs, *device;
int i = 0;
int iNum;
u_int netmask;
struct bpf_program fcode;
pcap_t * adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
//修改这里可以更改捕获的数据包使用的协议类型
char packet_filter[] = "(ip and udp) or (ip and tcp) or (ip and icmp)";
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{ //获取设备列表
fprintf(stderr,"无法打开网络设备:%s\n", errbuf);
return 1;
}
for (device = alldevs; device != NULL; device = device->next)
{ //打印列表
if (i == 0)
{
printf("请按CTRL + C退出!\n\n");
printf("网络设备如下:\n");
}
printf("%d. %s\n", ++i, device -> name);
if (device->description)
printf(" (%s)\n", device->description);
else
printf("没有设备描述信息!");
}
if (i == 0)
{
printf("\n请先安装WinPcap!");
return -1;
}
printf("请选择网络设备接口:(1 - %d):", i);
scanf("%d", &iNum);
getchar();
if (iNum < 1 || iNum > i)
{
printf("设备不存在!\n");
pcap_freealldevs(alldevs);
return -1;
}
//跳转到已选设备
for (device = alldevs, i = 0;i < iNum -1 ; device = device -> next,i++);
// 打开适配器
if ( (adhandle= pcap_open(device->name, // 设备名
65536, // 要捕捉的数据包的部分
// 65535保证能捕获到不同数据链路层上的每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式
1000, // 读取超时时间
NULL, // 远程机器验证
errbuf // 错误缓冲池
) ) == NULL)
{
fprintf(stderr,"\n不能打开适配器!\n");
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}
if (pcap_datalink(adhandle) != DLT_EN10MB)
{ //检查数据链路层,为了简单,只考虑以太网
fprintf(stderr, "\n系统网卡链路出错!\n");
pcap_freealldevs(alldevs); //释放设备列表
return -1;
}
if (device->addresses != NULL) //获得接口第一个地址的掩码
netmask = ((struct sockaddr_in *)(device->addresses->netmask))->sin_addr.S_un.S_addr;
else //如果接口没有地址,那么我们假设一个C类的掩码
netmask = 0xffff00;
if (pcap_compile(adhandle, &fcode, packet_filter, 1, netmask) < 0)
{ //编译过滤器
fprintf(stderr, "不能监听过滤该数据报!\n");
pcap_freealldevs(alldevs);
return -1;
}
if (pcap_setfilter(adhandle, &fcode) < 0)
{ //设置过滤器
fprintf(stderr, "过滤设置错误!\n");
pcap_freealldevs(alldevs);
return -1;
}
printf("请输入是否要输出捕捉到的报文信息(y/n) : ");
scanf("%c",&judge);
if (judge!='n')
{
printf("请输入要限制要输出报文信息长度(-1不限制) : ");
scanf("%d",&length);
}
printf("\n正在监听通过%s的数据报...\n", device->description);
pcap_freealldevs(alldevs); //释放设备列表
pcap_loop(adhandle, 0, packet_handler, NULL); //开始捕捉
return 0 ;
}
void packet_handler(u_char *dumpfile, const struct pcap_pkthdr *header, const u_char *pkt_data)
{ //回调函数,当收到每一个数据包时会被libpcap所调用
if(header->caplen>400) return;
int len;
struct tm *ltime;
char timestr[16];
ip_header * ip_hd;
udp_header * udp_hd;
tcp_header * tcp_hd;
ethe_header * ethe_hd;
int ip_len,tcp_len,start;
u_short sport,dport;
printf("\n");
ltime=localtime(&header->ts.tv_sec); //将时间戳转换为可读字符
strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);
printf("时间:%s\n",timestr);
ethe_hd = (ethe_header *)pkt_data;
ip_hd = (ip_header *)(pkt_data + 14);
ip_len = (ip_hd ->ver_ihl & 0xf) * 4; //ip首部长度
udp_hd = (udp_header *)((u_char *)ip_hd + ip_len);
sport = ntohs(udp_hd->sport);
dport = ntohs(udp_hd->dport);
if(ip_hd->proto==17)
{
printf("协议:UDP");
start=ip_len+8;
}
else if(ip_hd->proto==6)
{
printf("协议:TCP");
tcp_hd = (tcp_header *)((u_char *)ip_hd + ip_len);
tcp_len=ntohs(tcp_hd->sum)>>12;
start=ip_len+tcp_len*4;
}
else if(ip_hd->proto==1)
{
printf("协议:ICMP");
start=ip_len+23;
}
else printf("协议:其他");
//printf("start=%d\n",start);
printf(" 数据报的长度:%d\n",header->caplen);
printf("IP头的长度:%d IP包存活时间:%d\n",ip_hd->tlen,ip_hd->ttl);
printf("源IP地址: %d.%d.%d.%d:%d 目的IP地址:%d.%d.%d.%d:%d\n源端口:%d 目的端口:%d\n源物理地址: %x-%x-%x-%x-%x-%x 目的物理地址:%x-%x-%x-%x-%x-%x\n",
ip_hd->saddr.b1, ip_hd->saddr.b2, ip_hd->saddr.b3, ip_hd->saddr.b4,
ip_hd->daddr.b1, ip_hd->daddr.b2, ip_hd->daddr.b3, ip_hd->daddr.b4, sport, dport,
ethe_hd->mac_source_address.b1, ethe_hd->mac_source_address.b2, ethe_hd->mac_source_address.b3,
ethe_hd->mac_source_address.b4, ethe_hd->mac_source_address.b5, ethe_hd->mac_source_address.b6,
ethe_hd->mac_dest_address.b1, ethe_hd->mac_dest_address.b2, ethe_hd->mac_dest_address.b3,
ethe_hd->mac_dest_address.b4, ethe_hd->mac_dest_address.b5, ethe_hd->mac_dest_address.b6);
//输出数据部分
if (judge=='y')
{
printf("数据部分内容为:\n");
if(length==-1) len=(header->caplen) + 1 ;
else len=(length>header->caplen + 1-start)?(header->caplen+1)-start:length;
for (int i=start; (i < start + len ) ; i++)
{
printf("%.2x ", pkt_data[i-1]); //也可以改为 %c 以 ascii码形式输出。
if ( (i % LINE_LEN) == 0) printf("\n");
}
printf("\n\n");
}
}