Linux 套接字过滤器:嗅探网络上的字节
如果您从事网络管理或安全管理工作,或者您仅仅是对本地网络上正在发生的事情感到好奇,那么从网卡上抓取一些数据包可能是一个有用的练习。 凭借一点 C 语言编程和网络基础知识,即使数据包不是发往您的计算机的,您也能够捕获数据。 在本文中,我们将参考以太网网络,这是迄今为止最广泛使用的 LAN 技术。 此外,出于稍后将解释的原因,我们将假设源主机和目标主机属于同一 LAN。
首先,我们将简要回顾一下常见的以太网网卡是如何工作的。 那些已经熟悉这个领域的人可以安全地跳到下一段。 来自用户应用程序的 IP 数据包被封装到以太网帧中(这是数据包在以太网段上发送时的名称),以太网帧只是更大的底层数据包,其中包含原始 IP 数据包和将其传输到目标地址所需的一些信息(参见图 1)。 特别是,目标 IP 地址通过称为 ARP 的机制映射到 6 字节的目标以太网地址(通常称为 MAC 地址)。 因此,包含数据包的帧通过连接它们的电缆从源主机传输到目标主机。 帧很可能会经过诸如集线器和交换机之类的网络设备,但是由于我们假设没有跨越 LAN 边界,因此不会涉及路由器或网关。
在以太网级别不会发生路由过程。 换句话说,源主机发送的帧不会直接发往目标主机; 相反,帧将被复制到构成 LAN 的所有电缆上,并且所有网卡都将看到它通过(参见图 2)。 每个网卡将开始读取帧的前六个字节(其中恰好包含上述目标 MAC 地址),但是只有一张网卡会识别目标字段中自己的地址并拾取该帧。 此时,帧将被网络驱动程序分解,原始 IP 数据包将被恢复并通过网络协议栈传递到接收应用程序。

图 2. 通过 LAN 发送以太网帧
更准确地说,网络驱动程序将查看以太网帧头中的协议类型字段(参见图 1),并根据该值,将数据包转发到适当的协议接收功能。 大多数情况下,协议将是 IP,接收功能将删除 IP 报头并将有效负载传递给 UDP 或 TCP 接收功能。 这些协议反过来会将其传递给套接字处理功能,套接字处理功能最终会将数据包数据传递到用户空间的接收应用程序。 在此过程中,数据包会丢失与其相关的所有网络信息,例如源地址(IP 和 MAC)和端口、IP 选项、TCP 参数等等。 此外,如果目标主机没有使用正确参数打开的套接字,则数据包将被丢弃,并且永远不会到达应用程序级别。
因此,我们在嗅探网络上的数据包时有两个不同的问题。 一个与以太网寻址有关——我们无法读取不是发往我们主机的数据包; 另一个与协议栈处理有关——为了使数据包不被丢弃,我们应该为每个端口都有一个侦听套接字。 此外,部分数据包信息在协议栈处理期间丢失。
第一个问题不是根本性的,因为我们可能对其他主机的包不感兴趣,并且可能倾向于嗅探所有发往我们计算机的包。 然而,第二个问题必须解决。 我们将看到如何分别解决这些问题,从后者开始。
当您使用标准调用 sock = socket(domain, type, protocol) 打开套接字时,您必须指定要与该套接字一起使用的域(或协议族)。 常用的族是 PF_UNIX(用于绑定在本地计算机上的通信)和 PF_INET(用于基于 IPv4 协议的通信)。 此外,您必须为套接字指定类型,可能的值取决于您指定的族。 处理 PF_INET 族时,类型的常用值包括 SOCK_STREAM(通常与 TCP 关联)和 SOCK_DGRAM(与 UDP 关联)。 套接字类型会影响数据包在传递到应用程序之前内核如何处理数据包。 最后,您指定将处理流经套接字的数据包的协议(有关更多详细信息,请参见 socket(3) 手册页)。
在最新版本的 Linux 内核(2.0 版本之后)中,引入了一个新的协议族,名为 PF_PACKET。 该族允许应用程序直接与网卡驱动程序发送和接收数据包,从而避免了通常的协议栈处理(例如,IP/TCP 或 IP/UDP 处理)。 也就是说,通过套接字发送的任何数据包都将直接传递到以太网接口,并且通过接口接收的任何数据包都将直接传递到应用程序。
PF_PACKET 族支持两种略有不同的套接字类型:SOCK_DGRAM 和 SOCK_RAW。 前者将添加和删除以太网级别报头的负担留给内核。 后者使应用程序可以完全控制以太网报头。 socket() 调用中的协议字段必须与 /usr/include/linux/if_ether.h 中定义的以太网 ID 之一匹配,该 ID 表示可以在以太网帧中传送的已注册协议。 除非处理非常特定的协议,否则您通常使用 ETH_P_IP,它包含所有 IP 套件协议(例如,TCP、UDP、ICMP、原始 IP 等)。
由于它们具有非常严重的安全隐患(例如,您可以使用欺骗性 MAC 地址伪造帧),因此 PF_PACKET 族套接字只能由 root 用户使用。
PF_PACKET 族轻松解决了与我们嗅探到的数据包的协议栈处理相关的问题。 让我们通过清单 1 中的示例来看一下它是如何做到的。 我们打开一个属于 PF_PACKET 族的套接字,指定 SOCK_RAW 套接字类型和 IP 相关协议类型。 然后我们开始从套接字读取,并在进行一些健全性检查后,我们打印出从以太网级别和 IP 级别报头中提取的一些信息。 通过将打印的地址与图 1 中的偏移量进行交叉检查,您将看到应用程序访问网络级别数据是多么容易。
假设您的计算机已连接到以太网 LAN,您可以通过运行我们的简短示例进行实验,同时从另一台计算机生成定向到您主机的包(您可以 ping 或 Telnet 到您的主机)。 您将能够看到所有发往您的数据包,但您不会看到任何发往其他主机的数据包。
PF_PACKET 族允许应用程序检索在网卡级别接收到的数据包,但仍然不允许它读取未寻址到其主机的数据包。 正如我们之前看到的,这是由于网卡丢弃了所有不包含其自身 MAC 地址的数据包——一种称为非混杂的操作模式,这基本上意味着每个网卡都在关注自己的业务,并且仅读取定向到它的帧。 此规则有三个例外:目标 MAC 地址是特殊广播地址 (FF:FF:FF:FF:FF:FF) 的帧将被任何网卡拾取; 目标 MAC 地址是多播地址的帧将被启用多播接收的网卡拾取,并且已设置为混杂模式的网卡将拾取其看到的所有数据包。
当然,最后一种情况对于我们的目的是最有趣的。 要将网卡设置为混杂模式,我们所要做的就是向该网卡上的打开套接字发出特定的 ioctl() 调用。 由于这是一种潜在的威胁安全的操作,因此该调用仅允许 root 用户使用。 假设“sock”包含一个已打开的套接字,则以下指令将完成这项工作
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ); ioctl(sock, SIOCGIFFLAGS, ðreq); ethreq.ifr_flags |= IFF_PROMISC; ioctl(sock, SIOCSIFFLAGS, ðreq);
(其中 ethreq 是 ifreq 结构,在 /usr/include/net/if.h 中定义)。 第一个 ioctl 读取以太网卡标志的当前值; 然后将标志与 IFF_PROMISC 进行 OR 运算,这会启用混杂模式,并通过第二个 ioctl 写回卡。
让我们在一个更完整的示例中看一下它(参见清单 2,网址为 ftp://ftp.linuxjournal.com/pub/lj/listings/issue86/)。 如果您在连接到 LAN 的计算机上以 root 身份编译并运行它,您将能够看到电缆上流动的所有数据包,即使它们不是发往您的主机的。 这是因为您的网卡现在以混杂模式工作。 您可以通过给出 ifconfig 命令并观察输出中的第三行来轻松检查它。
请注意,如果您的 LAN 使用以太网交换机而不是集线器,您将仅看到在您所属的交换机分支中流动的数据包。 这是由于交换机的工作方式造成的,您对此几乎无能为力(除了使用 MAC 地址欺骗来欺骗交换机,这超出了本文的范围)。 有关集线器和交换机的更多信息,请查看“资源”部分中引用的文章。
我们所有的嗅探问题现在似乎都已解决,但是仍然有一件重要的事情需要考虑:如果您实际上尝试了清单 2 中的示例,并且如果您的 LAN 甚至提供适度的流量(几个 Windows 主机就足以浪费一些带宽和大量的 NETBIOS 数据包),您会注意到我们的嗅探器打印出太多数据。 随着网络流量的增加,嗅探器将开始丢失数据包,因为 PC 将无法足够快地处理它们。
解决此问题的方法是过滤您接收到的数据包,并且仅打印出您感兴趣的数据包的信息。 一种想法是在嗅探器的源代码中插入“if 语句”; 这将有助于改进嗅探器的输出,但是就性能而言,效率不高。 内核仍然会提取网络上流动的所有数据包,从而浪费处理时间,并且嗅探器仍然会检查每个数据包报头以决定是否打印出相关数据。
解决此问题的最佳方案是将过滤器尽可能早地放置在数据包处理链中(它从网络驱动程序级别开始,到应用程序级别结束,请参见图 3)。 Linux 内核允许我们将一个名为 LPF 的过滤器直接放在 PF_PACKET 协议处理例程中,该例程在网卡接收中断服务后不久运行。 过滤器决定哪些数据包应中继到应用程序,哪些数据包应丢弃。

图 3. 数据包处理链
为了尽可能灵活,并且不将程序员限制为一组预定义的条件,数据包过滤引擎实际上被实现为运行用户定义程序的有限状态机。 该程序是用一种称为 BPF(Berkeley 数据包过滤器)的特定伪机器代码语言编写的,其灵感来自于 Steve McCanne 和 Van Jacobson 撰写的一篇旧论文(请参阅“资源”)。 BPF 实际上看起来像一种真实的汇编语言,具有几个寄存器和一些用于加载和存储值、执行算术运算和有条件分支的指令。
过滤器代码在要检查的每个数据包上运行,BPF 处理器在其中运行的内存空间是包含数据包数据的字节。 过滤器的结果是一个整数,用于指定套接字应将数据包的多少字节(如果有)传递到应用程序级别。 这是一个额外的优势,因为通常您只对数据包的前几个字节感兴趣,并且可以通过避免复制多余的字节来节省处理时间。
即使 BPF 语言非常简单易学,我们大多数人可能更喜欢用人类可读的表达式编写的过滤器。 因此,我们将讨论如何从逻辑表达式开始获取工作过滤器的代码,而不是介绍 BPF 语言的细节和指令(您可以在上述论文中找到)。
首先,您需要从 LBL 安装 tcpdump 程序(请参阅“资源”)。 但是,如果您正在阅读本文,则很可能您已经知道并使用 tcpdump。 最初的版本是由编写 BPF 论文及其第一个实现的人员编写的。 实际上,tcpdump 使用 BPF(以名为 libpcap 的库的形式)来捕获和过滤数据包。 该库是 BPF 引擎的独立于操作系统的包装器。 在 Linux 机器上使用时,BPF 功能由 Linux 数据包过滤器执行。
libpcap 提供的最有用的功能之一是 pcap_compile(),它将包含逻辑表达式的字符串作为输入,并输出 BPF 过滤器代码。 tcpdump 使用此功能将用户传递的命令行表达式转换为工作 BPF 过滤器。 对于我们的目的而言有趣的是,tcpdump 有一个很少使用的开关 -d,它可以打印过滤器的代码。
例如,键入 tcpdump host 192.168.9.10 将开始嗅探,并且仅抓取源或目标 IP 地址与 192.168.9.10 匹配的数据包。 键入 tcpdump -d host 192.168.9.10 将打印识别过滤器的 BPF 代码,如清单 3 所示。
让我们简要评论一下这段代码; 第 0-1 行和第 6-7 行通过将它们的协议 ID(参见 /usr/include/linux/if_ether.h)与在帧的偏移量 12 处找到的值(参见图 1)进行比较,来验证捕获的帧实际上是否正在传输 IP、ARP 或 RARP 协议。 如果测试失败,则丢弃数据包(第 13 行)。
第 2-5 行和第 8-11 行将源 IP 地址和目标 IP 地址与 192.168.9.10 进行比较。 请注意,根据协议的不同,这些地址的偏移量也不同; 如果协议是 IP,则偏移量为 26 和 30,否则为 28 和 38。 如果其中一个地址匹配,则过滤器接受数据包,并将前 68 个字节传递给应用程序(第 12 行)。
过滤器代码并不总是最优化的,因为它是为通用 BPF 机器生成的,而不是为运行过滤器引擎的特定体系结构量身定制的。 在 LPF 的特定情况下,过滤器由 PF_PACKET 处理例程运行,这些例程可能已经检查了以太网协议。 这取决于您在初始 socket() 调用中指定的协议字段:如果不是 ETH_P_ALL(这意味着应捕获每个以太网帧),则只有具有指定以太网协议的帧才会到达过滤器。 例如,在 ETH_P_IP 套接字的情况下,我们可以按如下方式重写更快、更紧凑的过滤器
(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
安装 LPF 是一项简单的操作:您所要做的就是创建一个包含过滤器的 sock_filter 结构,并将其附加到打开的套接字。
通过将 tcpdump 的 -d 开关替换为 -dd,可以轻松获得过滤器结构。 过滤器将以 C 数组的形式打印出来,您可以将其复制并粘贴到您的代码中,如清单 4 所示。 之后,您只需发出 setsockopt() 调用即可将过滤器附加到套接字。
我们将以一个完整的示例结束本文(参见清单 5,网址为 ftp://ftp.linuxjournal.com/pub/lj/listings/issue86/)。 它与前两个示例完全相同,只是添加了 LSF 代码和 setsockopt() 调用。 过滤器已配置为仅选择 UDP 数据包,其源 IP 地址或目标 IP 地址为 192.168.9.10,源 UDP 端口等于 5000。
为了测试此清单,您将需要一种生成任意 UDP 数据包的简单方法(例如 sendip 或 apsend,可在 http://freshmeat.net/ 上找到)。 此外,您可能希望调整 IP 地址以匹配您自己的 LAN 中使用的地址。 为此,只需将过滤器代码中的 0xc0a8090a 替换为您选择的 IP 地址的十六进制表示法。
最后一点说明是关于您退出程序时以太网卡的状态。 由于我们没有重置以太网标志,因此该卡将保持混杂模式。 要解决此问题,您只需安装一个 Control-C (SIGINT) 信号处理程序,该处理程序将在退出程序之前将以太网标志重置为其先前的值(您将在与 IFF_PROMISC 进行 OR 运算之前保存该值)。
在您的 LAN 上嗅探数据包是调试网络问题或收集测量数据的宝贵工具。 有时,常用的工具(例如 tcpdump 或 ethereal)可能无法完全满足您的需求,而编写自己的嗅探器可能会有很大帮助。 感谢 LPF,您可以用简单有效的方式做到这一点。
