1. 简介
github: github.com/google/gopa…
gopacket是一个golang语言的网络包抓取库,当然这只是一个最简单的用法。比如你还能用他来做流复制、防火墙等功能;gopacket包含 npcap 和 libpcap,前者是windows中的 wireshark 依赖的包,后者是linux中 tcpdump 的依赖包;
1.1 Linux 中使用
- 安装依赖开发库
yum install -y libpcap libpcap-devel - 下载gopacket
go get github.com/google/gopacket
2. 示例
2.1 获取网络设备信息
- cap1.go
package main import ( "fmt" "github.com/google/gopacket/pcap" "log" ) func main() { // 获取网络设备 devs, err:= pcap.FindAllDevs() if err != nil { log.Fatalln(err) } for _,dev:=range devs{ fmt.Println("网卡:",dev.Name) if (len(dev.Addresses) != 0) { maskSize, _ := dev.Addresses[0].Netmask.Size() fmt.Printf(" IP: %s/%d\n",dev.Addresses[0].IP, maskSize) fmt.Println(" 广播地址:",dev.Addresses[0].Broadaddr) } fmt.Println() } } - 输出
网卡: en0 IP: 192.168.19.91/24 广播地址: 192.168.19.255 网卡: lo0 IP: 127.0.0.1/8 广播地址: <nil>
2.2 抓取 TCP 8080 端口数据包
-
先启动一个http服务
http_server.go,go run http_server.gopackage main import ( "io" "log" "net/http" ) // hello world, the web server func HelloServer(w http.ResponseWriter, req *http.Request) { _, _ = io.WriteString(w, "hello, world - 2!\n") } func main() { http.HandleFunc("/hello", HelloServer) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } -
cap2.go
go run cap2.gopackage main import ( "fmt" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "log" "time" ) func main() { const ( // 网络设备 EthDev string = "eth0" // 抓取数据长度 SnapLen int32 = 65535 // 是否开启混合模式,混合模式就是不属于本网卡IP的流量 Promisc bool = false // 展示细节的时间 Timeout time.Duration = time.Second * 3 // 抓取端口 DstPort layers.TCPPort = 8080 ) // 对网卡流量进行实时捕获 handler, err := pcap.OpenLive(EthDev, SnapLen, Promisc, Timeout) if err!=nil{log.Fatalln(err)} defer handler.Close() // 获取包源 source := gopacket.NewPacketSource(handler, handler.LinkType()) var isApp bool for pk := range source.Packets() { // 网络四层模型 // LinkLayer returns the first link layer in the packet // LinkLayer() LinkLayer // NetworkLayer returns the first network layer in the packet // NetworkLayer() NetworkLayer // TransportLayer returns the first transport layer in the packet // TransportLayer() TransportLayer // ApplicationLayer returns the first application layer in the packet // ApplicationLayer() ApplicationLayer isApp = false if layer := pk.TransportLayer(); layer != nil { if tcpLayer, ok := layer.(*layers.TCP); ok && tcpLayer.DstPort == DstPort{ fmt.Println() fmt.Printf("TCP Payload: %s\n", string(tcpLayer.Payload)) fmt.Printf("TCP ContentsLenght: %d \n", len(tcpLayer.Contents)) fmt.Println(pk.String()) if len(tcpLayer.Payload) > 0 { isApp = true } } } if isApp { if appLayer := pk.ApplicationLayer(); appLayer != nil { fmt.Println("APP Contents:", string(appLayer.LayerContents())) fmt.Println("APP Contents Length:", len(appLayer.LayerContents())) } } } } -
请求http
curl http://127.0.0.1:8080/hello -
cap2.go 程序输出
... TCP Payload: GET /hello HTTP/1.1 Host: 139.198.31.27:8080 User-Agent: curl/7.64.1 Accept: */* TCP ContentsLenght: 32 PACKET: 153 bytes, wire length 153 cap length 153 @ 2022-03-01 14:29:53.387448 +0800 CST - Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..139..] SrcMAC=02:54:9e:2e:5f:64 DstMAC=52:54:9e:b9:70:e8 EthernetType=IPv4 Length=0} - Layer 2 (20 bytes) = IPv4 {Contents=[..20..] Payload=[..119..] Version=4 IHL=5 TOS=0 Length=139 Id=0 Flags=DF FragOffset=0 TTL=51 Protocol=TCP Checksum=4238 SrcIP=112.94.5.214 DstIP=192.168.0.3 Options=[] Padding=[]} - Layer 3 (32 bytes) = TCP {Contents=[..32..] Payload=[..87..] SrcPort=52063 DstPort=8080(http-alt) Seq=1410279372 Ack=1228926593 DataOffset=8 FIN=false SYN=false RST=false PSH=true ACK=true URG=false ECE=false CWR=false NS=false Window=2053 Checksum=5780 Urgent=0 Options=[TCPOption(NOP:), TCPOption(NOP:), TCPOption(Timestamps:2080988029/851079750 0x7c095b7d32ba7246)] Padding=[]} - Layer 4 (87 bytes) = Payload 87 byte(s) APP Contents: GET /hello HTTP/1.1 Host: 139.198.31.27:8080 User-Agent: curl/7.64.1 Accept: */* APP Contents Length: 87 ...
2.3 演示抓取 TCP 三次握手及四次分手
-
先回顾下三次握手及四次分手,若有兴趣更详细的了解该过程请看这篇文章
-
三次握手
-
四次分手 (常规场景)
-
-
先写一个tcp 服务端
tcp_server.go,由于一些无法描述的原因客观上的客户端主动调用Close()函数时无法呈现常规场景的4次分手,当客户端调用Close()的时候必定会抛出了复位包(RST)导致不能走正常的4次分手。所以下面代码采用服务端主动断开了链接来呈现(在tcp协议原语描述中其实是主动断开方即为客户端,所以不妨碍场景的呈现)。package main import ( "fmt" "log" "net" ) func main() { l, err := net.Listen("tcp", "0.0.0.0:8081") if err != nil { log.Fatalln(err) } defer l.Close() for { conn, err := l.Accept() if err != nil {log.Fatalln(err)} fmt.Printf("Received message %s -> %s \n", conn.RemoteAddr(), conn.LocalAddr()) go handler(conn) } } func handler(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 1024) num, err := conn.Read(buf) if err != nil { fmt.Println(err) if err.Error() == "EOF" { return } break } fmt.Printf("Received data: %v\n", string(buf[:num])) // 由于在关闭时tcp队列内依然有数据会发送rst包, // 所以这里屏蔽掉数据发送直接关闭链接 //if num, err =conn.Write([]byte("88")) ; err != nil { // fmt.Println(err) //} // 设置了服务端主动断开链接,主要是为了完整演示4次分手 if err := conn.Close(); err != nil { fmt.Println(err) } //return } } -
写一个tcp客户端
tcp_client.go。下面曾经想用tcp 参数 linger 来阻止客户端close时发送RST包,让tcp正常进入四次分手,但测试后发现没效果;package main import ( "log" "net" "time" ) func main() { con, err := net.Dial("tcp", "192.168.0.3:8081") if err!=nil{ log.Fatalln(err) } c := con.(*net.TCPConn) //if err := c.SetLinger(1); err != nil { // log.Fatalln(err) //} if _ , err := c.Write([]byte("hello")); err != nil { log.Fatalln(err) } time.Sleep(2 * time.Second) if err := c.Close(); err != nil { log.Fatalln(err) } } -
新的 capture,
cap4.gopackage main import ( "fmt" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "log" "time" ) // 可观测三次握手及四次分手 func main() { const ( EthDev string = "eth0" SnapLen int32 = 1024 Promisc bool = false Timeout time.Duration = time.Second * 2 Port layers.TCPPort = 8081 ) fmt.Println("Capture dev:", EthDev, "port:", Port) handler, err := pcap.OpenLive(EthDev, SnapLen, Promisc, Timeout) if err != nil { log.Fatalln(err) } defer handler.Close() source := gopacket.NewPacketSource(handler, handler.LinkType()) for pk := range source.Packets() { if layer2 := pk.NetworkLayer(); layer2 != nil { ipLayer, ok := layer2.(*layers.IPv4) if ! ok { continue } if layer3 := pk.TransportLayer() ; layer3 != nil { if tcpLayer, ok := layer3.(*layers.TCP); ok { if tcpLayer.DstPort == Port || tcpLayer.SrcPort == Port { fmt.Printf("%s:%d --> %s:%d, SYN=%v, ACK=%v, RST=%v, PSH=%v, FIN=%v, URG=%v, ECE=%v," + " Payload length=%d, seq=%v, ackNum=%v, window=%v \n", ipLayer.SrcIP, tcpLayer.SrcPort, ipLayer.DstIP, tcpLayer.DstPort, tcpLayer.SYN, tcpLayer.ACK, tcpLayer.RST, tcpLayer.PSH, tcpLayer.FIN, tcpLayer.URG, tcpLayer.ECE, len(tcpLayer.Payload), tcpLayer.Seq, tcpLayer.Ack, tcpLayer.Window, ) } } } } } } -
运行代码
go run tcp_server.go go run cap4.go go run tcp_client.go -
观测cap4.go的输出,下面经过调试后的正确输出,如果你禁止掉
tcp_server.go中的主动close的代码,则会发现最后的包时客户端主动断开的 RST 包,四次分手无法正常走下去;# 三次握手 192.168.0.2:51260 --> 192.168.0.3:8081, SYN=true, ACK=false, RST=false, PSH=false, FIN=false, URG=false, ECE=false, Payload length=0, seq=3786003339, ackNum=0, window=29200 192.168.0.3:8081 --> 192.168.0.2:51260, SYN=true, ACK=true, RST=false, PSH=false, FIN=false, URG=false, ECE=false, Payload length=0, seq=3563580175, ackNum=3786003340, window=28960 192.168.0.2:51260 --> 192.168.0.3:8081, SYN=false, ACK=true, RST=false, PSH=false, FIN=false, URG=false, ECE=false, Payload length=0, seq=3786003340, ackNum=3563580176, window=229 # 传输了数据 192.168.0.2:51260 --> 192.168.0.3:8081, SYN=false, ACK=true, RST=false, PSH=true, FIN=false, URG=false, ECE=false, Payload length=5, seq=3786003340, ackNum=3563580176, window=229 192.168.0.3:8081 --> 192.168.0.2:51260, SYN=false, ACK=true, RST=false, PSH=false, FIN=false, URG=false, ECE=false, Payload length=0, seq=3563580176, ackNum=3786003345, window=227 # 四次分手 192.168.0.3:8081 --> 192.168.0.2:51260, SYN=false, ACK=true, RST=false, PSH=false, FIN=true, URG=false, ECE=false, Payload length=0, seq=3563580176, ackNum=3786003345, window=227 192.168.0.2:51260 --> 192.168.0.3:8081, SYN=false, ACK=true, RST=false, PSH=false, FIN=false, URG=false, ECE=false, Payload length=0, seq=3786003345, ackNum=3563580177, window=229 192.168.0.2:51260 --> 192.168.0.3:8081, SYN=false, ACK=true, RST=false, PSH=false, FIN=true, URG=false, ECE=false, Payload length=0, seq=3786003345, ackNum=3563580177, window=229 192.168.0.3:8081 --> 192.168.0.2:51260, SYN=false, ACK=true, RST=false, PSH=false, FIN=false, URG=false, ECE=false, Payload length=0, seq=3563580177, ackNum=3786003346, window=227 -
补充知识点
- tcp 参数 SO_LINGER, 设置函数close()关闭TCP连接时的行为,下面是其结构体。
type Linger struct { Onoff int32 Linger int32 } - Onoff 非 0,Linger为0,close()用以下方式关闭连接。
- 立即关闭该连接,通过发送RST分组(而不是用正常的FIN|ACK|FIN|ACK四个分组)来关闭该连接。至于发送缓冲区中如果有未发送完的数据,则丢弃。主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。网上很多人想利用这一点来解决服务器上出现大量的TIMEWAIT状态的socket的问题,但是,这并不是一个好主意,这种关闭方式的用途并不在这儿,实际用途在于服务器在应用层的需求
- Onoff 非 0, Linger非0,close()用以下方式关闭连接。
- 将连接的关闭设置一个超时。如果socket发送缓冲区中仍残留数据,进程进入睡眠,内核进入定时状态去尽量去发送这些数据。在超时之前,如果所有数据都发送完且被对方确认,内核用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接,close()成功返回。如果超时之时,数据仍然未能成功发送及被确认,用上述a方式来关闭此连接。close()返回EWOULDBLOCK。
- tcp 参数 SO_LINGER, 设置函数close()关闭TCP连接时的行为,下面是其结构体。
2.4 通过gopacket构造tcp包,并发生SYN
- syn_cli.go,可进行用上面cap4.go进行capture该程序发送的SYN包;
package main import ( "fmt" "github.com/google/gopacket" "github.com/google/gopacket/layers" "golang.org/x/sys/unix" "log" "net" "syscall" ) func getLocalIP(destIP net.IP) (net.IP, int) { // 向目标地址使用udp发送 serverAddr, err := net.ResolveUDPAddr("udp", destIP.String()+":23456") if err != nil { log.Fatalln(err) } if con, err := net.DialUDP("udp", nil, serverAddr); err == nil { if udpAddr, ok := con.LocalAddr().(*net.UDPAddr); ok { return udpAddr.IP, udpAddr.Port } } else { log.Fatalln("Can not get local ip: " + err.Error()) } return nil, -1 } func main() { // 构建destination ip const ( IPv4 = "ip4" Addr = "192.168.0.3" Port = 8081 ) dstIP, _ := net.ResolveIPAddr(IPv4, Addr) dstPort := layers.TCPPort(Port) sIP, sPort := getLocalIP(dstIP.IP) srcPort := layers.TCPPort(sPort) fmt.Printf("Local IP:PORT - %s:%d \n", sIP, srcPort) // 构建IP包 ipPack := &layers.IPv4{ SrcIP: sIP, DstIP: dstIP.IP, Protocol: layers.IPProtocolTCP, } // 构建tcp报文段 tcpPack := &layers.TCP{ SrcPort: srcPort, DstPort: dstPort, Seq: 1205014776, SYN: true, } if err := tcpPack.SetNetworkLayerForChecksum(ipPack); err != nil { log.Fatalln(err) } serialBuf := gopacket.NewSerializeBuffer() serialOpts := gopacket.SerializeOptions{ ComputeChecksums: true, FixLengths: true, } if err := gopacket.SerializeLayers(serialBuf, serialOpts, ipPack, tcpPack); err != nil { log.Fatalln(err) } // 创建套接字 fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW) if err != nil { log.Fatalln(err) } defer unix.Close(fd) //defer syscall.Close(fd) // 构建目标地址对象 destAddr := syscall.SockaddrInet4{ Port: 0, Addr: [4]byte{ dstIP.IP.To4()[0], dstIP.IP.To4()[1], dstIP.IP.To4()[2], dstIP.IP.To4()[3], }, } err = syscall.Sendto(fd, serialBuf.Bytes(), 0, &destAddr) if err != nil { log.Fatalln("Send error: ", err) } log.Println("Send succeed.") }
3. 体验 hping3 伪造SYN包
3.1. SYN 攻击
-
攻击者发送大量的SYN包,源IP是伪造的 服务器回应(SYN+ACK)包, 攻击者不回应ACK包
-
服务器不知道(SYN+ACK)是否发送成功,默认情况下会重试6次(tcp_syn_retries)cat /etc/sysctl.conf (/proc/sys/net/ipv4/tcp_syn_retries)
3.2 hping3介绍
- 一款 TCP / IP 数据包编辑器/分析器,常用来做安全审计、防火墙测试等工作。支持TCP,UDP,ICMP 和 RAW-IP 协议,具有跟踪路由模式,在覆盖通道之间发送文件的功能以及许多其他功能(如SYN攻击)。 RAW_SOCKET(SYSCALL)
3.3 安装与入门 hping3
-
安装
yum install hping3 -y -
使用
# -a 参数是模拟一个不存在的客户端地址进行发送SYN 包到 192.168.0.3:8080 hping3 -I eth0 -c 1 -a 192.168.10.60 192.168.0.3 --syn -p 8080 # 向192.168.0.3:8080 发送一个SYN包 hping3 -I eth0 -c 1 192.168.0.3 --syn -p 8080 # 加入--flood 选项,就会变态发
4. 构造并发送TCP包(扩展)
-
前提条件需要知道两端的mac地址,构造TCP包的过程有利于理解整个数据报的分层
package main import ( "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "log" "net" "time" ) func getLocalIP(dstip net.IP) (net.IP, int) { serverAddr, err := net.ResolveUDPAddr("udp", dstip.String()+":23456") if err != nil { log.Fatal(err) } if con, err := net.DialUDP("udp", nil, serverAddr); err == nil { if udpaddr, ok := con.LocalAddr().(*net.UDPAddr); ok { return udpaddr.IP, udpaddr.Port } } log.Fatal("could not get local ip: " + err.Error()) return nil, -1 } func main() { //发送端的 mac srcMac, _ := net.ParseMAC("fa:16:3e:2a:b6:07") //接收端 distMac, _ := net.ParseMAC("fa:16:3e:a7:07:05") //发送端IP srcIP := net.ParseIP("192.168.0.204") dstIP, _ := net.ResolveIPAddr("ip4", "192.168.0.21") _, srcPort := getLocalIP(dstIP.IP) // 链路层 eth := layers.Ethernet{ SrcMAC: srcMac, //发送端的 mac DstMAC: distMac, //发送端的 mac EthernetType: layers.EthernetTypeIPv4, } //IP层 ipLayer := layers.IPv4{ SrcIP: srcIP, DstIP: dstIP.IP, Version: 4, TTL: 64, Protocol: layers.IPProtocolTCP, } // 四层 tcp tcpLayer := layers.TCP{ SrcPort: layers.TCPPort(srcPort), DstPort: layers.TCPPort(80), // 假设接收方是 80 } data := []byte(`abc`) // 应用数据 payload := gopacket.Payload(data) err := tcpLayer.SetNetworkLayerForChecksum(&ipLayer) if err != nil { panic(err) } buf := gopacket.NewSerializeBuffer() opts := gopacket.SerializeOptions{ FixLengths: true, ComputeChecksums: true, } err = gopacket.SerializeLayers(buf, opts, ð, &ipLayer, &tcpLayer, payload) if err != nil { panic(err) } handle, err := pcap.OpenLive("eth0", 2048, false, 30*time.Second) if err != nil { log.Fatal("pcap打开网络设备失败:", err) } defer handle.Close() //向 我们的网络设备 发包 err = handle.WritePacketData(buf.Bytes()) if err != nil { log.Fatal("发送数据失败:", err) } log.Print("数据包已经发送\n") }
5. 写在最后
- 看官们都看到这了,如果文章对你有用就左手边点个👍再走呗;