key: 70 title: 使用 tcpdump 抓包 tags: [tools, network]
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 则是相应的值, 通常是数字, 地址或名字. 修饰词有三种
host
, net
, port
, portrange
等. 例如 host localhost
, net 128.3
, port 20
, portrange 6000-6008
. 如果没有指定类型, 则默认是 host
.src
或 dst
. 因为类型字段通常会区分传输方向, 例如 IP 分组中有源地址和目标地址, TCP 报文段中有源端口和目标端口. 使用方向修饰词可以限定匹配那个方向的类型字段. 如果没有指定方向修饰词, 则匹配双向的类型字段.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
, or
和 not
. 可以用括号改变运算优先级, 例如 host luyuhuang.tech && (port 80 || port 443)
.
在组合表达式中, 有时可以省略修饰词. 如果一个基本表达式只提供 ID 而没有修饰词, 则认为它的修饰词与前一个基本表达式相同. 例如表达式 port 22 or 80 or 443
, 其中 80
和 443
没有修饰词, 则认为它们的修饰词为 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 协议端口范围在 PORT1
和 PORT2
之间的报文.ip proto PROTOCOL
: 匹配 IPv4 的协议号为 PROTOCOL
的分组. PROTOCOL
可以是一个表示协议号的数字, 例如 6 表示 TCP, 17 表示 UDP; 或者是一个协议名, 可选的值有 icmp
, icmp6
, igmp
, igrp
, pim
, ah
, esp
, vrrp
, udp
, 或 tcp
. 注意 icmp
, tcp
和 udp
是关键字, 要使用反斜杠 \
转义, 如 \tcp
.ip6 proto PROTOCOL
: 匹配 IPv6 的协议号 (在 IPv6 中其实是 next header) 为 PROTOCOL
的分组.proto PROTOCOL
: 匹配 IPv4 和 IPv6 的协议号为 PROTOCOL
的分组.tcp
, udp
和 icmp
: 其实是 proto \tcp
, proto \udp
和 proto \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
.