程序的实现流程
首先是找到要嗅探的接口。在Linux里它可能是eth0,可以用字符串定义它,也可以通过pcap查询。
初始化pcap。这里我们要告诉pcap对什么设备进行嗅探。如果我们只想嗅探特定通信(例如:仅TCP/IP包,仅流向23端口的包等等),我们必须建立一个规则集。
最后,让pcap进入它的主循环开始嗅探。
在嗅探到我们需要的东西以后,关闭会话,任务完成。
实际上这是一个很简单的过程。总共五步,其中一步是可选的,下面我们开始研究每个步骤及如何实现。
设置嗅探设备
这一步极其简单。有两种方法来设置我们要嗅探的设备。
第一种是简单地让用户告诉我们,程序如下:
#includestdio.h #includepcap.h intmain(intargc,char*argv[]) { char*dev=argv[1]; printf(Device:%s,dev); return(0); }
用户指定设备名作为程序的第一个参数。现在,字符串dev保存了我们要嗅探的,pcap可以认识的接口名
另一种方法也同样简单,看这个程序:
#includestdio.h #includepcap.h intmain(intargc,char*argv[]) { char*dev,errbuf[PCAP_ERRBUF_SIZE]; dev=pcap_lookupdev(errbuf); if(dev==NULL){ fprintf(stderr,Couldntfinddefaultdevice:%s,errbuf); return(2); } printf(Device:%s,dev); return(0); }
在这里,由pcap自己来设置设备。很多pcap命令允许我们把这个字符串作为参数。它的用途是:如果命令执行失败,它会得到出错的细节信息。在这段代码里,如果pcap_lookupdev()失败,errbuf会保存有一个错误信息。
打开设备
建立嗅探会话的任务真的很简单,用pcap_open_live()就可以了。这个函数的原型(取自pcapman)如下:
pcap_t*pcap_open_live(char*device,intsnaplen,intpromisc,intto_ms,char*ebuf)
第一个参数是设备名,我们在上一节已经介绍过了。snaplen是一个整型值,定义由pcap抓取的包的最大字节数。promisc,当设置为true时,使接口处于混杂模式(不管怎样,使设置为false,在一些特定情形下接口可能还是处于混杂模式)。to_ms是读超时(readtimeout),单位为毫秒(0表示没有超时;在一些平台上,可能会一直等待直到收到足够数量的包,所以应该使用一个非零值)。最后,ebuf用于保存出错信息(就象我们前面的errbuf)。函数返回会话句柄。为了演示,参考如下代码段:
#includepcap.h ... pcap_t*handle; handle=pcap_open_live(somedev,BUFSIZ,1,1000,errbuf); if(handle==NULL){ fprintf(stderr,Couldntopendevice%s:%s,somedev,errbuf); return(2); }
这个代码打开somedev字符串指定的设备,告诉它每次读BUFSIZ字节(定义于pcap.h中),置设备于混杂模式。嗅探直到有错误发生,错误信息保存到errbuf中,用于后面的错误信息输出。
关于混杂模式vs.非混杂模式:这是两个非常不同的风格。非混杂模式嗅探只监听与本地有直接关系的包。只有发往、源自或本地路由的包会被嗅探器捕获。混杂模式监听所有线上的通信。在无交换环境中(non-switchedenvironment),所以网络通信都会被监听。它可以让我们得到更多的包,但是,这是可以被检测到:可以通过测试强可靠性来发现网络中是否有主机正在以混合模式监听,另外混杂工作模式仅仅在非交换式的网络中有效,而且在一个高负载的网络环境中,混杂模式将消耗大量的系统资源。
通信过滤
通常我们只对特定网络通信感兴趣。比如我们只打算嗅探23端口(telnet)用于搜索密码信息,或者劫持发往21端口的文件(FTP),也可能是DNS通信(port53UDP)。无论哪种情形,我们很少会盲目地嗅探所有的网络通信。考虑使用pcap_compile()和pcap_setfilter()
函数。
这个步骤也是相当的简单。调用pcap_open_live()之后我们已经有了一个可用的嗅探会话,可以应用我们的过滤器。应用我们的过滤器之前,我们必须“编译”它。过滤器表达式为一个规则字符串(char数组)。在tcpdump的man文档里有其语法的说明书。我们将会尽量使用简单的过滤器表达式。
通过pcap_compile()函数来“编译”。它的原型为:
intpcap_compile(pcap_t*p,structbpf_program*fp,char*str,intoptimize,bpf_u_int32netmask)
第一个参数是我们的会话句柄(前文的例子里是pcap_t*handle)。接下来的参数用于指向存放编译后过滤器的空间。然后是过滤表达式。下一个optimize整数决定表达式是否是“优化的”(0为false、1为true)。最后,我们要指定网络掩码。函数失败时返回-1;其它值表示成功。
表达式被编译之后,就可以应用它了。使用pcap_setfilter()函数,下面是pcap_setfilter()
原型:
intpcap_setfilter(pcap_t*p,structbpf_program*fp)
很直白,第一个参数是会话句柄,第二个是编译后的表达式。
代码示例:
#includepcap.h ... pcap_t*handle;/*会话句柄*/ chardev[]=rl0;/*被嗅探的设备*/ charerrbuf[PCAP_ERRBUF_SIZE];/*错误信息*/ structbpf_programfp;/*编译后的过滤表达式*/ charfilter_exp[]=port23;/*过滤表达式*/ bpf_u_int32mask;/*嗅探设备的网络掩码*/ bpf_u_int32net;/*嗅探设备的IP*/ if(pcap_lookupnet(dev,&net,&mask,errbuf)==-1){ fprintf(stderr,Cantgetnetmaskfordevice%s,dev); net=0; mask=0; } handle=pcap_open_live(dev,BUFSIZ,1,1000,errbuf); if(handle==NULL){ fprintf(stderr,Couldntopendevice%s:%s, somedev,errbuf); return(2); } if(pcap_compile(handle,&fp,filter_exp,0,net)==-1){ fprintf(stderr,Couldntparsefilter%s:%s, filter_exp,pcap_geterr(handle)); return(2); } if(pcap_setfilter(handle,&fp)==-1){ fprintf(stderr,Couldntinstallfilter%s:%s, filter_exp,pcap_geterr(handle)); return(2); }
这个程序在rl0设备上以混杂模式嗅探发往或源自23端口的所有通信。
这个例子中有一个之前没讨论过的函数:pcap_lookupnet()。给一个设备名,得到它的IP和网络掩码。为了应用过滤器,我们就要知道网络掩码,这个函数就可以派上用场了。
开始嗅探
到这里我们已经学习了如何定义设备、准备嗅探以及应用过滤器来过滤我们不想嗅探的部分。现在开始嗅探数据包了。
嗅探数据包有两种主要方法。我们可以一次捕获一个单独的包,也可以进入一个循环,等待N个包。我们首先关注如何捕获单个包,之后再研究循环的方法。就单个包而言,我们用pcap_next()。
pcap_next()的原型很简单:
u_char*pcap_next(pcap_t*p,structpcap_pkthdr*h)
首个参数是会话句柄。第二个参数是一个指针,它指向的结构用于存放数据包的一般信息,如捕获的时间,包长度,组成包的各部分长度。pcap_next()返回的*u_char指向捕获的包,稍后我们将会讨论读取数据包本身的方法。
这个例子演示怎样使用pcap_next()来嗅探数据包:
#includepcap.h #includestdio.h intmain(intargc,char*argv[]) { pcap_t*handle;/*会话句柄*/ char*dev;/*嗅探的设备*/ charerrbuf[PCAP_ERRBUF_SIZE];/*错误信息*/ structbpf_programfp;/*编译的过滤器*/ charfilter_exp[]=port23;/*过滤表达式*/ bpf_u_int32mask;/*网络掩码*/ bpf_u_int32net;/*IP*/ structpcap_pkthdrheader;/*pcap头*/ constu_char*packet;/*数据包*/ /*定义设备*/ dev=pcap_lookupdev(errbuf); if(dev==NULL){ fprintf(stderr,Couldntfinddefaultdevice:%s,errbuf); return(2); } /*取得设备属性*/ if(pcap_lookupnet(dev,&net,&mask,errbuf)==-1){ fprintf(stderr,Couldntgetnetmaskfordevice%s:%s, dev,errbuf); net=0; mask=0; } /*以混杂模式打开会话*/ handle=pcap_open_live(dev,BUFSIZ,1,1000,errbuf); if(handle==NULL){ fprintf(stderr,Couldntopendevice%s:%s, somedev,errbuf); return(2); } /*编译并应用过滤器*/ if(pcap_compile(handle,&fp,filter_exp,0,net)==-1){ fprintf(stderr,Couldntparsefilter%s:%s, filter_exp,pcap_geterr(handle)); return(2); } if(pcap_setfilter(handle,&fp)==-1){ fprintf(stderr,Couldntinstallfilter%s:%s, filter_exp,pcap_geterr(handle)); return(2); } /*抓取一个数据包*/ packet=pcap_next(handle,&header); /*输出数据包长度*/ printf(Jackedapacketwithlengthof[%d], header.len); /*关闭会话*/ pcap_close(handle); return(0); }
这个程序用pcap_lookupdev()取得设备并将其设置为混杂模式,然后开始嗅探。它取得23端口(telnet)上的首个包后输出这个包的大小(字节)。
另一种方法要复杂一些,不过更有用。通常很少有嗅探器直接调用pcap_next()函数,它们更常用的是pcap_loop()或pcap_dispatch()。要学会这两个函数,你必须先理解回调函数。
回调函数并不新鲜,它们在不少API中普遍存在。回调的概念很简单。假设我的程序要等待某个事件,为简单起见,就说是等待用户输入吧。用户每按一次键,我想要通过函数来决定接下来做什么。这个函数就可以是回调函数,每次按下键盘,我的程序就会调用这个回调函数。回到pcap中,回调函数被调用的时机由用户按下一个键改为pcap嗅探到一个数据包。pcap_loop()和pcap_dispatch()的用法很相似。每次嗅探到一个符合过滤要求(如果存在过滤器的话)的包后就会调用回调函数。
pcap_loop()的原型是:intpcap_loop(pcap_t*p,intcnt,pcap_handlercallback,u_char*user)
首个参数是会话句柄。接下来的cnt参数告诉pcap_loop()返回之前应该嗅探到多少个包(负数表示一直嗅探直到出错为止)。第三个就是之前讨论的回调函数啦。最后一个参数的作用是传递附加的自定义数据给回调函数,在一些应用时有用,很多时候直接设为NULL就行。
后面我们将会以例子的形式看到pcap用u_char指针传递一些很有意思的信息。
pcap_dispatch()的用法几乎一样,唯一的区别是pcap_dispatch()只处理第一批从系统中收到的包,而pcap_loop()会继续处理接下来的包直到达到指定数量为止。关于它们的细节差异,请参考pcap的说明文档。
拿出cap_loop()的例子之前,我们得了解一下回调函数的原型:
voidgot_packet(u_char*args,conststructpcap_pkthdr*header,constu_char*packet);
首先,它是一个无返回值的函数。
第一个参数就是我们传给pcap_loop()的最后一个数据。每次回调函数被调用时都可以取得这个数据。
第二个参数是pcap头结构,它含有包何时到达,多大等信息。pcap_pkthdr结构定义于pcap.h之中:
structpcap_pkthdr{ structtimevalts;/*timestamp*/ bpf_u_int32caplen;/*lengthofportionpresent*/ bpf_u_int32len;/*lengththispacket(offwire)*/ };
最后的那个参数constu_char*packet是我们最关心的,也是最容易引起pcap初学者混乱的。它是一个u_char指针,指向被pcap_loop()嗅探到的整个数据包的第一个字节。
怎样使用这个packet参数呢?数据包有很多属性,只要思考一下就会知道,它不是一个真正的字符串,而是一系列的结构(例如,TCP/IP包应该有以太头、IP头、TCP头,最后,还有包的载荷)。这个u_char指针指向的正是这些数据结构的序列化版本,所以在使用之前,要做类型转换工作。
首先,我们要定义这些结构,下面定义的是以太网TCP/IP数据包结构。
/*以太网的地址占6字节*/ #defineETHER_ADDR_LEN6 /*以太网头*/ structsniff_ethernet{ u_charether_dhost[ETHER_ADDR_LEN];/*目的地址*/ u_charether_shost[ETHER_ADDR_LEN];/*源地址*/ u_shortether_type;/*IP?ARP?RARP?等*/ }; /*IP头*/ structsniff_ip{ u_charip_vhl;/*version4|headerlength2*/ u_charip_tos;/*typeofservice*/ u_shortip_len;/*totallength*/ u_shortip_id;/*identification*/ u_shortip_off;/*fragmentoffsetfield*/ #defineIP_RF0x8000/*reservedfragmentflag*/ #defineIP_DF0x4000/*dontfragmentflag*/ #defineIP_MF0x2000/*morefragmentsflag*/ #defineIP_OFFMASK0x1fff/*maskforfragmentingbits*/ u_charip_ttl;/*timetolive*/ u_shortip_sum;/*checksum*/ structin_addrip_src,ip_dst;/*sourceanddestaddress*/ }; #defineIP_HL(ip)(((ip)-ip_vhl)&0x0f) #defineIP_V(ip)(((ip)-ip_vhl)4) /*TCP头*/ structsniff_tcp{ u_shortth_sport;/*sourceport*/ u_shortth_dport;/*destinationport*/ tcp_seqth_seq;/*sequencenumber*/ tcp_seqth_ack;/*acknowledgementnumber*/ u_charth_offx2;/*dataoffset,rsvd*/ #defineTH_OFF(th)(((th)-th_offx2&0xf0)4) u_charth_flags; #defineTH_FIN0x01 #defineTH_SYN0x02 #defineTH_RST0x04 #defineTH_PUSH0x08 #defineTH_ACK0x10 #defineTH_URG0x20 #defineTH_ECE0x40 #defineTH_CWR0x80 #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;/*urgentpointer*/ };
建议在包含所有头文件之前先加入一行:
#define_BSD_SOURCE1
这样能确保使用BSD风格的API。当然,如果你不想用预定义,你可以简单地改一下结构,就象我在这里做的那样。
那么,如何把u_char指针应用到pcap工作中来呢?这些结构定义了包中的头部数据,那怎样提取这些部分呢?答案是用指针来实现我们还是假设处理以太网的TCP/IP包。同样的方法可用于任何数据包,唯一的区别是你实际所使用的结构类型。让我们从用于解析数据包的变量声明及预处理定义开始:
/*以太网头总是14字节*/ #defineSIZE_ETHERNET14 conststructsniff_ethernet*ethernet;/*Theethernetheader*/ conststructsniff_ip*ip;/*TheIPheader*/ conststructsniff_tcp*tcp;/*TheTCPheader*/ constchar*payload;/*Packetpayload*/ u_intsize_ip; u_intsize_tcp; 现在开类型转换: ethernet=(structsniff_ethernet*)(packet); size_ip=IP_HL(ip)*4; if(size_ip20){ printf(*InvalidIPheaderlength:%ubytes,size_ip); return; } tcp=(structsniff_tcp*)(packet+SIZE_ETHERNET+size_ip); size_tcp=TH_OFF(tcp)*4; if(size_tcp20){ printf(*InvalidTCPheaderlength:%ubytes,size_tcp); return; } payload=(u_char*)(packet+SIZE_ETHERNET+size_ip+size_tcp);
u_char指针只是一个包含内存地址的变量,这就是指针的实质,指出内存所在位置。
为了简单起见,就说这个指针指向的地址为X吧。如果我们的这三个结构是线性存储的,那么第一个(sniff_ethernet)结构就位于地址为X的内存上,接下来我们可以很简单地找到后面的结构:X地址加上14(或SIZE_ETHERNET)字节的以太网头长度。
简而言之,如果我们有头地址,那么后面的结构地址就是当前头地址加上头长度。IP头和以太网头不一样,这的长度是不固定的。它的长度由它的成员指定,以字(4byte)为单位,所以得到字节长度还得剩上4。最小长度是20字节。
TCP头也是变长的,同样以4字节为一个单位,最小长度也是20字节。
整理一下变量位置(bytes)
sniff_ethernet sniff_ip sniff_tcp X X+SIZE_ETHERNET X+SIZE_ETHERNET+{IPheaderlength}+{TCP headerlength} payload
第一行的sniff_ethernet结构,正好在X处。sniff_ip,紧跟在sniff_ethernet之后,为X加上以太网头所占空间(14字节,或SIZE_ETHERNET)。sniff_tcp在sniff_ip后面,因此它的位置是X加上以太网头和IP头的大小。现在你可以用pcap写一个嗅探器了。
本文内容所提及均为本地测试或经过目标授权同意,旨在提供教育和研究信息,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,作者不鼓励或支持任何形式的非法行为。