PCAP 文件格式详解:从字节布局到逐层解析
一.前言
pcap 有两种常见格式,一个是经典 pcap(.pcap,libpcap/tcpdump 的老格式),另一个是pcapng(.pcapng,Wireshark 默认的新格式),本文章描述的是经典 pcap,为后面的pcap 解析器——网络抓包项目打下基础,但我们还是简单的说一下这两种格式的背景。
为什么会有两种格式?
- pcap(经典格式):由 libpcap / tcpdump 在 1990 年代定义,长期是事实标准。结构极简,一个文件只能记录一种链路类型。
- pcapng(PCAP Next Generation):为弥补 pcap 的局限而生——支持多网卡接口、每接口独立的时间戳精度、注释与元数据、多段拼接。Wireshark 现在默认存 pcapng,但 libpcap 也早已能读写(1.8+ 读、1.9+ 写)。
所以拿到一个抓包文件,第一步永远是看前 4 字节:
| 前 4 字节 | 格式 |
|---|---|
a1 b2 c3 d4 | pcap,微秒时间戳 |
a1 b2 3c 4d | pcap,纳秒时间戳 |
0a 0d 0d 0a | pcapng(SHB 块) |
1f 8b | gzip 压缩过的抓包文件(先解压) |
| 其他 | 不是抓包文件 |
二.pcap概念
pcap 文件就是一串抓到的原始链路层字节,每条前面加 16 字节的"什么时候抓的、抓了多少"的小标头,整份文件开头再加 24 字节的"这份文件是哪种链路、什么精度"的说明。它没有任何压缩和加密,文件大小 ≈24 + 16 × 包数 + 所有包字节数。
三、pcap整体结构
┌──────────────────────────────────────────┐ │ 全局头 Global Header 固定 24 字节 │ ← 整份文件只有 1 个 ├──────────────────────────────────────────┤ │ 记录头 Packet Header 固定 16 字节 │ │ 包数据 Packet Data incl_len 字节 │ ← 第 1 个包 ├──────────────────────────────────────────┤ │ 记录头 16 字节 + 包数据 … │ ← 第 2 个包 ├──────────────────────────────────────────┤ │ … 一直到文件末尾(EOF) │ └──────────────────────────────────────────┘注意:文件里没有"包总数"字段,也没有"文件总长"字段。只能一条一条顺序往下读,靠"读不下去"来判断结束;所以文件尾部被截断是常见现象,解析器必须容忍。
四、全局头(24 字节)
| 偏移 | 长度 | 字段 | 类型 | 含义 |
|---|---|---|---|---|
| 0 | 4 | magic_number | u32 | 魔数,同时用来说明字节序和时间戳精度 |
| 4 | 2 | version_major | u16 | 主版本号,通常2 |
| 6 | 2 | version_minor | u16 | 次版本号,通常4 |
| 8 | 4 | thiszone | i32 | GMT 与本地时间差,历史遗留,恒为 0 |
| 12 | 4 | sigfigs | u32 | 时间戳有效位数,历史遗留,恒为 0 |
| 16 | 4 | snaplen | u32 | 抓包时最多截取多少字节,常见 65535 / 262144 |
| 20 | 4 | network | u32 | 链路层类型(LinkType),1 = 以太网 |
4.1 魔数:同时表示字节序和时间戳精度
它的首要职责是判断字节序,但同一个 32 位值还兼任"时间戳精度"标记。原因是:纳秒版是在原始格式上改了一位扩展出来的——把0xa1b2c3d4改成0xa1b23c4d(c3d4→3c4d),既和其他格式区分得开,翻转后的形态(0x4d3cb2a1)也唯一可辨。
下面这张表以"我现在的 x86(小端)机器"为前提:
| 文件开头 4 字节 | 写入方字节序 | 时间戳精度 | 我这台机器要翻转吗 |
|---|---|---|---|
d4 c3 b2 a1 | 小端 | 微秒 | 不用 |
a1 b2 c3 d4 | 大端 | 微秒 | 要 |
4d 3c b2 a1 | 小端 | 纳秒(libpcap 1.5+ 支持) | 不用 |
a1 b2 3c 4d | 大端 | 纳秒 | 要 |
翻转与否,只取决于"文件字节序"和"读取方字节序"是否相同。读取方是 x86(小端),所以规则就是"小端文件不翻、大端文件翻"。
4.2 snaplen:截断从哪来
抓包时如果每个包都完整保存,磁盘和内存会吃不消(尤其大包和突发流量)。snaplen就是"每个包最多存这么多字节",超出部分直接丢弃。于是:
- 记录头里的
incl_len(实际存了多少)可能小于orig_len(网线上真实长度) - 分析时"这个包有多少有效数据"要用
incl_len;"这个包在线路上多大"要用orig_len - 一个 TCP 报文只抓到前 54 字节(以太网 14 + IP 20 + TCP 20)时,应用层数据就是永远看不到的——这不是 bug,是截断
4.3 network:决定从哪里开始解析
这个字段是整份文件共享的,它告诉我们"包数据的第一个字节是什么协议的头"。常见值:
| 值 | LinkType | 说明 |
|---|---|---|
| 0 | NULL | BSD 回环,开头是 4 字节地址族(主机字节序) |
| 1 | ETHERNET | 以太网(绝大多数文件) |
| 9 | PPP | 点对点 |
| 101 | RAW | 裸 IP,包数据直接就是 IPv4 / IPv6 头 |
| 105 | IEEE802_11 | Wi-Fi 帧 |
| 113 | LINUX_SLL | Linux “any” 抓包,16 字节 SLL 头 |
| 127 | IEEE802_11_RADIOTAP | Wi-Fi + Radiotap |
| 228 / 229 | IPV4 / IPV6 | 纯 IP |
| 276 | LINUX_SLL2 | 新版 SLL,20 字节头 |
五、数据包记录
5.1 包头(记录头,16 字节)
同一个东西有两个叫法:包头和记录头,指的都是这 16 字节。
别和后面包的"以太网头 14 字节"混了:包头是 pcap 文件加的,以太网头是网络包自带的,两者毫无关系,只是都叫"头"。
| 偏移 | 长度 | 字段 | 含义 |
|---|---|---|---|
| 0 | 4 | ts_sec | 秒部分(Unix 时间戳) |
| 4 | 4 | ts_usec | 微秒部分(纳秒版 magic 时这里是纳秒) |
| 8 | 4 | incl_len | 本条记录里实际存了多少字节 |
| 12 | 4 | orig_len | 该包在网线上的原始长度 |
紧接着就是incl_len字节的包数据,没有任何对齐填充,下一条记录紧跟在后面。
5.2 两个长度字段为什么要分开
orig_len ────────────────► 网线上真实长度(比如 1514 字节) snaplen = 65535 时 incl_len ────────────────► 文件里存了 1514 字节(没截断,两者相等) snaplen = 96 时 orig_len ────────────────► 1514 incl_len ──────────────► 96(只留了头部,载荷被砍掉)判断数据完整性、计算"这条流一共传了多少字节",靠的就是这两个字段的区别。
六、包数据里面:套娃式分层(链路 → 网络 → 传输 → 应用)
包数据的起点由network决定(下文按最常见的以太网讲)。每层都在自己头部里声明"我的载荷是什么",解析就是一个不断推进偏移、递减剩余长度的循环。
6.0 每一层的"数据部分",就是下一层的"整体"。
包数据 = 以太网帧 └─ [以太网头 14B] + IP 数据报 └─ [IPv4 头 20B] + TCP 段 └─ [TCP 头 20B] + 应用数据 18B“头多大”“下一层是谁”,都是每层自己告诉你的:
| 层 | 头多大?谁说的 | 下一层是谁?谁说的 | 数据部分从哪开始 |
|---|---|---|---|
| 记录层(容器) | 固定 16 字节 | 不适用(它不是协议) | 上一条记录结束处 + 16 |
| 链路层 · 以太网 | 固定 14 字节(有 VLAN 就是 18) | 偏移 12 的Type:0x0800= IPv4 | 帧内偏移 14 |
| 网络层 · IPv4 | IHL × 4(典型 20) | 偏移 9 的Protocol:6= TCP | IP 头内偏移IHL × 4 |
| 传输层 · TCP | DataOffset × 4(典型 20) | 没有这个字段 → 靠端口号认应用协议(80 = HTTP) | TCP 头内偏移DataOffset × 4 |
| 应用层 | 没有头 | —— | 剩下的全是它 |
最上面(应用层)没有"下一层是谁"的字段——TCP / UDP 只有端口号,所以"这是不是 HTTP"往往是猜出来的,这也是 Wireshark 会"猜协议"的原因。
读下面的字段表之前先记住:每张表里的偏移,都是相对本层起点的,不是相对文件、也不是相对整个包。IP 头那张表的"偏移 0",指的是 IP 头的第一个字节。
6.1 数据链路层:以太网 II(14 字节头)
| 偏移 | 长度 | 字段 |
|---|---|---|
| 0 | 6 | 目的 MAC |
| 6 | 6 | 源 MAC |
| 12 | 2 | 类型 (EtherType) |
| 14 | — | 载荷 |
| 末尾 4 字节 | 4 | FCS 帧校验(libpcap 通常不保存) |
常见 EtherType:
| 值 | 协议 |
|---|---|
0x0800 | IPv4 |
0x0806 | ARP |
0x86DD | IPv6 |
0x8100 | VLAN(802.1Q) |
0x88A8 | QinQ(802.1ad) |
0x8847 | MPLS |
0x8864 | PPPoE 会话 |
0x88CC | LLDP |
VLAN 会改变偏移:0x8100后面是 2 字节 TCI + 2 字节"真正的 EtherType",于是头从 14 变成 18 字节,还能叠两层(QinQ)。正确写法是循环读 EtherType,直到不是 VLAN 为止,而不是写死 14。
6.2 网络层:IPv4(20 ~ 60 字节头)
IPv4 头紧跟在 14 字节以太网头之后,所以它的起点 = 以太网头起点 + 14。
完整字段表(IPv4 头 20 ~ 60 字节,下表偏移都相对 IP 头起点):
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 1 | 版本 + 首部长度 | 高 4 位 = 版本(4);低 4 位 =IHL,头长 =IHL × 4(最小 5 → 20 字节) |
| 1 | 1 | DSCP + ECN | 服务质量与拥塞标记,一般忽略 |
| 2 | 2 | 总长度 | 头 + 载荷的总字节数;可能大于实际捕获到的字节(被 snaplen 截断时) |
| 4 | 2 | 标识 | 同一份原始数据切出来的所有分片共用这个编号 |
| 6 | 2 | 标志 + 分片偏移 | 高 3 位:R / DF / MF;低 13 位是分片偏移,单位 8 字节 |
| 8 | 1 | TTL | 每过一跳减 1,减到 0 就被丢弃(防止包永远打转) |
| 9 | 1 | 协议 | 载荷类型 → 决定下一层是谁 |
| 10 | 2 | 头校验和 | 只校验 IP 头,不含数据 |
| 12 | 4 | 源 IP | |
| 16 | 4 | 目的 IP | |
| 20 | IHL×4 − 20 | 选项 | 少见(记录路由、时间戳等);有选项时头会长于 20 字节 |
协议字段 → 下一层:
| 值 | 下一层 | 值 | 下一层 |
|---|---|---|---|
| 1 | ICMP | 47 | GRE(隧道) |
| 2 | IGMP | 50 | ESP(IPsec 加密) |
| 4 | IP-in-IP(隧道) | 51 | AH(IPsec 认证) |
| 6 | TCP | 58 | ICMPv6 |
| 17 | UDP | 89 | OSPF(路由协议) |
| 41 | IPv6 封装 | 132 | SCTP |
偏移怎么推进:传输层起点 = IP 头起点 + 首部长度。注意不能写死 20——首部长度写在 IP 头第 1 个字节的低 4 位里,要用第 1 个字节 & 0x0F再乘以 4 算出来。
进这一层前的防御:这一条记录至少要有34字节——
34 = 以太网头 14 + IP 头最小 20 不足 34 字节 → 连一个完整 IPv4 头都放不下 → 不解析分片是这里最大的陷阱:分片偏移 ≠ 0的包(后续分片)里装的是纯数据片段,没有 TCP / UDP 头;MF=1的首片虽有传输层头,但载荷不完整。想正确解析就得先做重组,否则只能跳过。
分片:IPv4 会把一份大数据切成多片,每片单独成一个包发出:
MF = 1:后面还有分片;MF = 0:这是最后一片DF = 1:不许分片(分片会被丢弃)分片偏移 ≠ 0:这不是第一片,里面没有 TCP / UDP 头,只有一段裸数据- 想还原完整报文,得按"标识 + 分片偏移"把同一组的片拼起来(重组)
日常上网(网页、ping、下载)里基本见不到分片,等真遇到再处理。
6.3 网络层:IPv6(固定 40 字节头)
怎么认出来:以太网类型是0x86DD(而不是0x0800),说明这一层装的不是 IPv4。
固定 40 字节头:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | 版本(4b) + 流量类别(8b) + 流标签(20b) | 版本恒为 6 |
| 4 | 2 | 载荷长度 | 只算 40 字节固定头之后的部分 |
| 6 | 1 | 下一头部 | 载荷类型,但不一定是 TCP / UDP |
| 7 | 1 | 跳数限制 | 等价于 IPv4 的 TTL |
| 8 | 16 | 源地址 | 128 位 |
| 24 | 16 | 目的地址 | 128 位 |
与 IPv4 的三个关键差别:
- 头是定长 40 字节,没有 IHL,不用算头长
- 地址是16 字节(128 位),不是 4 字节
- "下一层是谁"写在一连串扩展头里,要走一遍链才能找到 TCP / UDP:
| 值 | 扩展头 | 值 | 扩展头 |
|---|---|---|---|
| 0 | 逐跳选项 | 51 | AH |
| 43 | 路由 | 59 | 无下一头部 |
| 44 | 分片 | 60 | 目的选项 |
| 50 | ESP | 6 / 17 / 58 | TCP / UDP / ICMPv6 |
所以 IPv6 的解析本质是个循环:读下一头部 → 是扩展头就按它自己的长度往下跳 → 直到遇到 6 / 17 / 58 为止。
6.4 传输层:TCP(典型 20 字节头)
这一层的任务:读出端口号认出应用;TCP 还多几个字段值得看。
TCP 头紧跟在 IP 头之后,起点 = IP 头起点 + 首部长度。
| 偏移 | 长度 | 字段 | 用途 |
|---|---|---|---|
| 0 / 2 | 2 / 2 | 源端口 / 目的端口 | 认应用:53 = DNS、80 = HTTP、443 = HTTPS |
| 4 | 4 | 序号 | 本报文第一个数据字节的编号;把同一条连接的数据按顺序拼起来要靠它 |
| 8 | 4 | 确认号 | 期望下次收到对方哪个序号,ACK 置位时有效 |
| 12 | 1 | 数据偏移(高 4 位) + 保留(3) + NS(1) | 头长 =高 4 位 × 4(最小 5 → 20 字节) |
| 13 | 1 | 标志位(8 个经典标志) | 见下表 |
| 14 | 2 | 窗口大小 | 对方还能接收多少字节(流量控制) |
| 16 | 2 | 校验和 | 覆盖 TCP 头 + 数据 + 伪首部 |
| 18 | 2 | 紧急指针 | 只有 URG 置位时才有意义 |
| 20 | 数据偏移×4 − 20 | 选项 | MSS、窗口扩大、SACK、时间戳等;常见 12 字节 → 头变成 32 字节 |
标志位(偏移13)怎么读:把偏移 12 那 2 字节的低 8 位取出来(用& 0x00FF把高 8 位掩掉),一位一个标志:
| bit | 值 | 标志 | 含义 |
|---|---|---|---|
| 0 | 0x01 | FIN | 断开连接 |
| 1 | 0x02 | SYN | 请求建立连接 |
| 2 | 0x04 | RST | 强制复位 |
| 3 | 0x08 | PSH | 尽快交给应用层 |
| 4 | 0x10 | ACK | 确认号有效 |
| 5 | 0x20 | URG | 紧急指针有效 |
| 6 | 0x40 | ECE | 显式拥塞通知(ECN)回显 |
| 7 | 0x80 | CWR | 拥塞窗口已减小 |
所以你看到的SYN, ACK,就是这两位同时为 1——TCP 三次握手的第二步。
6.5 传输层:UDP(固定 8 字节)
固定8 字节,字段一览:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 / 2 | 2 / 2 | 源端口 / 目的端口 | 认应用:53 = DNS、67 / 68 = DHCP |
| 4 | 2 | 长度 | UDP 头 + 数据的总字节数(最小 8,即空数据) |
| 6 | 2 | 校验和 | 可选,IPv4 里允许填 0 表示"没算" |
关键结论:UDP 头后面直接就是应用数据——应用层报文的起点 = UDP 头起点 + 8。
小坑:
长度字段在实际抓包里不一定可靠(有的实现不填、IPv6 巨型帧里甚至是 0),稳妥做法是用IP 总长 − IP 头长 − 8推算应用数据长度。
6.6 应用层
传输层载荷就是应用层报文本身。pcap只保存原始字节流,不会标记这一段是什么应用协议,识别和解析完全靠我们自己写的解析器。
识别依据:传输层端口号(约定俗成,不是绝对可靠)
常用应用协议速览
| 应用协议 | 默认传输层 | 端口 | 说明 |
|---|---|---|---|
| HTTP | TCP | 80 / 8080 | TCP头后的载荷就是HTTP报文(GET/POST等请求) |
| HTTPS(TLS) | TCP | 443 | TCP头后是TLS加密报文,无法直接读出明文 |
| DNS | UDP | 53 | UDP头后直接是DNS报文(12B固定DNS头 + 域名查询/应答) |
| DNS | TCP | 53 | TCP头后载荷,最前面多2字节长度字段,之后才是DNS报文 触发场景:UDP应答>512B(TC=1截断)、主从服务器区域传输 |
| DHCP | UDP | 67(服务器) / 68(客户端) | UDP载荷为DHCP报文,局域网分配IP用 |
| SSH | TCP | 22 | 远程登录,加密TCP载荷 |
| SMTP | TCP | 25 | 邮件发送协议 |
七.解析通用逻辑(抓包代码流程)
- 解析以太网帧 → 拿到IP报文
- 解析IP头,判断协议号:TCP(6) / UDP(17)
- 解析TCP/UDP头,读取源端口、目的端口
- 根据端口判断应用类型,取出传输层载荷交给对应应用层解析函数
- UDP:应用起点 = 以太网14 + IP头长 + 8
- TCP:应用起点 = 以太网14 + IP头长 + (TCP数据偏移 × 4)
- 对载荷按照该应用协议的格式解析