tcpdump的使用

完整的英文文档
https://www.tcpdump.org/tcpdump_man.html

  • -A 以ASCII格式打印出所有分组,并将链路层的头最小化。 
  • -c 在收到指定的数量的分组后,tcpdump就会停止。 
  • -C 在将一个原始分组写入文件之前,检查文件当前的大小是否超过了参数file_size 中指定的大小。如果超过了指定大小,则关闭当前文件,然后在打开一个新的文件。参数 file_size 的单位是兆字节(是1,000,000字节,而不是1,048,576字节)。 
  • -d 将匹配信息包的代码以人们能够理解的汇编格式给出。 
  • -dd 将匹配信息包的代码以C语言程序段的格式给出。 
  • -ddd 将匹配信息包的代码以十进制的形式给出。 
  • -D 打印出系统中所有可以用tcpdump截包的网络接口。 
  • -e 在输出行打印出数据链路层的头部信息。 
  • -E 用spi@ipaddr algo:secret解密那些以addr作为地址,并且包含了安全参数索引值spi的IPsec ESP分组。 
  • -f 将外部的Internet地址以数字的形式打印出来。 
  • -F 从指定的文件中读取表达式,忽略命令行中给出的表达式。 
  • -i 指定监听的网络接口。 
  • -l 使标准输出变为缓冲行形式,可以把数据导出到文件。 
  • -L 列出网络接口的已知数据链路。 
  • -m 从文件module中导入SMI MIB模块定义。该参数可以被使用多次,以导入多个MIB模块。 
  • -M 如果tcp报文中存在TCP-MD5选项,则需要用secret作为共享的验证码用于验证TCP-MD5选选项摘要(详情可参考RFC 2385)。 
  • -b 在数据-链路层上选择协议,包括ip、arp、rarp、ipx都是这一层的。 
  • -n 不把网络地址转换成名字。 
  • -nn 不进行端口名称的转换。 
  • -N 不输出主机名中的域名部分。例如,‘nic.ddn.mil‘只输出’nic‘。 
  • -t 在输出的每一行不打印时间戳。 
  • -O 不运行分组分组匹配(packet-matching)代码优化程序。 
  • -P 不将网络接口设置成混杂模式。 
  • -q 快速输出。只输出较少的协议信息。 
  • -r 从指定的文件中读取包(这些包一般通过-w选项产生)。 
  • -S 将tcp的序列号以绝对值形式输出,而不是相对值。 
  • -s 从每个分组中读取最开始的snaplen个字节,而不是默认的68个字节。 
  • -T 将监听到的包直接解释为指定的类型的报文,常见的类型有rpc远程过程调用)和snmp(简单网络管理协议;)。 
  • -t 不在每一行中输出时间戳。 
  • -tt 在每一行中输出非格式化的时间戳。 
  • -ttt 输出本行和前面一行之间的时间差。 
  • -tttt 在每一行中输出由date处理的默认格式的时间戳。 
  • -u 输出未解码的NFS句柄。 
  • -v 输出一个稍微详细的信息,例如在ip包中可以包括ttl和服务类型的信息。 
  • -vv 输出详细的报文信息。 
  • -w 直接将分组写入文件中,而不是不分析并打印出来。

报文获取

如果那样干巴巴地讲这个东西比较晕,而且网上的文章一大堆,没有什么创新。我选择换一个角度来切入 TCP/IP 协议。首先通过 tcpdump 准备报文。
【1】我在192.168.1.22这台机器的10000端口启一个redis服务。
【2】通过 tcpdump 这个工具来抓取数据包,命令如下:

tcpdump -w /tmp/logs -i eth0 port 10000 -s0

【3】在192.168.1.26这台机器上访问192.168.1.22:10000这个 redis 实例,可以用redis-cli客户端,也可以用telnet,发送一个ping, 得到对端回复pong
【4】停止抓包,用 tcpdump 读取这个数据包(-x以16进制形式展示,便于后面分析)

tcpdump -r /tmp/logs -n -nn -A -x| vim -

其中有一个数据包是这样的,这也是这篇文章要分析的:

10:54:54.270967 IP 192.168.1.26.61096 > 192.168.1.22.10000: Flags [P.], seq 1041414875:1041414889, ack 658186233, win 115, options [nop,nop,TS val 2377448931 ecr 2741547141], length 14
        0x0000: [4560 0042   7567 0000  3d06 6F3C C0A8 011A
        0x0010:  C0A8 0116] {eea8 2710  3e12 badb 273b 1ff9
        0x0020:  8018 0073   64b0 0000  0101 080a 8db4 fde3
        0x0030:  a368 b085}  2a31 0d0a  2434 0d0a 7069 6e67
        0x0040:  0d0a

注意:
【1】之前在文章常用 shell中介绍过抓包神器tcpdump,还不会的小伙伴可以偷瞄一眼。
【2】上面报文数据中的[]{}是为了方便区分数据,我自己加上的。[]包围的部分为本报文中的 IP 头,{}包围的部分为本报文中的 TCP 头。

报文分析

IP 报文整体结构如下,因为抓到的数据包是redis服务,因此在传输层为 TCP 协议。
ip报文结构

IP 层解析

解析数据包之前,先把 IP 协议拿出来,如下:
ip协议
可以看到,IP 报文头部采用固定长度(20B) + 可变长度构成,下面的 TCP 头部也是这样。
然后下面对着抓到的数据包进行分析:
【1】0x44bit,ip 协议版本
0x4表示 IPv4。
【2】0x54bit,ip首部长度
该字段表示单位是32bits(4字节) ,所以这个 ip 包的头部有5*4=20B,这就可以推出,该 IP 报文头没有可选字段。4bit 可以表示最大的数为 0xF,因此,IP 头部的最大长度为15*4=60B。该报文的 IP 头部我已经在报文中标注出来了。
【3】0x608bit,服务类型 TOS
该段数据组成为 3bit 优先权字段(现已被忽略) + 4bit TOS 字段 + 1bit 保留字段(须为0)。
4bit TOS 字段分别表示自小时延、最大吞吐量、最高可用性和最小费用。只能置其中 1bit,全为 0 表示一般服务。现在大多数的TCP/IP实现都不支持TOS特性。可以看到,本报文 TOS 字段为全 0。
【4】0x004216bit,IP 报文总长度
单位字节,换算下来,该数据报的长度为 66 字节,数一下上面的报文,恰好 66B。
从占位数来算, IP 数据报最长为2^16=65535B,但大部分网络的链路层 MTU(最大传输单元)没有这么大,一些上层协议或主机也不会接受这么大的,故超长 IP 数据报在传输时会被分片。
【5】0x756716bit,标识
唯一的标识主机发送的每一个数据报。通常每发送一个报文,它的值+1。当 IP 报文分片时,该标识字段值被复制到所有数据分片的标识字段中,使得这些分片在达到最终目的地时可以依照标识字段的内容重新组成原先的数据。
【6】0x00003bit标志+ 13bit片偏移
3bit 标志对应 R、DF、MF。目前只有后两位有效,DF位:为1表示不分片,为0表示分片。MF:为1表示“更多的片”,为0表示这是最后一片。
13bit 片位移:本分片在原先数据报文中相对首位的偏移位。(需要再乘以8)
【7】0x3d8bit生存时间TTL
IP 报文所允许通过的路由器的最大数量。每经过一个路由器,TTL减1,当为 0 时,路由器将该数据报丢弃。TTL 字段是由发送端初始设置一个 8 bit字段.推荐的初始值由分配数字 RFC 指定。发送 ICMP 回显应答时经常把 TTL 设为最大值 255。TTL可以防止数据报陷入路由循环。本报文该值为 61。
【8】0x068bit协议
指出 IP 报文携带的数据使用的是哪种协议,以便目的主机的IP层能知道要将数据报上交到哪个进程。TCP 的协议号为6,UDP 的协议号为17。ICMP 的协议号为1,IGMP 的协议号为2。该 IP 报文携带的数据使用 TCP 协议,得到了验证。
【9】0x6F3C16bitIP 首部校验和
由发送端填充。以本报文为例,先说这个值是怎么计算出来的。

# 将校验和字段 16bit 值抹去变为 `0x0000`,然后将首部 20字节值相加
0x4560 + 0x0042 + 0x7567 + 0x0000 + 0x3d06 + 0x0000 + 0xC0A8 + 0x011A + 0xC0A8 +0x0116 = 0x27B95

# 将上述结果的进位 2 与低 16bit 相加
0x7B95 + 0x2 = 0x7B97

# 0x7B97 按位取反
~(0x7B97) = 0x8468

结果0x8468即为该字段值!
接收端验证的时候,进行以下计算

# 20B 首部值相加
0x27B95 + 0x8468 = 0x2FFFD

# 将上述结果的进位 2 与低 16bit 相加
0xFFFD + 0x2 = 0xFFFF

# 0xFFFF 按位取反
~(0xFFFF) = 0  <-- 正确

【10】0xC0A8011A32bit 源地址
可以通过一下 python 程序将 hex 转换成我们熟悉的点分 IP 表示法

>>> import socket
>>> import struct
>>> int_ip=int("0xC0A8011A",16)
>>> socket.inet_ntoa(struct.pack('I',socket.htonl(int_ip)))
'192.168.1.26'

本报文中的 src addr 为192.168.1.26,恰好就是发起请求的 IP。
【11】0xC0A8011632bit 目的地址
经过计算为192.168.1.22,恰好就是启 redis 服务那台机器的 IP。


由于该报文首部长度为 20B,因此没有可变长部分

传输层解析

本报文携带的数据使用的 TCP 协议,因此下面开始分析 TCP 协议。
与上面的 IP 报文一样, TCP 报文头也才用采用固定长度(20B) + 可变长度的形式。
首先还是看 TCP 协议的格式,从网上找了一张图,如下:
TCP报文格式
注:TCP 的头部必须是 4字节的倍数,而大多数选项不是4字节倍数,不足的用NOP填充。
【1】0xeea816bit,源端口
解析得到 61096,这与 tcpdump 读包显示的是一致的。16bit 决定了端口号的最大值为 65535.
【2】0x271016bit,目的端口
解析得到 10000。
【3】0x273b1ff932bit,序号
解析得到 1041414875,这与上面 tcpdump 显示的seq段是一致的。
【4】0x273b1ff932bit,确认号
解析得到 658186233,这与上面 tcpdump 显示的ack段是一致的。
【5】0x84bit,TCP 报文首部长度
也叫 offset,其实也就是数据从哪里开始。8 * 4 = 32B,因此该 TCP 报文的可选部分长度为32 - 20 = 12B,这个资源还是很紧张的! 同 IP 头部类似,最大长度为60B
【6】0b0000006bit,保留位
保留为今后使用,但目前应置为 0。
【7】0b0110006bit,TCP 标志位
上图可以看到,从左到右依次是紧急 URG、确认 ACK、推送 PSH、复位 RST、同步 SYN 、终止 FIN。
从抓包可以看出,该报文是带了 ack 的,所以 ACK 标志位置为 1。关于标志位的知识这里就不展开了。
【8】0x007316bit,滑动窗口大小
解析得到十进制 115,跟 tcpdump 解析的win字段一致。
【9】0x64b016bit,校验和
由发送端填充,接收端对 TCP 报文段执行 CRC 算法,以检验 TCP 报文段在传输过程中是否损坏,如果损坏这丢弃。
检验范围包括首部和数据两部分,这也是 TCP 可靠传输的一个重要保障。
【10】0x000016bit,紧急指针
仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数。
当 URG = 1 时,发送方 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。


下面是 TCP 可选项,其格式如下:
tcp option
常见的可选项如下图:
tcp常见可选项
【11】0x01
NOP 填充,没有 Length 和 Value 字段, 用于将TCP Header的长度补齐至 32bit 的倍数。
【12】0x01
同上。
【13】0x080a
可选项类型为时间戳,len为 10B,value 为0x8db4 0xfde3 0xa368 0xb085,加上0x080a,恰好 10B!
启用 Timestamp Option后,该字段包含2 个 32bit 的Timestamp(TSval 和 TSecr)。
【14】0x8db4 0xfde3
解析后得到 2377448931,恰好与 tcpdump 解析到的 TS 字段的val一致!
【15】0xa368 0xb085
解析后得到 2741547141,恰好与 tcpdump 解析到的 TS 字段的ecr一致!

数据部分解析

上面分析得知,该 IP 报文长度为 66B,IP 头长度为 20B,TCP 头部长度为 32B,因此得到数据的长度为66 - 20 - 32 = 14B,这与 tcpdump 解析到的len字段一致!下面来分析这个具体的数据。
这里涉及到 redis 协议,不知道的小伙伴可以查看这篇文档redis 协议说明
在抓包时,用客户端向 redis 服务端发送了一个ping命令,转换成 redis 协议如下:

*1\r\n
$4\r\n
ping\r\n

下面看抓包数据解析,这需要对照 ascii 码表来看,在 linux 下可以用man 7 ascii这个命令来获得,或者在这里查看ascii码表

0x2a31         -> *1
0x0d0a         -> \r\n
0x2434         -> $4
0x0d0a         -> \r\n
0x7069 0x6e67  -> ping
0x0d0a         -> \r\n

tcpdump 补充

既然详细说到 TCP/IP 协议,那补充一下 tcpdump filter 的几点用法。
filter可以简单地分为三类:type,dirproto

type 区分报文的类型,主要由 host(主机), net(网络,支持 CIDR) 和 port(支持范围,如 portrange 21-23) 组成。
dir 区分方向,主要由 src 和 dst 组成。
proto 区分协议支持 tcp、udp 、icmp 等。

下面说几个 filter 表达式。
proto[x:y]start at offset x into the proto header and read ybytes
[x]abbreviation for[x:1]
注意:单位是字节,不是位!
举几个栗子:
【1】打印 80 端口,有数据的 tcp 包

tcpdump 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

ip[2:2]从 ip 报文的第3个字节开始读2个字节,这个恰好就是 ip 包的总长度,单位是字节
ip[0]&0xf取的是 ip 报文第 1 个字节的低 4 位,<< 2(乘以 4),为 ip 头部长度,单位是字节
tcp[12]&0xf0取的是 tcp 报文第 13 个字节的高 4 位,>> 2其实等价于>> 4然后<< 2,为 tcp 头部长度,单位是字节。
所以((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2))表示的数据长度。

分解:
1) 获取tcp报文头长度:tcp[12:1]&0xf0 >> 2

首先,因为tcp 报文的data-offset(数据偏移)字段长度为4位,我们取 data-offset所在字节,并AND 0xf0取数据偏移位,即 tcp[12:1]&0xf0

其次,因为data-offset字段位于字节高位,帮右移4位后才是真实的数据长度:tcp[12:1]&0xf0 >> 4

最后,因为data-offset字段单位为32位字(1字长为4字节),因此需要将结果乘以4(左移2位),因此得 tcp[12:1]&0xf0 >> 4 << 2,得到最后结果为:tcp[12:1] & 0xf0) >> 2
2) 获取tcp报文内容头4字节:

‘GET ’ = 0x47455420

‘POST’ = 0x504f5354

查ASCII表:

0x47:‘G’

0x45:‘E’

0x54:‘T’

0x20:’空格‘

【2】打印 80 端口,长度超过 576 的 ip 包

tcpdump 'port 80 and ip[2:2] > 576'

【3】打印特定 TCP Flag 的数据包
TCP Flags 在 tcpdump 抓取的报文中的体现:
[S]:SYN(开始连接)
[.]: 没有 Flag
[P]: PSH(推送数据)
[F]: FIN (结束连接)
[R]: RST(重置连接)
[S.]SYN-ACK,就是 SYN 报文的应答报文。

tcpdump 'tcp[13] & 16!=0'
# 等价于
tcpdump 'tcp[tcpflags] == tcp-ack'

打印出所有的 ACK 包。

tcpdump 'tcp[13] & 4!=0'

# 等价于
tcpdump 'tcp[tcpflags] == tcp-rst'

打印出所有的 RST 包,即包含[R]标志的包。

更多 tcpdump filter 可以查看PCAP-FILTER或者man tcpdump

好了,这个 IP 包的解析就到此为止了,照着 TCP/IP 协议分析了一遍, 发现协议也就那么回事儿,没有想象的那么难,不要害怕协议!
【4】只抓取有数据的HTTP报文内容
tcpdump 'tcp port 80 and(((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

1) 从IP报文中获取I报文总长度: ip[2:2]

2) 从IP报文中获取IP报文头长度: (ip[0]&0xf)<<2

3) 从TCP报文中获取TCP报文头长度:(tcp[12]&0xf0)>>2

4) 计算数据包长度:报文总长度-IP报文头长度-TCP报文头长度

5) 获取HTTP数据包长度非空的报文:(((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

参考

【1】常用的TCP Option
【2】IP报文格式详解
【3】TCP 报文结构


君临天下
0 声望0 粉丝