阅读 1018

使用go解析二进制tcp数据包

tcp全名是传输控制协议,tcp协议在ip协议基础上增加了数据包完整性检查等保证传输完整性的机制,使其在现在的数据领域得到了广泛的应用

按照下面的步骤可以快速了解tcp数据包中包含的信息

tcp协议rfc文档解读

rfc参考:tools.ietf.org/html/rfc793

核心tcp数据包结构如下

可以看到tcp报文由十余个字段组成,最后一个data字段代表了本次tcp数据报文承载的数据,这个数据一般是应用层的数据,比如http报文数据就是在这个tcp包的data字段中

其中常用字段如下

字段 作用
Source Port 发包机器的端口号
Destination Port 收包机器的端口号
Sequence Number 包编号
Acknowledgment Number 确认包号
urg/ack/psh/rst/syn/fin 标志位,设置是/否的操作标志
Window 流量控制窗口
Checksum 包完整性校验

注意:客户端和服务端使用独立的包编号计数器器 checksum服务端和客户端会分别计算,客户端依靠这个值判断tcp包在传输过程中是否被异常改变/篡改

获取案例数据包

可以使用wireshark获取一个tcp数据包,在tcp层单击右键->复制->as a Hex Stream即可

这里得到的tcp层包数据如下

1f90f04f3747d146dcae23f3801831bf14ef00000101080a3176450b31764503
复制代码

下面就可以编写程序从这个tcp的16进制中解析出报文的具体数据了

解析tcp数据报文

二进制数字位数和16进制位数换算关系:1个16进制数可以表示4个二进制数 比如二进制:00011111 10010000 可以使用16进制表示为:1f90

可以使用下面的代码将16进制转成二进制字符串

func hex2bin(hex string) string {
	var bin string

	for i := 0; i < len(hex); i++ {
		hex2int, _ := strconv.ParseInt(string(hex[i]), 16, 64)
		bin = bin + fmt.Sprintf("%04b", hex2int)
	}

	return bin
}
复制代码

然后就可以按二进制位读取tcp数据报信息了,参考代码如下

func main() {
	atcp := "1f90f04f3747d146dcae23f3801831bf14ef00000101080a3176450b31764503"
	bintcp := hex2bin(atcp)

	sourcePort, _  := strconv.ParseInt(bintcp[0:16], 2, 64)
	fmt.Printf("sourcePort is %d \n", sourcePort)

	destinationPort, _  := strconv.ParseInt(bintcp[16:32], 2, 64)
	fmt.Printf("destinationPort is %d \n", destinationPort)

	sequenceNumber, _  := strconv.ParseInt(bintcp[32:64], 2, 64)
	fmt.Printf("sequenceNumber is %d \n", sequenceNumber)

	acknowledgmentNumber, _  := strconv.ParseInt(bintcp[64:96], 2, 64)
	fmt.Printf("acknowledgmentNumber is %d \n", acknowledgmentNumber)

	dataOffset, _  := strconv.ParseInt(bintcp[96:100], 2, 64)
	fmt.Printf("dataOffset is %d \n", dataOffset)

	reserved, _  := strconv.ParseInt(bintcp[100:106], 2, 64)
	fmt.Printf("reserved is %d \n", reserved)

	// Control Bits 控制位,从106-1012共有6位,每位表示一个控制位的开关
	urg, _ := strconv.ParseInt(bintcp[106:107], 2, 64)
	ack, _ := strconv.ParseInt(bintcp[107:108], 2, 64)
	psh, _ := strconv.ParseInt(bintcp[108:109], 2, 64)
	rst, _ := strconv.ParseInt(bintcp[109:110], 2, 64)
	syn, _ := strconv.ParseInt(bintcp[110:111], 2, 64)
	fin, _ := strconv.ParseInt(bintcp[111:112], 2, 64)
	fmt.Printf("控制位标识如下:\n")
	fmt.Printf("    urg: %d\n", urg)
	fmt.Printf("    ack: %d\n", ack)
	fmt.Printf("    psh: %d\n", psh)
	fmt.Printf("    rst: %d\n", rst)
	fmt.Printf("    syn: %d\n", syn)
	fmt.Printf("    fin: %d\n", fin)

	// 数据窗口 16位
	window, _  := strconv.ParseInt(bintcp[112:128], 2, 64)
	fmt.Printf("window is %d \n", window)

	// checksum 16位
	checksum, _  := strconv.ParseInt(bintcp[128:144], 2, 64)
	fmt.Printf("checksum is %d \n", checksum)

	// urgentPointer
	urgentPointer, _ := strconv.ParseInt(bintcp[144:160], 2, 64)
	fmt.Printf("urgentPointer is %d \n", urgentPointer)

	// options and padding
	optionsAndPaddings := bintcp[160:]
	fmt.Printf("optionsAndPaddings is %s \n", optionsAndPaddings)

	fmt.Printf("tcp raw data is %s \n", atcp)
	fmt.Printf("tcp bin data is %s \n", bintcp)
	fmt.Printf("tcp bin data length is %d\n", len(bintcp))
}
复制代码

执行效果如下

wireshark解析结果如下

可以看到解析是ok的

一些注意的点

wireshark中复制的tcp包数据是16进制的,但是tcp协议中,部分字段仅占用一位,16进制是2进制的4的整数倍,直接解析16进制会导致tcp中的某些字段无法获取,需要先转成二进制进行处理

tcp包数据最终会进行32位对齐,整个tcp数据包大小如果不是正好是32位长度的整数倍,会用0在末尾填充到32位整数倍

tcp协议在rfc3168中新增了cwr和ece标志位,可以参考:tools.ietf.org/html/rfc316…

抓包可以使用tcpdump: tcpdump -n -XX -i lo0 -s0 'tcp port 8080'

参考资料

  1. github.com/google/pack…
  2. klamath.stanford.edu/~nickm/pape…
  3. pcapplusplus.github.io/docs/tutori…
  4. tools.ietf.org/html/rfc793…
  5. forums.ni.com/t5/LabVIEW/…