赞
踩
使用本文档,需要有一些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任务的基本概念了,嗅探数据包,应用过滤器,使用回调函数。是时候来到战场嗅探这无尽的网络了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。