第一个试验是:
#include <pcap.h>
#include <remote-ext.h>
int main() {
pcap_if_t *alldevs;
pcap_if_t *d;
int i = 0;
char errbuf[PCAP_ERRBUF_SIZE];
/* Retrieve the device list from the local machine*/
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL /* auth is not needed */, &alldevs, errbuf) == -1)
{
printf("Error in pcap_findalldevs_ex: %s/n", errbuf);
exit(1);
}
/* Print the list */
for (d = alldevs; d != NULL; d = d->next)
{
/* Print the device's name */
printf("%d. %s", ++ i, d->name);
/* Print the device's dscription */
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 0;
}
/* We don't need any more the device list. Free it */
pcap_freealldevs(alldevs);
return 1;
}
编译的时候又遇到问题——“无法打开pcap.h”。又查看开发手册才找到解决方法:
1.安装Winpcap驱动。下载地址:http://www.winpcap.org/install/bin/WinPcap_3_1.exe。
2.将Winpcap的Include,Lib目录添加进VC6.0的环境变量中;
3. 针对每一个项目,先用VC打开项目,然后在"Project->Settings",标签栏出选择"C/C++",在"Preprocessor definitions"的输入框里添加"WPCAP",再选择"Link",在"Object/library modules"的输入框里添加"wpcap.lib Packet.lib"。
再编译时终于OK了。之后,阅读代码并查看开发手册学到了下面的东西:
pcap_if是一个结构体,具体点它是一个链表的结点,他的定义如下:
struct pcap_if {
struct pcap_if *next;
char *name;
char *description;
struct pcap_addr *addresses;
u_int flags;
}
另外,在pcap.h中有一句“typedef struct pcap_if pcap_if_t;”,所以也可以用pcap_if_t代替pcap_if。
int pcap_findalldevs_ex(char * source,
struct pcap_rmtauth * auth,
pcap_if_t ** alldevs,
char * errbuf
)
这个函数是’pcap_findalldevs()’的一个超集。’pcap_findalldevs()’比较老,他只允许列出本地机器上的设备。然而,’pcap_findalldevs_ex()’除了可以列出本地及其上的设备,还可以列出远程机器上的设备。此外,它还能列出所有可用的pcap文件到指定的文件夹。’pcap_findalldevs_ex()’是平台无关的,然而它以来于标准的’pcap_findalldevs()’来获得本地机器的地址。
’pcap_findalldevs_ex()’除了可以列出本地及其上的设备,还可以列出远程机器上的设备。
那怎么知道是本地的还是远程的呢?
有什么区别呢?
第一个问题:却别是本地还是远程是由pcap_findalldevs_ex()的第一个参数(source)决定的,source指定所要监控的网络适配器。它有特定的“source语法”。pcap_findalldevs_ex()支持的source语法有一下几种:
其中,'host'和'port'参数既可以是数字形式(numeric),又可以是字符形式(literal),下面形式都是合法的:
下面举一些实际例子:
另外,在Winpcap里,字符串"file://"和字符串"rpcap://"都被宏定义过:
#define PCAP_SRC_FILE_STRING "file://"
#define PCAP_SRC_IF_STRING "rpcap://"
所以也可以用宏名来代替它们。
第二个问题:列出本地适配器跟远程适配器的区别(作用),本地的就不多说了,这是Winpcap的基本功能的体现;而列出远程的可用的适配器,就可以建立一个到该远程主机的控制连接,就可以使用该主机上存在的套结字,说通俗点就是可以通过远程主机上的适配器连接到该主机上并对该主机进行控制。
今天在阅读 Winpcap Manual 的时候发现一句话:
“This means that on shared media (like non-switched Ethernet), WinPcap will be able to capture the packets of other hosts.”
我理解为:如果在通过没有交换功能的集线器连接的网络上,只要把网卡设置为混杂(promiscuous)模式,winpcap能够捕获到其他主机通信的数据包。如果是具有交换功能的集线器连接的网络winpcap还能管用吗?这个在后边的实习中将会进行试验。
试验程序2:
/*
* 截获数据包的试验。先打印出所有网络适配器的列表,然后选择
* 想在哪个适配器上截获数据包。然后通过pcap_loop()函数将截获
* 的数据包传给回调函数packet_handler()处理。
* 通过该程序初步了解了使用winpcap截获数据包的步骤以及一些在
* 截获数据包时非常重要的函数和结构体。
* 2006-1-26
*/
#include <pcap.h>
#include <remote-ext.h>
/* Prototype of the packet handler */
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);
int main() {
pcap_if_t *alldevs;
pcap_if_t *d;
int inum;
int i = 0;
pcap_t *adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
/* Retrieve the devices list on the local machine */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr, "Error in pcap_findalldevs: %s/n", errbuf);
exit(1);
}
/* Print the list */
for (d = alldevs; d; d = d->next)
{
/* Print name */
printf("%d. %s", ++ i, d->name);
/* Print description */
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 -1;
}
/* Select an adapter */
printf("Enter the interface number (1 - %d):", i);
scanf("%d", &inum);
if (inum < 1 || inum > i)
{
printf("/nInterface number out of range./n");
/* Free the device list */
pcap_freealldevs(alldevs);
return -1;
}
/* Jump to the selected adapter */
for (d = alldevs, i = 0; i < inum - 1; d = d->next, ++ i);
/*Open the device */
if ((adhandle = pcap_open(d->name, /* name of the device */
65536, /* portion of the packet to capture */
/* 65535 guarantees that the whole packet will be captured on all the link layers */
PCAP_OPENFLAG_PROMISCUOUS, /* promiscuous mode */
1000, /* read timeout */
NULL, /* authentication on the remote machine */
errbuf /* error buffer */
)) == NULL)
{
fprintf(stderr, "/nnable to open the adapter. %s is not supported by Winpcap/n", d->name);
/* Free the devices list */
pcap_freealldevs(alldevs);
return -1;
}
printf("/nlistening on %s.../n", d->description);
/* At this point, we don't need any more the device list. Free it */
pcap_freealldevs(alldevs);
/* start the capture */
pcap_loop(adhandle, 0, packet_handler, NULL);
return 1;
}
/* Callback function invoked by libpcap for every incoming packet */
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) {
struct tm *ltime;
char timestr[16];
/* convert the timestamp to readable format */
ltime = localtime(&header->ts.tv_sec);
strftime(timestr, sizeof(timestr), "%H:%M:%S", ltime);
printf("%s, %.6d len:%d/n", timestr, header->ts.tv_usec, header->len);
}
函数1:
pcap_t *pcap_open(const char * source,
int snaplen,
int flags,
int read_timeout,
struct pcap_rmtauth * auth,
char * errbuf
)
为捕获/发送数据打开一个普通的源。pcap_open()能够替代所有的pcap_open_xxx()函数,它隐藏了不同的pcap_open_xxx()之间的差异,所以程序员不必使用不同的open函数。
source:的是包含要打开的源名称的以’/ 0’ 结尾的字符串。源名称得包含新的源规范语法(Source Specification Syntax),并且它不能为NULL。为了方便的使用源语法,请记住:(1)pcap_findalldevs_ex()返回的适配器(网卡)可以直接被pcap_open()使用;(2)万一用户想传递他自己的源字符串给pcap_open(),pcap_createsrcstr()可以创建正确的源标识。
snaplen:需要保留的数据包的长度。对每一个过滤器接收到的数据包,第一个‘snaplen’字节的内容将被保存到缓冲区,并且传递给用户程序。例如,snaplen等于100,那么仅仅每一个数据包的第一个100字节的内容被保存。简言之就是从每一个包的开头到snaplen的那段内容将被保存。
flags:保存一些由于抓包需要的标志。Winpcap定义了三种标志:
l PCAP_OPENFLAG_PROMISCUOUS:1,它定义了适配器(网卡)是否进入混杂模式(promiscuous mode)。
l PCAP_OPENFLAG_DATATX_UDP:2,它定义了数据传输(假如是远程抓包)是否用UDP协议来处理。
l PCAP_OPENFLAG_NOCAPTURE_RPCAP:4,它定义了远程探测器是否捕获它自己产生的数据包。
read_timeout:以毫秒为单位。read timeout被用来设置在遇到一个数据包的时候读操作不必立即返回,而是等待一段时间,让更多的数据包到来后从OS内核一次读多个数据包。并非所有的平台都支持read timeout;在不支持read timeout的平台上它将被忽略。
auth:一个指向’struct pcap_rmtauth’的指针,保存当一个用户登录到某个远程机器上时的必要信息。假如不是远程抓包,该指针被设置为NULL。
errbuf:一个指向用户申请的缓冲区的指针,存放当该函数出错时的错误信息。
返回值是一个’pcap_t’指针,它可以作为下一步调用(例如pcap_compile()等)的参数,并且指定了一个已经打开的Winpcap会话。在遇到问题的情况下,它返回NULL并且’errbuf’变量保存了错误信息。
函数2:
int pcap_loop( pcap_t* p,
int cnt,
pcap_hander callback,
u_char* user
)
收集一群数据包。pcap_loop()与pcap_dispatch()类似,但是它会一直保持读数据包的操作直到cnt包被处理或者发生了错误。当有活动的读超时(read timeout)时它并不返回。然而,对pcap_open_live()指定一个非0的读超时(read timeout),当发生超时的时候调用pcap_dispatch()来接收并处理到来的所有数据包更好。Cnt指明了返回之前要处理数据包的最大数目。如果cnt为负值,pcap_loop()将一直循环(直到发生错误才停止)。如果出错时返回-1;如果cnt用完时返回0;如果在任何包被处理前调用pcap_breakloop()来中止循环将返回-2。所以,如果程序中使用了pcap_breakloop(),必须准确的来判断返回值是-1还是-2,而不能简单的判断<0。
函数3:
hypedef void (* pcap_handler)(u_char* user,
const struct pcap_pkthdr* pkt_header,
const u_char* pkt_data)
接收数据包的回调函数原型。当用户程序使用pcap_dispatch()或者pcap_loop(),数据包以这种回调的方法传给应用程序。用户参数是用户自己定义的包含捕获会话状态的参数,它必须跟pcap_dispatch()和pcap_loop()的参数相一致。pkt_hader是与抓包驱动有关的头。pkt_data指向包里的数据,包括协议头。
结构体1:
struct pcap_pkthdr {
struct timeval ts;
bpf_u_int32 caplen;
bpf_u_int32 len;
}
ts:时间戳
cpalen:当前分组的长度
len:数据包的长度
试验代码3:
#include <pcap.h>
#include <remote-ext.h>
int main() {
pcap_if_t* alldevs;
pcap_if_t* d;
int inum;
int i = 0;
pcap_t* adhandle;
int res;
char errbuf[PCAP_ERRBUF_SIZE];
struct tm* ltime;
char timestr[16];
struct pcap_pkthdr* header;
u_char* pkt_data;
/* Retrieve the device list on the local machine */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr, "Error in pcap_findalldevs: %s/n", errbuf);
exit(1);
}
/* Print the list */
for (d = alldevs; d; d = d->next)
{
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 -1;
}
/* Select an adapter */
printf("Enter the interface number (1 - %d):", i);
scanf("%d", &inum);
if (inum < 1 || inum > i)
{
printf("/nInterface number out of range./n");
/* Free the device list */
pcap_freealldevs(alldevs);
return -1;
}
/* Jump to the selected adpater */
for (d = alldevs, i = 0; i < inum - 1; d = d->next, ++ i);
/* Open the device */
if ((adhandle = pcap_open(d->name, /* name of the device */
65536, /* portion of the packet to capture */
/* 65536 guarantees that the whole packet will be captured on all the link layers */
PCAP_OPENFLAG_PROMISCUOUS, /* promiscuous mode */
1000, /* read timeout */
NULL, /* authentication on the remote machine */
errbuf /* error buffer */
)) == NULL)
{
fprintf(stderr, "/nUnable to open the adapter. %s is not supported by Winpcap/n", d->name);
/* Free the devices list */
pcap_freealldevs(alldevs);
return -1;
}
printf("/nlistening on %s .../n",d->description);
/* At this point, we don't need any more the device list. Free it */
pcap_freealldevs(alldevs);
/* Retrieve the packets */
while ((res = pcap_next_ex(adhandle, &header, &pkt_data)) >= 0)
{
if (res == 0)
{
/* Timeout elapsed */
continue;
}
/* convert the timestamp to readable format */
ltime = localtime(&header->ts.tv_sec);
strftime(timestr, sizeof(timestr), "%H:%M:%S", ltime);
printf("%s, %.6d len:%d/n", timestr, header->ts.tv_usec, header->len);
}
if (res == -1)
{
printf("Error reading the packets: %s/n", pcap_geterr(adhandle));
return -1;
}
return 1;
}
函数1:
pcap_next_ex(pcap_t* p,
struct pcap_pkthdr** pkt_header,
const u_char* pkt_data
)
从一个网络接口或离线捕获方式(例如读文件)读取一个数据包。该函数被用来重新获得下一个可用的数据包,没有使用libpcap提供的传统的回调方法。pcap_next_ex用指向头和下一个被捕获的数据包的指针为pkt_header和pkt_data参数赋值。
返回值有下列几种情况:
1,数据包被正确读取
0,pcap_open_live()设置的超时时间到。在这种情况下pkt_header和pkt_data不指向有效数据包
-1,发生错误
-2,离线捕获的时候读取到EOF
我们通常使用pcap_next_ex()而不是pcap_next(),因为pcap_next()有些缺点。首先,pcap_next()效率低,因为它隐藏了回调方法但是还是依赖于pcap_dispatch;第二,它不能检测EOF,所以当从一个文件获取数据包时它不是很有用。
函数2:
u_char* pcap_next(pcap_t* p,
struct pcap_pkthdr* h
)
返回下一个可用的数据包并且返回一个u_char指向该数据包数据部分的指针。如果发生错误或者活动的抓包没有读取到数据包(例如:数据包不能通过包过滤器而被丢弃,或者在支持读超时(read timeout)的平台上在任何数据包到来之前就超时终止,又或者是抓包设备的文件描述符在非阻塞(non-blocking)模式下并且没有数据包可以被读取),或者文件已被读完时返回NULL。不幸的是,没有办法检测是否发生错误。
Winpcap提供(libpcap也提供)的一个强大特性是过滤引擎(filtering engine)。它提供了一个非常有效的接收网络流量的方法,并且它通常与Winpcap提供的抓包机制集成在一起。用于过滤数据包的函数是pcap_complie()和pcap_setfilter()。
pcap_complie()使用一个包含高级布尔表达式的字符串并且产生一个能被过滤引擎集成到数据包驱动中的低级字节码。
pcap_setfilter()把一个过滤器与核心驱动抓包会话关联起来。一旦pcap_setfilter()被调用,相关的过滤器将被应用到所有的来自网络的数据包上,并且所有的一致的数据包将被复制给应用程序。
抓包和过滤
经过前面几天的知识准备,现在我们将把前面的知识综合后应用于一个简单的实际应用程序。下面试验的目的是如何解析和解释被捕获的数据包的协议头。应用程序运行的结果是打印出一组我们的网络上的UDP通信数据。我们之所以选择解析和显示UDP协议是因为它比起其他协议(例如:TCP)更易于理解,对于初学者更适合。
试验代码:
#include <pcap.h>
#include <remote-ext.h>
/* 4 bytes IP address */
typedef struct ip_address
{
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
}ip_address;
/* IPv4 header */
typedef struct ip_header
{
u_char ver_ihl; /* Version (4 bits) + Internet header length (4 bits)*/
u_char tos; /* Type of service */
u_short tlen; /* Total length */
u_short identification; /* Identification */
u_short flags_fo; /* Flags (3 bits) + Fragment offset (13 bits)*/
u_char ttl; /* Time to live */
u_char proto; /* Protocol */
u_short crc; /* Header checksum */
ip_address saddr;/* Source address */
ip_address daddr;/* Destination address */
u_int op_pad; /* Option + Padding */
}ip_header;
/* UDP header */
typedef struct udp_header
{
u_short sport; /* Source port */
u_short dport; /* Destination port */
u_short len; /* Datagram length */
u_short crc; /* Checksum */
}udp_header;
/* Prototype of the pachet handler */
void packet_handler(u_char* param, const struct pcap_pkthdr* header, const u_char* pkt_data);
int main() {
pcap_if_t* alldevs;
pcap_if_t* d;
int inum;
int i = 0;
pcap_t* adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
u_int netmask;
char packet_filter[] = "ip and udp";
struct bpf_program fcode;
/* Retrieve the device list */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr, "Error in pcap_findalldevs: %s/n", errbuf);
exit(1);
}
/* Print the list*/
for (d = alldevs; d; d = d->next)
{
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 -1;
}
printf("Enter the interface number (1 - %d):", i);
scanf("%d", &inum);
if (inum < 1 || inum > i)
{
printf("/nInterface number out of range./n");
/* Free the device list */
pcap_freealldevs(alldevs);
return -1;
}
/* Jump to the selected adapter */
for (d = alldevs; d; d = d->next);
/* Open the adapter */
if ((adhandle = pcap_open(d->name, /*name of the device */
65536, /* portion of the packet to capture */
/* 65536 grants that the whole packet will be captured on all the MACs */
PCAP_OPENFLAG_PROMISCUOUS, /* promiscuous mode */
1000, /* read timeout */
NULL, /* remote authentication */
errbuf /* error buffer */
)) == NULL)
{
fprintf(stderr, "/nUnable to open the adapter. %s is not supported by Winpcap/n");
/* Free the devices list */
pcap_freealldevs(alldevs);
return -1;
}
/* Check the link layer. We support only Ethernet for simplicity */
if (pcap_datalink(adhandle) != DLT_EN10MB)
{
fprintf(stderr, "/nThis program works only on Ethernet networks./n");
/* Free the devices list */
pcap_freealldevs(alldevs);
return -1;
}
if (d->addresses != NULL)
{
/* Retrieve the mask of the first address of the interface */
netmask = ((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr;
}
else
{
/* If the interface is without addresses we suppose to be in a C class network */
netmask = 0xffffffff;
}
/* complie the filter */
if (pcap_compile(adhandle, &fcode, packet_filter, 1, netmask) < 0)
{
fprintf(stderr, "/nUnable to compile the packet filter. Check the syntax./n");
/* Free the devices list */
pcap_freealldevs(alldevs);
return -1;
}
/* set the filter */
if (pcap_setfilter(adhandle, &fcode) < 0)
{
fprintf(stderr, "/nError setting the filter./n");
/* Free the devices list */
pcap_freealldevs(alldevs);
return -1;
}
printf("/nlistening on %s .../n", d->description);
/* At this point,we don't need any more the device list. Free it */
pcap_freealldevs(alldevs);
/* Start the capture */
pcap_loop(adhandle, 0, packet_handler, NULL);
return 1;
}
/* Callback function invoked by libpcap for every incoming packet */
void packet_handler(u_char* param, const struct pcap_pkthdr* header, const u_char* pkt_data){
struct tm* ltime;
char timestr[16];
ip_header* ih;
udp_header* uh;
u_int ip_len;
u_short sport, dport;
/* convert the timestamp to readable format */
ltime = localtime(&header->ts.tv_sec);
strftime(timestr, sizeof(timestr), "%H:%M:%S", ltime);
/* print timestamp and length of the packet */
printf("%s.%.6d len: %d ", timestr, header->ts.tv_usec, header->len);
/* retrieve the position of the ip header */
ih = (ip_header*)(pkt_data + 14); /* length of ethernet header */
/* retrieve the position of the udp header */
ip_len = (ih->ver_ihl & 0xf) * 4;
uh = (udp_header*)((u_char*)ih + ip_len);
/* convert from network byte order to host byte order */
/*sport = ntohs(uh->sport);
dport = ntohs(uh->dport);*/
/* print ip addresses and udp ports */
printf("%d.%d.%d.%d -> %d.%d.%d.%d/n",
ih->saddr.byte1,
ih->saddr.byte2,
ih->saddr.byte3,
ih->saddr.byte4,
/*sport,*/
ih->daddr.byte1,
ih->daddr.byte2,
ih->daddr.byte3,
ih->daddr.byte4
/*dport*/);
}
函数1:
int pcap_datalink(pcap_t* p)
返回链路层上的一个适配器。返回链路层的类型,链路层的类型包括:
l DLT_NULL: BSD回路封装;链路层协议头是一个4字节的域,以主机字节顺序(host byte order),包含一个从socket.h来的PF_value。主机字节顺序(host byte order)是捕获数据包的机器的字节顺序,而PF_value是捕获数据包的机器的OS。如果一个读取一个文件,字节顺序和PF_value不一定是抓取文件的那些机器。
l DLT_EN10MB: 以太网(10Mb, 100Mb, 1000Mb, 或者更高)。
l DLT_IEEE802: IEEE802.5令牌环网。
l DLT_ARCNET:ARCNET。
l DLT_SLIP:SLIP。
l DLT_PPP:PPP;如果第一个字节是0xff或0x03,它是类HDLC帧上的PPP。
l DLT_FDDI:FDDI
l DLT_ATM_RFC1483:RFC1483LLC/SNAP ATM;数据包以IEEE802.2 LLC头开始。
l DLT_RAW:原始IP(raw IP);数据包以IP头开始。
l DLT_PPP_SERIAL:按照RFC1662,基于类HDLC帧的PPP,或者按照RFC1547的 4.3.1 ,基于HDLC帧的Cisco PPP;前者的第一个字节是0xFF,后者的第一个字节是0x 0F 或0x 8F 。
l DLT_PPP_ETHER:按照RFC2516,PPPoE;数据包以PPPoE头开始。
l DLT_C_HDLC:按照RFC1547的 4.3.1 ,基于HDLC帧的Cisco PPP。
l DLT_IEEE802_11:IEEE 802.11无线局域网。
l DLT_FRELAY:帧中继(Frame Relay)。
l DLT_LOOP:OpenBSD回路封装。
l DLT_LINUX_SLL:Linux抓包封装。
l DLT_LTALK:苹果的LocalTalk,数据包以AppleTalk LLAP头开始。
l DLT_PFLOG:OpenBSD pflog。
l DLT_PRISM_HEADER:后接802.11头的棱镜监视器模式(Prism monitor mode)信息。
l DLT_IP_OVER_FC:RFC2625 IP-over-Fiber 频道,以RFC2625中定义的Network_Header开始。
l DLT_SUNATM:SunATM设备。
l DLT_IEEE802_11_RADIO:后接802.11头的链路层信息。
l DLT_ARCNET_LINUX:没有异常帧的ARCNET。
l DLT_LINUX_IRDA:Linux-IrDA数据包,DLT_LINUX_SLL头后接IrLAP头
函数2:
int pcap_compile(pcap_t* p,
struct bpf_program* fp,
char* str,
int optimize,
bpf_u_int32 netmask)
编译一个数据包过滤器,将一个能被核心态(kernel-level)过滤器引擎解释的程序中的高层过滤表达式(filtering expression)进行转化。pcap_compile()被用来将字符串str编译进过滤器程序(fp),程序(fp)是一个指向bpf_program结构体并被pcap_compile()赋值的指针。optimize控制是否对目标代码(resulting code)的性能进行优化。Netmask表明IPv4掩码,它仅在检查过滤器程序中的IPv4广播地址的时候被使用。如果网络掩码对于程序是不可知的或者数据包是在Linux的”任何(any)”伪接口上被捕获的,则赋值0;IPv4广播地址的测试将不被正确进行,但过滤器程序中的其他所有的测试都不会有问题。返回-1表示发生了错误,此时,pcap_geterr()将被用来显示错误信息。
函数3:
int pcap_setfilter(pcap_t* p,
struct bpf_program* fp)
把一个过滤器同一次抓包关联起来。pcap_setfilter被用来指定一个过滤器程序。fp是一个指向bpf_program结构体的指针,通常是pcap_compile()执行的结果。当失败时返回-1,此时,pcap_geterr()被用来显示错误信息;返回0表示成功。
首先,首先我们设置过滤器为“ip and udp”。用这个方法我们可以确保packet_handler()仅接收IPv4上的UDP数据包:这就简化了解析过程并且提高了程序的效率。
我们还创建了IP和UDP两个结构体,这些结构体被packet_handler()用来定位不同的头域。
packet_handler()展现了像tcpdump/WinDump这些复杂的嗅探器(sniffer)如何解析网络数据包。因为我们不关心MAC头,所以我们跳过它。为了简便起见,抓包前我们用pcap_datalink()检查MAC层,以确保我们处理的是以太网。该方法可以确保MAC头是14字节。
IP头的定位在MAC头定位之后。我们从IP包头中取出源IP地址和目标IP地址。
展开UDP包头有点复杂,因为IP包头不是定长的。因此,我们使用IP包头的长度域来获得它的大小。一旦我们知道了UDP头的位置,我们可以取出源端口和目的端口。
处理离线的存储文件(offline dump file)
Winpcap提供了一些函数把网络通信保存到文件并且可以读取这些文件的内容。dump文件的格式跟libpcap是一样的。它以二进制形式保存了被捕获的数据包的数据并且其他网络工具(包括WinDump,Ethereal,Snort)也以此为标准。
处理dump文件的程序结构跟前面的程序结构大致是一样的,只是这里出现了几个新的函数:(试验代码略)
函数1:
pcap_dumper_t* pcap_dump_open(pcap_t* p,
const char* fname)
打开一个保存数据包的文件。“-”的含义跟stdout是一样的。发生错误时返回NULL。p是一个由pcap_open_offline()或pcap_open_live()返回的pcap结构体;fname指定了文件的名字。你也可以调用pcap_dump_fopen()把数据包保存到一个已存在的流fp中,在Windows中,这个流必须以二进制方式打开。如果返回NULL,pcap_geterr()会显示错误信息。
函数2:
void pcap_dump(u_char* user,
const struct pcap_pkthdr* h,
const u_char* sp)
把数据包保存到硬盘。pcap_dump()把数据包输出到pcap_dump_open()打开的文件中。注意,它的参数要跟pcap_dispatch()或pcap_loop()的回调函数的参数一致。如果直接被调用,则user这个参数的类型应该是pcap_dumper_t,也就是pcap_dump_open()的返回值类型。
在读取dump文件的内容时,用pcap_open_offline()来打开dump文件,然后用pcap_loop()来顺序读取数据包。读数据包跟接收数据包的过程是一样的。
函数3:
int pcap_live_dump(pcap_t* p,
char* filename,
int maxsize,
int maxpacks)
保存数据包到文件。pcap_live_dump()把网络通信从一个接口保存到一个文件中。这个函数将运行在核心态(kernel level),因此它的效率比pcap_dump()的更高。它的参数是一个用pcap_open_live()获得的接口描述符,一个dump文件名的字符串,文件的最大尺寸(maxsize,以字节为单位)和文件能容纳的数据包的最大个数(maxpcaks)。把maxsize或maxpacks设置为0意味着没有限制。当达到maxsize或maxpacks时,存储结束。注意,当两个限制(maxsize,maxpacks)中的一个到达时,存储将停止,但是文件仍然是被打开的。为了正确的保存数据并且使文件处于一致状态,必须用pcap_close()关闭适配器。
pcap_live_dump()和pcap_ dump()的区别是设置限制和性能。pcap_live_dump()使用Winpcap NPF驱动在核心态进行转储,最大限度的降低了上下文的交换次数和内存副本数。pcap_live_dump()在其他操作系统上是不可用的,它是Winpcap特定的函数,只能在Win32上使用。
函数4:
pcap_t* pcap_open_offline(const char* fname,
char* errbuf)
为读数据包打开一个tcpdump/libpcap格式的文件。Fname指定了要打开的文件名,“-”的含义与stdin是一样的。你也可以调用pcap_fopen_offlline()从一个已经存在的流fp中读取数据。注意,在Windows中,这个流必须以二进制方式打开。errbuf保存pcap_open_offline()出错是返回的错误信息。
尽管Winpcap清楚地指出了它的目的是数据包的截获,但是它还提供了一些对于原始网络(raw networking)的有用特性。用户可以找到一组完整的发包(send packets)函数。需要注意的是,libpcap目前并没有提供任何的发包的方法。
用pcap_sendpacket()发送单个数据包
下面的代码片断表现了最简单的发送一个数据包的过程。打开适配器后,pcap_sendpacket()被用来发送一个手工的(hand
-crafted)数据包。
试验代码:
#include <stdlib.h>
#include <stdio.h>
#include <pcap.h>
#include <remote-ext.h>
void main(int argc, char** argv){
pcap_t* fp;
char errbuf[PCAP_ERRBUF_SIZE];
u_char packet[100];
int i;
/* Check the validity of the command line*/
if (argc != 2)
{
printf("/tusage: %s interface (e.g. 'rpcap://eth0')", argv[0]);
return;
}
/* Open the output device */
if ((fp = pcap_open(argv[1], /* name of the device */
100, /* portion of the packet to capture (only the first 100 bytes)*/
PCAP_OPENFLAG_PROMISCUOUS, /* promiscuous mode */
1000, /* read timeout */
NULL, /* authentication on the remote machine */
errbuf /* error buffer */
)) == NULL)
{
fprintf(stderr, "/nUnable to open the adapter. %s is not supported by Winpcap/n", argv[1]);
return;
}
/* Supposing to be on ethernet, set mac destinat to 1:1:1:1:1:1 */
packet[0] = 1;
packet[1] = 1;
packet[2] = 1;
packet[3] = 1;
packet[4] = 1;
packet[5] = 1;
/* set mac source to 2:2:2:2:2:2 */
packet[6] = 2;
packet[7] = 2;
packet[8] = 2;
packet[9] = 2;
packet[10] = 2;
packet[11] = 2;
/* Fill the rest of the packet */
for (i = 12; i < 100; ++ i)
{
packet[i] = i % 256;
}
/* Send down the packet */
if (pcap_sendpacket(fp, packet, 100 /* size */) != 0)
{
fprintf(stderr, "/nError sending the packet: /n", pcap_geterr(fp));
return;
}
return;
}
函数1:
int pcap_sendpacket(pcap_t* p,
u_char* buf,
int size)
发送一个原始数据包(raw packet)到网络上。p是用来发送数据包的那个接口,buf包含着要发送的数据包的数据(包括各种各样的协议头),size是buf所指的缓冲区的尺寸,也就是要发送的数据包的大小。MAC循环冗余码校验不必被包含,因为它很容易被计算出来并被网络接口驱动添加。如果数据包被成功发送,返回0;否则,返回-1。
发送队列(Send queues)
pcap_sendpacket()提供了一个简单快捷的发送单个数据包的方法,发送队列(send queues)提供了一个高级的,强大的,优化的发送一组数据包的机制。发送队列是一个用来保存将要发送到网络上的的众多数据包的容器。它有一个大小,描述了它所能容纳的最大字节数。
通过指定发送队列的大小,pcap_sendqueue_alloc()函数创建一个发送队列。一旦发送队列被创建好,pcap_sendqueue_queue()可以把一个数据包添加到发送队列里。函数pcap_sendqueue_alloc()的参数必须与pcap_next_ex()和pcap_handler()的相同,因此,从一个文件捕获或读取数据包的时候,如何进行pcap_sendqueue_alloc()的参数传递是一个问题。
试验代码:
函数1:
pcap_send_queue* pcap_sendqueue_alloc(u_int memsize)
为一个发送队列分配空间,即创建一个用来存储一组原始数据包(raw packet)的缓冲区,这些数据包将用pcap_sendqueue_transmit()提交到网络上。memsize是队列容纳的字节数,因此它决定了队列所能容纳的最大数据量。使用pcap_sendqueue_queue()可以在发送队列中插入数据包。
函数2:
int pcap_sendqueue_queue(pcap_send_queue* queue,
const struct pcap_pkthdr* pkt_header,
const u_char* pkt_data)
添加一个数据包到发送队列中。queue指向发送队列的尾部;pkt_header指向一个pcap_pkthdr结构体,该结构体包含时间戳和数据包的长度;pkt_data指向存放数据包数据部分的缓冲区。
为了提交一个发送队列,Winpcap提供了pcap_sendqueue_transmit()函数。
函数3:
u_int pcap_sendqueue_transmit(pcap_t* p,
pcap_send_queue* queue,
int sync)
该函数将队列里的内容提交到线路上。p是一个指向适配器的指针,数据包将在这个适配器上被发送;queue指向pcap_send_queue结构体,它包含着要发送的所有数据包;sync决定了发送操作是否被同步:如果它是非0(non-zero),发送数据包关系到时间戳,否则,他们将以最快的速度发送(即不考虑时间戳)。
返回值是发送的字节数。如果它小于size参数,将发生一个错误。该错误可能是由于驱动/适配器(driver/adapter)问题或发送队列的不一致/伪造(inconsistent/bogus)引起。
注意:
l 使用该函数的效率比使用pcap_sendpacket()发送一系列数据包的效率高,因为数据包在核心态(kernel-level)被缓冲,所以降低了上下文的交换次数。因此,使用pcap_sendqueue_transmit()更好。
l 当sync被设置为TRUE时,随着一个高精度的时间戳,数据包将在内核伴被同步。这就要求CPU的数量是不可忽略的,通常允许以一个微秒级的精度发送数据包(这依赖于机器性能计数器的准确度)。然而,用pcap_sendpacket()发送数据包不能达到这样一个精确度。
如果第三个参数非0,发送将被同步(synchronized),即相关的时间戳将被注意。这个操作要求注意CPU的数量,因为使用“繁忙 等待(busy wait)”循环,同步发生在内核驱动。
当不再需要一个队列时,可以用pcap_sendqueue_destroy()来删除之,这将释放与该发送队列相关的所有缓冲区。
Winpcap的内部结构
这是引自winpcap主页上的一句话:
WinPcap is an architecture for packet capture and network analysis for the Win32 platforms. It includes a kernel-level packet filter, a low-level dynamic link library (packet.dll), and a high-level and system-independent library (wpcap.dll).
我解释为:Winpcap是针对Win32平台上的抓包和网络分析的一个架构。它包括一个核心态的包过滤器,一个底层的动态链接库(packet.dll)和一个高层的不以来于系统的库(wpcap.dll)。
为什么使用“architecture”而不是“library”呢?因为抓包是一个要求与网络适配器(网卡)和操作系统交互的底层机制,而且与网络的实施也有密切关系,所以仅用“library”不能充分表达Winpcap的作用。
下图表明了Winpcap的各个组成部分:
Winpcap的主要组成部分 |
其次,抓包系统必须有用户级的程序接口,通过这些接口,用户程序可以利用内核驱动提供的高级特性。Winpcap提供了两个不同的库:packet.dll和wpcap.dll。前者提供了一个底层API,伴随着一个独立于Microsoft操作系统的编程接口,这些API可以直接用来访问驱动的函数;后者导出了一组更强大的与libpcap一致的高层抓包函数库(capture primitives)。这些函数使得数据包的捕获以一种与网络硬件和操作系统无关的方式进行。
NPF驱动
网络数据包过滤器(Netgroup Packet Filter,NPF)是Winpcap的核心部分,它是Winpcap完成困难工作的组件。它处理网络上传输的数据包,并且对用户级提供可捕获(capture)、发送(injection)和分析性能(analysis capabilities)。
1. NPF和NDIS
NDIS(Network Driver Interface Specification)是一个定义网络适配器(或者说成是管理网络适配器的驱动程序)与协议驱动(例如TCP/IP的实现)之间通信的规范。NDIS最主要的目的是作为一个允许协议驱动发送和接收网络(LAN或WAN)上的数据包而不必关心特定的适配器或特定的Win32操作系统的封装。
NDIS支持三种类型的网络驱动:
(1) 网络接口卡或NIC驱动(Network interface card or NIC drivers)。NIC驱动直接管理着网络接口卡(NIC)。NIC驱动接下边与硬件连接,从上边表现为一个接口,该接口允许高层发送数据包到网络上,处理中断,重置NIC,停止NIC,查询和设置驱动的运行特征。NIC驱动可以是小端口(miniport)或完全的NIC驱动(full NIC driver)。
l Miniport驱动仅仅实现了管理NIC的必要操作,包括在NIC上发送和接收数据。对于所有最底层的NIC驱动的操作由NDIS提供,例如同步(synchronization)。小端口(miniport)不直接调用操作系统函数,它们对于操作系统的接口是NDIS。
小端口仅仅是向上传递数据包给NDIS并且NDIS确保这些数据包被传递给正确的协议。
l 完全NIC驱动(Full NIC driver)完成硬件细节的操作和所有由NDIS完成的同步和查询操作。例如,完全NIC驱动维持接收到的数据的绑定信息。
(2) 中间层驱动(Intermediate drivers)中间层驱动位于高层驱动(例如协议驱动)和小端口之间。对于高层驱动,中间层驱动看起来像是小端口;对于小端口,中间层驱动看起来像协议驱动。一个中间层协议驱动可以位于另一个中间层驱动之上,尽管这种分层可能对系统性能带来负面影响。开发中间层驱动的一个关键原因是在现存的遗留协议驱动(legacy protocol driver)和小端口之间形成媒体的转化。例如,中间层驱动可以将LAN协议转换成ATM协议。中间层驱动不能与用户模式的应用程序通信,但可以与其他的NDIS驱动通信。
(3) 传输驱动或协议驱动(Transport drivers or protocol drivers)协议驱动实现了网络协议栈,例如IPX/SPX或TCP/IP,在一个或多个网络接口卡上提供它的服务。在协议驱动的上面,它为应用层客户程序服务;在它的下面,它与一个或多个NIC驱动或中间层NDIS驱动连接。
NPF是一个协议驱动。从性能方面来看,这不是最好的选择,但是它合理地独立于MAC层并且有权使用原始通信(raw traffic)。
下图表现了NPF在NDIS栈中的位置:
图1 NDIS中的NPF |
2. NPF结构基础
下图表现了伴随着NPF驱动细节的Winpcap的结构。
图2 NPF设备驱动 |
NPF可以完成许多不同的操作:抓包(capture)、监视(monitoring)、转储到硬盘(dump to disk)、数据包发送(packet injection)。下面将简要介绍这些操作。
抓包
抓包是NPF最重要的操作。在抓包的时候,驱动使用一个网络接口监视着数据包,并将这些数据包完整无缺地投递给用户级应用程序。
抓包过程依赖于两个主要组件:
l 一个数据包过滤器,它决定着是否接收进来的数据包并把数据包拷贝给监听程序。数据包过滤器是一个有布尔输出的函数。如果函数值是true,抓包驱动拷贝数据包给应用程序;如果是false,数据包将被丢弃。NPF数据包过滤器更复杂一些,因为它不仅决定数据包是否应该被保存,而且还得决定要保存的字节数。被NPF驱动采用的过滤系统来源于BSD Packet Filter(BPF),一个虚拟处理器可以执行伪汇编书写的用户级过滤程序。应用程序采用用户定义的过滤器并使用wpcap.dll将它们编译进BPF程序。然后,应用程序使用BIOCSETF IOCTL写入核心态的过滤器。这样,对于每一个到来的数据包该程序都将被执行,而满足条件的数据包将被接收。与传统解决方案不同,NPF不解释(interpret)过滤器,而是执行(execute)它。由于性能的原因,在使用过滤器前,NPF提供一个JIT编译器将它转化成本地的80x86函数。当一个数据包被捕获,NPF调用这个本地函数而不是调用过滤器解释器,这使得处理过程相当快。
l 一个循环缓冲区,用来保存数据包并且避免丢失。一个保存在缓冲区中的数据包有一个头,它包含了一些主要的信息,例如时间戳和数据包的大小,但它不是协议头。此外,以队列插入的方式来保存数据包可以提高数据的存储效率。可以以组的方式将数据包从NPF缓冲区拷贝到应用程序。这样就提高了性能,因为它降低了读的次数。如果一个数据包到来的时候缓冲区已经满了,那么该数据包将被丢弃,因此就发生了丢包。无论是核心态还是用户态的缓冲区都可以在运行时被改变以达到最佳性能:packet.dll和wpcap.dll提供了实现该目的的函数。
#include <stdlib.h>
#include <stdio.h>
#include <pcap.h>
#include <remote-ext.h>
void usage();
void main(int argc, char **argv)
{
pcap_t *indesc,*outdesc;
char errbuf[PCAP_ERRBUF_SIZE];
char source[PCAP_BUF_SIZE];
FILE *capfile;
int caplen, sync;
u_int res;
pcap_send_queue *squeue;
struct pcap_pkthdr *pktheader;
u_char *pktdata;
float cpu_time;
u_int npacks = 0;
/* Check the validity of the command line */
if (argc <= 2 || argc >= 5)
{
usage();
return;
}
/* Retrieve the length of the capture file */
capfile=fopen(argv[1],"rb");
if(!capfile){
printf("Capture file not found!/n");
return;
}
fseek(capfile , 0, SEEK_END);
caplen= ftell(capfile)- sizeof(struct pcap_file_header);
fclose(capfile);
/* Chek if the timestamps must be respected */
if(argc == 4 && argv[3][0] == 's')
sync = TRUE;
else
sync = FALSE;
/* Open the capture */
/* Create the source string according to the new WinPcap syntax */
if ( pcap_createsrcstr( source, // variable that will keep the source string
PCAP_SRC_FILE, // we want to open a file
NULL, // remote host
NULL, // port on the remote host
argv[1], // name of the file we want to open
errbuf // error buffer
) != 0)
{
fprintf(stderr,"/nError creating a source string/n");
return;
}
/* Open the capture file */
if ( (indesc= pcap_open(source, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf) ) == NULL)
{
fprintf(stderr,"/nUnable to open the file %s./n", source);
return;
}
/* Open the output adapter */
if ( (outdesc= pcap_open(argv[2], 100, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf) ) == NULL)
{
fprintf(stderr,"/nUnable to open adapter %s./n", source);
return;
}
/* Check the MAC type */
if (pcap_datalink(indesc) != pcap_datalink(outdesc))
{
printf("Warning: the datalink of the capture differs from the one of the selected interface./n");
printf("Press a key to continue, or CTRL+C to stop./n");
getchar();
}
/* Allocate a send queue */
squeue = pcap_sendqueue_alloc(caplen);
/* Fill the queue with the packets from the file */
while ((res = pcap_next_ex( indesc, &pktheader, &pktdata)) == 1)
{
if (pcap_sendqueue_queue(squeue, pktheader, pktdata) == -1)
{
printf("Warning: packet buffer too small, not all the packets will be sent./n");
break;
}
npacks++;
}
if (res == -1)
{
printf("Corrupted input file./n");
pcap_sendqueue_destroy(squeue);
return;
}
/* Transmit the queue */
cpu_time = (float)clock ();
if ((res = pcap_sendqueue_transmit(outdesc, squeue, sync)) < squeue->len)
{
printf("An error occurred sending the packets: %s. Only %d bytes were sent/n", pcap_geterr(outdesc), res);
}
cpu_time = (clock() - cpu_time)/CLK_TCK;
printf ("/n/nElapsed time: %5.3f/n", cpu_time);
printf ("/nTotal packets generated = %d", npacks);
printf ("/nAverage packets per second = %d", (int)((double)npacks/cpu_time));
printf ("/n");
/* free the send queue */
pcap_sendqueue_destroy(squeue);
/* Close the input file */
pcap_close(indesc);
/*
* lose the output adapter
* IMPORTANT: remember to close the adapter, otherwise there will be no guarantee that all the
* packets will be sent!
*/
pcap_close(outdesc);
return;
}
void usage()
{
printf("/nSendcap, sends a libpcap/tcpdump capture file to the net. Copyright (C) 2002 Loris Degioanni./n");
printf("/nUsage:/n");
printf("/t sendcap file_name adapter [s]/n");
printf("/nParameters:/n");
printf("/nfile_name: the name of the dump file that will be sent to the network/n");
printf("/nadapter: the device to use. Use /"WinDump -D/" for a list of valid devices/n");
printf("/ns: if present, forces the packets to be sent synchronously, i.e. respecting the timestamps in the dump file. This option will work only under Windows NTx./n/n");
exit(0);
}