使用 tcpdump 抓包
tcpdump 是一个很实用的抓包工具. 一直以来我都只是复制网上的常用命令, 对其使用逻辑缺乏理解. 最近我仔细阅读了它的 manual, 总结一下 tcpdump 的用法.
命令格式
如果使用 tcpdump --help
查看它的使用方法, 总是会得到一大堆参数选项, 至于如何使用还是一头雾水. tcpdump 的用法实际是这样的:
1 |
|
tcpdump 会读取网络中的数据, 解析协议, 然后与表达式相匹配. 如果能匹配上, 则用指定的方式输出数据包的内容. 选项则用于指定如何从网络中读取数据 (如指定网络接口) 以及如何输出抓取到的数据.
在深入了解选项和表达式语法前, 先看个简单的例子. 选项 -A
表示用 ASCII 以文本的形式打印数据包的内容, -i
指定网络接口; 表达式 tcp && port 80
表示抓取协议为 tcp
, 且端口为 80
的数据包.
1 |
|
这个使用如果我们在这个机器上执行 curl http://luyuhuang.tech
就可以看到 tcpdump 打印出:
1 |
|
上面是 TCP 的三次握手. 每个包会先打印一行基础信息, 称为 “dump line”, 如当前时间, 通讯双方的 IP 地址和端口, TCP 的标志位, 序列号, 以及选项等内容. 接下来是包体内容, 以文本形式打印. 如果不是 ASCII 码则打印为 .
. 接下来就是 HTTP 请求:
1 |
|
我们可以看到文本形式的 HTTP 请求 GET / HTTP/1.1
, 接着是服务器发的 ACK. 然后是服务器发的响应报文 HTTP/1.1 301 Moved Permanently
, 最后是客户端发的 ACK.
常用选项
tcpdump 的选项很多, 这里我们只介绍常用的一些选项. 其它的等真正要用的时候再去查 manual 或者 Google 也不迟.
-i INTERFACE
指定网络接口. 使用-i any
表示抓取所有网络接口的数据.-A
用 ASCII 以文本的形式打印数据包的内容, 不包括链路层头部. 适合抓取文本协议.-X
同时以十六进制和文本的形式打印数据包的内容, 不包括链路层头部. 类型这样的格式:适合抓取二进制协议.1
2
3
4
5
60x0000: 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
的使用例子:1
2
3
4
5
6
7
8
9
10
11$ 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 则是相应的值, 通常是数字, 地址或名字. 修饰词有三种
- 类型修饰词, 表示 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 的语法了.
1 |
|
高级用法
tcpdump 还支持比较协议中的某些字节, 抓取满足条件的报文. tcpdump 提供一种称为包数据访问器 (packet data accessor) 的语法, 用于获取指定的字节:
1 |
|
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 "
这四个字符:1
0x47455420 == 'G' << 24 | 'E' << 16 | 'T' << 8 | ' '
扩展阅读
以上的内容基本上足够举一反三, 了解 tcpdump 的使用. 如果想知道更多选项的用法, 可以参考 man tcpdump
; 如果需要深入学习表达式, 可以参考 man pcap-filter
.