赞
踩
(转载请标明出处,请勿用于商业用途)
http://blog.csdn.net/linux_embedded/article/details/8826429
Linux 下赫赫有名的抓吧工具tcpdump,想必使用过的人都十分的清楚。但是,其实现的原理却很少人提及过,今天就tcpdump的实现原理做简单的介绍。
tcpdump 首先利用libpcap工具,将linux网络栈中的数据包抓取上来,然后,tcpdump在按照用户的需求完成数据包的分析工作。下面就如何通过libpcap实现数据包的抓取做简单的介绍。
首先,我们需要了解一下pcap 嗅探器使用的一般布局,下面分为几个部分简单介绍。
设备的设置十分的简单,存在两种方式可以完成网络设备的设置,即:通过传递参数完成设定;通过pcap提供的函数完成设备的检测、设定。
- #include <stdio.h>
- #include <pcap.h>
-
- int main(int argc, char *argv[])
- {
- char *dev = argv[1];
-
- printf("Device: %s\n", dev);
- return(0);
- }
- 2. 第二种方法就是,通过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't find default device: %s\n", errbuf);
- return(2);
- }
- printf("Device: %s\n", dev);
- return(0);
-
- }
通过上面的函数pcap_lookupdev()就可以实现设备的自动获取。其中参数,errbuf为了记录函数调用失败时的原因。可以通过man手册参看其使用方法。
可以通过函数pcap_open_live()打开需要监测的网络设备。函数原型以及使用方式如下:
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,char *ebuf)
参数:函数返回会话的句柄。下面是该函数的使用示例:
- device:前面定义的网络设备名称。
- snaplen:一个整型数据,用来定义pcap抓取的数据的字节数。
- promise:如果该参数为真,表示王珂会被设置为混杂模式(如果参数为假,在某些情况下,网卡还是会被设置为混杂模式)。
- to_ms:读取数据包的超时时间,单位为ms(0表示没有超时,一般不为0)。
- ebuf:记录错误信息。
其中,BUFSIZE 被定义在 pcap.h中,程序会一直运行直到错误产生,错误信息会记录在errbuf中。
#include <pcap.h> ... pcap_t *handle; handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL { fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf); return(2); }
Note:
简单介绍一下,混杂模式和非混杂模式。非混杂模式下,主机只会检测与其相关的数据。这些数据包括:源、目的为本主机,或者经过本机的路由数据包。
- 非混杂模式:
混杂模式下,主机会检测线路上所有的数据包。在没有交换机的网络中,检测所有的数据包。这种模式的好处是,可以检测的数据会增加。利弊主要取决于你的目的。但是,混杂模式是可以探测的。一个主机可以通过可靠地手段检测到另一个主机的网卡是否被设置为混杂模式。再一个,混杂模式只会工作在没有交换机的网络,例如通过hub连接的,或者交换机被ARP淹没。混杂模式会导致,在网络流量巨大的时候,增加系统的负担。
- 混杂模式:
一般情况下,我们只会检测我们感兴趣的数据包。例如,我们会监听23(telnet)端口来搜索密码,监听21端口截获一个文件,或者仅仅为了监听DNS数据(端口53)。我们一般不会盲目监听网络上的数据包。
通过,pcap_compile()和pcap_setfilter()就可以实现对特定数据包的监听工作。
当我们创建了一个监听事务后,下一步我们就可以设置过滤的规则了。之所以使用pcap内置的过滤机制,而不是传统的if/else if 模式实现数据包的过滤主要考虑到两个原因:
在应用过滤规则之前,我们需要”编译“它,过滤规则一般被存储在一个字符数组中,存储格式我们可以通过查看man tcpdump的规则。pcap_compile()和pcap_setfilter()函数的原型和使用方式如下:
- pcap的数据包的过滤机制十分的高效,其直接通关BPF实现,我们省去了很多步骤。
- 使用pcap十分的简单。
- pcap_compile():
参数:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask) 如果该函数运行成功,返回一个非-1的整数。
- pt:会话句柄。
- fp:表示编译过的过滤规则存储的位置。
- str:字符串格式的过滤规则。
- optimize:表示过滤规则是否需要的优化(1:need,0:no)
- netmask:表示过滤应用的网络的子网掩码.
2. pcap_setfilter():参数:
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
下面是两个函数使用的示例:
- p:会话句柄。
- fp:表示编译过的过滤规则存储的位置。
上面表示,eth0工作在混杂模式,检测端口号为23的数据包。
#include <pcap.h> ... pcap_t *handle; /* Session handle */ char dev[] = "eth0"; /* 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 for device %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", somedev, 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); }
上面存在一个没有介绍过的函数pcap_lookupnet(),该函数的作用是通过dev获得其对应的IPV4 网络号和其对应的网络的子网掩码。后面的杂项中介绍该函数。
经过了上面一系列的准备工作,现在开始正的抓包。通常可以通过两张方法实现数据的抓取:一次只抓取一个数据包;每次抓取n个数据包。下面我们分别介绍一下,这两种方法。
single
利用函数pcap_next(),我们可以实现每次只抓取一个数据包,该函数的原型和使用方法如下:
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
参数:该函数的返回值指向抓取的数据包,下面是该函数使用的示例。
- p:会话句柄。
- h:指向存储关于数据包的一些主要信息的指针,这些信息包括:数据包抓取的时间、数据包的长度、数据包一些特殊部分的长度。
#include <pcap.h> #include <stdio.h> int main(int argc, char *argv[]) { pcap_t *handle; /* Session handle */ char *dev; /* The device to sniff on */ char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */ struct bpf_program fp; /* The compiled filter */ char filter_exp[] = "port 23"; /* The filter expression */ bpf_u_int32 mask; /* Our netmask */ bpf_u_int32 net; /* Our IP */ struct pcap_pkthdr header; /* The header that pcap gives us */ const u_char *packet; /* The actual packet */ /* 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", somedev, 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("Jacked a packet with length of [%d]\n", header.len); /* And close the session */ pcap_close(handle); return(0); }
上面的应用利用pcap_lookupdev()得到了一个可用的设备,然后开始在端口23(telnet)开始监测数据包,当获得一个数据包后,就将该数据包以字节的形式告诉用户。
multiple
另一种技术相对比较复杂,其实pcap_next()其实一般很少使用,更多的我们会使用pcap_loop()和pcap_dispatch()函数,上述两个函数的实现用到了回调函数机制。我们需要实现定义好的自己的回调函数,然后注册到pcap_next()或者pcap_dispatch()中,两个函数的使用方式和工作方式基本相同,每当检测到一个符合过滤规则的数据包,回调函数就会调用(当然,如果没有定义过滤规则,每个数据包都会调用其对应的回调函数)。pcap_look()函数的原型和函数的参数如下:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
参数:
- p:检测会话句柄;
- cnt:表示结束监听之前需要截获的数据包的数量(负数表示一直监听知道出错)。
- callback:我们定义的回调函数的名字。
- user:表示传递给回调函数的参数
pcap_dispatch()的使用方法几乎和pcap_look()一样,多不同是pcap_dispatch()只会读取"一些"数据包,之后就会退出循环。可以通过man手册获得更详细的关于pcap_look()和pcap_dispatch()的区别。接下来我么有必要介绍一些回调函数的格式,回调函数不能随便定义,否则pcap_dispatch()和pcap_look()会不知道该如何调用回调函数的。回调函数的原型和参数如下:
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
参数:
- args:该参数就是我们在pcap_loop()和pcap_dispatch()最后的用户传递的参数,每当回调函数被调用时该参数都将被传递一次。
- header:保存数据包截获的具体时间和数据包大小等信息。
- packet:指向数据包的第一个字节(数据包已经被序列化)
pcap_pkthdr()结构体的具体格式如下:
struct pcap_pkthdr { struct timeval ts; /* time stamp */ bpf_u_int32 caplen; /* length of portion present */ bpf_u_int32 len; /* length this packet (off wire) */ };
在开始读取数据包中内同时我们首先需要对数据包就行反序列化,所以我们有必要了解一下TCP/IP下各个字段的含义,下面就对TCP/IP协议中各个数据包头进行简单的解释。假设TCP/IP的链路层协议为Ethernet:
/* Ethernet addresses are 6 bytes */ #define ETHER_ADDR_LEN 6 /* Ethernet header */ struct sniff_ethernet { u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */ u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */ u_short ether_type; /* IP? ARP? RARP? etc */ }; /* IP header */ struct sniff_ip { u_char ip_vhl; /* version << 4 | header length >> 2 */ u_char ip_tos; /* type of service */ u_short ip_len; /* total length */ u_short ip_id; /* identification */ u_short ip_off; /* fragment offset field */ #define IP_RF 0x8000 /* reserved fragment flag */ #define IP_DF 0x4000 /* dont fragment flag */ #define IP_MF 0x2000 /* more fragments flag */ #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */ u_char ip_ttl; /* time to live */ u_char ip_p; /* protocol */ u_short ip_sum; /* checksum */ struct in_addr ip_src,ip_dst; /* source and dest address */ }; #define IP_HL(ip) (((ip)->ip_vhl) & 0x0f) #define IP_V(ip) (((ip)->ip_vhl) >> 4) /* TCP header */ struct sniff_tcp { u_short th_sport; /* source port */ u_short th_dport; /* destination port */ tcp_seq th_seq; /* sequence number */ tcp_seq th_ack; /* acknowledgement number */ u_char th_offx2; /* data offset, rsvd */ #define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4) u_char th_flags; #define TH_FIN 0x01 #define TH_SYN 0x02 #define TH_RST 0x04 #define TH_PUSH 0x08 #define TH_ACK 0x10 #define TH_URG 0x20 #define TH_ECE 0x40 #define TH_CWR 0x80 #define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR) u_short th_win; /* window */ u_short th_sum; /* checksum */ u_short th_urp; /* urgent pointer */ };现在,开始反序列化传递给回调函数的数据包,假设TCP/IP协议运行在Ethernet上。
/* ethernet headers are always exactly 14 bytes */ #define SIZE_ETHERNET 14 const struct sniff_ethernet *ethernet; /* The ethernet header */ const struct sniff_ip *ip; /* The IP header */ const struct sniff_tcp *tcp; /* The TCP header */ const char *payload; /* Packet payload */ u_int size_ip; u_int size_tcp; And now we do our magical typecasting: ethernet = (struct sniff_ethernet*)(packet); ip = (struct sniff_ip*)(packet + SIZE_ETHERNET); size_ip = IP_HL(ip)*4; if (size_ip < 20) { printf(" * Invalid IP header length: %u bytes\n", size_ip); return; } tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip); size_tcp = TH_OFF(tcp)*4; if (size_tcp < 20) { printf(" * Invalid TCP header length: %u bytes\n", size_tcp); return; } payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
我们解释一下,假设我们定义数据包的起始地址为X,则Ethernet头、IP、TCP/UDP头的起始地址分别为:
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}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。