tcpdump 命令TCP Flag筛选表达式的解析

今天在使用tcpdump命令筛选SYN包的过程中,突然对tcpdump筛选的参数感觉到好奇:为什么是这样一个形式?

[root@mgt ~]# tcpdump -i ens192 tcp[13] == 2
22:33:13.596472 IP QianPC.lan.55210 > mgt.ssh: Flags [S], seq 4249441509, win 64240, options [mss 1460,nop,wscale 8,sackOK,TS val 823987189 ecr 0], length 0
22:33:14.064380 IP QianPC.lan.55211 > mgt.ssh: Flags [S], seq 2902570129, win 64240, options [mss 1460,nop,wscale 8,sackOK,TS val 823987656 ecr 0], length 0

为什么使用tcp[13] == 2就能表示筛选SYN包呢?

[root@mgt ~]# tcpdump -i ens192 'tcp[13] & 2 == 2'
22:41:05.704482 IP mgt.35016 > 14.215.177.39.http: Flags [S], seq 355247955, win 29200, options [mss 1460,sackOK,TS val 3577377865 ecr 0,nop,wscale 7], length 0
22:41:05.709742 IP 14.215.177.39.http > mgt.35016: Flags [S.], seq 2076010157, ack 355247956, win 8192, options [mss 1452,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 5], length 0

为什么使用tcp[13] & 2 == 2就能表示筛选SYN、SYN+ACK包呢?

解析

回忆一下TCP报文头结构

    0                            15                              31
   -----------------------------------------------------------------
   |             源端口             |            目的端口           |
   -----------------------------------------------------------------
   |                            序列号                              |
   -----------------------------------------------------------------
   |                            确认号                              |
   -----------------------------------------------------------------
   | 头部长 |  保留  |    标识符    |           窗口大小            |
   -----------------------------------------------------------------
   |             校验码             |           紧急指针            |
   -----------------------------------------------------------------
   |                         选项(长度可变)                         |

一个TCP报文头标准长度为20字节,上图中第一行是源端口和目的端口,占据第0 – 3个字节。 第二行是序列号,占据第4 – 7个字节;同理,第3行为确认号,占据了第8-11个字节。

接下来我们关注第4行的内容,这里是TCP报文头的第12-15个字节。

为了美观,我还是沿用英文缩写绘图。

    0             7|             15|             23|             31
   ----------------|---------------|---------------|----------------
   |  HL   | rsvd  |C|E|U|A|P|R|S|F|        window size            |
   ----------------|---------------|---------------|----------------
   |  12th octet   |  13th octet   |  14th octet   |  15th octet   |

第0 – 3位是头部长度,因为选项字段的长度是可变的。

第4 – 7位是保留字段,注意,国内有些教材会把保留字段写为6位长度。 我这边参照的是 《TCP/IP详解 卷1》中的描述。

第8 – 15位是标识,我们后面将展开说明。

第16 – 31位是窗口大小,用于流量控制。

接下来把目光聚焦在第8 – 15位,也就是整个TCP头部的第13字节:

                   |               |
                   |---------------|
                   |C|E|U|A|P|R|S|F|
                   |---------------|
                   |7   5   3     0|

这边描述一下每一位的作用:

C = CWR 拥塞窗口减速标识,用于发送方降低发送速率

E = ECE ECN回显

U = URG 紧急指针,很少被使用

A = ACK 确认

P = PSH 推送,较少使用

R = RST 重置,重置TCP连接

S = SYN 初始化,用于初始化一个连接的同步序列号

F = FIN 结束,结束向对方发送数据

回到第13字节,我们从右往左排序,则FIN标识在第0位,CWR标识在第7位。那么,这时候看看一个SYN包的报文头是怎么样的:

                       |C|E|U|A|P|R|S|F|
                       |---------------|
                       |0 0 0 0 0 0 1 0|
                       |---------------|
                       |7 6 5 4 3 2 1 0|

可以看到,SYN标识位的值被设置为1,其他标识位都是0。那么第13字节的二进制值就可以表示为:

                       00000010

那么,第13字节的十进制就可以表示为:

          7      6       5       4       3       2       1       0
       0*2^7 + 0*2^6 + 0*2^5 + 0*2^4 + 0*2^3 + 0*2^2 + 1*2^1 + 0*2^0  =  2

那么,到这里我们的思路就大致清晰了: 当只设置SYN标识为1时,TCP报文头的第13字节十进制值就为2。于是有了以下表达式:

                   tcpdump -i ens192 'tcp[13] == 2'

这个命令表明,我要过滤出经过ens192这个网卡,所有TCP报文头第13字节值为2的包。

延伸

从上文出发,我们再延伸思考一下tcp[13] & 2 == 2 这个表达式的内容。

我们知道,SYN标识被设置为1的包,不仅仅有SYN包,还有SYN-ACK包。

我们来看看一个SYN-ACK包的第13字节是什么样的:

            |C|E|U|A|P|R|S|F|
            |---------------|
            |0 0 0 1 0 0 1 0|
            |---------------|
            |7 6 5 4 3 2 1 0|

可以看到,ACK位和SYN位都被设置成了1,其余都是0。

同样,它的二进制表示为:

            00010010

那么就可以得到对应的十进制值:

          7      6       5       4       3       2       1       0
       0*2^7 + 0*2^6 + 0*2^5 + 1*2^4 + 0*2^3 + 0*2^2 + 1*2^1 + 0*2^0  =  18

假设我们现在想要关注的是所有SYN为1,同时不在乎ACK是否为1的包,那么我们该如何设置呢?

如果使用 tcp[13] == 2 ,我们只能得到SYN包; 如果使用 tcp[13] == 18 ,我们只能得SYN-ACK包。

为了达到目标,我们需要某种方式处理这两种包,得到一个共性的表达式。

既然我们只关注SYN位为1,那么我们让两种报文包同 00000010 相与 :

                 00010010 SYN-ACK              00000010 SYN
            AND  00000010 (we want SYN)   AND  00000010 (we want SYN)
                 --------                      --------
            =    00000010                 =    00000010

可以看到,经过与操作之后,不管ACK位是否设置为1,我们都得到了同样的结果。

由此,可以得出这样的十进制表达式:

( ( 第13字节的值 ) AND ( 2 ) ) == ( 2 )

即:

tcp[13] & 2 == 2

所以,tcpdump -i ens192 tcp[13] & 2 == 2 就表示经过ens192这个网卡,所有包含SYN的包。

结尾

至此,我们就了解了tcpdump筛选特定包的表达式的含义。 如果man一下tcpdump的文档,还可以发现tcpdump不仅支持这种数字的表示方式,同样也支持字段名的表示方式。 比如上面的表示式就可以表示为 tcp[tcpflags] & 2 == 2 或者 tcp[tcpflags] & tcp-syn == 2 。 但我觉得,这样的方式反而不如数字来得直接,只要理解了其中的原理,就能快速过滤出想要的结果。

0 条评论
发表一条评论