12 13
基于pcap写一个简单的tcpdump

PCAP

pcap (packet capture) 通涵盖两个概念:API 和 库(libpcap),其中API是使用C进行编写的; pcap的用途很广,我们常见的工具例如tcpdump、netstat、wireshark、gor等跟网络嗅探相关的都离不开libpcap库的支持。

libpcap的开发常见主要有以下几种:

  • 数据包捕获:捕获流经网卡的原始数据包
  • 自定义数据包发送:构造任何格式的原始数据包
  • 流量采集与统计:采集网络中的流量信息
  • 规则过滤:提供自带规则过滤功能,按需要选择过滤规则

它的应用范围非常广泛,典型应用包括玩罗协议分析器,网络流量发生器,网络入侵检测系统,网络扫描器和其他安全工具。

libpcap工作原理

作为捕捉网络数据包的库,它是一个独立于系统的用户级的API接口,为底层网络检测提供了一个可移植的框架。

一个包的捕捉分为三个主要部分,包括面向底层包捕获、面向中间层的数据包过滤和面向应用层的用户接口。 这与Linux操作系统对数据包的处理流程是相同的(网卡->网卡驱动->数据链路层->IP层->传输层->应用程序)。

工作流程图:

pcap

包捕获机制是在数据链路层增加一个旁路处理(并不干扰系统自身的网络协议栈的处理),对发送和接收的数据包通过Linux内核做过滤和缓冲处理,最后直接传递给上层应用程序。

接下来我们归纳下libpcap的流程:

  • 查找网络设备:目的是发现可用的网卡,实现的函数为pcap_lookupdev(),如果当前有多个网卡,函数就会返回一个网络设备名的指针列表。

  • 打开网络设备:利用上一步中的返回值,可以决定使用哪个网卡,通过函数pcap_open_live()打开网卡,返回用于捕捉网络数据包的秒数字。

  • 获得网络参数:这里是利用函数pcap_lookupnet(),可以获得指定网络设备的IP地址和子网掩码。

  • 编译过滤策略:Lipcap的主要功能就是提供数据包的过滤,函数pcap_compile()来实现。

  • 设置过滤器:在上一步的基础上利用pcap_setfilter()函数来设置。

  • 利用回调函数,捕获数据包:函数pcap_loop()和pcap_dispatch()来抓去数据包,也可以利用函数pcap_next()和pcap_next_ex()来完成同样的工作。

  • 关闭网络设备:pcap_close()函数关系设备,释放资源。

代码实现

了解了原理和工作流程后,我们可以自己尝试做一个嗅探工具。

我们以原生的tcpdump为参考,用Go语言来做一个“轮子”

代码如下:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "github.com/gophil/pcap"
    "os"
    "os/signal"
    "strconv"
    "time"
)

const (
    TYPE_IP  = 0x0800
    TYPE_ARP = 0x0806
    TYPE_IP6 = 0x86DD

    IP_ICMP = 1
    IP_INIP = 4
    IP_TCP  = 6
    IP_UDP  = 17
)

var (
    device  = flag.String("i", "", "interface")      //设备名: en0,bond0
    ofile   = flag.String("d", "", "dump file path") //生成离线文件
    read    = flag.String("r", "", "read dump file") //生成离线文件
    snaplen = flag.Int("s", 65535, "snaplen")
    hexdump = flag.Bool("X", false, "hexdump")
    help    = flag.Bool("h", false, "help")
    count   = flag.String("c", "", "capture count of the dump line")
    timeout = flag.String("t", "", "timeout")
)

func main() {
    expr := ""

    flag.Usage = func() {
        fmt.Fprintf(os.Stderr,
            "usage: %s \n [ -i interface ] \n [ -t timeout ] \n [ -c count ] \n [ -s snaplen ] \n [ -X hexdump ] \n [ -d dump file ] \n [ -r read file ] \n [ -h show usage] \n [ expression ] \n", os.Args[0])
        os.Exit(1)
    }

    flag.Parse()

    if len(flag.Args()) > 0 {
        expr = flag.Arg(0)
    }

    if *help {
        flag.Usage()
    }

    if *read != "" {
        src := *read
        f, err := os.Open(src)
        if err != nil {
            fmt.Printf("couldn't open %q: %v\n", src, err)
            return
        }
        defer f.Close()
        reader, err := pcap.NewReader(bufio.NewReader(f))
        if err != nil {
            fmt.Printf("couldn't create reader: %v\n", err)
            return
        }
        for {
            pkt := reader.Next()
            if pkt == nil {
                break
            }
            pkt.Decode()
            fmt.Println(pkt)
            if *hexdump {
                Hexdump(pkt)
            }
        }
        return
    }

    if *device == "" {
        devs, err := pcap.FindAllDevs()
        if err != nil {
            fmt.Fprintln(os.Stderr, "tinydump: couldn't find any devices:", err)
        }
        if 0 == len(devs) {
            flag.Usage()
        }
        *device = devs[0].Name
    }

    //在线方式读取
    h, err := pcap.OpenLive(*device, int32(*snaplen), true, 500)
    if h == nil {
        fmt.Fprintf(os.Stderr, "tinydump:", err)
        return
    }
    defer h.Close()

    //设置过滤
    if expr != "" {
        fmt.Println("tinydump: setting filter to", expr)
        ferr := h.SetFilter(expr)
        if ferr != nil {
            fmt.Println("tinydump:", ferr)
        }
    }

    cs := *count
    lineCoint := 1
    useCount := false
    if cs != "" {
        useCount = true
        lineCoint, err = strconv.Atoi(cs)
        if err != nil {
            lineCoint = 1
        }
    }

    //生成离线分析文件
    if *ofile != "" {
        dumper, oerr := h.DumpOpen(ofile)
        signalNotify(h, dumper)
        if oerr != nil {
            fmt.Fprintln(os.Stderr, "tinydump: couldn't write to file:", oerr)
        }
        _, lerr := h.PcapLoop(lineCoint-1, dumper)
        if lerr != nil {
            fmt.Fprintln(os.Stderr, "tinydump: loop error:", lerr, h.Geterror())
        }
        defer h.PcapDumpClose(dumper)
        return
    }

    //超时处理
    ts := *timeout
    if ts != "" {
        t, err := strconv.Atoi(ts)
        if err == nil {
            time.AfterFunc(time.Second*time.Duration(t), func() {
                h.Close()
                os.Exit(1)
            })
        }
    }

    //监听事件消息输出
    for pkt, r := h.NextEx(); r >= 0; pkt, r = h.NextEx() {
        if r == 0 {
            // 超时, continue(100)
            continue
        }

        if useCount {
            lineCoint = lineCoint - 1
            if lineCoint < 0 {
                h.Close()
                os.Exit(1)
            }
        }

        pkt.Decode()
        fmt.Println(pkt)
        if *hexdump {
            Hexdump(pkt)
        }

    }
    fmt.Fprintln(os.Stderr, "tinydump:", h.Geterror())

}

func signalNotify(h *pcap.Pcap, dumper *pcap.PcapDumper) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    go func() {
        for sig := range c {
            fmt.Fprintln(os.Stderr, "tinydump: received signal:", sig)
            if os.Interrupt == sig {
                //关闭退出
                h.PcapDumpClose(dumper)
                h.Close()
                os.Exit(1)
            }
        }
    }()
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func Hexdump(pkt *pcap.Packet) {
    for i := 0; i < len(pkt.Data); i += 16 {
        Dumpline(uint32(i), pkt.Data[i:min(i+16, len(pkt.Data))])
    }
}

//行dump
func Dumpline(addr uint32, line []byte) {
    fmt.Printf("\t0x%04x: ", int32(addr))
    var i uint16
    for i = 0; i < 16 && i < uint16(len(line)); i++ {
        if i%2 == 0 {
            fmt.Print(" ")
        }
        fmt.Printf("%02x", line[i])
    }
    for j := i; j <= 16; j++ {
        if j%2 == 0 {
            fmt.Print(" ")
        }
        fmt.Print("  ")
    }
    fmt.Print("  ")
    for i = 0; i < 16 && i < uint16(len(line)); i++ {
        if line[i] >= 32 && line[i] <= 126 {
            fmt.Println("%c", line[i])
        } else {
            fmt.Print(".")
        }
    }
    fmt.Println()
}

使用方法:

go build

生成 tinydump 执行文件

命令参数

sudo ./tinydump -h

 [ -i interface ]
 [ -t timeout ]
 [ -c count ]
 [ -s snaplen ]
 [ -X hexdump ]
 [ -d dump file ]
 [ -r read file ]
 [ -h show usage]
 [ expression ]

使用范例:

1.直接使用

$ sudo ./tinydump 

2.host过滤

$ sudo ./tinydump 'src 192.168.1.101 or dst 192.168.1.101'

3.端口过滤

$ sudo ./tinydump 'port 8000'

4.超时捕获 (时间单位为秒)

$ sudo ./tinydump -t 60

5.生成快照文件

$ sudo ./tinydump -d /tmp/test.cap

6.读取快照文件

$ sudo ./tinydump -r /tmp/test.cap

7.捕获指定行数的包

$ sudo ./tinydump -c 10

总结

到这里为止,我们结合原理实现了一个Go的tcpdump工具。我们日常开发中,理解透原理也是我们做好工具的第一步。