基于libpcap的sniffer实现(一)

数据包嗅探(packet sniffing)和欺骗(spoofing)是网络安全中重要的概念;他们也是网络通信面临的主要威胁。网络嗅探的工具有许多,比如WireShark、Tcpdump、Networx等。这些工具既被网络安全专家使用,用作网络测试或是对网络异常进行诊断,也可以作为被动攻击工具被黑客使用,用于窃取网络传输的信息。如何使用这些工具是比较重要的,但是对于研究者来说,理解这类嗅探工具的原理才是更为重要的,通过学习嗅探工具的编写,我们可以在日后定制属于自己的嗅探器,专用于某个协议的分析。如网络上流传有专门针对国内某著名通信软件的协议所做的嗅探器。

嗅探器的工作原理:
首先来看下正常的数据包的处理流程。当网卡接受到以太网传输的数据帧时,就会检查该帧的目的MAC地址是否与自己的相同。如果相同,则会产生一个中断请求,负责该中断的中断处理程序是系统网卡的驱动程序。该中断处理程序会将接受到的数据从网卡的缓冲区拷贝到内核空间的一块内存区域。然后,就会根据以太网的头部ether_type域,判断接受到的数据包是什么协议(IP,ARP、RARP或是其他协议),然后传递给协议栈中特定协议处理程序进行处理。如果是IPv4的数据包,则交由Ipv4协议的处理函数进行处理,经过检查后再移除IP头,交由UDP或是TCP协议的处理函数进行处理,这样经过协议栈的层层处理,最终解包得到应用层的数据。
在有如tcpdump等的链路层嗅探器的存在下,其还是要经过上面的流程。稍有不同的是在网卡驱动程序在将数据包拷贝给内核缓冲区时,也会拷贝一份给包过滤器,包过滤器使得不经过协议栈而能捕获到链路层的数据包成为可能。图1所示为正常的数据包和sniffer下的数据包在计算机系统中的处理流程。
haha
                                                                         图1. 数据包的处理流程
本次我们将使用libpcap库在linux下编写嗅探工具,至于Windows平台,则需要使用WinPcap库。这里对WinPcap库进行概括的讲述,其特性与libpcap也是一致的。根据WinPcap官网的描述,WinPcap是为微软Windows操作系统平台下提供链路层网络数据访问和网络分析的开源库。其重要的特性就是能够绕过网络协议栈,开发人员使用其应用层API就能嗅探到到网络数据包。WinPcap提供了函数库,这些库的函数其实和libpcap提供的API是非常类似的。基于WinPcap开发的网络安全工具有很多,包括网络与协议分析工具、网络监视器、网络扫描器、网络入侵检测系统,网络安全工具、比如WireShark、Nmap、Snort等等。需要注意的是,WinPcap能够独立的通过主机协议发生和接受数据,但不能过滤或者阻止主机上的应用程序的网络通信(被动攻击工具),所以不能实现防火墙的功能。
WinPcap最新的版本是2013年3月8日发布的4.1.3版本,该版本增加了对Windows 8.1和 Windows Sever 2012的支持。另外,WinPcap官网提供了教程文档方便开发者进行查阅,4.1.2版本的英文版本在这里,另外,也提供了4.0.1版本的中文文档,链接在这里
libpcap:
libpcap是由劳伦斯伯克利实验室的网络研究小组(Network Research Group)开发者们实现的,其功能与WinPcap基本一致的,也是提供了应用层API给开发人员实现数据包的过滤和嗅探功能。基于libpcap的应用有tcpdump,dsniff,snort,以及ettercap等。
要使用libcap编程,首先要到tcpdump官网下载libpcap,另外要安装依赖项M4 、flex 以及bison,可参阅这里
对于如何使用libcap进行编程,官方给出的一个文档是 Programming with pcap.
另外,有两篇教程也是需要提及的:
国内的博客有篇文章值得推荐,其链接在这里这篇文章对libpcap抓包的流程解析得很清晰。
这里,我想介绍的是,网络嗅探的初衷并不是用于被动攻击一类的恶意行为,而是为了分析网络中存在的问题。在20世纪80年代末,加州伯克利的劳伦斯伯克利实验室网络研究小组四位大神中的Van jacobson在处理TCP拥塞控制的问题,需要找出Arpanet持续崩溃的原因(对于Arpanet的历史感兴趣的,可查看这里),所以,Van就需要通过查看网络数据包分析网络问题,最初是通过etherfind命令来进行分析的,该命令基于Unix”find”而来,但其缺点很多,比如过滤的语句比较复杂,执行的效率非常低下。鉴于这样的原因,所以他们就开发了tcpdump,tcpdump能够在数据包到达协议栈前就能对其过滤,这样就避免了所有的数据包到达到应用层造成性能瓶颈。在应用层指定数据包过滤的条件,再通过编译进底层驱动层次上来过滤数据包。该内核模块就是Berkeley Packet Filter,简称BPF。在编写libpacap嗅探数据包时,就需要使用BPF构造过滤器过滤自己感兴趣的数据包。如果要监听到经过网卡的所有数据包,需要将网卡设置为混杂模式,在linux下设置混杂模式的方法是:如果网卡名为eth0,那么命令是:ifconfig eth0 promisc,取消混杂模式的命令为ifconfig eth0 -promisc注意需要root权限。
下面来看一个例子,具体的功能和相关函数的解释已经在文档中。
/*
      源文件:netdev.c
      通过使用libpcap获取自己的网卡设备名、网络号、以及掩码信息
      Author:BingXian
      date: 2015.1.12
      编译: root@bingxian:/home/bingxian/sniffer# gcc -o netdev netdev.c -lpcap
      本次运行结果:
 
      root@bingxian:/home/bingxian/sniffer# ./netdev
      the net device name is:  eth0
      the net is :192.168.199.0
      the mask is 255.255.255.0
*/
 
 
#include <stdio.h>
#include <pcap.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void main()
  {
  char *device ;
  char errbuf [ PCAP_ERRBUF_SIZE ] = { 0};
   /*
      char* pcap_lookupdev( char * errbuf)
      该函数返回找寻到的第一个网络设备名的指针。
      参数 errbuf用于当发生错误时存储错误信息,用户分配给该 buffer 的空间至少
       PCAP_ERRBUF_SIZE的大小( 256 字节)
   */
  device = pcap_lookupdev ( errbuf);
  if (device == NULL )
  {
    printf (“%s\n” , errbuf);
      exit ( 1);
  }
  printf (“the net device name is:  %s\n” , device);
  bpf_u_int32  *mask ;
  bpf_u_int32  *net_addr ;
   /*
      int pcap_lookupnet(const char *device, bpf_u_int32 *netp,
      bpf_u_int32 *maskp, char *errbuf);
      能够通过参数指定设备的网络号和掩码
      发生错误,则返回错误码 -1
    */
  int retcode = pcap_lookupnet (device ,&net_addr ,& mask, errbuf );
  if (retcode == - 1)
  {
    printf (“%s\n” , errbuf);
    exit (1 );
  }
 struct in_addr addr_net ;
  addr_net .s_addr = net_addr ;
  /*
       char *inet_ntoa (struct in_addr);
       函数功能:将网络地址转换成点分十进制形式的字符串格式
  */
  char *net = inet_ntoa (addr_net );
  printf (“the net is :%s\n” , net);
  addr_net .s_addr = mask;
  char *real_mask = inet_ntoa (addr_net );
  printf (“the mask is %s\n” , real_mask );
}
上面一个简单的程序说明了libpcap的用法,下面的一个例子说明了如何利用libpcap过滤并抓取数据包。
接下来的一个程序则是一个根据BPF语法,得到数据链路层数据包的例子:
/*
文件名: test.c
功能:根据 BPF规则过滤数据包,得到原始数据(以太网帧)
author: bingxian
编译方式: root@bingxian:/home/bingxian/sniffer# gcc -o test test.c -lpcap
测试执行: root@bingxian:/home/bingxian/sniffer# ./test arp
device is eth0
the length is 60
number of bytes:60
receive time: Mon Jan 12 07:53:08 2015
ff ff ff ff ff ff 00 24 2b f4 ec 74 08 06 00 01
08 00 06 04 00 01 00 24 2b f4 ec 74 c0 a8 c7 d8
00 00 00 00 00 00 c0 a8 c7 01 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
*/
#include <stdio.h>
#include <pcap.h>
#include <stdlib.h>
#include <time.h>
  void get(u_char * ags, const struct pcap_pkthdr * pkthdr , const u_char * packet )
{        
    printf(“the length is %d\n” , pkthdr->len);
    printf(“number of bytes:%d\n” , pkthdr->caplen);
    printf(“receive time: %s” , ctime(( const time_t *)&pkthdr-> ts.tv_sec));
    int i =0 ;
    for(i; i < pkthdr->len; i ++)
    {
        printf (“%02x “,packet[i ]);
         if((i + 1 ) % 16 == 0)
            printf (“\n”);
    }
    printf(“\n\n” );
}
 void main (int argc, char * argv[])
{     
        if( argc < 2 )
    {
        printf (“Usage:./%s  BPF synatx\n”,argv [0]);
        exit (1);
    }
 
    char *dev , errbuf[PCAP_ERRBUF_SIZE];
    dev = pcap_lookupdev (errbuf); /*查找网络可用设备*/
    if(dev == NULL)
    {
        printf (” no devcie find\n”);
        exit (0);
    }
    printf(“device is %s\n” ,dev);
    char erro [100] = {‘\0′ };
    /*
        pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
         获得用于捕获网络数据包的数据包捕获描述字。
        device 参数为指定打开的网络设备名。
        snaplen 参数定义捕获数据的最大字节数。 65535的字节能够容纳任何网络数据包
        promisc 指定是否将网络接口置于混杂模式, 1表示为混杂模式。
        to_ms 参数指将内核空间的数据包复制到应用层空间前的等待时间(毫秒)。
        ebuf 参数则仅在 pcap_open_live()函数出错返回NULL时用于传递错误消息。,值为 0
    */
    pcap_t * handle = pcap_open_live(dev,65535 ,1, 0,erro );
    if(!handle)
    {
        printf (” binding device fail\n “);
        exit (0);
    
    }
  struct bpf_program filt ;
    /*
        int pcap_compile(pcap_t *p, struct bpf_program *fp,   char *str, int optimize,
        bpf_u_int32 netmask)
        pcap_compile() 即是将BPF表达式语句进行编译,过滤指定的数据包,如: arp or tcp
    */
    pcap_compile (handle,& filt, argv [1], 1,0 );
    /* 
       int pcap_setfilter(pcap_t *p, struct bpf_program *fp);
       pcap_setfilter()  is used to specify a filter program.  fp is a pointer
       to a bpf_program struct, usually the result  of  a  call  to  pcap_com-
       pile().
    */
    pcap_setfilter (handle,& filt);
    int id = 0;
    /*
        int pcap_loop(pcap_t * p,int cnt, pcap_handler callback, uchar * user)
         该函数用于开始捕获数据包,
        p 为打开的描述字
        cn 为捕获数据包的数目 cn -1时,表示无线循环
        callback 为程序员编写的回调函数 用于处理
        user 为程序员需要传递的参数,会传递给回调函数的第一个参数
        
    */
   pcap_loop(handle , - 1, get ,(u_char *)id);
    /*
       void pcap_close(pcap_t *p);
       关闭用于捕获数据包的描述字
        
    */
    pcap_close(handle );
}
具体的编程细节可以参照提供的相关文章,这里就不在细节上表述。需要注意的是,由于捕获的数据是数据链路层的数据,所以需要我们自己编程实现对包的解析。至于针对各层数据包头的剥离,留待第二篇进行讲述。