防火墙的一切判断都从「把字节流拆成字段」开始, 这一步叫解析,它的正确性决定了后面所有功能的对错。 而解析最容易出事的地方是「长度字段」与「校验和」。
上一节讲会话表记什么,这一节讲那些字段是怎么从线路上读出来的。 这一节的 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 秒实验
- 用 Wireshark 抓一个包,展开 IP 头数一数是不是 20 字节。
- 找一个带选项的 IP 包,看 IHL 是不是大于 5。
- 想一想:为什么 IPv6 取消了首部校验和?
常见误区
- 以为校验和能防篡改。攻击者改完可以重算,它只防传输错误。
- 以为 IP 头固定 20 字节。带选项可以到 60 字节,必须看 IHL。
- 以为分片是罕见情况。大包与隧道场景下分片很常见,必须处理。
自测三问(点开对答案)
1. IPv4 头最长多少字节?由哪个字段决定? —— 60 字节,由 4 位的「首部长度」字段乘 4 得到(最大 15 × 4)。 2. TCP 校验和为什么要算伪首部? —— 把地址与协议号也纳入校验,这样地址被改错时也能查出来。 3. 解析时最关键的一条防护原则是什么? —— 先读出长度字段,再验证它是否落在缓冲区内,两者都通过才继续。下一节:解析出来的字段,按「信任程度」分组,就是安全区域。
- ☐ (本节完,下一节 05.07.03 安全区域)