tcpdump笔记以及三次握手和四次关闭

参数笔记

  • -n 一个n表示不反向解析地址,两个n表示不反向解析端口。
  • -S 表示查看绝对序号,如客户端中的ack为1,这是相对的,要查看绝对序号需要加上该参数。
  • -v 表示查看详细的报文数据。
  • -r 该参数接收一个pcap文件的路径,不抓取数据包,而是去读取指定的pcap文件,并格式化输出。
  • -i 指定抓取的网卡,any表示监听全部。
  • -e 每行的打印输出中将包括数据包的数据链路层头部信息,我经常用它来看truck口的vlan id。
    详情请参考: http://linux.51yip.com/search/tcpdump

语法编写

所有经过目的或原地址是192.168.1.1的数据包

1
$ tcpdump host 192.168.1.1

源端口或者是目的端口为80的数据包

1
$ tcpdump port 80

所有目的地址为192.168.1.1的数据包

1
$ tcpdump dst host 192.168.1.1

相同的,所有目的端口为80的数据包为dst port

所有的源地址为192.168.1.1的数据包

1
$ tcpdump src host 192.168.1.1

相同的,所有源端口为80的数据包为src port

协议过滤

1
2
3
4
5
tcpdump arp
tcpdump ip
tcpdump tcp
tcpdump udp
tcpdump icmp

常用的逻辑表达式

  • 非:! 或者是:not
  • 且:&& 或者是:and
  • 或:|| 或者是:or

详情可以参考:https://linuxwiki.github.io/NetTools/tcpdump.html

抓取http数据包

抓取TCP说明

在讲如何抓取HTTP包之前应该先讲一下TCP的包头,不然等会直接去看肯定会懵逼。

TCP数据包详解图
TCP数据包详解图

注:上图中的位表示1bit

  • 源端口、目的端口号没什么好说的,2的16次方也就是65536。
  • 32为序列号,sequence number,保证网络传输数据的顺序性。
  • 32位确认号,acknowledgment number,用来确认确实有收到相关封包,内容表示期望收到下一个报文的序列号,用来解决丢包的问题。
  • 头部大小,4位,偏移量:最大值为0x0F,即15,单位为32位(bit),也就是4字节,所以TCP的头部最大为15*4=60字节。
  • Reserved 4位 ,预留字段,都为0
  • TCP,标识段位
    • CWR:Congestion window reduced,拥塞窗口减少。拥塞窗口减少标志被发送主机设置,用来表明它接收到了设置ECE标志的TCP包。拥塞窗口是被TCP维护的一个内部变量,用来管理发送窗口大小。
    • ECN-Echo:显式拥塞提醒回应。当一个IP包的ECN域被路由器设置为11时,接收端而非发送端被通知路径上发生了拥塞。ECN使用TCP头部来告知发送端网络正在经历拥塞,并且告知接收端发送段已经受到了接收端发来的拥塞通告,已经降低了发送速率。
    • URG:为1时,紧急指针(urgent pointer)有效,配合紧急指针使用
    • ACK:为1时,确认号有效
    • PSH: 为1时,接收方应该尽快将这个报文段交给应用层
    • RST:为1时,释放连接,重连。
    • SYN:为1时,发起一个连接。
    • FIN:为1时,关闭一个连接。
  • 16位窗口大小:占16bit。此字段用来进行流量控制,主要用于解决流控拥塞的问题。单位为字节数,这个值是本机期望一次接收的字节数。
  • 16位校验值: 占16bit。对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,并由目标端进行验证。
  • 16位紧急指针:占16bit。它是一个偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。
  • 32位Tcp选项:一般包含在三次握手中。

tcpdump flags 对应关系

由于tcpdump中tcp协议的flags不是直接以文件输出的,通常是以下方式出现,

1
Flags [S.]

下面直接指出了对应关系。

1
2
3
4
5
6
7
8
Unskilled =  URG  =  (Not Displayed in Flag Field, Displayed elsewhere) 
Attackers = ACK = (Not Displayed in Flag Field, Displayed elsewhere)
Pester = PSH = [P] (Push Data)
Real = RST = [R] (Reset Connection)
Security = SYN = [S] (Start Connection)
Folks = FIN = [F] (Finish Connection)
SYN-ACK = [S.] (SynAcK Packet)
[.] (No Flag Set)

说明,在上表格中[.]表示没有设置flag,但是经过我的测试,其实[.]表示ACK标志

三次握手四次关闭

看完了上面的TCP的介绍,下面我们拿三次握手做一次实验

console1

1
$ sleep 10 ;curl baidu.com

console2

1
$ tcpdump -nnn  -S -vvv -i ens33 'port 80 and host baidu.com'
1
2
02:55:40.348976 IP (tos 0x0, ttl 64, id 44247, offset 0, flags [DF], proto TCP (6), length 60)
172.16.117.132.56878 > 220.181.38.148.80: Flags [S], cksum 0x250d (incorrect -> 0x8119), seq 39940599, win 29200, options [mss 1460,sackOK,TS val 4294958368 ecr 0,nop,wscale 7], length 0

客户端发起建立连接请求,SYN标志为1,以及随机生成了一个值为39940599seq的值。

1
2
02:55:40.381298 IP (tos 0x0, ttl 128, id 10245, offset 0, flags [none], proto TCP (6), length 44)
220.181.38.148.80 > 172.16.117.132.56878: Flags [S.], cksum 0xbd69 (correct), seq 2621492164, ack 39940600, win 64240, options [mss 1460], length 0

服务端收到客户端发送的SYN包,返回带有SYN、ACK标志的包,包中还有一个为2621492164的seq的值,以及一个ack值为39940600,这里的ack的值就是上个包中,客户端发送的seq+1,下面的操作也是类似的。

1
2
02:55:40.381378 IP (tos 0x0, ttl 64, id 44248, offset 0, flags [DF], proto TCP (6), length 40)
172.16.117.132.56878 > 220.181.38.148.80: Flags [.], cksum 0x24f9 (incorrect -> 0x5e07), seq 39940600, ack 2621492165, win 29200, length 0

客户端收到服务端返回带有SYN、ACK标志的包,只后返回一个带有ACK标志的包,其中还有值为39940600的seq值,也就是服务器返回的包里面的ack的值,还有一个值为ack值为2621492165,这个也就是服务端返回的包的seq的值加一得来的。三次握手完毕。下面开始正常通讯。

1
2
3
4
5
6
02:55:40.381995 IP (tos 0x0, ttl 64, id 44249, offset 0, flags [DF], proto TCP (6), length 113)
172.16.117.132.56878 > 220.181.38.148.80: Flags [P.], cksum 0x2542 (incorrect -> 0xc0bf), seq 39940600:39940673, ack 2621492165, win 29200, length 73: HTTP, length: 73
GET / HTTP/1.1
User-Agent: curl/7.29.0
Host: baidu.com
Accept: */*

客户端发送带有HTTP协议的数据包,注意seq的值,,:前面的是服务器端返回的数据包ack的值,后面的是前面的值+数据包包体的长度,也是就39940600 + 73 =39940673,其实这种格式: seq 39940600:39940673并不是协议内是这样做的,这个只是tcpdump特意这样显示的,在协议中这次请求发送的seq的值还是39940600

1
2
02:55:40.382277 IP (tos 0x0, ttl 128, id 10246, offset 0, flags [none], proto TCP (6), length 40)
220.181.38.148.80 > 172.16.117.132.56878: Flags [.], cksum 0xd4dd (correct), seq 2621492165, ack 39940673, win 64240, length 0

服务端返回ack标志的数据包,告诉客户端,我收到了你发送的长度为73的数据包,注意一下ack的值变成了39940673=39940600+73(客户端发送数据包的长度)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
02:55:40.416335 IP (tos 0x0, ttl 128, id 10247, offset 0, flags [none], proto TCP (6), length 426)
220.181.38.148.80 > 172.16.117.132.56878: Flags [P.], cksum 0x6701 (correct), seq 2621492165:2621492551, ack 39940673, win 64240, length 386: HTTP, length: 386
HTTP/1.1 200 OK
Date: Fri, 03 Jan 2020 07:55:40 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Sat, 04 Jan 2020 07:55:40 GMT
Connection: Keep-Alive
Content-Type: text/html

<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>

这里服务器端返回HTTP Response数据包,再次强调一下,seq目前还是2621492165

1
2
02:55:40.416398 IP (tos 0x0, ttl 64, id 44250, offset 0, flags [DF], proto TCP (6), length 40)
172.16.117.132.56878 > 220.181.38.148.80: Flags [.], cksum 0x24f9 (incorrect -> 0x590c), seq 39940673, ack 2621492551, win 30016, length 0

客户端返回ACK标志的数据包,告诉服务端已经收到了长度为386的数据包,ack为2621492165+386=2621492551

下面就是HTTP请求完毕,关闭连接。

1
2
02:55:40.416919 IP (tos 0x0, ttl 64, id 44251, offset 0, flags [DF], proto TCP (6), length 40)
172.16.117.132.56878 > 220.181.38.148.80: Flags [F.], cksum 0x24f9 (incorrect -> 0x590b), seq 39940673, ack 2621492551, win 30016, length 0

通讯完毕,客户端发送FIN标志的数据包请求关闭连接。

1
2
02:55:40.417278 IP (tos 0x0, ttl 128, id 10248, offset 0, flags [none], proto TCP (6), length 40)
220.181.38.148.80 > 172.16.117.132.56878: Flags [.], cksum 0xd35b (correct), seq 2621492551, ack 39940674, win 64239, length 0

服务端发送数据包给客户端,表示收到客户端发来的FIN标志的数据包。seq的值为上一次ack的值+1,后面不再重复说明。

1
2
3
02:55:40.450349 IP (tos 0x0, ttl 128, id 10249, offset 0, flags [none], proto TCP (6), length 40)

220.181.38.148.80 > 172.16.117.132.56878: Flags [FP.], cksum 0xd352 (correct), seq 2621492551, ack 39940674, win 64239, length 0

服务端再次发送FIN和PSH标志的数据包。

1
2
02:55:40.450403 IP (tos 0x0, ttl 64, id 63792, offset 0, flags [DF], proto TCP (6), length 40)
172.16.117.132.56878 > 220.181.38.148.80: Flags [.], cksum 0x590a (correct), seq 39940674, ack 2621492552, win 30016, length 0

客户端发送数据包给客户端表示已经收到你的FIN和PSH标志的数据包,此时客户端和服务端正式关闭连接。

抓取GET请求

1
$ tcpdump 'tcp[(tcp[12]>>2):4] = 0x47455420'

我第一次看到这个格式也是懵逼的,但是仔细的对照协议的数据之后就可以理解了(其实我看来很久,哈哈)

首先想一下,如果我们要过滤HTTP请求的’GET’参数,就需要定位到TCP包头的结束为止,从结束为止开始读取三个字节字符串,然后判断这三个字符串是不是为GET,上面那个过滤语法大概就是这个意思,下面开始详解上面的语法。

0x47455420表示字符串GET 后面有一个空格哦。

从最里开始看,开始,首先是tcp[12]表示整个tcp数据包的从0开始数第12个byte(字节)(为了文章便于阅读,后面技术都是从0计数)然后我们对应上面TCP的图看。上图中都是以bit来计算的,1byte=8bit,把上面的协议图每一层分成4份,数一下从0开始数到第十二个正好就是TCP头部大小。总共8位,前四位为头部长度,后四位为余留的位,一般不使用。

下面的我以wireshark抓过来的GET请求的包,把tcp协议的头部信息带出拿下来,详细解释一下

1
2
3
4
5
6
7
8
9
10
11
12
# 从wireshark抓包拷贝下来的hex dump
>>> tcp_header = "37 22 00 50 0a d9 01 a1 b7 69 4e 90 50 18 04 01 57 dc 00 00".split()
# 以空格切分成列表
>>> tcp_header
['37', '22', '00', '50', '0a', 'd9', '01', 'a1', 'b7', '69', '4e', '90', '50', '18', '04', '01', '57', 'dc', '00', '00']

# 取出从零开始输查看第十三位的值,将其转换成整数,并将其向右位移两位,得出的结果是: 5
# 右移是为了将预留的后四位去掉
# 计算原理为: 01010000 >> 0101 == 0101(binary) == 5(decimalism)
>>> int('0x' + tcp_header[12], 16) >> 4
5
# 上面可以看到得出的结果是5,那么上面说了,TCP首部长度定义的单位是32bits,所以TCP的首部长度等于 5 * 32(bit) / 8(bit) = 20bytes

根据上面的计算可以理解为TCP

所以我们只需要前四位,下面我们开始分析tcp[12]>>2,刚说了我们只需要四位,但是为什么只向右移动了两位?上面也说了因为TCP头部这个值表示的单位是32bytes,这里他们就刷了一个小聪明,只去掉两位,加上多出来的两个比特位正好就是4的倍数, 那么这么就不用再写一个5 * 4的运算了,所以010100 == 20(decimalism),这样直接可以得出正确的TCP头部的结束位置。

(tcp[12]>>2):4] 学过编程的都知道这是切片。根据上面的分析可以把:前面的值理解为20。也就是说是从TCP Body第开始的地方开始切(20确实是TCP头的长度,但是在tcpdump切片中是从0开始输的,所以20对应的是tcp body第一个值),:后面的4表示切到第四位时停止,正好是4个字符,结果为GET (注意GET后面的空)的16进制的值0x47455420

所以到了最后的最后这个表达式以简单明了的方式写出来就是这个样子的

1
2
3
$ python -c 'print "0x"+"GET ".encode("hex")'
0x47455420
$ tcp 'tcp[20:4] = 0x47455420'

最终的结果如下图所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
step:    0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19   20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 
data: 37 22 00 50 0a d9 01 a1 b7 69 4e 90 50 18 04 01 57 dc 00 00 | 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 63 75 72 6c 2f 37 2e 32 39 2e 30 0d 0a 48 6f 73 74 3a 20 73 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a
↑ | ↑
tcp header | | | tcp body
tcp header | | | tcp body
tcp header | | | tcp body
| | |---|----------|
|-----------| | |TCP body开|
|TCP头结束的| | |始的地方第|
|位置正好是 | | |21位 |
|第20个 | | |----------|
|-----------| |
|
|
|
|
|
|
我是分割线
  • 第一行标识的是长度,从0开始计数。
  • 忽略掉第二行中间的|,那是分割线…
  • 第二行是从wireshark 复制下来tcp的hex dump的值。

抓取POST请求

1
$ tcpdump 'tcp[(tcp[12]>>2):5] = 0x504f535420'

上面都讲清楚了,没啥好说的了,POST 的hex值0x504f535420可以用python来得到

1
2
$ python -c 'print "0x"+"POST ".encode("hex")'
0x504f535420

记得把:后面的数字改成对应的切割的长度。