今天在使用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 条评论