“反码求和”听起来很抽象,实际上就是几行代码。这篇先手算一个 IPv4 头和一个 UDP 数据报的校验和,再用本机抓到的 53 个真实包核对,看它的盲区、增量更新公式,以及 UDP 里那个“0 表示没有校验和”的特例。
头部 10 个 16 位字(校验和字段先填 0): 4500 003c 1c46 4000 4011 0000 c000 0201 c633 6407
直接相加 = 0x2cdce(超过 16 位);把高位的进位加回低位:0x2cdce → 0xcdd0;取反得校验和 0x322f
填回去以后,接收方把整个头再加一遍:折回后的和 = 0xffff,取反 = 0x0000(全 1 取反得 0,表示校验通过)
真实抓包(en0)共 53 个 IP 包,用同一种方法检验:
收到 的 ICMP: 5/5 校验通过
收到 的 IP 头: 25/25 校验通过
收到 的 TCP : 15/15 校验通过
收到 的 UDP : 5/5 校验通过
本机发出 的 ICMP: 5/5 校验通过
本机发出 的 IP 头: 28/28 校验通过
本机发出 的 TCP : 18/18 校验通过
本机发出 的 UDP : 5/5 校验通过把数据按 16 位字相加,进位折回低位,再取反。校验和字段先填 0 参与计算;接收方把含校验和的整段再加一遍,折回后是 0xffff 就通过。
伪头部 12 字节: c000 0201 c633 6407 0011 0017(源 192.0.2.1、目的 198.51.100.7、0、协议 17、UDP 长度 23) 伪头部各字之和 0x1ec63;UDP 头 + 载荷各字之和 0x3ad05;合计折回后 0x996d,取反得 UDP 校验和 0x6692 为什么要伪头部:让校验和也能发现“包被送到了错误的主机”——只有 UDP / TCP 头和载荷的话,地址被改错了也查不出来 lo0 上抓到 32 个 TCP 包:IP 头校验和正确 0 个(字段为 0 的 32 个);TCP 校验和正确 0 个(字段为 0 的 0 个) lo0 上抓到 5 个 UDP 包:IP 头校验和正确 0 个(字段为 0 的 5 个);UDP 校验和正确 0 个(字段为 0 的 0 个) 所以 lo0 上抓到的“错误校验和”不是传输错误:本机内的包校验和被卸载 / 省略了(en0 上抓到的同样检验全部通过,见第一节)
TCP / UDP 的校验和还覆盖一个不发出去的 12 字节伪头部(源 / 目的地址、协议、长度),用来发现地址错误。lo0 上抓到的校验和是 0 或不可验证,因为本机内部的包没有真的算校验和;en0 上的全部通过。
数据 A = 1122334455667788:校验和 0xeeaa,CRC-32 0x9118e1c2
数据 B = 3344112255667788(前两个 16 位字 1122 / 3344 对调):校验和 0xeeaa,CRC-32 0x9e3bdbe6
校验和相同: True;CRC-32 相同: False
数据 C = 1234abcd0000 和 D = 1235abcc0000(一个字 +1,另一个 −1):校验和 0x41fe / 0x41fe,相同: True;CRC-32 相同: False
用真实抓到的一个 960 字节的 TCP 段做翻转比特实验(模拟,不是网络上的实测):
翻转任意 1 个比特(共 7680 种):校验和能发现 7680 种(全部发现:True)
随机翻转 2 个比特(20000 次):校验和没发现 507 次(2.535%);CRC-32 没发现 0 次
校验和漏掉的 2 比特翻转:两个比特在各自 16 位字里位置相同、一个 0→1 一个 1→0,变化互相抵消加法可交换,16 位字的顺序被调换、一个字加一个字减等错误查不出来。模拟显示 1 比特错误必被发现,2 比特错误漏掉约 2.5%,CRC-32 一个也没漏。
把 53 个真实抓到的包的 TTL 减 1:增量公式算出的新校验和与整头重算的相同 53 个 NAT 式改写:把本机发出的 18 个 TCP 包的源地址改成 203.0.113.9、源端口改成 61234(3 个 16 位字),增量更新的 TCP 校验和与整段重算的相同 18 个 Python 里的耗时(只看相对大小):整个 20 字节头重算 0.43 µs,增量更新一个字 0.17 µs;对 960 字节的 TCP 段整段重算 6.5 µs(增量更新则与段长无关)
只改了 TTL 或 NAT 改写了地址端口的包,不需要整个重算,用 RFC 1624 的公式更新即可,53 个 TTL 与 18 个 NAT 改写的结果都与整体重算一致。
先让末尾的 16 位字为 0,各字之和折回后是 0x41b2;把末尾字设成 0xffff − 0x41b2 = 0xbe4d,现在载荷 22 字节,各字之和折回后 0xffff,按公式算出的校验和 = 0x0000 线上抓到 1 个包:UDP 校验和字段 = 0xffff;按接收方的检验方法(含伪头部全部相加)结果:通过 0 和 0xffff 在反码里都表示“零”,所以把 0 换成 0xffff 不改变校验;字段 0 留给“发送方没算校验和”
IPv4 的 UDP 校验和字段 0 表示没有校验和,所以算出来是 0 就改成 0xffff 发出,线上抓到 0xffff,接收方检验仍通过。