📡 第五章 防火墙功能与防范威胁的对策
📄 一句话概括

防火墙的一切判断都从「把字节流拆成字段」开始, 这一步叫解析,它的正确性决定了后面所有功能的对错。 而解析最容易出事的地方是「长度字段」与「校验和」。

📝 本节和前面几节的关系

上一节讲会话表记什么,这一节讲那些字段是怎么从线路上读出来的。 这一节的 Demo 是真跑——亲手构造一个 IP 头和一个 TCP 头,再算校验和。

一、一个 IP 头长什么样

IPv4 头最少 20 字节,最长 60 字节(因为「首部长度」字段是 4 位,单位是 4 字节, 最大 15 × 4 = 60)。

字段 字节 用途


版本与首部长度 1 各占 4 位 总长度 2 含头与载荷 标识与片偏移 4 分片重组用 TTL 与协议 2 防环与上层协议 首部校验和 2 只校验头 源与目的地址 8 各 4 字节

注意「首部校验和只校验头,不校验载荷」:这是 IPv4 的设计选择, 载荷的完整性交给上层(TCP 或 UDP)负责。 好处是路由器改 TTL 后只需重算头,不用碰整个包。

二、TCP 头多出来的东西

TCP 头固定 20 字节,比 IP 头多了三样防火墙最关心的东西:端口、序号、标志位。

端口:确定「哪个进程」。序号与确认号:确定「说到哪了」,也是状态检测的依据。 标志位:SYN、ACK、FIN、RST、PSH、URG,六个位组合出「这个包想干什么」。

标志位的组合是有讲究的: SYN 和 FIN 同时置位、或者六个位全置位,在正常通信里绝不会出现——这正是「畸形包检测」能生效的原因。 而这类检查只需要读一个字节。

还有一件常被忽略的事: TCP 校验和要算「伪首部」——把源地址、目的地址、协议号、TCP 长度也算进去。 这样做的目的是让「地址被改错」也能被校验出来, 否则 NAT 改了地址,校验和还是对的,就查不出篡改。

三、解析为什么容易出错

三个经典坑,每一个都被攻击利用过:

一、长度字段与实际长度不符: 总长度说 100 字节,实际只来了 60 字节——解析器若按字段读,就会读到缓冲区外面去。 二、分片重叠:两个分片的偏移故意重叠,不同系统取「先到的」还是「后到的」不一样, 这就成了绕过检测的口子(泪滴攻击的原理)。 三、首部长度与实际不符: IHL 说 5(20 字节),但选项字段还有内容——有的实现会漏掉选项,有的会读越界。

所以「解析」这件事的正确做法是: 先按字段读出长度,再验证「读出来的长度是否在缓冲区范围内」,两者都通过才继续往下解析。 这一步没有捷径,必须每条都查。

四、动手 Demo:手搓一个 IP 头和一个 TCP 头

这段用 struct 真的构造一个 20 字节 IP 头和一个 20 字节 TCP 头,算校验和, 再改一个字节看校验和怎么变。

ip = struct.pack("!BBHHHBBH4s4s",
                 0x45, 0, ip_total, 0x1234, 0x4000, 64, 6, 0, src, dst)
ip = ip[:10] + struct.pack("!H", _csum(ip)) + ip[12:]

pseudo = src + dst + struct.pack("!BBH", 0, 6, tcp_len)
tcp = tcp[:16] + struct.pack("!H", _csum(pseudo + tcp)) + tcp[18:]

实跑输出:

--------------------------------------------------------------------
手搓一个 IP 头和一个 TCP 头
--------------------------------------------------------------------
**防火墙要判包,第一步是把字节流拆成字段。**
**这件事可以自己动手做一遍。**

  IP 头 20 字节:4500002d1234400040062adfc0a8010acb007105
  总长字段 = 45,协议号 = 6,TTL = 64。

  TCP 头 20 字节:c35001bb000003e8000007d050182000c14b0000
  源端口 50000 → 目的端口 443,序号 1000,确认号 2000。
  TCP 校验和 = 0x0000(含伪首部)。

**改一个字节,校验和立刻变**:
  序号最低字节 +1 后重算 = 0xFEFF,与原来差 0xFEFF。

**校验和是「反码求和再取反」**,
**它的妙处是:接收方把整段连同校验和一起求和,结果为 0xFFFF 即正确。**

这段代码的真价值不是「会造包」,是「知道字段在第几字节」: 0x45 这个字节同时编码了版本(4)与首部长度(5 × 4 = 20)。 所以「版本说 4、长度说 20」这一个字节,就决定了后面怎么解。

反码求和这个算法的妙处要体会一下: 发送方把校验和填进去,接收方把整段(含校验和)再求一次和,结果应该是 0xFFFF。 也就是说「校验」与「计算」是同一个操作, 这省掉了接收方的一次特殊处理,是那个年代的精巧设计。

最后那句「改一个字节校验和立刻变」是这一节的落点: 它说明校验和能查出错,但查不出「故意的篡改」——因为攻击者改完可以重算。 所以校验和不是安全机制,是可靠性机制,两件事别混。

30 秒实验

  1. 用 Wireshark 抓一个包,展开 IP 头数一数是不是 20 字节。
  2. 找一个带选项的 IP 包,看 IHL 是不是大于 5。
  3. 想一想:为什么 IPv6 取消了首部校验和?

常见误区

  1. 以为校验和能防篡改。攻击者改完可以重算,它只防传输错误。
  2. 以为 IP 头固定 20 字节。带选项可以到 60 字节,必须看 IHL。
  3. 以为分片是罕见情况。大包与隧道场景下分片很常见,必须处理。
自测三问(点开对答案) 1. IPv4 头最长多少字节?由哪个字段决定? —— 60 字节,由 4 位的「首部长度」字段乘 4 得到(最大 15 × 4)。 2. TCP 校验和为什么要算伪首部? —— 把地址与协议号也纳入校验,这样地址被改错时也能查出来。 3. 解析时最关键的一条防护原则是什么? —— 先读出长度字段,再验证它是否落在缓冲区内,两者都通过才继续。

下一节:解析出来的字段,按「信任程度」分组,就是安全区域。

相关笔记