65.9K
CodeProject 正在变化。 阅读更多。
Home

逐步开发 SOHO 网络过滤器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (22投票s)

2007年8月6日

CPOL

22分钟阅读

viewsIcon

214073

downloadIcon

4974

本文的主要目标是解释低级别网络安全编程的实际细节。

Screenshot - screenshot.gif

Screenshot - blocked.jpg

1. 摘要

大多数网页过滤器都在线工作,这意味着所有传出和传入的数据包都通过过滤器驱动程序。虽然这种方法有其优点,但它有一个很大的缺点:过滤过程会影响数据传输吞吐量。本项目通过将过滤引擎置于嗅探器模式来实验性地解决这个问题。这样,过滤过程和数据传输就可以独立进行。

2. 要求

  • 本文要求读者熟悉 C++ 和 TCP/IP 概念。
  • 源代码使用以下库:
    • Winsock。
    • Ethereal:一个数据包捕获和网络分析器。
  • 代码在 Windows Server 2003 上用 VC7 编译构建。

3. 引言

本文的主要目标是解释低级网络编程的实际细节。在 Linux 和 Windows 上都有许多商业和开源的防火墙和 HTTP 过滤器。但是,在内部,它们大多数都遵循相同的方法来查找其目标。本文的参考资料部分提供了与此主题相关的有用书籍。

特别是,网页过滤器可以以两种模式检查传出数据包中的黑名单关键字:内联模式和嗅探器模式。我们还将解释这两种操作模式并进行比较。

4. 背景

本节解释 TCP/IP 协议栈、HTTP 协议行为以及用于快速模式匹配的 Boyer-Moore 算法。如果您认为自己有足够的经验和知识并希望亲自动手,请继续阅读第 7 节(实现)。

一个任务关键型系统与办公应用程序完全不同。想象一下,您的团队计划开发一个防火墙、网页过滤器,甚至一个安全代理服务器,它可以在几秒钟内处理大量数据包。那么,这些系统的主要特征是什么?

安全可靠、高性能、功能齐全、绿色环保,这些都是其中的几个。我所说的绿色环保,是指系统不应耗尽托管平台的 CPU 和内存。同时,在这样一个昂贵的项目中,由于缺乏技术知识而导致逻辑错误是没有空间的!在典型的环境中,您的最终结果必须按以下模型部署:

Screenshot - HttpFilter.jpg

4.1 TCP/IP 协议栈

顾名思义,TCP/IP 指的是许多协议,每个协议都是为了特定目的而开发的。为了理解 HTTP 会话建立过程,我们至少应该了解以下协议:以太网 II、ARP、IP、TCP、UDP、HTTP 和 DNS。

4.2 以太网 II

以太网 II 帧格式由 Digital、Intel 和 Xerox 在 IEEE 802.3 规范之前创建的以太网规范定义。以太网 II 帧格式也称为 DIX 帧格式。

以太网 II 由以下字段组成(总共 26 字节 + 46 字节到 1500 字节的有效载荷):

  • 前导码(8 字节),由 7 字节交替的 1 和 0(每个字节是位序列 10101010)组成,用于同步接收站,以及一个 1 字节的 10101011 序列,表示帧的开始。前导码提供接收器同步和帧定界服务。
  • 目标地址(6 字节)表示目标地址。目标可以是单播、多播或以太网广播地址。单播地址也称为个人、物理、硬件或 MAC 地址。对于以太网广播地址,所有 48 位都设置为 1,以创建地址 0xFF-FF-FF-FF-FF-FF。
  • 源地址(6 字节)指示发送节点的单播地址。
  • EtherType(2 字节)指示以太网帧中包含的上层协议。对于 IP 数据报,该字段设置为 0x0800。对于 ARP 消息,EtherType 字段设置为 0x0806。有关完整列表,请参阅参考资料部分。
  • Payload 由上层协议的数据单元 (PDU) 组成。以太网 II 可以发送最大 1500 字节的有效负载。由于以太网的冲突检测机制,以太网 II 帧必须发送最小 46 字节的有效负载。
  • FCS(4 字节)使用 CRC 算法对以太网 II 帧中的位进行位级完整性验证。

典型的捕获会显示以下字段(FCS 和前导码被排除):

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    | destination   | source       | protocol |                   |
    | mac address   | mac address  | type     | IP DATAGRAM       |
    | (6 bytes)     | (6 bytes)    | 0X0800   |                   |
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

4.3 ARP

ARP 是一种基于广播的请求-回复协议,提供动态地址解析功能,用于将下一跳 IP 地址映射到其相应的 MAC 地址。

关于数据链路层有两个事实表明我们需要 ARP:

  1. 当一个以太网帧从局域网上的一个主机发送到另一个主机时,是 48 位以太网地址决定了该帧的目的接口。设备驱动程序软件从不查看 IP 数据报中的目的 IP 地址。
  2. 下一跳 IP 地址不一定与 IP 数据报的目的 IP 地址相同。每个传出 IP 数据报的路由确定过程的结果是一个下一跳接口和一个下一跳 IP 地址。对于直接交付到同一子网上的目的地,下一跳 IP 地址是数据报的目的 IP 地址。对于间接交付到远程目的地,下一跳 IP 地址是与转发主机位于同一子网上的路由器的 IP 地址。要将该设备作为下一跳,数据包需要其硬件地址。

4.4 IP

IP 是 TCP/IP 协议套件的核心,提供无连接、不可靠的数据传输。不可靠是指不能保证数据报成功到达目的地。无连接是指 IP 不维护有关连续数据报的任何信息。另一方面,每个数据报都是独立处理的。IP 尽力将数据包传递到下一跳或最终目的地。端到端可靠性是上层协议(如 TCP)的责任。

IP 头包含以下字段,我们稍后需要了解它们以进行数据包处理:

  • 版本(4 字节):指示 Internet 头的格式。
  • IHL(4 字节):IP 头的长度,以 32 位字为单位。
  • 服务类型或 TOS(1 字节)

顾名思义,它指定了 IP 数据包对我们来说有多重要。在负载较高的情况下,一些中间设备会评估此字段并对数据报进行优先级排序。在 RFC 791 (Internet Protocol) 中,此字段的结构如下:

     0     1     2     3     4     5     6     7

      +-----+-----+-----+-----+-----+-----+-----+-----+
      |                 |     |     |     |     |     |
      |   PRECEDENCE    |  D  |  T  |  R  |  0  |  0  |
      |                 |     |     |     |     |     |
      +-----+-----+-----+-----+-----+-----+-----+-----+
  D >> Delay
  T    >> Throughput
  R    >> Reliability
  Bit6 and Bit7    >> reserved

标头 + 有效载荷的总大小。从概念上讲,总长度字段允许数据报的长度最大为 65,535 字节,尽管对于大多数主机和网络设备来说,如此长的分组是不切实际的。稍后,我们将研究 MTU 是什么以及它如何帮助我们实现这一点。

无论如何,请记住 IP 头只有 20 个字节,如果有任何选项,长度可以高达 60 个字节。不能再多了!

由发送方分配,以便接收方可以根据 MTU 值递减分片的 IP 数据包。

我第二次提到“MTU”,所以让我们看看 MTU 是什么。例如,你在浏览网页时生成的大多数数据都是大块数据。这意味着数据的大小很大。底层媒体访问协议将大块数据分割成更小的部分,以便它们可以无缝地通过网络基础设施发送。在 HTTP 802.3 以太网协议的情况下,数据报的最大大小是 1500 字节。这个数字称为 MTU,代表最大传输单元。如果你想将 15000 字节的数据传输给你的朋友,协议栈会将你的消息分成 10 * 1500 字节,并逐个传输。如果我们为 IP 头留出 20 字节,那么传输层头和有效载荷还剩下 1480 字节。这就是 ID 登场的地方。协议栈将消息分成 10 个较小的消息,并在 IP 头中分配一个唯一的 ID。当接收方收到所有片段时,它可以对整个消息进行进一步处理。

表示数据报是否为分片数据的一部分。

         0   1   2
        +---+---+---+
        |   | D | M |
        | 0 | F | F |
        +---+---+---+

一个 8 字节的数据块称为一个分段块。分段偏移字段中的数字报告以分段块为单位的偏移量大小。分段偏移字段为 13 位长,因此偏移量可以从 0 到 8191 个分段块——对应于从 0 到 65,528 字节的偏移量。

此字段表示数据报在网络系统中可以存活多久。它以秒为单位测量时间。

指示上层协议类型。例如:

1 >> ICMP
2 >> GIMP
4 >> IP in IP encapsulation
6 >> TCP
17 >> ADP
41 >> IPV6
47 >> Generic routing encapsulation (ARE)
50 >> IP security encapsulation security payload (ESP)
51 >> IP security authentication header (AH)
89 >> ASP

为了衡量报头的完整性,协议栈处理程序会对报头执行 CRC,并将其与校验和值进行比较。这是一种健全性检查。

维护数据报的可选信息列表。

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
  • 总长度(2 字节)
  • ID(2 字节)
  • 标志(3 位)
  • 片段偏移(13 位)
  • TTL(1 字节)
  • 协议(1 字节)
  • 报头校验和(2 字节)
  • 源 IP 地址和目的 IP 地址(各 4 字节)。
  • 选项(可变长度)

4.5 TCP

TCP 是一种完全成型的传输层协议,它提供可靠的数据传输服务和一种将 TCP 封装数据传递给应用层协议的方法。TCP 具有以下特性:

  • 面向连接:在发送或接收任何数据之前,数据传输双方必须使用 TCP 三次握手过程协商 TCP 连接。TCP 连接通过优雅的 TCP 四次终止过程终止。
  • 全双工:通过一个管道,数据可以同时发送和接收。TCP 完美地完成了这项工作,这得益于其头部中的序列号和确认号字段。
  • 可靠:在 TCP 连接上发送的数据是按顺序的,并且期望从接收方收到肯定确认。如果未收到确认,则重新传输段。
  • 流量控制:为了避免一次发送太多数据并堵塞 IP 互联网的路由器,TCP 实现了发送方流量控制,逐渐调整一次发送的数据量。为了避免发送方发送接收方无法缓冲的数据,TCP 实现了接收方流量控制,指示接收方缓冲区中剩余的空间量。
  • 应用层数据分段:TCP 将从应用层进程获得的数据分段,使其能够适应网络接口层链路上发送的 IP 数据报。TCP 对等方交换彼此能接收的最大分段大小,并通过路径最大传输单元 (PMTU) 发现调整 TCP 最大分段大小。
  • 一对一数据传输:TCP 连接是两个应用层协议之间的逻辑点对点电路。TCP 不提供一对多数据传输服务。

下图显示了封装在 IP 数据报中的 TCP 段:

<--------------- IP datagram ------------------>
++++++++++++++++++++++++++++++++++++++++++++++++
|   IP    |  TCP    |          TCP             |
| Header  | Header  |         Data             |
++++++++++++++++++++++++++++++++++++++++++++++++
20 bytes  20 bytes
At-least  At-least

TCP 报头

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

为了计算 TCP 校验和,我们需要考虑一个 TCP 伪报头。TCP 伪报头用于将 TCP 段与 IP 报头关联起来。TCP 伪报头仅在校验和计算期间添加到 TCP 段的开头,不作为 TCP 段的一部分发送。使用 TCP 伪报头可确保接收方路由或分片过程未不当地修改 IP 报头中的关键字段。

+--------+--------+--------+--------+
|           Source Address          |
+--------+--------+--------+--------+
|         Destination Address       |
+--------+--------+--------+--------+
|  zero  |PROTOCOL|    TCP Length   |
+--------+--------+--------+--------+
  • 源端口(2 字节):源端口号。
  • 目的端口(2 字节):目的端口号。
  • 序列号(4 字节):指示段的第一个八位字节的出站字节流式序列号。
  • 确认号(4 字节):如果 ACK 控制位已设置,则此字段指示接收方期望接收的传入字节流中下一个八位字节的序列号。
  • 数据偏移(4 位):指示 TCP 段数据从何处开始。数据偏移字段也是 TCP 报头的长度。与 IP 报头的报头长度字段一样,数据偏移字段是 TCP 报头中 32 位字(4 字节块)的数量。对于最小的 TCP 报头(无选项),数据偏移字段设置为 5 (0x5),表示段数据从 TCP 段开始的第 20 个八位字节偏移处开始(偏移量从 0 开始计数)。如果数据偏移字段设置为其最大值 15 (0xF),则包含 TCP 选项的最大 TCP 报头可以长达 60 字节。
  • 保留(6 位):保留供将来使用。必须为零。
  • 控制位(从左到右 6 位)
    • URG:紧急指针字段有效
    • ACK:确认字段有效
    • PSH:推送功能
    • RST:重置连接
    • SYN:同步序列号
    • FIN:发送方不再发送数据
  • 窗口(2 字节):发送此段的发送方愿意接受的数据八位字节数,从确认字段中指示的那个开始。
  • 校验和(2 字节):校验和字段是报头和文本中所有 16 位字的一的补码和的一的补码。在计算校验和时,校验和字段本身被替换为零。
  • 紧急指针(2 字节):此字段将紧急指针的当前值作为此分段中序列号的正偏移量进行通信。紧急指针指向紧急数据之后的八位字节的序列号。此字段仅在设置了 URG 控制位的分段中进行解释。
  • 选项(可变):选项可以占用 TCP 报头末尾的空间,并且长度是 8 位的倍数。
  • 填充(可变):TCP 报头填充用于确保 TCP 报头结束和数据开始于 32 位边界。填充由零组成。

4.6 UDP

UDP 是一种简单的、面向数据报的传输层协议:进程的每个输出操作都精确地生成一个 UDP 数据报,从而导致发送一个 IP 数据报。这与面向流的协议(如 TCP)不同,在 TCP 中,应用程序写入的数据量可能与单个 IP 数据报中实际发送的数据量关系不大。

+--------+--------+--------+--------+  ----
|     Source   2  | Destination   2 |    |
|      Port  bytes|    Port    bytes|    |
+--------+--------+--------+--------+  8 bytes
|     2 bytes     |     2 bytes     |    |
|     Length      |    Checksum     |    |
+--------+--------+--------+--------+  ----
|                                   |  
|          data octets              |
+-----------------------------------+

4.7 DNS

域名系统 (DNS) 是一个分布式数据库,TCP/IP 应用程序使用它在主机名和 IP 地址之间进行映射,并提供电子邮件路由信息。我们使用“分布式”一词是因为互联网上没有一个单一站点知道所有信息。每个站点(例如,大学系、校园、公司或公司内的部门)维护自己的信息数据库,并运行一个服务器程序,互联网上的其他系统(客户端)可以查询该程序。DNS 提供了允许客户端和服务器相互通信的协议。

从应用程序的角度来看,对 DNS 的访问是通过解析器进行的。在 Windows 和 Unix 主机上,解析器主要通过两个库函数 gethostbyname()gethostbyaddr() 进行访问,这些函数在应用程序构建时与应用程序链接。第一个函数接受一个主机名并返回一个 IP 地址,第二个函数接受一个 IP 地址并查找一个主机名。解析器会联系一个或多个名称服务器来完成映射。

请注意,DNS 是一种应用层协议,它是在 UDP 传输协议之上实现的。

4.8 HTTP

简单来说,HTTP 是万维网背后的协议。每次网络事务都会调用 HTTP。

HTTP 头部

HTTP 有四种类型的头部:

  • 通用头部指示通用信息,例如日期,或者是否应保持连接。它们由客户端和服务器共同使用。
  • 请求头仅用于客户端请求。它们将客户端的配置和所需文档格式传达给服务器。
  • 响应头仅用于服务器响应。它们描述了服务器的配置以及有关请求 URL 的信息。
  • 实体头描述了客户端和服务器之间发送的数据的文档格式。

尽管实体头部最常被服务器在返回请求文档时使用,但客户端在使用 POST 或 PUT 方法时也会使用它们。

客户请求

  • GET 用于检索服务器上的资源。
  • HEAD 用于检索有关文档的一些信息,但不需要文档本身。
  • POST 表示您正在提供一些自己的信息(即在表单中)。这可能会以某种方式改变服务器的状态,例如在数据库中创建一条记录。
  • PUT 用于替换或在服务器上创建新文档。
  • DELETE 用于删除服务器上的文档。
  • TRACE 用于协议调试。
  • OPTIONS 用于客户端查找可在文档上使用的其他方法。
  • CONNECT 用于客户端需要通过代理服务器与 HTTPS 服务器通信时。

您可能会看到(LINK、UNLINK 和 PATCH)的其他 HTTP 方法定义不太明确。

对于本文的读者,只需理解 GET 方法即可。这是用于文档检索的主要方法。GET 请求的响应可以通过多种方式由服务器生成。

客户端在请求中使用 GET 方法后,服务器会以状态行、头部和客户端请求的数据进行响应。如果服务器由于错误或缺乏授权而无法处理请求,服务器通常会在响应的实体主体中发送解释。

例如

一个客户请求

GET / HTTP/1.1\r\n
Accept: */*\r\n
Referer: http://www.google.com\r\n
Accept-Language: en-us\r\n
UA-CPU: x86\r\n
Accept-Encoding: gzip, deflate\r\n
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; 
                         .NET CLR 1.1.4322)\r\n
Host: www.google.com\r\n
Connection: Keep-Alive\r\n
Cache-Control: no-cache\r\n
Cookie: PREF=ID=76325601198c7def:LD=en:CR=2:TM=1184565890:
              LM=1186242386:S=ofiC-IBOMCVySJCP\r\n
\r\n

服务器响应

HTTP/1.0 200 OK\r\n
Cache-Control: private\r\n
Content-Type: text/html; charset=UTF-8\r\n
Content-Encoding: gzip\r\n
Server: GWS/2.1\r\n
Content-Length: 2976\r\n
Date: Sat, 04 Aug 2007 15:46:45 GMT\r\n
X-Cache: MISS from netcache-1\r\n
Connection: keep-alive\r\n
\r\n

4.9 使用 Boyer-Moore 算法执行模式搜索

Boyer-Moore 方法非常有趣。它不需要实际检查要搜索字符串的每个字符,而是跳过其中一些字符。假设我们想在类似于 HTTP://WWW.WEBSERVER.COM/ROTTEN HTTP/1.1 的字符串中查找单词“ROTTEN”。使用 Boyer-Moore 算法,我们有以下步骤:

HTTP://WWW.WEBSERVER.COM/ROTTEN HTTP/1.1

ROTTEN
  • 首先,我们获取“/”,因为它与模式的最后一个字符位于相同位置。
  • 模式中没有“/”。所以搜索失败。
  • 将模式向右移动 6 个字符。所以 'N' 位于 'W' 下方。匹配失败,因为也没有 'W'。
  • 将模式向右移动 6 个字符。因此,“N”位于“V”下方。匹配失败,因为也没有“V”。同样,对于“M”,也会出现类似的情况。
  • 将模式向右移动 6 个字符。所以 'N' 位于 'E' 下方。找到了匹配项。模式中有一个 'E'。所以,将模式向右移动一个位置,使两个 'E' 对齐。然后,从右到左比较,直到找到精确匹配。

这正是我们想要的。我将 Boyer-Moore 与其他不区分大小写的模式匹配方法进行了比较。当我使用 Boyer-Moore 时,结果效率高得多。有关 Boyer-Moore 模式匹配算法的更多信息,请在 Google 上搜索该主题。

5. HTTP 请求的生命周期

在本节中,我们将描述直到从 Internet 下载页面所发生的事情。假设您打开您最喜欢的 Web 浏览器,并在地址栏中键入 http://www.google.com。请再次记住我们的网络模型。

请运行 Ethereal,选择您的出站接口,然后开始捕获。您可以在 Ethereal 中查看以下所有信息。

“三次握手”是用于建立连接的过程。此过程通常由一个 TCP 发起,并由另一个 TCP 响应。

下图显示了我们示例中的确切过程:

      You                                                         web server

1.  CLOSED                                                       LISTEN

2.  SYN-SENT    --> <SEQ=ISN1><CTL=SYN><ACK=0>               --> SYN-RECEIVED

3.  ESTABLISHED <-- <SEQ=ISN2><ACK=ISN1+1><CTL=SYN,ACK>      <-- SYN-RECEIVED

4.  ESTABLISHED --> <SEQ=ISN1+1><ACK=ISN2+1><CTL=ACK>        --> ESTABLISHED

5.  ESTABLISHED --> <SEQ=ISN1+1><ACK=ISN2+1><CTL=ACK><DATA>  --> ESTABLISHED

在图中的第 2 行,您开始发送一个 SYN 段,表示您将使用从序列号 ISN1 开始的序列号。在第 3 行,服务器发送一个 SYN 并确认它从您那里收到的 SYN。请注意,确认字段指示服务器现在期望收到序列 ISN1+1,确认占据序列 ISN1SYN。在第 4 行,您用一个包含服务器 SYNACK 的空段进行响应。

在第 5 行,您发送一些数据。请注意,第 5 行中段的序列号与第 4 行中相同,因为 ACK 不占用序列号空间。因此,<DATA> 包含以下语句:

GET / HTTP/1.1\r\n
Accept: */*\r\n
Referer: http://www.google.com\r\n
Accept-Language: en-us\r\n
UA-CPU: x86\r\n
Accept-Encoding: gzip, deflate\r\n
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; 
                         .NET CLR 1.1.4322)\r\n
Host: www.google.com\r\n
Connection: Keep-Alive\r\n
Cache-Control: no-cache\r\n
Cookie: PREF=ID=76325601198c7def:LD=en:CR=2:
          TM=1184565890:LM=1186242386:S=ofiC-IBOMCVySJCP\r\n
\r\n

TCP 重置通常在客户端或服务器出现问题时发送。要查看真实的 TCP 重置,只需在地址栏中输入 https://codeproject.org.cn,等待片刻后按 Esc 键。TCP 重置数据包是一个 40 字节的数据报,其 TCP 标志设置为 0x0004 (RST)。另一方面,我们有一个优雅的连接终止,这是一个 4 次序列。

         You                                               web server

  1.  ESTABLISHED                                          ESTABLISHED

  2.  (Close)
      FIN-WAIT-1  --> <SEQ=ISN1><ACK=ISN2><CTL=FIN,ACK>    --> CLOSE-WAIT

  3.  FIN-WAIT-2  <-- <SEQ=ISN2><ACK=ISN1+1><CTL=ACK>      <-- CLOSE-WAIT

  4.                                                       (Close)
      TIME-WAIT   <-- <SEQ=ISN2><ACK=ISN1+1><CTL=FIN,ACK>  <-- LAST-ACK

  5.  TIME-WAIT   --> <SEQ=ISN1+1><ACK=ISN2+1><CTL=ACK>    --> CLOSED

  6.  (2 MSL)
      CLOSED

请注意,TCP 连接终止不一定使用四个段。在某些情况下,段 2 和 3 会合并。结果是一个 FIN-ACK/FIN-ACK/ACK 序列。

  1. 浏览器尝试将地址解析为其对应的 IP 地址。为此,它对 gethostbyname() 进行一次函数调用。此函数生成一个 DNS 查询。如第 4.7 节所述,DNS 查询是一个 UDP 数据报,其目的地是 TCP/IP 属性中设置的 DNS 服务器。TCP/IP 处理程序查看目标 IP 地址,并意识到客户端 IP 地址和服务器 IP 地址位于不同的子网中,因此决定将数据包转发到下一跳,即我们的网关。为此,驱动程序在本地 ARP 表中查找网关的 MAC 地址。如果由于任何原因,网关的 MAC 地址不在已解析的地址中,TCP/IP 会在子网中广播 ARP 请求以获取相应的 MAC 地址。收到网关的 MAC 地址后,客户端会创建最终数据包并将其放入线路上。然后,数据包由网关接收。网关查看目标 IP 地址,并意识到此数据包不属于自身,因此决定将其转发到下一跳。从现在开始,数据包会遍历全局路由,直到成功到达目的地。然后,目的地协议处理程序将数据包交给适当的应用层服务,在这种情况下是 DNS。最后,DNS 服务器获取相关的 IP 地址并生成一个发往发送方的 DNS 响应。
  2. 手头有目的 IP 地址,我们的客户端协议栈现在可以与对等方建立 TCP 会话。
  3. 应用层事务开始。
  4. 服务器收到客户端的请求并用一些数据进行响应。
  5. 完成数据传输。为此,TCP 为您提供两种选择:TCP 复位和 TCP 优雅终止。

6. 我们实现网络过滤器的解决方案

该解决方案相当简单。以下序列用于过滤匹配的网页请求:

  1. 捕获数据包。
  2. 区分目标端口为 80 或 8080 的 TCP 数据包。
  3. 在数据的前四个字节中查找“GET /”。如果未找到,则继续处理下一个捕获的数据包。
  4. 对数据执行 Boyer-Moore 模式匹配,比对黑名单关键字。如果未找到,则继续处理下一个捕获的数据包。
  5. 向服务器发送 TCP 重置,并向客户端发送阻止页面。

7. 实现

本节详细解释了我们的 HTTP 过滤器的核心功能。所有功能都可在 CProcessPacket 类中的源代码中找到。

7.1 使用原始套接字捕获数据包

在不使用 NDIS 低级驱动程序的情况下捕获传入流量的最快方法是使用 Winsock 原始套接字。但是,您应该注意,原始套接字无法在混杂模式下捕获。这意味着套接字只捕获发往其自身地址的流量。

if((m_sniffSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP))==SOCKET_ERROR)
{        
    return 0;
}
PIP_ADAPTER_INFO pAdapterInfo = m_AdapterInfo; // Adapter information

u_long in = 0;
do {
    if (strcmp (in_szSourceDevice, pAdapterInfo->AdapterName ) == 0)
    {
        break; // we found selected adapter

    }
    in++;
    pAdapterInfo = pAdapterInfo->Next;    // Progress through
}
while(pAdapterInfo);  
        

struct sockaddr_in src;
memset(&src, 0, sizeof(src));
src.sin_addr.S_un.S_addr = 
   inet_addr (pAdapterInfo->IpAddressList.IpAddress.String);        
src.sin_family      = AF_INET;
src.sin_port        = 0;
if (bind(m_sniffSocket,(struct sockaddr *)&src,sizeof(src)) == SOCKET_ERROR)
{            
    return 0;
}

int j=1;
if (WSAIoctl(m_sniffSocket, SIO_RCVALL, &j, 
             sizeof(j), 0, 0, &in,0, 0) == SOCKET_ERROR)
{            
    return 0;
}

然后,在一个单独的线程中,我们处理捕获的数据包:

    int  res = 0;
    char *pkt_data = (char *)malloc(65536); 
    char                    m_pLogString[256];        
    Packet                    p;
    
    if (pkt_data == NULL)
    {        
        return 0;
    }

    SetEvent(_this->m_hThrdReadyEvent);
    
    // Capture packet

    do
    {
        res = recvfrom(_this->m_sniffSocket,pkt_data,65536,0,0,0); 
        if(res > 0)    
        {            
            ZeroMemory(&p, sizeof (Packet));
            
            DecodeIP((u_int8_t*)pkt_data, res, &p);

            if (p.banned == 1)
            {
                FilterHttpRequest(&p);

                char ip_string_src[17];
                char ip_string_dst[17];

                memcpy (ip_string_src, inet_ntoa(p.iph->ip_src), 17);
                memcpy (ip_string_dst, inet_ntoa(p.iph->ip_dst), 17);

                sprintf (m_pLogString,
                         "Keyword \'%s\' is detected in a request" 
                         " from %s to %s. Session Droped!", 
                         p.matched, ip_string_src, ip_string_dst);

                m_pFilterLog->AddLog(m_pLogString);
                
            }

        }
    } 
    while (res > 0);

7.2 区分 HTTP 数据包

在本节中,我们将处理捕获的数据包的各个部分,以查找以太网、IP 和 TCP 报头字段。为此,我们需要几个方便的结构来在其上放置数据。

struct IPHdr
{
    u_int8_t ip_verhl;      /* version & header length */
    u_int8_t ip_tos;        /* type of service */
    u_int16_t ip_len;       /* datagram length */
    u_int16_t ip_id;        /* identification  */
    u_int16_t ip_off;       /* fragment offset */
    u_int8_t ip_ttl;        /* time to live field */
    u_int8_t ip_proto;      /* datagram protocol */
    u_int16_t ip_csum;      /* checksum */
    struct in_addr ip_src;  /* source IP */
    struct in_addr ip_dst;  /* dest IP */
};

struct TCPHdr
{
    u_int16_t th_sport;     /* source port */
    u_int16_t th_dport;     /* destination port */
    u_int32_t th_seq;       /* sequence number */
    u_int32_t th_ack;       /* acknowledgement number */
    u_int8_t th_offx2;     /* offset and reserved */
    u_int8_t th_flags;
#define    TH_FIN    0x01
#define    TH_SYN    0x02
#define    TH_RST    0x04
#define    TH_PSH    0x08
#define    TH_ACK    0x10
#define    TH_URG    0x20
    u_int16_t th_win;       /* window */
    u_int16_t th_sum;       /* checksum */
    u_int16_t th_urp;       /* urgent pointer */
};

// our helper struct to hold packet information in one place

struct Packet
{
    
    u_int8_t            *pkt;    /* base pointer to the raw packet data */
    
    
    IPHdr                *iph;   /* and orig. headers for ICMP_*_UNREACH family */
    u_int32_t            ip_options_len;
    u_int8_t            *ip_options_data;


    TCPHdr                *tcph;
    u_int32_t            tcp_options_len;
    u_int8_t            *tcp_options_data;


    u_int8_t            *data;            /* packet payload pointer */
    u_int16_t            dsize;           /* packet payload size */

    
    
    u_int8_t            http_state;       /* HTTP request / HTTP response */
    u_int8_t            banned;           /* Indicate if the request should be sensored */
    unsigned char        matched[128];    /* Keyword that this request 
                                             matched to - maximum 128 byte*/
    
#define    CLIENT_REQUEST    0x01
#define    SERVER_RESPONSE    0x02
#define    NOT_HTTP        0x04

    
    u_int8_t frag_flag;     /* flag to indicate a fragmented packet */
    u_int16_t frag_offset;  /* fragment offset number */
    u_int8_t mf;            /* more fragments flag */
    u_int8_t df;            /* don't fragment flag */
    u_int8_t rf;            /* IP reserved bit */
};

现在,我们可以检索捕获的数据包中存储的所有报头信息。要执行此步骤,您应该了解我们在第 4 节中解释的相关协议规范。

Packet p;
    
/* lay the IP struct over the raw data */
p->iph = (IPHdr *) (pkt_data); 
    
/* Decode only TCP headers */
if (p->iph->ip_proto == 6)
{
    /* lay TCP on top of the data cause there is enough of it! */
    p->tcph = (TCPHdr *) (pkt_data + IP_HEADER_LEN);
    // set pinter at the beginning of TCP header 

    if (p->tcph->th_flags & TH_ACK /* If is request */
        && p->tcph->th_flags & TH_PSH)
         {
             if(p->tcph->th_dport != htons(80) &&
                /* If target service is not HTTP */
                p->tcph->th_dport != htons(8080) )
             return ;
             // continue in 7-3 }

         }
}

7.3 在前 4 个字节中查找“GET /”

p->data = (unsigned byte*)(pkt_data + ETHERNET_HEADER_LEN + 
           IP_HEADER_LEN + TCP_HEADER_LEN)
// find payload position


if( p->data[0] == 'G' &&
        p->data[1] == 'E' &&
        p->data[2] == 'T' &&
        p->data[3] == ' ' &&
        p->data[4] == '/' 
        )
{
    // continue in 7-4

}

7.4 对有效载荷执行 Boyer-Moore 模式匹配算法

正如我之前解释的,Boyer-Moore 是一种快速模式匹配算法,它采用非线性方法来查找匹配项。

if (CheckPattern(p->data, len))
{
    // A match found, session must be filtered!
    // continue in 7-5
}

7.5 通过向两个方向发送大量数据包来完成数据事务

也许,本节是本文中最棘手的部分。在这里,我再次提醒,我们的 HTTP 过滤器在嗅探器模式下工作。因此,我们应该尽力防止双方可靠的 TCP 引擎通过重新同步来激活会话。

SEQ = ISN1 ACK = ISN2 
Direction = To Server
SEQ = ISN1 ACK = ISN2
FLAGS = RST 
Direction = To Server.
SEQ = ISN2 ACK = ISN1 + Len(GET)
FLAGS = FIN | ACK
Direction = To Client.

请注意,已实现的例程可在 CProcessPacket::SensorHttpRequest 中找到。

  1. 假设我们捕获了一个目标 HTTP GET 数据包,其中包含以下 TCP 信息:
  2. 同样,由于我们在监控模式下运行,我们应该摆脱服务器重新同步尝试。为此,我向服务器发送了一个 TCP 重置,以暂时停止 TCP 状态机。
  3. 向客户端发送阻止页面

8. 特别考虑

虽然适用于 SOHO 环境,但我们的 HTTP 过滤器对于企业网络来说是一种不太常见的过滤解决方案。这是因为嗅探模式无法保证对 TCP 会话进行同步响应,因为它不与数据包流对抗。将其与比赛前后足球场进行比较。比赛前,保安可以逐一检查球迷并核对他们的门票。比赛结束后,球迷们只是冲出场外,对于叛乱分子来说,很容易就消失不见。至少在我的城市就是这样!;)

这是一对一检查的问题!除了可靠性之外,一对一检查还有一个很大的缺点:“一个缓慢的检查员可能会拖慢整个进程”。

9. 未来工作

  • 通过在后台线程中执行异步分析,使概念更可靠
  • 将核心捕获功能替换为 Windows 和 Unix 平台上均可用的网络驱动程序钩子
  • 将 Boyer-Moore 替换为多模式搜索,以便在一次搜索中评估数十种模式
  • 期待您的想法或要求

10. 修订历史

  • 首次发布 - 2007-08-06
  • 代码注释改进 - 2007-08-07
  • 全面审查;将 Winpcap 和 Libnet 从项目中排除;通过使用 WinSock 中提供的原始套接字功能执行捕获和发送原始数据包 - 2007-08-26

11. 我的测试环境

  • 以太网局域网
  • WLAN 802.11/bg 标准子网

无论如何,如果您遇到任何故障,请报告。提前致谢!

12. 参考文献

  1. 《TCP/IP 详解,卷 1 协议》W. Richard Stevens
  2. RFC 793, RFC 791, RFC 768, RFC 826, RFC 1034, RFC 1035。请参阅 IETF
  3. 网络编程和网络安全编程论坛
  4. Ethereal
  5. Winpcap
  6. 《Windows Server 2003 TCP/IP 和服务。技术参考》Josef Davies 和 Thomas Lee。
  7. 《HTTP 袖珍参考》Clinton Wong,O'Reilly 2000 年 5 月。
© . All rights reserved.