当前位置:   article > 正文

使用WinPCAP接口编程抓取数据包_(handle = pcap_open_live(dev->name, bufsiz, 1, 100

(handle = pcap_open_live(dev->name, bufsiz, 1, 1000, errbuf)) == null)

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

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

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

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

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

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

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

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

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

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

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

  

  1. #include<stdio.h>
  2. #include<pcap.h>
  3. int main(intargc, char *argv[])
  4. {
  5. char *dev = argv[1];
  6. printf("Device: %s\n", dev);
  7. return(0);
  8. }


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

另一种方法也是相当的容


  1. #include <stdio.h>
  2. #include<pcap.h>
  3. int main(int argc,char *argv[])
  4. {
  5. char *dev, errbuf[PCAP_ERRBUF_SIZE];
  6. dev = pcap_lookupdev(errbuf);
  7. if (dev == NULL) {
  8. fprintf(stderr, "Couldn'tfind default device: %s\n", errbuf);
  9. return(2);
  10. }
  11. printf("Device: %s\n", dev);
  12. return(0);
  13. }


这种方法,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

示范代码片段:

  1. #include <pcap.h>
  2. ...
  3. pcap_t *handle;
  4. handle =pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
  5. if (handle == NULL) {
  6. fprintf(stderr,"Couldn't open device %s: %s\n", dev, errbuf);
  7. return(2);
  8. }

这段代码将打开的设备保存到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. 这种错误由于设备不支持以太网数据包头,下面的代码适用于这种情况,因为它假设以太网包头。

  1. if (pcap_datalink(handle) != DLT_EN10MB) {
  2. fprintf(stderr,"Device %s doesn't provide Ethernet headers - not supported\n", dev);
  3. return(2);
  4. }

流量过滤

大多数情况,我们的嗅探器只对特定的流量感兴趣。例如 ,在密码搜索时我们只想要端口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()的第二个参数一样。

废话少说,上代码:

      

  1. #include<pcap.h>
  2. ...
  3. pcap_t *handle; /* Session handle */
  4. char dev[] = "rl0"; /* Device to sniff on */
  5. char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
  6. struct bpf_program fp; /* The compiled filter expression */
  7. char filter_exp[] = "port 23"; /* The filter expression */
  8. bpf_u_int32 mask; /* The netmask of our sniffing device */
  9. bpf_u_int32 net; /* The IP of our sniffing device */
  10. if (pcap_lookupnet(dev, &net, &mask,errbuf) == -1) {
  11. fprintf(stderr, "Can't get netmask fordevice %s\n", dev);
  12. net = 0;
  13. mask = 0;
  14. }
  15. handle = pcap_open_live(dev, BUFSIZ, 1, 1000,errbuf);
  16. if (handle == NULL) {
  17. fprintf(stderr, "Couldn't open device %s:%s\n", dev, errbuf);
  18. return(2);
  19. }
  20. if (pcap_compile(handle, &fp, filter_exp,0, net) == -1) {
  21. fprintf(stderr, "Couldn't parse filter%s: %s\n", filter_exp, pcap_geterr(handle));
  22. return(2);
  23. }
  24. if (pcap_setfilter(handle, &fp) == -1) {
  25. fprintf(stderr, "Couldn't install filter%s: %s\n", filter_exp, pcap_geterr(handle));
  26. return(2);
  27. }

程序将嗅探所有来自或抵达设备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。

       

  1. #include<pcap.h>
  2. #include <stdio.h>
  3. int main(int argc, char *argv[])
  4. {
  5. pcap_t*handle; /* Session handle*/
  6. char*dev; /* The device tosniff on */
  7. charerrbuf[PCAP_ERRBUF_SIZE]; /* Error string*/
  8. structbpf_program fp; /* The compiledfilter */
  9. charfilter_exp[] = "port 23"; /* Thefilter expression */
  10. bpf_u_int32mask; /* Our netmask */
  11. bpf_u_int32net; /* Our IP */
  12. structpcap_pkthdr header; /* The header thatpcap gives us */
  13. constu_char *packet; /* The actualpacket */
  14. /*Define the device */
  15. dev =pcap_lookupdev(errbuf);
  16. if(dev == NULL) {
  17. fprintf(stderr,"Couldn't find default device: %s\n", errbuf);
  18. return(2);
  19. }
  20. /*Find the properties for the device */
  21. if(pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
  22. fprintf(stderr,"Couldn't get netmask for device %s: %s\n", dev, errbuf);
  23. net= 0;
  24. mask= 0;
  25. }
  26. /*Open the session in promiscuous mode */
  27. handle= pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
  28. if(handle == NULL) {
  29. fprintf(stderr,"Couldn't open device %s: %s\n", dev, errbuf);
  30. return(2);
  31. }
  32. /*Compile and apply the filter */
  33. if(pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
  34. fprintf(stderr,"Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
  35. return(2);
  36. }
  37. if(pcap_setfilter(handle, &fp) == -1) {
  38. fprintf(stderr,"Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
  39. return(2);
  40. }
  41. /*Grab a packet */
  42. packet= pcap_next(handle, &header);
  43. /*Print its length */
  44. printf("Jackeda packet with length of [%d]\n", header.len);
  45. /*And close the session */
  46. pcap_close(handle);
  47. return(0);
  48. }

上面的代码在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中定义,如下:

  1. structpcap_pkthdr {
  2. structtimeval ts; /* time stamp */
  3. bpf_u_int32caplen; /* length of portion present */
  4. bpf_u_int32len; /* length this packet (off wire) */
  5. };

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

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

  1. /* Ethernet addresses are 6 bytes */
  2. #define ETHER_ADDR_LEN 6
  3. /* Ethernetheader */
  4. structsniff_ethernet {
  5. u_charether_dhost[ETHER_ADDR_LEN]; /* Destination host address */
  6. u_charether_shost[ETHER_ADDR_LEN]; /* Source host address */
  7. u_shortether_type; /* IP? ARP? RARP? etc */
  8. };
  9. /* IP header*/
  10. structsniff_ip {
  11. u_charip_vhl; /* version << 4 |header length >> 2 */
  12. u_charip_tos; /* type of service */
  13. u_shortip_len; /* total length */
  14. u_shortip_id; /* identification */
  15. u_shortip_off; /* fragment offset field */
  16. #defineIP_RF 0x8000 /* reservedfragment flag */
  17. #defineIP_DF 0x4000 /* dont fragmentflag */
  18. #defineIP_MF 0x2000 /* more fragmentsflag */
  19. #defineIP_OFFMASK 0x1fff /* mask forfragmenting bits */
  20. u_charip_ttl; /* time to live */
  21. u_charip_p; /* protocol */
  22. u_shortip_sum; /* checksum */
  23. structin_addr ip_src,ip_dst; /* source and dest address */
  24. };
  25. #defineIP_HL(ip) (((ip)->ip_vhl)& 0x0f) //得到后四位,即 header length>>2
  26. #defineIP_V(ip) (((ip)->ip_vhl)>> 4) //得到前四位,即version
  27. /* TCPheader */
  28. typedefu_int tcp_seq;
  29. structsniff_tcp {
  30. u_shortth_sport; /* source port */
  31. u_shortth_dport; /* destination port */
  32. tcp_seqth_seq; /* sequence number */
  33. tcp_seqth_ack; /* acknowledgement number*/
  34. u_charth_offx2; /* data offset, rsvd */
  35. #defineTH_OFF(th) (((th)->th_offx2 &0xf0) >> 4)//得到前四位
  36. u_charth_flags;
  37. #defineTH_FIN 0x01
  38. #defineTH_SYN 0x02
  39. #defineTH_RST 0x04
  40. #defineTH_PUSH 0x08
  41. #defineTH_ACK 0x10
  42. #defineTH_URG 0x20
  43. #defineTH_ECE 0x40
  44. #defineTH_CWR 0x80
  45. #defineTH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
  46. u_shortth_win; /* window */
  47. u_shortth_sum; /* checksum */
  48. u_shortth_urp; /* urgent pointer */
  49. };


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

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

  1. /* ethernet headers are always exactly 14 bytes */
  2. #define SIZE_ETHERNET 14
  3. const structsniff_ethernet *ethernet; /* The ethernet header */
  4. const structsniff_ip *ip; /* The IP header */
  5. const structsniff_tcp *tcp; /* The TCP header */
  6. const char*payload; /* Packet payload */
  7. u_intsize_ip;
  8. u_intsize_tcp;

这里是神奇的类型转换

     

  1. ethernet =(struct sniff_ethernet*)(packet);
  2. ip = (structsniff_ip*)(packet + SIZE_ETHERNET); //以太网包头长14个字节
  3. size_ip =IP_HL(ip)*4; //IP包头长度由IP_HL(ip)*4指出,因为TCP包头和IP包头都是以“4个字节”为单位的
  4. if (size_ip< 20) {//IP包头长度应该}>=20
  5. printf(" * Invalid IP header length: %ubytes\n", size_ip);
  6. return;
  7. }
  8. tcp =(struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
  9. size_tcp =TH_OFF(tcp)*4; //TCP包头长度由TCP_OFF(ip)*4指出,因为TCP包头和IP包头都是以“4个字节”为单位的
  10. if (size_tcp< 20) {
  11. printf(" * Invalid TCP header length: %ubytes\n", size_tcp);
  12. return;
  13. }
  14. 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任务的基本概念了,嗅探数据包,应用过滤器,使用回调函数。是时候来到战场嗅探这无尽的网络了。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/162522
推荐阅读
相关标签
  

闽ICP备14008679号