使用 tcpdump 抓包

 

tcpdump 是一个很实用的抓包工具. 一直以来我都只是复制网上的常用命令, 对其使用逻辑缺乏理解. 最近我仔细阅读了它的 manual, 总结一下 tcpdump 的用法.

命令格式

如果使用 tcpdump --help 查看它的使用方法, 总是会得到一大堆参数选项, 至于如何使用还是一头雾水. tcpdump 的用法实际是这样的:

$ tcpdump [选项] [表达式]

tcpdump 会读取网络中的数据, 解析协议, 然后与表达式相匹配. 如果能匹配上, 则用指定的方式输出数据包的内容. 选项则用于指定如何从网络中读取数据 (如指定网络接口) 以及如何输出抓取到的数据.

在深入了解选项和表达式语法前, 先看个简单的例子. 选项 -A 表示用 ASCII 以文本的形式打印数据包的内容, -i 指定网络接口; 表达式 tcp && port 80 表示抓取协议为 tcp, 且端口为 80 的数据包.

$ tcpdump -i eth0 -A 'tcp && port 80'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

这个使用如果我们在这个机器上执行 curl http://luyuhuang.tech 就可以看到 tcpdump 打印出:

16:43:22.947734 IP 172.29.57.43.41858 > luyuhuang.tech.http: Flags [S], seq 3607076262, win 64240, options [mss 1460,sackOK,TS val 2356831936 ecr 0,nop,wscale 7], length 0
E..<..@.@.....9++..+...P.............&.........
.zf.........
16:43:22.961963 IP luyuhuang.tech.http > 172.29.57.43.41858: Flags [S.], seq 1991848100, ack 3607076263, win 65160, options [mss 1424,sackOK,TS val 1839405528 ecr 2356831936,nop,wscale 7], length 0
E..<..@.1...+..+..9+.P..v.0.........g..........
m....zf.....
16:43:22.962003 IP 172.29.57.43.41858 > luyuhuang.tech.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 2356831951 ecr 1839405528], length 0
E..4..@.@.....9++..+...P....v.0............
.zf.m...

上面是 TCP 的三次握手. 每个包会先打印一行基础信息, 称为 "dump line", 如当前时间, 通讯双方的 IP 地址和端口, TCP 的标志位, 序列号, 以及选项等内容. 接下来是包体内容, 以文本形式打印. 如果不是 ASCII 码则打印为 .. 接下来就是 HTTP 请求:

16:43:22.962049 IP 172.29.57.43.41858 > luyuhuang.tech.http: Flags [P.], seq 1:79, ack 1, win 502, options [nop,nop,TS val 2356831951 ecr 1839405528], length 78: HTTP: GET / HTTP/1.1
E.....@.@..g..9++..+...P....v.0......l.....
.zf.m...GET / HTTP/1.1
Host: luyuhuang.tech
User-Agent: curl/7.68.0
Accept: */*


16:43:22.975713 IP luyuhuang.tech.http > 172.29.57.43.41858: Flags [.], ack 79, win 509, options [nop,nop,TS val 1839405541 ecr 2356831951], length 0
E..4..@.1...+..+..9+.P..v.0................
m....zf.
16:43:22.975715 IP luyuhuang.tech.http > 172.29.57.43.41858: Flags [P.], seq 1:368, ack 79, win 509, options [nop,nop,TS val 1839405542 ecr 2356831951], length 367: HTTP: HTTP/1.1 301 Moved Permanently
E.....@.1...+..+..9+.P..v.0..........7.....
m....zf.HTTP/1.1 301 Moved Permanently
Server: nginx/1.20.2
Date: Sat, 26 Nov 2022 08:43:22 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: https://luyuhuang.tech/

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.20.2</center>
</body>
</html>

16:43:22.975748 IP 172.29.57.43.41858 > luyuhuang.tech.http: Flags [.], ack 368, win 501, options [nop,nop,TS val 2356831964 ecr 1839405542], length 0
E..4..@.@.....9++..+...P....v.2............
.zf.m...

我们可以看到文本形式的 HTTP 请求 GET / HTTP/1.1, 接着是服务器发的 ACK. 然后是服务器发的响应报文 HTTP/1.1 301 Moved Permanently, 最后是客户端发的 ACK.

常用选项

tcpdump 的选项很多, 这里我们只介绍常用的一些选项. 其它的等真正要用的时候再去查 manual 或者 Google 也不迟.

  • -i INTERFACE 指定网络接口. 使用 -i any 表示抓取所有网络接口的数据.
  • -A 用 ASCII 以文本的形式打印数据包的内容, 不包括链路层头部. 适合抓取文本协议.
  • -X 同时以十六进制和文本的形式打印数据包的内容, 不包括链路层头部. 类型这样的格式:
      0x0000:  4500 0082 6aa1 4000 4006 27dd ac1d 392b  E...j.@.@.'...9+
      0x0010:  2b84 972b cf7e 0050 1b87 34d8 b04b de5b  +..+.~.P..4..K.[
      0x0020:  8018 01f6 a86c 0000 0101 080a 8cca db1f  .....l..........
      0x0030:  6df3 8eff 4745 5420 2f20 4854 5450 2f31  m...GET./.HTTP/1
      0x0040:  2e31 0d0a 486f 7374 3a20 6c75 7975 6875  .1..Host:.luyuhu
      0x0050:  616e 672e 7465 6368 0d0a 5573 6572 2d41  ang.tech..User-A
    

    适合抓取二进制协议.

  • -XX 同时以十六进制和文本的形式打印数据包的内容, 包括链路层头部.
  • -t 不在 dump line 打印时间.
  • -tt 以 UNIX 时间戳的格式打印时间.
  • -ttt 打印与上一个包之间的时间间隔.
  • -v 在 dump line 显示详细信息. 例如会显示 IP 分组的 ttl, id, 总长度; TCP 段的校验和等信息.
  • -vv 显示更详细的信息.
  • -vvv 显示更更详细的信息.
  • -n 不将地址转换成名称. 例如上面的例子显示服务器地址是 luyuhuang.tech.http, 如果指定 -n 就是显示 IP 地址和端口号 80.
  • -c COUNT 抓取指定数量的包, 达到这个数量自动退出.
  • -s SNAPLEN 抓取包的前 SNAPLEN 个字节, 默认为 262144. 根据需要适当调小这个值可以提升性能.
  • -# 打印出数据包的编号.
  • -w FILE 将原始包数据写入到指定的文件, 而不是在终端打印它们. 文件扩展名通常是 .pcap, 保存的数据可以随后使用 tcpdump 分析.
  • -r FILE 读取分析指定的 pcap 文件, 而不是抓取网络接口的数据. 下面是一个 -w-r 的使用例子:
      $ tcpdump -i eth0 -w luyu.pcap 'tcp && port 80'
      11 packets captured
      11 packets received by filter
      0 packets dropped by kernel
    
      $ tcpdump -r luyu.pcap -# -ttt 'dst port 80'  # 筛选发送到 80 端口的包
      reading from file luyu.pcap, link-type EN10MB (Ethernet)
          1   00:00:00.000000 IP 172.19.180.38.34716 > luyuhuang.tech.http: Flags [S], seq 3218407543, win 64240, options [mss 1460,sackOK,TS val 4127289318 ecr 0,nop,wscale 7], length 0
          2   00:00:00.026788 IP 172.19.180.38.34716 > luyuhuang.tech.http: Flags [.], ack 1418465742, win 502, options [nop,nop,TS val 4127289345 ecr 1941966167], length 0
          3   00:00:00.000199 IP 172.19.180.38.34716 > luyuhuang.tech.http: Flags [P.], seq 0:78, ack 1, win 502, options [nop,nop,TS val 4127289345 ecr 1941966167], length 78: HTTP: GET / HTTP/1.1
          4   00:00:00.028462 IP 172.19.180.38.34716 > luyuhuang.tech.http: Flags [.], ack 368, win 501, options [nop,nop,TS val 4127289374 ecr 1941966194], length 0
    

    值得一提的是 pcap 文件还可以被 wireshark 读取. 如果你喜欢用 wireshark 的图形界面查看抓包数据, 使用 -w 导出 pcap 文件是个不错的选择.

表达式语法

表达式告诉 tcpdump 抓取哪些报文, 它由一个或多个基本表达式组成, 支持用 &&, || 这样的布尔运算符组合它们. 基本表达式的格式为一个或多个修饰词 + ID. 修饰词是预定义的关键字, 如 tcp, host, port 等; ID 则是相应的值, 通常是数字, 地址或名字. 修饰词有三种

  1. 类型修饰词, 表示 ID 所指的类型. 它可以是 host, net, port, portrange 等. 例如 host localhost, net 128.3, port 20, portrange 6000-6008. 如果没有指定类型, 则默认是 host.
  2. 方向修饰词, 指定数据传输的方向. 可以是 srcdst. 因为类型字段通常会区分传输方向, 例如 IP 分组中有源地址和目标地址, TCP 报文段中有源端口和目标端口. 使用方向修饰词可以限定匹配那个方向的类型字段. 如果没有指定方向修饰词, 则匹配双向的类型字段.
  3. 协议修饰词, 指定协议. 可以是 tcp, udp, ip, ip6, arp, ether 等. 因为一些协议有相同的类型字段, 例如 TCP 和 UDP 都有端口. 使用协议修饰词可以限定抓取的协议. 如果没有指定协议修饰词, 则会抓取所有有这个类型字段的协议.

举几个基本表达式的例子

  • tcp: 抓取所有 TCP 协议数据.
  • port 20: 抓取 TCP 和 UDP 协议源端口或目标端口为 20 的数据. 因为没有指定协议, 且 TCP 和 UDP 有 port 字段, 于是抓取所有有 port 字段的协议; 又因为没有指定方向, 于是抓取双向的数据.
  • tcp dst port 80: 抓取目标端口为 80 的 TCP 数据. 这里有协议修饰词限定只会抓取 TCP, 方向修饰词 dst 限定匹配目标端口.

基本表达式可以用逻辑运算符组合起来. tcpdump 的逻辑运算符有与, 或, 非, 可以写作 &&, ||!, 或者 and, ornot. 可以用括号改变运算优先级, 例如 host luyuhuang.tech && (port 80 || port 443).

在组合表达式中, 有时可以省略修饰词. 如果一个基本表达式只提供 ID 而没有修饰词, 则认为它的修饰词与前一个基本表达式相同. 例如表达式 port 22 or 80 or 443, 其中 80443 没有修饰词, 则认为它们的修饰词为 port. 因此这个表达式等价于 port 22 or port 80 or port 443.

下面列出一些修饰词的用法

  • dst host HOST: 匹配 IPv4 和 IPv6 协议目标地址为 HOST 的分组. HOST 可以是 IP 地址或者名字.
  • src host HOST: 匹配 IPv4 和 IPv6 协议源地址为 HOST 的分组.
  • ip src host HOST: 匹配 IPv4 协议源地址为 HOST 的分组.
  • host HOST: 匹配 IPv4 和 IPv6 协议源地址或目标地址为 HOST 的分组.
  • ether host EHOST: 匹配以太网协议源地址或目标地址为 EHOST 的帧. 这里的 EHOST 是 MAC 地址.
  • net NET/LEN: 匹配 IPv4 和 IPv6 协议源地址或目标地址的网络号为 NET/LEN 的分组. 例如 net 192.168.1.1/16 匹配地址前缀 192.168.
  • tcp port PORT: 匹配 TCP 协议源端口或目标端口为 PORT 的报文.
  • tcp src port PORT: 匹配 TCP 协议源端口为 PORT 的报文.
  • port PORT: 匹配 TCP 和 UDP 协议源端口或目标端口为 PORT 的报文
  • portrange PORT1-PORT2: 匹配 TCP 和 UDP 协议端口范围在 PORT1PORT2 之间的报文.
  • ip proto PROTOCOL: 匹配 IPv4 的协议号为 PROTOCOL 的分组. PROTOCOL 可以是一个表示协议号的数字, 例如 6 表示 TCP, 17 表示 UDP; 或者是一个协议名, 可选的值有 icmp, icmp6, igmp, igrp, pim, ah, esp, vrrp, udp, 或 tcp. 注意 icmp, tcpudp 是关键字, 要使用反斜杠 \ 转义, 如 \tcp.
  • ip6 proto PROTOCOL: 匹配 IPv6 的协议号 (在 IPv6 中其实是 next header) 为 PROTOCOL 的分组.
  • proto PROTOCOL: 匹配 IPv4 和 IPv6 的协议号为 PROTOCOL 的分组.
  • tcp, udpicmp: 其实是 proto \tcp, proto \udpproto \icmp 的简称. 因为这三个协议太常用了, tcpdump 提供了这三个简称.

可以认为一个基本表达式就是在表达某层协议某个字段值为多少. 清楚这一点就很容易理解 tcpdump 的语法了.

   ip    src host   192.168.1.1
|------|----------|--------------|
  协议:    字段:         值:
  TCP     源地址     192.168.1.1

   tcp   dst port     8080
|------|----------|-----------|
  协议:    字段:        值:
  TCP     目标端口     8080

   ip     proto     igmp
|------|---------|-----------|
  协议:    字段:      值:
   IP     协议号     IGMP(2)

高级用法

tcpdump 还支持比较协议中的某些字节, 抓取满足条件的报文. tcpdump 提供一种称为包数据访问器 (packet data accessor) 的语法, 用于获取指定的字节:

PROTO [ POS : SIZE ]

PROTO 表示协议, 可以是 ether, ppp, ip, arp, rarp, tcp, udp, icmp, ip6 等; POS 表示自这层协议起始第几个字节; SIZE 表示在这个位置取几个字节, 其值可以是 1, 2 或 4. 若省略 SIZE 则表示取一个字节. 包数据访问器的值为一个 32 位无符号整数.

包数据访问器可以执行一些算术运算 (+, -, *, /, %, &, |, ^, <<, >>), 然后执行比较运算 (>, <, >=, <=, =, !=). 例如:

  • ip[0] & 0xf != 5 表示抓取所有没有选项的 IP 分组. 因为 IPv4 协议的 4 至 7 位, 也就是第一个字节的低 4 位为首部长度, 如果首部长度不为 5, 说明首部有选项数据.
  • ip[6:2] & 0x1fff = 0 表示抓取分片偏移字段为 0 的 IP 分组.
  • tcp[((tcp[12] & 0xf0) >> 4) * 4] == 42 表示抓取 TCP 载荷的第一个字节等于 42 的报文段. 因为 TCP 首部的第 12 字节的高 4 位为 Data offset 字段, 表示 TCP 首部有多少个字 (word). 这里使用 tcp[12] & 0xf0) >> 4 取到 Data offset 字段, 再乘以 4, 因为一个字为 4 字节. 这样 tcp[((tcp[12] & 0xf0) >> 4) * 4] 就取到 TCP 载荷的第一个字节.

例子

这里举一些常用的例子.

  • tcp port 80 抓取源端口或目标端口为 80 的 TCP 报文.
  • tcp && host luyuhuang.tech && (port 80 || 443) 抓取源地址或目标地址为 luyuhuang.tech, 且源端口或目标端口为 80 或 443 的 TCP 报文.
  • icmp && src host 172.27.211.226 && dst host 172.27.208.1 抓取 172.27.211.226 发给 172.27.208.1 的 ICMP 包.
  • tcp && host 172.27.211.226 && ! port 22 抓取与 172.27.211.226 的 TCP 通信, 端口不为 22 的包.
  • tcp[((tcp[12] & 0xf0) >> 4) * 4 : 4] == 0x47455420 && tcp dst port 80 抓取 HTTP GET 请求. tcp[((tcp[12] & 0xf0) >> 4) * 4 : 4] 取到 TCP 载荷的前 4 个字节, 而 0x47455420 其实是 "GET " 这四个字符:

      0x47455420 == 'G' << 24 | 'E' << 16 | 'T' << 8 | ' '
    

扩展阅读

以上的内容基本上足够举一反三, 了解 tcpdump 的使用. 如果想知道更多选项的用法, 可以参考 man tcpdump; 如果需要深入学习表达式, 可以参考 man pcap-filter.