原文: http://www.linuxjournal.com/article/4659
如果你的工作涉及网络管理或是安全,或者你仅仅是对你的本地网络传输了什么好奇,从网络上抓取几个数据包会是一个有用的经验。通过一点C代码和网络的基本知识,你甚至可以抓取目的主机不是你的机器的数据。本文,我们会涉及Ethnet,甚至是最广泛使用的LAN技术。同样,我们会假设其源和目的主机属于同一个LAN,其原因稍后解释。
首先,我们会简单回忆一下一个普通的Ethnet网卡如何工作。如果你以了解这部分,那么可以跳过。用户的ip package被封装成 Ethnet frame(当package通过一个Ethnet segment时叫这个名字)。
Ethnet package仅仅是跟大的底层package,它包含源的IP以及一些必须带到目的主机的必要信息(见图一)。特殊的,目的地址会通过一个叫ARP的机制被映射成6 bytes目的Ethnet地址(通常叫做MAC地址)。
所以,frame包含需要通过连接他们的网线从源主机发送到目的主机的package。大致是这样的,frame将会经过hug/switch这类设备,但是因为我们假设只在LAN内,所以不涉及路由/网关。
相应的,我们当我们嗅探网络的package时,将会有两个问题。一个是Ethnet寻址-我们不能读一个目的地址不是我们主机的Ethnet package;另一个与协议栈处理有关-为了使package不被丢弃,我们对于每一个port都应该有一个监听socket。而且packet的部分信息将会在协议栈处理的时候被丢弃。
第一个问题不是根本,因为我们不关心其他主机的packet,并且趋于嗅探心目的地址是我们主机的package第二个问题必须解决。我们稍后将会一个一个地看到这些问题。
The PF_PACKET Protocol
当你用标准的socket调用打开一个socket sock = socket(domain, type, protocol),你必须指明你打算使用哪个domain(或者family)。
常用的family对于在LAN中的主机是PF_UNIX,对于基于IPv4的通信,则是PF_INET。并且,你必须指明你的socket类型,以及一个基于你使用的family的可能值。
常用的值,对于PF_INET,包含SOCK_STREAM(对应于TCP连接),SOCK_DGRAM(对应于UDP)。socke类型的作用是packet在被传到应用层之前,内核应该如何处理。
最后,你声明的协议将会处理经过socket的packet(跟多细节请参考socket的man(3))。
在2.0以后的版本中,引进了一个新的protocol family,叫PF_PACKET。它允许application直接处理网卡驱动接收/发送的packet,这样可以避免网络协议栈的处理。任何经过socket的packet将会直接送到Ethnet interface,任何interface接收的packet将会直接送到application。
PF_PACKET family支持两种socket类型,SOCK_DGRAM和SOCK_RAW。前者把增加/删除Ethnet层的header的任务交给kernel。后者把这个control交给应用层。socket()函数的protocol字段必须符合定义在/usr/include/linux/if_ether.h的Ethnet标识,这些标识标识一个可以处理Ethnet的协议。除非处理一个非常特殊的协议,你可以使用ETH_P_IP,通常他包含所有适合IP的协议(例如TCP,UDP,ICMP,raw IP等)。
因为他们有非常严格的安全含义(例如你可以迫使一个frame带有欺骗性的MAC地址),PF_PACKET只允许root使用。
PF_PACKET family轻易的解决了使用协议栈的问题。通过example 1,我们打开一个属于PF_PACKET family的socket,声明一个SOCK_RAW类型的sock,并且使用IP-related的协议类型。然后我们开始从socket读数据,经过一些检查,我们输出从Ethnet层和IP header中提取的一些信息。通过以图一显示的偏移交叉检查,你将会发现对于应用层来获得network层的数据是多么简单。
#include <stdio.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <linux/in.h> #include <linux/if_ether.h> int main(int argc, char **argv) { int sock, n; char buffer[2048]; unsigned char *iphead, *ethhead; if ( (sock=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)))<0) { perror("socket"); exit(1); } while (1) { printf("----------\n"); n = recvfrom(sock,buffer,2048,0,NULL,NULL); printf("%d bytes read\n",n); /* Check to see if the packet contains at least * complete Ethernet (14), IP (20) and TCP/UDP * (8) headers. */ if (n<42) { perror("recvfrom():"); printf("Incomplete packet (errno is %d)\n", errno); close(sock); exit(0); } ethhead = buffer; printf("Source MAC address: " "%02x:%02x:%02x:%02x:%02x:%02x\n", ethhead[0],ethhead[1],ethhead[2], ethhead[3],ethhead[4],ethhead[5]); printf("Destination MAC address: " "%02x:%02x:%02x:%02x:%02x:%02x\n", ethhead[6],ethhead[7],ethhead[8], ethhead[9],ethhead[10],ethhead[11]); iphead = buffer+14; /* Skip Ethernet header */ if (*iphead==0x45) { /* Double check for IPv4 * and no options present */ printf("Source host %d.%d.%d.%d\n", iphead[12],iphead[13], iphead[14],iphead[15]); printf("Dest host %d.%d.%d.%d\n", iphead[16],iphead[17], iphead[18],iphead[19]); printf("Source,Dest ports %d,%d\n", (iphead[20]<<8)+iphead[21], (iphead[22]<<8)+iphead[23]); printf("Layer-4 protocol %d\n",iphead[9]); } } }假如你的机器连接到Ethnet LAN,当从另一台机器发送packet到你的机器时,你可以测试我们小程序(你可以ping/telnet你的主机)。你可以看到所有发送到你的主机的packet,但是你不能看到转发到其他的host的packet。
Promiscuous vs. Nonpromiscuous Mode
PF_PACKET family运行application接受net card层的数据,但是仍然不允许读不是到该主机的主机。如同我们之前看到的,这是由于network card会丢弃不含该host MAC地址的packet,这叫做(非混乱模式),通常指的是,每一个network card处理与自己相关的packet。但是有三种例外:目的MAC地址是广播地址(FF:FF:FF:FF:FF:FF)的frame将会被接收,目的MAC地址是多播地址的将会被启动接受多播的网卡接收,一个被置成混乱模式(promiscuous mode)的网卡将会接收所有经过它的网卡。
最后一种情况是最有意思的情况。我们可以通过在一个打开的socket调用ioctl来实现。因为这是一个潜在的安全威胁的操作,它只允许root用户操作。加入“sock”是一个已经打开的socket,下列的操作将会实现:
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ); ioctl(sock, SIOCGIFFLAGS, ðreq); ethreq.ifr_flags |= IFF_PROMISC; ioctl(sock, SIOCSIFFLAGS, ðreq);(ethrep是一个在/usr/include/net/if.h中定义的ifreq structure)。
第一个ioctl读取当前Ethnet card的设置;然后将设置“位或”IFF_PROMISC,IFF_PROMISC打开混乱模式(promiscuous),然后通过第二个ioctl写回网卡。
通过ftp://ftp.linuxjournal.com/pub/lj/listings/issue86/看一个更复杂的例子。在一个连接到LAN的机器上编译,运行它,将会看到所有网线上的packet,甚至不是到你的主机的packet。这时因为你的网卡现在是工作在混乱模式(promiscuous)下。你可以通过ifconfig命令的第三行输出确认。
如果你的LAN不是使用hub而是使用Ethnet switchs,你将会只会看到switch分发到你的主机的packet。这是有switch的工作方式决定的,并且你只能做有限的工作(除了MAC地址欺骗,这个不在本文范围)。对于hubs/switch的更多信息,参考Resource section
The Linux Packet Filter
我们的所欲的嗅探的问题都貌似解决了,但是仍有一个重要的问题:如果你尝试实验2中的example,并且你的LAN面临许多traffic(几个发送许多NETBIOS packet的主机就够浪费一些带宽),你将会发现我们的嗅探器输出太多的数据了。随着网络traffic的增加,由于pc不能足够迅速的处理packet,sniffer将会开始丢失packet。
解决这个问题的方法是过滤你接收到的packet,只输出你感兴趣的信息。一个想法就是在sniffer的代码中插入"if statement";这将会过滤掉输入,但是在从性能方面考虑不是非常有效。kernel仍需要向上传送网络中的所有的packet,这样会浪费时间,并且sniffer依旧会检查每个packet来决定是否输出相关数据。
另一个解决方案是在packet处理的过程中尽早的插入filter(他从network驱动层开始并且终止在应用层,图三)。Linux kernel允许我们在PF_PACKET处理协议中插入一个叫过LPF的filter,它在网卡接处理接收数据中断不久后就开始执行。filter决定哪些数据应该发送到application,以及那些数据应该抛弃。
每一个packet上都运行filter的代码,BPF处理的memory空间是packet数据中的bytes。filter的处理结果是一个表明该packet中有多少bytes(如果有)该传达application层的整数。这有一个好处,更多时候,你只关心一个packet的开头几个bytes,并且你可以省下copy其他bytes的处理时间。
(Not) Programming the Filter
即使BPF语言很简单易学,我们大多数更喜欢用可读的filter表达式。所以我们不用BPF语言的指令(这些可以通过上面提到的论文中到找),我们将会讨论如何从一个逻辑表达式中获得一个可用的机器指令。
首先,你得先从LBL安装一个tcpdump程序。但是如果你在读本文,你很可能早就知道并使用tcpdump。tcpdump的第一个版本由提交BPF提议的一些人实现。事实上,tcpdump通过一个叫libpcap的lib使用BPF抓包/过滤packet。该lib是独立于操作系统的。当在Linux中使用,就会使用Linux的packet filter实现BPF函数。
libpcap提供的最有用的函数当中的一个就是pcap_compile(),它接收一个逻辑表达式作为输入,输出BPF filter code。tcpdump使用这个函数把澀输入的命令行表达式转换成BPF filter。对我们来说,有趣的是tcpdump很少使用-d(输出filter的代码)。
例如,“tcpdump host 192.168.9.10”会开始嗅探和抓取源/目的IP地址是192.168.9.10的packet。“tcpdump -d host 192.168.9.10”将会输入组织filter的BPF代码。
Listing 3. Tcpdump -d Results
echer:~# tcpdump -d host 192.168.9.10 (000) ldh [12] (001) jeq #0x800 jt 2 jf 6 (002) ld [26] (003) jeq #0xc0a8090a jt 12 jf 4 (004) ld [30] (005) jeq #0xc0a8090a jt 12 jf 13 (006) jeq #0x806 jt 8 jf 7 (007) jeq #0x8035 jt 8 jf 13 (008) ld [28] (009) jeq #0xc0a8090a jt 12 jf 10 (010) ld [38] (011) jeq #0xc0a8090a jt 12 jf 13 (012) ret #68 (013) ret #0
让我们简要的看看代码:lines 0-1 and 6-7表示通过比较protocol IDs(/usr/include/linux/if_ether.h)与frame中第12个偏移,被抓取的packet实际是TP/ARP/RARP协议。如果比较失败,packet将会丢弃(line 13)。
Lines 2-5和8-11比较源/目的IP地址与192.168.9.10。注意不同的协议中的偏移是不同的:如果是IP,偏移是28/38。如果IP匹配,那么packet将会被filter接收,并且前68bytes将会被传到application(line 12)。
filter code不都是优化后的,因为它是有一个为通用的BPF生成的,并且不为当前运行filter程序的架构优化。LPF的特殊情况,被PF_PACKET处理程序运行的filter,也许已经检查Ethnet protocol了。这取决于你在socket()函数中你使用的protocol类型:如果不是“ETH_P_ALL”(指所有Ethnet frame都应该被抓取),那么只有声明Ethent protocol的packet会到达filter。例如:一个ETH_P_ALL的socket,我们可以重写一个如下的更快更紧凑的filter:
(000) ld [26] (001) jeq #0xc0a8090a jt 4 jf 2 (002) ld [30] (003) jeq #0xc0a8090a jt 4 jf 5 (004) ret #68 (005) ret #0
安装filter
安装filter是一个简单的操作:只需要创建一个包含filter的sock_filter structure,并且将它关联到一个打开的socket上。
filter structure可以通过tcpdump -dd而获得。输出的filter将会如同C中你可以copy&paste的数组,如List4.然后你可以用过setsockopt函数将它关联到一个socket上。
escher:~# tcpdump -dd host 192.168.9.01 { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 4, 0x00000800 }, { 0x20, 0, 0, 0x0000001a }, { 0x15, 8, 0, 0xc0a80901 }, { 0x20, 0, 0, 0x0000001e }, { 0x15, 6, 7, 0xc0a80901 }, { 0x15, 1, 0, 0x00000806 }, { 0x15, 0, 5, 0x00008035 }, { 0x20, 0, 0, 0x0000001c }, { 0x15, 2, 0, 0xc0a80901 }, { 0x20, 0, 0, 0x00000026 }, { 0x15, 0, 1, 0xc0a80901 }, { 0x6, 0, 0, 0x00000044 }, { 0x6, 0, 0, 0x00000000 },
一个复杂的例子:
我们将会通过一个复杂的例子(ftp://ftp.linuxjournal.com/pub/lj/listings/issue86/)来结束本文。他与前两个例子及其相似,除了增加LSFcode并且调用setsockopt。filter被配置成只选择UDP packet,源/目的IP地址为192.168.9.10并且UDP端口等于5000。
为了测试这个例子,你需要一个简易生成随机UDPpacket的办法(例如“sendip”或“apsend” http://freshmeat.net/)。同时,你也许想使其适用于在你的LAN中匹配的IP address。为了实现,把filter code中的0xc0a8090a改成你希望的IP地址的十六进制数形式。
最后值得关注的是当你退出程序时网卡的状态。因为我们没有重置Ethnet flags,网卡将会维持在混乱模式(promiscuous)。为了解决这个问题,你只需要在函数退出前,为Control-C(SIGINT)信号安装一个将Ethnet flag重启为它先前状态(在“位或”之前保存的值)的处理函数。
Conclusions
在LAN中sniffer packet是一个有测试网络问题/收集量度的有用工具。有时候,例如tcpdump/ethereal这类常用工具,不会完全满足你的需求,这时重写sniffer将会起到很大哦帮助。由于LPF,你可以有效方便的完成。
Resources
For further details on Ethernet networks, please refer to http://www.linuxpapers.org/, which contains some articles on networking basics and Ethernet networking.
The BPF language is described in the following paper by Steven McCanne and Van Jacobson: “The BSD Packet Filter: a New Architecture for User-level Packet Capture”, available at http://www-nrg.ee.lbl.gov/nrg.html.
The tcpdump program is available at http://www-nrg.ee.lbl.gov/nrg.html.