使用WinPcap编写Sniffer程序
内容介绍 嗅探器原理 Winpcap介绍 Winpcap安装 Winpcap应用程序结构
Sniffer(嗅探器)设计原理 通常的套接字程序只能响应与自己硬件地址相匹配的或是以广播形 式发出的数据帧,对于其他形式的数据帧,比如已到达网络接口但 却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地 址之后将不引起响应,也就是说应用程序无法收取到达的数据包。 网络嗅探器的目的恰恰在于从网卡接收所有经过它的数据包,这些 数据包既可以是发给本机的也可以是发往别处的。通过将网络接口 设置为混杂模式可以使它接收所有经过它的数据包(例如以太网帧 将会到达同一局域网的所有网络接口)。 但是,此时操作系统不再 行底层的细节操作(协议处理,流量均衡等) ,而是将拆封解释处 理接收到的数据帧(frame)的任务交给应用程序完成,这样应用程 序就可以灵活的获取各类信息。
什么是 Winpcap 网络数据包捕获库函数 直接访问网络,免费、公用 工作于驱动层,网络操作高效 为应用程序提供了一组API接口 Libpcap(UNIX)库 Winpcap(Windows)库 网络数据包捕获库函数 直接访问网络,免费、公用 工作于驱动层,网络操作高效 为应用程序提供了一组API接口 编程容易,源码级移植方便 Winpcap介绍
WinPcap主要功能 捕获原始数据包 将数据包发送给应用程序之前,按照用户规定的规范过滤数据包 将捕获到的数据包输出到文件中,并可以对这些文件进行再分析 向网络发送原始数据包 搜集网络传输统计数据 Winpcap介绍
哪些应用适合使用 WinPcap Winpcap介绍 网络和协议分析network and protocol analyzers 网络监控network monitors 流量记录traffic loggers 流量产生traffic generators 用户级网桥和路由器user-level bridges and routers 网络入侵检测network intrusion detection systems (NIDS) 网络扫描network scanners 安全工具security tools Winpcap介绍
WinPcap不能胜任的事情 立收发数据包。这意味着它不能阻塞、过 滤或者处理同一主机上其他程序产生的数 WinPcap从主机的协议(如TCP/IP)独 立收发数据包。这意味着它不能阻塞、过 滤或者处理同一主机上其他程序产生的数 据包:它仅仅嗅探网线上传输的数据包。 所以它不适合应用于流量均衡、QoS调度 和个人防火墙。 Winpcap介绍
Winpcap的安装 下载安装包和开发包 http://winpcap.polito.it 程序员开发包(WpdPack_3_1.zip) Winpcap的安装包(Winpcap_3_1.exe) 程序员开发包(WpdPack_3_1.zip) 运行Winpcap_3_1.exe 测试安装结果 Winpcap安装
编程环境设定 Winpcap安装 1. 以Administrator身份登录Windows(2000/XP),运行 2. 运行Visual C++ 6.0, 打开WpdPack_3_1\WpdPack\Examples-pcap\下的 任一项目(本例用basic_dump目录下 basic_dump.dsw) 3. 在“工程->设置-> Link->对象/库模块” 中加入 wsock32.lib ws2_32.lib wpcap.lib 在“工具->选择->目录”的include files和library files设置中引入winpcap开发包中的Include和Lib目录 4. 编译,运行 Winpcap安装
例程运行结果: Winpcap安装
获得设备列表 WinPcap的典型应用 打开一个适配器 过滤数据包 回调机制 直接方式 解析数据包 打开离线数据包文件 获得网络流量统计数字 获得已安装设 备的高级信息 获得设备列表 打开一个适配器 打开离线数据包文件 过滤数据包 回调机制 直接方式 解析数据包 获得网络流量统计数字
获得设备列表 (一) 一个基本的WinPcap应用程序所需的第一步 就是获得合适的网络适配器。 Libpcap提供 pcap_findalldevs() 函数完成 这个功能。这个函数返回一个相连的pcap_if结 构的列表,列表的每一项包含关于适配器的复杂 的信息。特别的,name和description域数据包 含设备的名称和可读的描述。
pcap_if_t *alldevs,*d; int i=0; char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(&alldevs, errbuf) == -1) { fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf); exit(1); } for(d=alldevs;d;d=d->next) { /* Print the list */ printf("%d. %s", ++i, d->name); if (d->description) printf(" (%s)\n", d->description); else printf(" (No description available)\n"); } if(i==0){ printf("\nNo interfaces found! Make sure WinPcap is installed.\n"); return; } pcap_freealldevs(alldevs);
获得设备列表 (二) 每个pcap_findalldevs() 返回的 pcap_if 结构也 包含了一个pcap_addr 结构的列表: 该接口的地址列表 网络掩码的列表(每个网络掩码对应地址列表中的一项) 广播地址的列表(每个广播地址对应地址列表中的一项) 目标地址的列表(每个目标地址对应地址列表中的一项) 通过返回的结构,我们可以得到探测到的网卡 设备的更详尽信息。
typedef struct pcap_if pcap_if_t struct pcap_if *next; char *name; char *description; struct pcap_addr *addresses; bpf_u_int32 flags; /* PCAP_IF_ interface flags */ }; struct pcap_addr { struct pcap_addr *next; struct sockaddr *addr; struct sockaddr *netmask; struct sockaddr *broadaddr; struct sockaddr *dstaddr;
打开一个适配器开始捕获数据包 设备标识 (字符串) 抓包长度 混杂模式 超时时间 pcap_t * pcap_open_live ( const char * device, int snaplen, int promisc, int to_ms, char * ebuf ) pcap_t *adhandle= pcap_open_live(d->name, 65536,1, 1000, errbuf ); 设备标识 (字符串) 抓包长度 混杂模式 超时时间
捕获数据包(回调机制) int pcap_loop ( pcap_t * p, int cnt, pcap_handler callback, u_char * user ) 例如: pcap_loop(adhandle, 0, packet_handler, NULL); typedef void(* pcap_handler) ( u_char *user, const struct pcap_pkthdr *pkt_header, const u_char *pkt_data)
捕获数据包(直接方式) int pcap_next_ex ( pcap_t * p, struct pcap_pkthdr ** pkt_header, const u_char ** pkt_data ) 该函数从接口或者脱机读取一个数据包。用于接收下一个可用的数 据包,而不使用libpcap提供的传统的回调机制。 pcap_next_ex用下 一个数据包的指向数据包头和数据的指针填充pkt_header和pkt_data 参数。 pcap_next_ex() 目前只在Win32下可用,因为它不是属libpcap 原始的API。这意味着含有这个函数的代码将不能被移植到Unix上。
过滤数据包 int pcap_compile ( pcap_t * p, struct bpf_program * fp, char * str, //过滤表达式 int optimize, bpf_u_int32 netmask ) //掩码 int pcap_setfilter ( pcap_t * p, struct bpf_program * fp ) pcap_compile() 编译一个包过滤器。将一个高级的、布尔形式表 示的字符串转换成低级的、二进制过滤语句,以便被包驱动使用。 pcap_setfilter() 在核心驱动中将过滤器和捕获过程结合在一起。从 这一时刻起,所有网络的数据包都要经过过滤,通过过滤的数据 包将被传入应用程序。
char packet_filter[] = "ip and udp"; 过滤设置举例 char packet_filter[] = "ip and udp"; struct bpf_program fcode; /* 获取接口地址的掩码,如果没有掩码,认为该接口属于一个C类网络 */ if(d->addresses != NULL) netmask=((struct sockaddr_in *) (d->addresses->netmask))->sin_addr.S_un.S_addr; else netmask=0xffffff; if(pcap_compile(adhandle, &fcode, packet_filter, 1, netmask) <0 ){ fprintf(stderr,"\nUnable to compile the filter. Check the syntax.\n"); pcap_freealldevs(alldevs); return -1; } if(pcap_setfilter(adhandle, &fcode)<0){ fprintf(stderr,"\nError setting the filter.\n"); pcap_freealldevs(alldevs); return -1; }
过滤表达式 表达式由一个或多个原语组成。原语通常由一个id(名称或者数字) 和在它前面的一个或几个修饰符组成。有3种不同的修饰符: 可能是host,net和port。 例如 “host foo”、“net 128.3”、“port 20”。如果没有类型修饰符, 缺省为host。 方向 指明向 和/或 从id传输等方向的修饰符。 可能的方向有src、dst、src or dst 和 src and dst。 例如“src foo”、“dst net 128.3”、“src or dst port ftp-data”。如果 缺省为src or dst。 协议 指明符合特定协议的修饰符。 目前的协议包括ether、fddi、ip、ip6、arp、rarp、tcp和udp等。 例如“ether src foo”、 “arp net 128.3”。 如果没有协议修饰符,则表示声明类型的所有协议。 例如“src foo”表示“(ip or arp or rarp) src foo” “port 53”表示“(tcp or udp) port 53”。
解析数据包 不同的网络使用不同的链路层协议,不知道网络类型就无法定位数 据帧(frame) ,所以,首先使用如下函数对网络类型进行判断: int pcap_datalink(pcap_t *p) 它返回适配器的链路层标志,例如DLT_EN10MB表示以太网(10Mb, 100Mb,1000Mb及以上),DLT_IEEE802表示令牌环网。可以在设 置过滤条件之前先对各个设备的网络类型进行探测,以获知物理层 的数据帧格式,接下来就可以针对不同的帧格式解析出封装在其中 的报文(packet),例如IP报文,继而拆封IP报文得到TCP或者UDP 等报文,再深层次的拆封就是应用程序数据了。
解析数据包 前面我们提到,用户在程序中调用的pcap_loop()(或者 pcap_dispatch())函数可以进行数据包的捕获,而每一 个数据包到达时该函数会调用pcap_handler()函数进行数 据包处理,返回一个指向捕获器头部和一个指向帧数据 的指针(包含协议头)。下面我们针对常用的以太网给 出这一过程及IP、TCP的头部格式,其他数据包的格式 请参考RFC文档。注意:在数据包解析时要检查校验和 以及包的顺序。
Ethernet帧头 struct sniff_ethernet { u_char ether_dhost[ETHER_ADDR_LEN]; u_char ether_shost[ETHER_ADDR_LEN]; u_short ether_type; /* IP? ARP? RARP? 等*/ };
IP报文头
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */ struct sniff_ip { #if BYTE_ORDER == LITTLE_ENDIAN u_int ip_hl:4, /* header length */ ip_v:4; /* version */ #endif #if BYTE_ORDER == BIG_ENDIAN u_int ip_v:4, /* version */ ip_hl:4; /* header length */ #endif /* not _IP_VHL */ u_char ip_tos; /* type of service */ u_short ip_len; /* total length */ u_short ip_id; /* identification */ u_short ip_off; /* fragment offset field */ #define IP_RF 0x8000 /* reserved fragment flag */ #define IP_DF 0x4000 /* don’t fragment flag */ #define IP_MF 0x2000 /* more fragments flag */ #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */ u_char ip_ttl; /* time to live */ u_char ip_p; /* protocol */ u_short ip_sum; /* checksum */ struct in_addr ip_src,ip_dst; /* source and dest address */ };
TCP报文头
struct sniff_tcp { u_short th_sport; /* source port */ u_short th_dport; /* destination port */ tcp_seq th_seq; /* sequence number */ tcp_seq th_ack; /* acknowledgement number */ #if BYTE_ORDER == LITTLE_ENDIAN u_int th_x2:4, /* (unused) */ th_off:4; /* data offset */ #endif #if BYTE_ORDER == BIG_ENDIAN u_int th_off:4, /* data offset */ th_x2:4; /* (unused) */ u_char th_flags; #define TH_FIN 0x01 #define TH_SYN 0x02 #define TH_RST 0x04 #define TH_PUSH 0x08 #define TH_ACK 0x10 #define TH_URG 0x20 #define TH_ECE 0x40 #define TH_CWR 0x80 #define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR) u_short th_win; /* window */ u_short th_sum; /* checksum */ u_short th_urp; /* urgent pointer */ };
获取包数据 struct sniff_ethernet* ethernet; struct sniff_ip* ip; pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user) handler_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet); struct sniff_ethernet* ethernet; struct sniff_ip* ip; struct sniff_tcp* tcp; ethernet = (struct sniff_ethernet*)(packet); ip = (struct sniff_ip*)(packet + size_ethernet); tcp = (struct sniff_tcp*)(packet + size_ethernet + size_ip); payload = (u_char *)(packet + size_ethernet + size_ip + size_tcp);
处理离线数据包文件 WinPcap提供了大批函数,用于将网络数据保存到文 件和将储存文件读出。 文件格式和libpcap保存的是一样的。这种格式比较简 单,用二进制格式保存捕获的数据包的数据,它也是很 多网络工具例如WinDump,Ethereal和Snort等使用的标 准。 这里的函数包括pcap_dump_open()、pcap_dump()、 pcap_open_offline()等。
将数据包存入文件 //主程序 pcap_dumper_t * dumpfile; dumpfile = pcap_dump_open(adhandle, argv[1]); //打开一个存储文件并将它和接口联系起来 if(dumpfile==NULL){ fprintf(stderr,"\nError opening output file\n");return -1; } //… pcap_loop(adhandle, 0, packet_handler, (unsigned char *)dumpfile); //packet_handler void packet_handler(u_char *dumpfile, const struct pcap_pkthdr *header, const u_char *pkt_data) { /* save the packet on the dump file */ pcap_dump(dumpfile, header, pkt_data);
从保存文件中读取数据包 第一步:打开离线数据文件 第二步:读取离线数据 (一)使用回调函数 (二)不使用回调函数 if ( (fp = pcap_open_offline(argv[1], errbuf) ) == NULL) { fprintf(stderr,"\nError opening dump file\n");return -1; } 第二步:读取离线数据 (一)使用回调函数 (二)不使用回调函数 pcap_loop(fp, 0, dispatcher_handler, NULL);//主程序中 void dispatcher_handler(u_char *temp1, const struct pcap_pkthdr *header, const u_char *pkt_data) { //处理代码 } while((res = pcap_next_ex( fp, &header, &pkt_data)) >= 0){ //处理代码 }//主程序中
发送数据包 pcap_sendpacket发送单个数据包 发送队列(查看winpcap手册)
pcap_sendpacket发送单个数据包 来发送一个手写的数据包。 pcap_sendpacket()用一个包含要发送的数据 的缓冲区、该缓冲区的长度和发送它的适配器作 为参数。注意该缓冲区是不经任何处理向外发出 的,这意味着,如果想发些有用的东西的话,应 用程序必须产生正确的协议头。
u_char packet[100]; if((fp = pcap_open_live(argv[1], 100, 1, 1000, error) ) == NULL) { fprintf(stderr,"\nError opening adapter: %s\n", error); return; } /* Supposing to be on ethernet, set mac destination to 1:1:1:1:1:1 */ packet[0…5]=1; /* set mac source to 2:2:2:2:2:2 */ packet[6…11]=2; /* Fill the rest of the packet */ for(i=12;i<100;i++){ packet[i]=i%256; /* Send down the packet */ pcap_sendpacket ( fp, packet, 100);
获得网络流量统计数字 统计的内容: 最后一个时间间隔内的数据包的数目和接收 的比特数 为了用这个功能来监视网络,程序员必须打开适配器并 将它设置为统计模式。这可以通过调用pcap_setmode() 完成,此时这个函数的mode参数应该为MODE_STAT。
获得网络流量统计数字 统计模式需要的数据拷贝和内容交换最少,所以CPU的 利用率比较高。并且所需要的内存很少。 注意:开始激活统计模式之前,用户可以设置过滤器,定义监视网络流量的一部分。如果没有设置过虑器,整个流量都要被监视。
fp = pcap_open_live(argv[1], 100, 1, 1000, error); struct timeval st_ts; fp = pcap_open_live(argv[1], 100, 1, 1000, error); pcap_setmode(fp, MODE_STAT); pcap_loop(fp, 0, dispatcher_handler, (PUCHAR)&st_ts); void dispatcher_handler(u_char *state, const struct pcap_pkthdr *header, const u_char *pkt_data) { struct timeval *old_ts = (struct timeval *)state; //注意这里指针的使用 u_int delay; //提供了两个64位计数器: //统计最后一个时间间隔内的数据包的数目和接收的比特数。 LARGE_INTEGER Bps,Pps; struct tm *ltime; char timestr[16]; //利用采样的时间戳计算自上一次采样以来的延迟时间(毫秒) delay=(header->ts.tv_sec - old_ts->tv_sec) * 1000000 – old_ts->tv_usec + header->ts.tv_usec; 注意:此处dispatch_handler每次回调时填入的 第二个和第三个参数,前者还是和驱动 有关的抓包头部,而后者指向封装了接 收包数目和接收字节数目的数据区
//计算每秒获取的二进制位数 Bps. QuadPart=(((. (LONGLONG. )(pkt_data + 8)). 8 //计算每秒获取的二进制位数 Bps.QuadPart=(((*(LONGLONG*)(pkt_data + 8)) * 8 * 1000000) / (delay)); /* 计算每秒获取的包数 */ Pps.QuadPart=(((*(LONGLONG*)(pkt_data)) * 1000000) / (delay)); //打印时间戳 ltime=localtime(&header->ts.tv_sec); strftime( timestr, sizeof timestr, "%H:%M:%S", ltime); printf("%s ", timestr); //打印采样值 printf("BPS=%I64u ", Bps.QuadPart); printf("PPS=%I64u\n", Pps.QuadPart); //保存当前的时间戳 old_ts->tv_sec=header->ts.tv_sec; old_ts->tv_usec=header->ts.tv_usec; }
例:应用程序模块及工作流程