Linux抓包技术简介
Author: zausiu@gmail.com
1. 简介
大多数应用常景下,我们在IP层上用AF_INET或AF_INET6作为socket的第一个参数,SOCK_STREAM或SOCK_DGRAM作为socket的第二个参数创建最为常见的IPv4或IPv6的TCP或UDP套接字。这样的套接字满足了绝大多数网络应用常景。
本文简单介绍三种Linux平台下抓包,截获包的技术。它们提供一些更酷的功能。每种技术的水都很深,作者个人水平有限,按照自己的理解做一般的介绍。仅仅旨在起到科谱的作用。
2. RAW SOCKET
参数SOCK_RAW可以作为socket函数的第二个参数传入,这样创建的套接字称为原始套接字。设置Socket的第一个参数可以让指定原始套接字开在数据链路层上或者网络层。
下面的语句创建在数据链路层上的原始socket。
Socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
在负载不大的情况下,读这个socket可以拿到本机收到的所有数据包。写入这个socket需要程序员负责填充各种包头的数据。
Socket(AF_INET, SOCK_RAW, xxxxx协议号);
在IP层上创建IPv4的RAW socket,可以拿到各种经由IPv4的数据包,比如ICMP包,IGMP包等。Steven的<Unix网络编程>第三版第28.4节,第二段:“系统收到的UDP包和TCP包不会传给原始套接字”。但读tcpcopy的源代码时,确实用了原始套接字在网络层读UDP和TCP包。难道书上写的过时了。见代码,来自tcpcopy:
#if (TCPCOPY_UDP)
fd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
#else
fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
#endif
另外,在IP层上创建的原始套接字有一个选项:IP_HDRINCL,它被开启时,则内核帮助填充IP包头。Linux默认没有开启这个选项。
3. NF_Queue
NF_Queue是用户空间的一个库,需要内核模块NETFILTER_NETLINK_QUEUE的支持。官方网址:http://www.netfilter.org。通过这个库可以在数据包被发往用户空间处理前截获数据包,或者在数据包被发往网卡前截获数据库。截获后,可以修改数据包的内容,并决定是否丢充这个数据库。
NF_Queue可以用来编写防火墙等安全软件。
Tcpcopy使用NF_Queue丢弃测试中的程序回复给真实客户程序的数据包,使测试中程序回复的报文不影响真实客户程序。
另一个提供类似功能的库是IP_Queue,它已经被废弃。NF_Queue可以完全取代IP_Queue。我们用的tlinux,内核中built-in了NF_Queue模块。
4. libpcap
大名鼎鼎的Libpcap是一个古老的可以工作在所有Unix-like平台上的抓包库,比linux年纪更大。Tcpdump的广泛使用让libpcap成为事实上的行业标准。它可以很方便的编写抓取数据链路层上的数据包,并可以设置过滤条件在内核级别只复制关心的数据包,而数据链路层上的原始套接字会尽最大努力复制所有的包,所以libpcap的工作效率高。而且,pcap使用方便。下面是一个很简单的使用pcap抓到发往80端口的tcp包并打印源IP,目的IP,和目的端口号的程序。非常短,很容易看,起到介绍libpcap的作用。
int main(int argc, char *argv[])
{
pcap_t *handle; // pcap用的句柄,和其它各种句柄一样。
char *dev; // 网络接口名,如eth0, eth1, tunl0 之类
char errbuf[PCAP_ERRBUF_SIZE]; // 存描述出错原因的缓冲
struct bpf_program fp; // pcap用来过滤数据包用
char filter_exp[] = "tcp and dst port 80"; // 过滤字符串,和tcpdump的写法一样
struct pcap_pkthdr header; // pcap自己用的包头
const u_char *packet; // pcap包头后的数据,即抓到的数据
int len_of_iphdr; // ip头的长度
struct in_addr src_ip; // 源IP地址
struct in_addr dst_ip; // 目的IP
const u_char *ip_hdr_addr; // IP头的首地址
const u_char *tcp_hdr_addr; // tcp头的首地址
struct tcphdr *tcp_hdr; // tcp头结构体
u_short dst_port; // 目的端口
// 如果命令行指定网卡名
if (argv[1])
dev = argv[1]; //就用指定的网卡
else
dev = "eth0"; //否则用eth0
}
// 用pcap打开这个网卡
handle = pcap_open_live(dev, BUFSIZ, 0, 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);
}
// 抓包
packet = pcap_next(handle, &header);
// 至此packet指向抓到的一个目标端口号是80的数据链路层的包
// 它的包结构是 14字节以太网包头 + 可变IPv4包头 + TCP包头
ip_hdr_addr = (u_char*)(packet + sizeof(struct ethernet_hdr));
src_ip.s_addr = ((struct iphdr*)ip_hdr_addr)->saddr;
dst_ip.s_addr = ((struct iphdr*)ip_hdr_addr)->daddr;
len_of_iphdr = ((struct iphdr*)ip_hdr_addr)->ihl * 4;
tcp_hdr_addr = ip_hdr_addr + len_of_iphdr;
tcp_hdr = (struct tcphdr*)tcp_hdr_addr;
dst_port = tcp_hdr->dest;
// 经过上面的各种指针偏移,下面打印源IP和目标IP和目标端口.
printf("src addr %s\n", inet_ntoa(src_ip));
printf("dst addr %s\n", inet_ntoa(dst_ip));
printf("dst_port %d\n", ntohs(dst_port));
/* And close the session */
pcap_close(handle);
return(0);
}
5. 总结
本文只是一个非常简单的介绍。如需要进阶学习,Stevens的网络编译有更详细地关于Raw Socket的内容,研究NF_Queue技术则需要读源代码和收集散在社区各个角落的资料了,而讲libpcap有两篇很好的文章:
"Programming with Libpcap – Sniffing the network from our own application "用Libpcap编程,在我们的程序中嗅探网络。非常漂亮的pdf文档。
http://recursos.aldabaknocking.com/libpcapHakin9LuisMartinGarcia.pdf
The Sniffer’s Guide to Raw Traffic 嗅探器教程。斯坦福大学的Geek于大概2001年写的。
http://yuba.stanford.edu/~casado/pcap/section1.html
copyright ykyi.net