原文:
如果你的工作涉及网络管理或是安全,或者你仅仅是对你的本地网络传输了什么好奇,从网络上抓取几个数据包会是一个有用的经验。通过一点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内,所以不涉及路由/网关。
Figure 1. IP Packets as Ethernet Frames
在Ethnet层,没有routing处理。换句话说,源的frame不会直接送到目的主机;而是frame将会copy到所有连接的网线,这时网卡会看到它(见图二)。
每个网卡会读frame的前6个bytes(及目的主机的MAC地址),但是只有自己的MAC地址和package中的MAC地址一致的网卡才会接受frame。
这时,frame将会被网络驱动解析,并且以前的IP package将会被恢复,并且通过网络协议上传到应用层。
Figure 2. Sending Ethernet Frames over the LAN
更准确的说,网络驱动会检查Ethnet frame头部的Protocol Type字段(见Figure 1),并且基于那个值,把package转发到适当的接受函数。大多数时候,接收函数将会把IP头去掉,并且把剩下部分上传给TCP/UDP接受函数。这些协议,相应的,会把它们送到socket处理函数。socket处理函数会最终把package数据送到应用程序。在这段时间,packet会失去所有与网络相关的信息,例如源地址(IP和MAC)以及端口号,ip option,TCP参数等。更多地,如果目的主机没有一个包含正确参数的打开的socket,那么packet将会被丢弃,并且不会上传到应用层。
相应的,我们当我们嗅探网络的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轻易的解决了使用协议栈的问题。通过Listing 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]);
-
}
-
}
-
-
}
Listing 1. Protocol Stack-Handling Sniffed Packets
假如你的机器连接到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写回网卡。
通过Listing 2看一个更复杂的例子。在一个连接到LAN的机器上编译,运行它,将会看到所有网线上的packet,甚至不是到你的主机的packet。这时因为你的网卡现在是工作在混乱模式(promiscuous)下。你可以通过ifconfig命令的第三行输出确认。
如果你的LAN不是使用hub而是使用Ethnet switchs,你将会只会看到switch分发到你的主机的packet。这是有switch的工作方式决定的,并且你只能做有限的工作(除了MAC地址欺骗,这个不在本文范围)。对于hubs/switch的更多信息,参考Resource section
-
#include <stdio.h>
-
#include <string.h>
-
#include <errno.h>
-
#include <unistd.h>
-
#include <sys/socket.h>
-
#include <sys/types.h>
-
#include <linux/in.h>
-
#include <linux/if_ether.h>
-
#include <net/if.h>
-
#include <sys/ioctl.h>
-
-
int main(int argc, char **argv) {
-
int sock, n;
-
char buffer[2048];
-
unsigned char *iphead, *ethhead;
-
struct ifreq ethreq;
-
-
if ( (sock=socket(PF_PACKET, SOCK_RAW,
-
htons(ETH_P_IP)))<0) {
-
perror("socket");
-
exit(1);
-
}
-
-
/* Set the network card in promiscuos mode */
-
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
-
if (ioctl(sock,SIOCGIFFLAGS,ðreq)==-1) {
-
perror("ioctl");
-
close(sock);
-
exit(1);
-
}
-
ethreq.ifr_flags|=IFF_PROMISC;
-
if (ioctl(sock,SIOCSIFFLAGS,ðreq)==-1) {
-
perror("ioctl");
-
close(sock);
-
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]);
-
}
-
}
-
-
}
Listing 2. Needs caption
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驱动层开始并且终止在应用层,Figure 3)。Linux kernel允许我们在PF_PACKET处理协议中插入一个叫过LPF的filter,它在网卡接处理接收数据中断不久后就开始执行。filter决定哪些数据应该发送到application,以及那些数据应该抛弃。
Figure 3. Packet-Processing Chain
为了尽可能的灵活,以及不限制与程序员的一组预定义的条件,packet-filter引擎被作为一个运行在user定义的状态机来实现的。这个程序用一种叫做BPF(Berkeley packet filter)的伪机器指令来实现,基于Steve McCanne和Van Jacobson的一篇论文(参考Resource).BPF看似像有一组寄存器和一些load/store指令计算算术和条件跳转的一种汇编语言。
每一个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代码。
-
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
Listing 3. Tcpdump -d Results
让我们简要的看看代码: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 },
Listing 4. tcpdump with --dd Switch
我们将会通过一个复杂的例子Listing 5来结束本文。他与前两个例子及其相似,除了增加LSFcode并且调用setsockopt。filter被配置成只选择UDP packet,源/目的IP地址为192.168.9.10并且UDP端口等于5000。
为了测试这个例子,你需要一个简易生成随机UDPpacket的办法(例如“sendip”或“apsend” )。同时,你也许想使其适用于在你的LAN中匹配的IP address。为了实现,把filter code中的0xc0a8090a改成你希望的IP地址的十六进制数形式。
最后值得关注的是当你退出程序时网卡的状态。因为我们没有重置Ethnet flags,网卡将会维持在混乱模式(promiscuous)。为了解决这个问题,你只需要在函数退出前,为Control-C(SIGINT)信号安装一个将Ethnet flag重启为它先前状态(在“位或”之前保存的值)的处理函数。
-
#include <stdio.h>
-
#include <string.h>
-
#include <errno.h>
-
#include <unistd.h>
-
#include <sys/socket.h>
-
#include <sys/types.h>
-
#include <linux/in.h>
-
#include <linux/if_ether.h>
-
#include <net/if.h>
-
#include <linux/filter.h>
-
#include <sys/ioctl.h>
-
-
int main(int argc, char **argv) {
-
int sock, n;
-
char buffer[2048];
-
unsigned char *iphead, *ethhead;
-
struct ifreq ethreq;
-
-
/*
-
udp and host 192.168.9.10 and src port 5000
-
(000) ldh [12]
-
(001) jeq #0x800 jt 2 jf 14
-
(002) ldb [23]
-
(003) jeq #0x11 jt 4 jf 14
-
(004) ld [26]
-
(005) jeq #0xc0a8090a jt 8 jf 6
-
(006) ld [30]
-
(007) jeq #0xc0a8090a jt 8 jf 14
-
(008) ldh [20]
-
(009) jset #0x1fff jt 14 jf 10
-
(010) ldxb 4*([14]&0xf)
-
(011) ldh [x + 14]
-
(012) jeq #0x1388 jt 13 jf 14
-
(013) ret #68
-
(014) ret #0
-
*/
-
struct sock_filter BPF_code[]= {
-
{ 0x28, 0, 0, 0x0000000c },
-
{ 0x15, 0, 12, 0x00000800 },
-
{ 0x30, 0, 0, 0x00000017 },
-
{ 0x15, 0, 10, 0x00000011 },
-
{ 0x20, 0, 0, 0x0000001a },
-
{ 0x15, 2, 0, 0xc0a8090a },
-
{ 0x20, 0, 0, 0x0000001e },
-
{ 0x15, 0, 6, 0xc0a8090a },
-
{ 0x28, 0, 0, 0x00000014 },
-
{ 0x45, 4, 0, 0x00001fff },
-
{ 0xb1, 0, 0, 0x0000000e },
-
{ 0x48, 0, 0, 0x0000000e },
-
{ 0x15, 0, 1, 0x00001388 },
-
{ 0x6, 0, 0, 0x00000044 },
-
{ 0x6, 0, 0, 0x00000000 }
-
};
-
struct sock_fprog Filter;
-
-
Filter.len = 15;
-
Filter.filter = BPF_code;
-
-
if ( (sock=socket(PF_PACKET, SOCK_RAW,
-
htons(ETH_P_IP)))<0) {
-
perror("socket");
-
exit(1);
-
}
-
-
/* Set the network card in promiscuos mode */
-
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
-
if (ioctl(sock,SIOCGIFFLAGS,ðreq)==-1) {
-
perror("ioctl");
-
close(sock);
-
exit(1);
-
}
-
ethreq.ifr_flags|=IFF_PROMISC;
-
if (ioctl(sock,SIOCSIFFLAGS,ðreq)==-1) {
-
perror("ioctl");
-
close(sock);
-
exit(1);
-
}
-
-
/* Attach the filter to the socket */
-
if(setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER,
-
&Filter, sizeof(Filter))<0){
-
perror("setsockopt");
-
close(sock);
-
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]);
-
}
-
}
-
-
}
Listing 5. Needs caption
Conclusions
在LAN中sniffer packet是一个有测试网络问题/收集量度的有用工具。有时候,例如tcpdump/ethereal这类常用工具,不会完全满足你的需求,这时重写sniffer将会起到很大哦帮助。由于LPF,你可以有效方便的完成。
Resources
For further details on Ethernet networks, please refer to , 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 .
The tcpdump program is available at .