Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1249402
  • 博文数量: 298
  • 博客积分: 10050
  • 博客等级: 上将
  • 技术积分: 3277
  • 用 户 组: 普通用户
  • 注册时间: 2007-03-14 13:36
文章分类
文章存档

2015年(7)

2012年(1)

2010年(5)

2009年(55)

2008年(73)

2007年(160)

我的朋友

分类: 网络与安全

2007-09-22 12:49:34

施聪
高级程序员、网络设计师
2005 
 5  

libpcap 
 unix/linux 平台下的网络数据包捕获函数包,大多数网络监控软件都以它为基础。Libpcap 可以在绝大多数类 unix 平台下工作,本文分析了 libpcap  linux 下的源代码实现,其中重点是 linux 的底层包捕获机制和过滤器设置方式,同时也简要的讨论了 libpcap 使用的包过滤机制 BPF
网络监控
绝大多数的现代操作系统都提供了对底层网络数据包捕获的机制,在捕获机制之上可以建立网络监控(Network Monitoring)应用软件。网络监控也常简称为sniffer,其最初的目的在于对网络通信情况进行监控,以对网络的一些异常情况进行调试处理。但随着互连网的快速普及和网络攻击行为的频繁出现,保护网络的运行安全也成为监控软件的另一个重要目的。例如,网络监控在路由器,防火墙、入侵检查等方面使用也很广泛。除此而外,它也是一种比较有效的黑客手段,例如,美国政府安全部门的"肉食动物"计划。

包捕获机制
从广义的角度上看,一个包捕获机制包含三个主要部分:最底层是针对特定操作系统的包捕获机制,最高层是针对用户程序的接口,第三部分是包过滤机制。

不同的操作系统实现的底层包捕获机制可能是不一样的,但从形式上看大同小异。数据包常规的传输路径依次为网卡、设备驱动层、数据链路层、IP 层、传输层、最后到达应用程序。而包捕获机制是在数据链路层增加一个旁路处理,对发送和接收到的数据包做过滤/缓冲等相关处理,最后直接传递到应用程序。值得注意的是,包捕获机制并不影响操作系统对数据包的网络栈处理。对用户程序而言,包捕获机制提供了一个统一的接口,使用户程序只需要简单的调用若干函数就能获得所期望的数据包。这样一来,针对特定操作系统的捕获机制对用户透明,使用户程序有比较好的可移植性。包过滤机制是对所捕获到的数据包根据用户的要求进行筛选,最终只把满足过滤条件的数据包传递给用户程序。

Libpcap 
应用程序框架
Libpcap 
提供了系统独立的用户级别网络数据包捕获接口,并充分考虑到应用程序的可移植性。Libpcap 可以在绝大多数类 unix 平台下工作,参考资料 A 中是对基于 libpcap 的网络应用程序的一个详细列表。在 windows 平台下,一个与libpcap 很类似的函数包 winpcap 提供捕获功能,其官方网站是

Libpcap 
软件包可从   下载,然后依此执行下列三条命令即可安装,但如果希望 libpcap 能在 linux 上正常工作,则必须使内核支持"packet"协议,也即在编译内核时打开配置选项 CONFIG_PACKET(选项缺省为打开)



./configure
./make
./make install



libpcap 
源代码由 20 多个 C 文件构成,但在 Linux 系统下并不是所有文件都用到。可以通过查看命令 make 的输出了解实际所用的文件。本文所针对的libpcap 版本号为 0.8.3,网络类型为常规以太网。Libpcap 应用程序从形式上看很简单,下面是一个简单的程序框架:



char * device; /* 
用来捕获数据包的网络接口的名称 */
pcap_t * p; /* 
捕获数据包句柄,最重要的数据结构 */
struct bpf_program fcode; /* BPF 
过滤代码结构 */

/* 
第一步:查找可以捕获数据包的设备 */
device = pcap_lookupdev(errbuf)


/* 
第二步:创建捕获句柄,准备进行捕获 */
p = pcap_open_live(device, 8000, 1, 500, errbuf)


/* 
第三步:如果用户设置了过滤条件,则编译和安装过滤代码 */
pcap_compile(p, &fcode, filter_string, 0, netmask)

pcap_setfilter(p, &fcode)


/* 
第四步:进入(死)循环,反复捕获数据包 */
for( ; ; )
{
while((ptr = (char *)(pcap_next(p, &hdr))) == NULL);
           
/* 
第五步:对捕获的数据进行类型转换,转化成以太数据包类型 */
eth = (struct libnet_ethernet_hdr *)ptr;

/* 
第六步:对以太头部进行分析,判断所包含的数据包类型,做进一步的处理 */
if(eth->ether_type == ntohs(ETHERTYPE_IP)) 
…………
if(eth->ether_type == ntohs(ETHERTYPE_ARP)) 
…………
}
     
/* 
最后一步:关闭捕获句柄,一个简单技巧是在程序初始化时增加信号处理函数,
以便在程序退出前执行本条代码 */
pcap_close(p)




检查网络设备
libpcap 
程序的第一步通常是在系统中找到合适的网络接口设备。网络接口在Linux 网络体系中是一个很重要的概念,它是对具体网络硬件设备的一个抽象,在它的下面是具体的网卡驱动程序,而其上则是网络协议层。Linux 中最常见的接口设备名 eth0  loLo 称为回路设备,是一种逻辑意义上的设备,其主要目的是为了调试网络程序之间的通讯功能。eth0 对应了实际的物理网卡,在真实网络环境下,数据包的发送和接收都要通过 eht0。如果计算机有多个网卡,则还可以有更多的网络接口,如 eth1,eth2 等等。调用命令 ifconfig 可以列出当前所有活跃的接口及相关信息,注意对 eth0 的描述中既有物理网卡的 MAC 地址,也有网络协议的 IP 地址。查看文件 /proc/net/dev 也可获得接口信息。

Libpcap 
中检查网络设备中主要使用到的函数关系如下图:




libpcap 
调用 pcap_lookupdev() 函数获得可用网络接口的设备名。首先利用函数 getifaddrs() 获得所有网络接口的地址,以及对应的网络掩码、广播地址、目标地址等相关信息,再利用 add_addr_to_iflist()add_or_find_if()get_instance() 把网络接口的信息增加到结构链表 pcap_if 中,最后从链表中提取第一个接口作为捕获设备。其中 get_instanced() 的功能是从设备名开始,找第一个是数字的字符,做为接口的实例号。网络接口的设备号越小,则排在链表的越前面,因此,通常函数最后返回的设备名为 eth0。虽然 libpcap 可以工作在回路接口上,但显然 libpcap 开发者认为捕获本机进程之间的数据包没有多大意义。在检查网络设备操作中,主要用到的数据结构和代码如下:



     /* libpcap 
自定义的接口信息链表 [pcap.h] */
struct pcap_if 
{
struct pcap_if *next; 
char *name; /* 
接口设备名 */
char *description; /* 
接口描述 */
           
/*
接口的 IP 地址地址掩码广播地址,目的地址 */
struct pcap_addr addresses; 
bpf_u_int32 flags;      /* 
接口的参数 */
};

char * pcap_lookupdev(register char * errbuf)
{
     pcap_if_t *alldevs;
     ……
           pcap_findalldevs(&alldevs, errbuf)

           ……
           strlcpy(device, alldevs->name, sizeof(device));
     }
     


打开网络设备
当设备找到后,下一步工作就是打开设备以准备捕获数据包。Libpcap 的包捕获是建立在具体的操作系统所提供的捕获机制上,而 Linux 系统随着版本的不同,所支持的捕获机制也有所不同。

2.0 
及以前的内核版本使用一个特殊的 socket 类型 SOCK_PACKET,调用形式是 socket(PF_INET, SOCK_PACKET, int protocol),但 Linux 内核开发者明确指出这种方式已过时。Linux  2.2 及以后的版本中提供了一种新的协议簇 PF_PACKET 来实现捕获机制。PF_PACKET 的调用形式为 socket(PF_PACKET, int socket_type, int protocol),其中 socket 类型可以是 SOCK_RAW  SOCK_DGRAMSOCK_RAW 类型使得数据包从数据链路层取得后,不做任何修改直接传递给用户程序,而 SOCK_DRRAM 则要对数据包进行加工(cooked),把数据包的数据链路层头部去掉,而使用一个通用结构 sockaddr_ll 来保存链路信息。

使用 2.0 版本内核捕获数据包存在多个问题:首先,SOCK_PACKET 方式使用结构 sockaddr_pkt 来保存数据链路层信息,但该结构缺乏包类型信息;其次,如果参数 MSG_TRUNC 传递给读包函数 recvmsg()recv()recvfrom() 等,则函数返回的数据包长度是实际读到的包数据长度,而不是数据包真正的长度。Libpcap 的开发者在源代码中明确建议不使用 2.0 版本进行捕获。

相对 2.0 版本 SOCK_PACKET 方式,2.2 版本的 PF_PACKET 方式则不存在上述两个问题。在实际应用中,用户程序显然希望直接得到"原始"的数据包,因此使用 SOCK_RAW 类型最好。但在下面两种情况下,libpcap 不得不使用 SOCK_DGRAM 类型,从而也必须为数据包合成一个""链路层头部(sockaddr_ll)。

某些类型的设备数据链路层头部不可用:例如 Linux 内核的 PPP 协议实现代码对 PPP 数据包头部的支持不可靠。
在捕获设备为"any"时:所有设备意味着 libpcap 对所有接口进行捕获,为了使包过滤机制能在所有类型的数据包上正常工作,要求所有的数据包有相同的数据链路头部。
打开网络设备的主函数是 pcap_open_live()[pcap-linux.c],其任务就是通过给定的接口设备名,获得一个捕获句柄:结构 pcap_tpcap_t 是大多数 libpcap 函数都要用到的参数,其中最重要的属性则是上面讨论到的三种 socket 方式中的某一种。首先我们看看 pcap_t 的具体构成。



struct pcap [pcap-int.h]

     int fd; /* 
文件描述字,实际就是 socket */
     
           /* 
 socket 上,可以使用 select()  poll()  I/O 复用类型函数 */
     int selectable_fd; 

     int snapshot; /* 
用户期望的捕获数据包最大长度 */
     int linktype; /* 
设备类型 */
     int tzoff;            /* 
时区位置,实际上没有被使用 */
     int offset;      /* 
边界对齐偏移量 */

     int break_loop; /* 
强制从读数据包循环中跳出的标志 */

     struct pcap_sf sf; /* 
数据包保存到文件的相关配置数据结构 */
     struct pcap_md md; /* 
具体描述如下 */
     
     int bufsize; /* 
读缓冲区的长度 */
     u_char buffer; /* 
读缓冲区指针 */
     u_char *bp;
     int cc;
     u_char *pkt;

     /* 
相关抽象操作的函数指针,最终指向特定操作系统的处理函数 */
     int      (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
     int      (*setfilter_op)(pcap_t *, struct bpf_program *);
     int      (*set_datalink_op)(pcap_t *, int);
     int      (*getnonblock_op)(pcap_t *, char *);
     int      (*setnonblock_op)(pcap_t *, int, char *);
     int      (*stats_op)(pcap_t *, struct pcap_stat *);
     void (*close_op)(pcap_t *);

     /*
如果 BPF 过滤代码不能在内核中执行,则将其保存并在用户空间执行 */
     struct bpf_program fcode; 

     /* 
函数调用出错信息缓冲区 */
     char errbuf[PCAP_ERRBUF_SIZE + 1]; 
     
     /* 
当前设备支持的、可更改的数据链路类型的个数 */
     int dlt_count;
     /* 
可更改的数据链路类型号链表,在 linux 下没有使用 */
     int *dlt_list;

     /* 
数据包自定义头部,对数据包捕获时间、捕获长度、真实长度进行描述 [pcap.h] */
     struct pcap_pkthdr pcap_header;      
};

/* 
包含了捕获句柄的接口、状态、过滤信息 [pcap-int.h] */
struct pcap_md {
/* 
捕获状态结构 [pcap.h] */
struct pcap_stat stat; 

     int use_bpf; /* 
如果为1,则代表使用内核过滤*/ 
     u_long      TotPkts; 
     u_long      TotAccepted; /* 
被接收数据包数目 */ 
     u_long      TotDrops;      /* 
被丢弃数据包数目 */ 
     long      TotMissed;      /* 
在过滤进行时被接口丢弃的数据包数目 */
     long      OrigMissed; /*
在过滤进行前被接口丢弃的数据包数目*/
#ifdef linux
     int      sock_packet; /* 
如果为 1,则代表使用 2.0 内核的 SOCK_PACKET 模式 */
     int      timeout;      /* pcap_open_live() 
函数超时返回时间*/ 
     int      clear_promisc; /* 
关闭时设置接口为非混杂模式 */ 
     int      cooked;            /* 
使用 SOCK_DGRAM 类型 */
     int      lo_ifindex;      /* 
回路设备索引号 */
     char *device;      /* 
接口设备名称 */ 
     
/* 
以混杂模式打开 SOCK_PACKET 类型 socket  pcap_t 链表*/
struct pcap *next;      
#endif
};



函数 pcap_open_live() 的调用形式

 pcap_t * pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *ebuf),其中如果 device  NULL "any",则对所有接口捕获,snaplen 代表用户期望的捕获数据包最大长度,promisc 代表设置接口为混杂模式(捕获所有到达接口的数据包,但只有在设备给定的情况下有意义),to_ms 代表函数超时返回的时间。本函数的代码比较简单,其执行步骤如下:

为结构 pcap_t 分配空间并根据函数入参对其部分属性进行初试化。
分别利用函数 live_open_new()  live_open_old() 尝试创建 PF_PACKET 方式或 SOCK_PACKET 方式的 socket,注意函数名中一个为"new",另一个为"old"
根据 socket 的方式,设置捕获句柄的读缓冲区长度,并分配空间。
为捕获句柄 pcap_t 设置 linux 系统下的特定函数,其中最重要的是读数据包函数和设置过滤器函数。(注意到这种从抽象模式到具体模式的设计思想在 linux 源代码中也多次出现,如 VFS 文件系统)
handle->read_op = pcap_read_linux
 handle->setfilter_op = pcap_setfilter_linux
下面我们依次分析 2.2  2.0 内核版本下的 socket 创建函数。

阅读(1170) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~