您的位置:首页 > 编程语言

使用WinPCAP接口编程抓取数据包

2015-11-07 20:42 411 查看
使用本文档,需要有一些C基础,除非你只是想了解基本的原理而不实现。有些地方需要有一些编程经验,我尽量详细的描述相关概念。此外,一些网络相关的知识可以帮助你理解此教程。教程中实现的嗅探器在FreeBSD 4.3 with a 原始内核上测试过了。

首先需要理解的是pcap嗅探器的大体步骤,以下内容就是

首先,需要确定我们将要嗅探的接口。

在linux下是类似eth0的东西。在BSD下是类似xll的东西。可以在一个字符串中声明设备,也可以让pcap提供备选接口(我们想要嗅探的接口)的名字。

初始化pcap:此时才真正告诉pcap我们要嗅探的具体接口,只要我们愿意,我们可以嗅探多个接口。但是如何区分多个接口呢,使用文件句柄。就像读写文件时使用文件句柄一样。我们必须给嗅探任务命名,以至于区分不同的嗅探任务。

当我们只想嗅探特殊的流量时(例如,仅仅嗅探TCP/IP包、仅仅嗅探经过端口23的包,等等)我们必须设定一个规则集,“编译”并应用它。这是一个三相的并且紧密联系的过程,规则集存储与字符串中,在“编译”之后会转换成pcap可以读取的格式。“编译过程”实际上是调用自定义的函数完成的,不涉及外部的函数。然后我们可以告诉pcap在我们想要过滤的任何任务上实施。

最后,告诉pcap进入主要的执行循环中,在此阶段,在接收到任何我们想要的包之前pcap将一直循环等待。在每次抓取到一个新的数据包时,它将调用另一个自定义的函数,我们可以在这个函数中肆意妄为,例如,解析数据包并显示数据内容、保存到文件或者什么都不做等等。

当嗅探完美任务完成时,记得关掉任务

这是一个相当简单的过程,只有五个步骤,其中步骤3是可选的,让我们看一下每个步骤的具体实施。

设定设备so easy! 有两种方法设定设备:

让用户自己指定设备,代码如下:

#include<stdio.h>

#include<pcap.h>

int main(intargc, char *argv[])

{

char *dev = argv[1];

printf("Device: %s\n", dev);

return(0);

}


用户使用第一个参数传入其所指定的设备名,变量dev以一种pcap可以理解的格式存放设备名,

另一种方法也是相当的容

#include <stdio.h>
#include<pcap.h>

int main(int argc,char *argv[])

{

char *dev, errbuf[PCAP_ERRBUF_SIZE];

dev = pcap_lookupdev(errbuf);

if (dev == NULL) {

fprintf(stderr, "Couldn'tfind default device: %s\n", errbuf);

return(2);

}

printf("Device: %s\n", dev);

return(0);

}


这种方法,pcap自己指定设备名,errbuf参数在调用pcap_lookupdev()函数出错时被赋值,内容是描述错误的信息。

打开嗅探设备

创建一个嗅探任务也是so easy ,使用函数pcap_open_live()就搞定啦,此函数原型如下:

pcap_t *pcap_open_live(char *device, int snaplen, intpromisc, int to_ms,   char *ebuf)


第一个参数是上一部分获取的设备名列表;snaplen是一个int型变量,表示pcap可以捕获的数据的最大字节数,promisc为TRUE时会把接口设置为promiscuous模式(是指一台机器能够接收所有经过它的数据流,而不论其目的地址是否是他),然而当promisc的值为false时,在特殊情况下也有可能是promiscuous模式;to_ms是读取时间溢出,单位是毫秒,它的值为0时意味着没有时间溢出,在某些平台上,在见到所有的包之前,你可能需要等特定数量的包到达,所以应该使用非零的timeout值;ebuf是存储错误信息的字符串(当有错误发生时)。函数返回值是此任务的handler

示范代码片段:

#include <pcap.h>

...

pcap_t *handle;

handle =pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);

if (handle == NULL) {

fprintf(stderr,"Couldn't open device %s: %s\n", dev, errbuf);

return(2);

}

这段代码将打开的设备保存到dev变量中,捕获的数据最大字节数为BUFSIZ(在pcap.h中定义)。设置设备为promiscuous模式;直到出现错误时嗅探才结束;如果出现错误将错误存储在errbuf字符串中。

Promiscuous sniffing vs. non-promiscuous sniffing:两种不同的技术,标准情况下non-promiscuous嗅探只嗅探和直接自己相关的流量,包括自己发送的、接受的和路由时经过自己的流量。Promiscuous模式则相反,它嗅探线路上的所有流量,在非变换环境下,可能是所有的流量。Promiscuous模式的显著优点是可以嗅探更多的数据包,对于嗅探着来说可能有用也可能没有用;当然这也是有缺点的,首先promiscuous模式的嗅探是可以被侦测到的,一台机器可以准确的侦测到是否有另一台机器在嗅探。其次这种模式只能工作在non-switched环境下(例如,中心节点,或者正在经受ARP泛洪的交换机)。最后一个缺点是:当网络流量很多时,嗅探所有流量将消耗大部分的系统资源。

不是所有的设备有相同的链路层包头。以太网和一些非以太网设备提供以太网包头,其他类型的设备却不是(例如,BSD和OS X中的回环设备、ppp接口、监控嗅探模式下的Wi-Fi接口)

你需要确定设备所提供的链路层包头的类型,处理数据包内容时使用到这个类型。pcap_datalink()函数返回值是链路层包头的类型详情参照the list of link-layer header type values. 这种错误由于设备不支持以太网数据包头,下面的代码适用于这种情况,因为它假设以太网包头。

if (pcap_datalink(handle) != DLT_EN10MB) {

fprintf(stderr,"Device %s doesn't provide Ethernet headers - not supported\n", dev);

return(2);

}

流量过滤

大多数情况,我们的嗅探器只对特定的流量感兴趣。例如 ,在密码搜索时我们只想要端口23(telnet)的流量、或者我们想截断端口21(FTP)正在发送的文件、有时我们只想要DNS流量(端口53UDP)。无论哪一种情况,我们几乎不会盲目的嗅探所有的流量。相关函数有pcap_compile()and pcap_setfilter()。

这个过程有时so easy!在调用pcap_open_live()之后我们拥有了一个嗅探会话,这时就可以使用过滤器了。使用过滤器而不使用if/else条件语句有两个原因:首先,pcap的过滤器非常高效,因为它直接调用BPF过滤器。其次是BPF驱动可以替我们做很多操作,这使得编程更简洁。

在使用我们的过滤器之前,必须“编译”它。过滤器表达式基于一个正则表达式字符串,主页tcpdump的开发文档有详细的语法规则,自己阅读去吧。我们使用简单的测试表达式,所以你可以很容易地从我的例子中搞明白它。

pcap_compile()函数的原型:

int pcap_compile(pcap_t *p, struct bpf_program *fp, char*str, int optimize, bpf_u_int32 netmask)


第一个参数是嗅探会话句柄,在前面的例子中出现,第二个参数是存储过滤器编译版本的结构体的指针,第三个参数是字符串类型的正则表达式,第四个参数指定过滤规则表达式是否被优化(0表示没有,1表示是)最后一个参数指定过滤器适用的网络的网络掩码。函数执行失败返回-1。

过滤规则表达式编译后就派上用场了,根据pcap文档使用pcap_setfilter()函数,下面是函数声明原型:

intpcap_setfilter(pcap_t *p, struct bpf_program *fp)


如此直截了当,第一个参数是嗅探会话句柄,第二个参数是存储过滤器编译版本的结构体的指针,和pcap_compile()的第二个参数一样。

废话少说,上代码:

#include<pcap.h>

...

pcap_t *handle;               /* Session handle */

char dev[] = "rl0";           /* Device to sniff on */

char errbuf[PCAP_ERRBUF_SIZE];       /* Error string */

struct bpf_program fp;               /* The compiled filter expression */

char filter_exp[] = "port 23";       /* The filter expression */

bpf_u_int32 mask;             /* The netmask of our sniffing device */

bpf_u_int32 net;              /* The IP of our sniffing device */

if (pcap_lookupnet(dev, &net, &mask,errbuf) == -1) {

fprintf(stderr, "Can't get netmask fordevice %s\n", dev);

net = 0;

mask = 0;

}

handle = pcap_open_live(dev, BUFSIZ, 1, 1000,errbuf);

if (handle == NULL) {

fprintf(stderr, "Couldn't open device %s:%s\n", dev, errbuf);

return(2);

}

if (pcap_compile(handle, &fp, filter_exp,0, net) == -1) {

fprintf(stderr, "Couldn't parse filter%s: %s\n", filter_exp, pcap_geterr(handle));

return(2);

}

if (pcap_setfilter(handle, &fp) == -1) {

fprintf(stderr, "Couldn't install filter%s: %s\n", filter_exp, pcap_geterr(handle));

return(2);

}


程序将嗅探所有来自或抵达设备r10端口23的流量,工作在混合模式下。

你也许已经发现前面的代码中有一个函数我们没有讨论过。Pcap_looupnet()通过设备名参数dev,返回设备IPV4网络号和相应的子网掩码(网络号是IPV4地址和子网掩码异或的结果,仅仅包含IP地址的网络部分)。这是最基本的,因为应用过滤器时需要直到子网掩码。这个函数的详细解释在文档末尾的Miscellaneous模块。据我所知,过滤器不能在所有的操作系统上正常工作。在我的环境下,我发现原版的OpenBSD2.9支持这种过滤器,但是原版FreeBSD4.3不支持。因人而异。

此刻,我我们已经学习了怎样定义设备、如何初始化设备和如何使用过滤器。是该行动的时候了。抓包有两个主要的技术,我们可以一次抓取一个包,也可以使用循环一次抓取n个包。先演示抓取一个包,在使用循环一次抓取多个包。使用pcap_next()可以完成这个目标,它的声明如下:

u_char*pcap_next(pcap_t *p, struct pcap_pkthdr *h)


第一个参数是任务句柄,第二个参数是一个指向存储数据包概略信息结构体的指针,结构体中数据成员time是嗅探时刻的时间,结构体中有数据域length(包的长度),函数返回一个包的结构体指针,是u_char型。后面我们会讨论解读包内容的技术。使用pcap_next()的demo。

#include<pcap.h>

#include <stdio.h>

int main(int argc, char *argv[])

{

pcap_t*handle;                /* Session handle*/

char*dev;                     /* The device tosniff on */

charerrbuf[PCAP_ERRBUF_SIZE]; /* Error string*/

structbpf_program fp;         /* The compiledfilter */

charfilter_exp[] = "port 23"; /* Thefilter expression */

bpf_u_int32mask;              /* Our netmask */

bpf_u_int32net;               /* Our IP */

structpcap_pkthdr header;     /* The header thatpcap gives us */

constu_char *packet;          /* The actualpacket */

/*Define the device */

dev =pcap_lookupdev(errbuf);

if(dev == NULL) {

fprintf(stderr,"Couldn't find default device: %s\n", errbuf);

return(2);

}

/*Find the properties for the device */

if(pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {

fprintf(stderr,"Couldn't get netmask for device %s: %s\n", dev, errbuf);

net= 0;

mask= 0;

}

/*Open the session in promiscuous mode */

handle= pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);

if(handle == NULL) {

fprintf(stderr,"Couldn't open device %s: %s\n", dev, errbuf);

return(2);

}

/*Compile and apply the filter */

if(pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {

fprintf(stderr,"Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));

return(2);

}

if(pcap_setfilter(handle, &fp) == -1) {

fprintf(stderr,"Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));

return(2);

}

/*Grab a packet */

packet= pcap_next(handle, &header);

/*Print its length */

printf("Jackeda packet with length of [%d]\n", header.len);

/*And close the session */

pcap_close(handle);

return(0);

}


上面的代码在promiscuous模式下嗅探所有由pcap_lookupdev()返回的设备。它发现第一个经过端口23(telnet)的数据包并打印包的长度。程序也调用了

另一种方法是较复杂的同时也是更有用的,很少的嗅探器使用pcap_next(),更多情况下使用pcap_loop()或者pcap_dispatch()(pcap_dispatch()内部调用pcap_next())。想要理解这两个函数就必须要了解回调单数。

回调函数不是新的内容,在许多API中都有。回调函数的概念也是很简单的,假设有一个等待排序事物的程序,程序的目的是,每当用户按下一个按键,就调用一个函数做一些处理,这个函数就叫做回调函数。额你当用户按键一次,程序将调用一次这个函数。在pcap中的调用中,“嗅探一个包”的操作类似前面例子中的“按键一次”。pcap_loop()和pcap_dispatch()都可以定义自己回调函数,两者回调操作的用法很相似。在每次有满足过滤规则的数据包被嗅探到的时候,他们都会调用回调。

int pcap_loop(pcap_t *p, int cnt, pcap_handler callback,u_char *user)


第一个参数是任务句柄。第二个参数是一个非负整数,它告诉pcap_loop()函数,在返回或出错之前应该嗅探几个数据包。第三个参数是回调函数的名字,只有函数名没有小括号。最后一个参数在一些应用中是有用的,但是很多时候设为NULL,显然只有把这个参数转换成u_char指针类型才能确保不出错。后面我们将看到,pcap将一些有趣的流经的信息设置为u_char指针型,在举了一个相关例子之后就会直到它怎么做到的了,那时如果还不明白的话,请阅读C语言中指针部分的内容,本文不对指针的相关知识做详细的介绍。Pcap_dispatch()的使用几乎一样。两者唯一的不同的地方时pcap_dispatch()仅仅处理从系统接收的第一批数据包,而pcap_loop()会继续处理剩余所有的包,如果想深入了解两者的不同请阅读pcap主页。

在举一个使用pcap_loop()的例子之前,必须坚持回调函数的格式,不能随意定义回调函数,如果那样做pcap_loop()将不知道该怎么使用它了。所以统一使用下面的格式定义回调函数:

voidgot_packet(u_char *args, const struct pcap_pkthdr *header,const u_char*packet);


深入了解。首先,函数没有返回值,其实这是符合逻辑的,因为pcap_loop()不知道怎样处理接收到的返回值。第一个参数和pcap_loop()的最后一个参数相同。每一次函数调用的时候,传给pcap_loop()的最后一个参数的值同时也传给回调函数的第一个参数。第二个参数是pcap头,这个pcap头包含了嗅探到的包的信息,比如包的大小等。Pcap_pkthdr结构体在pcap.h中定义,如下:

structpcap_pkthdr {

structtimeval ts; /* time stamp */

bpf_u_int32caplen; /* length of portion present */

bpf_u_int32len; /* length this packet (off wire) */

};


这些参数都很简单呐,最后一个是最感兴趣的,往往也是初学者最困惑的,它也是一个u_char类型的指针,它指向pcap_loop()嗅探的完整的一个数据包的第一个字节。

如何利用数据包呢?一个数据包有很多属性,它是一个结构体而不是一个简单的字符串(比如,TCP/IP包的内容有一个以太网包头,一个IP包头和包的有效负载)。u_char指针指向这些结构体的序列号,在使用之前需要做一些转换。在转换之前需要先定义这些结构体,下面这段代码就是我定义的一个以太网上的TCP/IP包的结构体:

/* Ethernet addresses are 6 bytes */

#define ETHER_ADDR_LEN 6

/* Ethernetheader */

structsniff_ethernet {

u_charether_dhost[ETHER_ADDR_LEN]; /* Destination host address */

u_charether_shost[ETHER_ADDR_LEN]; /* Source host address */

u_shortether_type; /* IP? ARP? RARP? etc */

};

/* IP header*/

structsniff_ip {

u_charip_vhl;         /* version << 4 |header length >> 2 */

u_charip_tos;         /* type of service */

u_shortip_len;        /* total length */

u_shortip_id;         /* identification */

u_shortip_off;        /* fragment offset field */

#defineIP_RF 0x8000           /* reservedfragment flag */

#defineIP_DF 0x4000           /* dont fragmentflag */

#defineIP_MF 0x2000           /* more fragmentsflag */

#defineIP_OFFMASK 0x1fff      /* mask forfragmenting bits */

u_charip_ttl;         /* time to live */

u_charip_p;           /* protocol */

u_shortip_sum;        /* checksum */

structin_addr ip_src,ip_dst; /* source and dest address */

};

#defineIP_HL(ip)              (((ip)->ip_vhl)& 0x0f)  //得到后四位,即 header length>>2

#defineIP_V(ip)               (((ip)->ip_vhl)>> 4) //得到前四位,即version

/* TCPheader */

typedefu_int tcp_seq;

structsniff_tcp {

u_shortth_sport;      /* source port */

u_shortth_dport;      /* destination port */

tcp_seqth_seq;        /* sequence number */

tcp_seqth_ack;        /* acknowledgement number*/

u_charth_offx2;       /* data offset, rsvd */

#defineTH_OFF(th)     (((th)->th_offx2 &0xf0) >> 4)//得到前四位

u_charth_flags;

#defineTH_FIN 0x01

#defineTH_SYN 0x02

#defineTH_RST 0x04

#defineTH_PUSH 0x08

#defineTH_ACK 0x10

#defineTH_URG 0x20

#defineTH_ECE 0x40

#defineTH_CWR 0x80

#defineTH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)

u_shortth_win;        /* window */

u_shortth_sum;        /* checksum */

u_shortth_urp;        /* urgent pointer */

};


这些东西是如何跟pcap、神秘的u_char指针联系起来的呢?这个结构体定义了数据包的包头,那么我们如何拆解它呢?准备见证最实用的指针的用法(所有认为指针是没有用的C新手们,开始打脸喽!)

依然假设我们在处理以太网TCP/IP数据包。同样的技术应用到所有包,唯一不同的是使用的结构体类型。先从定义变量和解析数据包时所用到的编译时间开始。

/* ethernet headers are always exactly 14 bytes */

#define SIZE_ETHERNET 14

const structsniff_ethernet *ethernet; /* The ethernet header */

const structsniff_ip *ip; /* The IP header */

const structsniff_tcp *tcp; /* The TCP header */

const char*payload; /* Packet payload */

u_intsize_ip;

u_intsize_tcp;


这里是神奇的类型转换

ethernet =(struct sniff_ethernet*)(packet);

ip = (structsniff_ip*)(packet + SIZE_ETHERNET); //以太网包头长14个字节

size_ip =IP_HL(ip)*4; //IP包头长度由IP_HL(ip)*4指出,因为TCP包头和IP包头都是以“4个字节”为单位的

if (size_ip< 20) {//IP包头长度应该}>=20

printf("   * Invalid IP header length: %ubytes\n", size_ip);

return;

}

tcp =(struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);

size_tcp =TH_OFF(tcp)*4; //TCP包头长度由TCP_OFF(ip)*4指出,因为TCP包头和IP包头都是以“4个字节”为单位的

if (size_tcp< 20) {

printf("   * Invalid TCP header length: %ubytes\n", size_tcp);

return;

}

payload =(u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);


这样能行吗?思考一下数据包在内存中的存放。U_char指针仅仅只是一个包含内存地址的变量,这就是指针的本质,它指向内存中的位置。

简单的说,指针指向的地址是X,三个结构体变量连续的存放在内存中,第一个(以太网包头)被放在地址X处,我们可以很简单的找到它后面的结构体的地址,就是X+以太网包头的长度,即X+14

类似地,如果我们知道包头的地址,那么包头后面的结构体的地址就是包头的地址加上包头的长度。IP包头和以太网包头不一样,它不是定长的,它的长度是一个4字节为计数单位的无符整数,由IP包头中的IP包头长度域给出,由于它是一个4字节为计数单位的无符整数,所以乘以4才是真实的字节数,IP包头的最小长度是20个字节。

用表显示更直观

Variable

Location (in bytes)

sniff_ethernet

X

sniff_ip

X + SIZE_ETHERNET

sniff_tcp

X + SIZE_ETHERNET + {IP header length}

payload

X + SIZE_ETHERNET + {IP header length} + {TCP header length}

此时,我们知道怎么编写回调函数了,调用它就能找出被嗅探到的数据包的属性。你可能想:“写一个能用的嗅探器吧!”不过由于篇幅问题我就不在这里贴代码了,想要的话去这里下载sniffex.c

总结:

此时你应该能自己使用pcap写一个嗅探器了。你已经大体上了解开始一个pcap任务的基本概念了,嗅探数据包,应用过滤器,使用回调函数。是时候来到战场嗅探这无尽的网络了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: