[Golang] gopacket 初体验

14,769 阅读9分钟

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.gogo run http_server.go

    package 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.go

    package 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 三次握手及四次分手

  • 先回顾下三次握手及四次分手,若有兴趣更详细的了解该过程请看这篇文章

    • 三次握手

      image.png

    • 四次分手 (常规场景)

      image.png

  • 先写一个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.go

    package 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。

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, &eth, &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. 写在最后

  • 看官们都看到这了,如果文章对你有用就左手边点个👍再走呗;