新开一个Go编程系列,主要讲Go语言的高级编程技术,希望能像《Go并发编程指南》一样形成一个系列,不要断更。
这一篇是使用gopacket库编程的一部分,主要是通过手工构造数据链路层、网络层、传输层的包,实现扫描全网(示例中是中国大陆的) ipv4的IP地址,看看对应的网络是否可达。 首先我们需要知道全网的IP地址,其实我们可以使用fping探测这些IP是否连通,然后我们自己基于ICMP快速扫描这些IP,找出全网活跃的IP地址,最后我们使用tcp scan的方式扫描全网的IP,甚至你可以扫描公网上暴露的Redis实例。
请勿用本文介绍的技术做任何违法的事情。本文只分享网络底层(高级)的编程技术,不涉及公司的业务逻辑,不涉及任何黑客行为。
获取全网公网的IP地址
全球IP地址块被IANA(Internet Assigned Numbers Authority)分配给全球五大地区性IP地址分配机构,它们分别是:
- ARIN (American Registry for Internet Numbers)
目前该机构主要负责北美地区的 IP地址分配。同时也负责为全球 NSP (Network Service Providers) 分配地址。
- RIPE (Reseaux IP Europeens)
目前该机构主要负责欧洲、中东、中亚等地区的 IP 地址分配。
- APNIC (Asia Pacific Network Information Center)
目前该机构负责亚洲、太平洋地区的 IP 地址分配。
- LACNIC (Latin America and the Caribbean Information Center)
目前该机构负责拉丁美洲和加勒比地区的 IP 地址分配。
- AFRINIC (African Network Information Centre)
目前该机构负责非洲区域的 IP 地址分配。
有些文章说还三大地区性中心,应该是比较古老的说法,现在是五大区域中心。
虽然本文中说的是全网的公网IP,但是我们还是聚焦我们国家自己的公网IP, 也就是由APNIC负责分配的IP地址。我国中国移动、中国联通、中国电信以及以前的中国铁通、中国卫通、中国网通、教育网等都申请了一大批的网络地址,包括现在阿里、腾讯、百度、华为等云服务商手里也积囤积了一大批的IP地址。
这五大区域中心提供了它们分配的IP地址端和自治系统列表,并且是公开的,你可以使用下面的连接获取:
1 2 3 4 5
| https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest
|
和我们最相关的是亚洲太平洋地区APNIC分配的地址。虽然本文中说的是全网的公网IP,但是我们还是聚焦我们国家自己的公网IP, 也就是由APNIC负责分配的IP地址。我国中国移动、中国联通、中国电信以及以前的中国铁通、中国卫通、中国网通、教育网等都申请了一大批的网络地址,包括现在阿里、腾讯、百度、华为等云服务商手里也积囤积了一大批的IP地址。
通过过滤,我们可以得到分配给中国大陆的IP地址:
1 2 3 4
| #!/bin/bash wget -c http://ftp.apnic.net/stats/apnic/delegated-apnic-latest cat delegated-apnic-latest | awk -F '|' '/CN/&&/ipv4/ {print $4 "/" 32-log($5)/log(2)}' | cat > ipv4.txt
|
这个ipv4.txt文件包含分配给中国大陆的IP网段1:
1 2 3 4 5 6 7 8 9
| 1.0.1.0/24 1.0.2.0/23 1.0.8.0/21 1.0.32.0/19 1.1.0.0/24 1.1.2.0/23 1.1.4.0/22 1.1.8.0/24 1.1.9.0/24
|
本文不涉及IP地址归属的问题。通过whois,我们能够查询某个网段隶属于某个运行商和省份,但是也不完全准确,可能会有IP地址借用等问题,所以有http://www.ipip.net/这样的服务商,通过其他一些手段,提供更精准的精确到县区市的IP归属信息。
现在公网所有的IP地址我们都准备好了,下一步就是探测这些IP地址是不是活着的。因为申请的IP地址不可能全部都使用,即使使用的IP也可能有关机或者被封禁的问题,所以不一定所有IP都能连接上,我们本文的例子,就是要把这些活着的IP快速找出来。
使用fping批量扫描
我们检查主机是否存活的最常用的工具就是ping。
ping是一种网络工具,用来测试数据包能否透过IP协议到达特定主机。ping的运作原理是向目标主机传出一个ICMP的请求回显数据包,并等待接收回显回应数据包。程序会按时间和成功响应的次数估算丢失数据包率(丢包率)和数据包往返时间(网络时延)。
在1983年12月,Mike Muuss编写了首个这样的程序,用于在IP网络出现问题时方便探查其根源。因为这个程序的运作原理与潜水艇的主动声纳相似,他便用声纳的声音来为程序取名。
当我们想知道某个主机是否存活时,常常说 “ping一下它的IP地址,看看是否能ping通”。
Linux的ping工具除了使用ICMP协议外,还支持使用UDP或者TCP的方式进行探活,因为大部分网络程序都使用的UDP和TCP协议,所以使用这两个协议更符合业务的网络协议,毕竟网络设备对ICMP和TCP/UDP处理很可能不是一样的,比如TCP程序交换机可以根据五元组进行哈希选择下一跳的端口。
虽然ping工具适合探测主机存活的场景,但是它一次只能探测一个目标IP,在我们本文中的场景中,我们想探测非常多的Ip地址,一个一个的探测的话不知道探到猴年马月了,所以我们会用到另外一个工具: fping。
fping类似于 ping 操作,但在 ping 多个主机时性能要好得多。fping有着非常悠久的历史:Roland Schemers在1992年发布了它的第一个版本,并且从那时起,它已成为标准的网络诊断和统计工具 。
下面就是扫描 8.8.8.8/24
网段的情况:
基于fping的功能,我们可以逐网段的探测ipv4.txt文件中的每一行的网段,输出探测结果:
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
| package main import ( "bufio" "fmt" "os" "os/exec" "strings" "github.com/kataras/golog" ) func main() { err := exec.Command("/bin/bash", "ip.sh").Run() if err != nil { golog.Fatal(err) } defer os.Remove("ipv4.txt") defer os.Remove("delegated-apnic-latest") ipList, err := os.Open("ipv4.txt") if err != nil { panic(err) } scanner := bufio.NewScanner(ipList) for scanner.Scan() { netmask := scanner.Text() fping(netmask) } }
|
首先我们通过脚本得到大陆的所有分配的IP地址段,存入到ipv4.txt文件中。这个文件中每一行都包含一个IP地址段,我们使用bufio.Scanner
逐行扫描。每得到一个地址段,就调用fping函数处理:
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
| func fping(netmask string) { fpingCmd := "fping -a -g " + netmask + " -C 1 -i 2 -H 32 -q -t 200 2>&1" cmd := exec.Command("/bin/bash", "-c", fpingCmd) r, err := cmd.StdoutPipe() if err != nil { return } err = cmd.Start() if err != nil { return } scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() if strings.Contains(line, ": -") { continue } if strings.Contains(line, "ICMP Time Exceeded") { continue } fmt.Println(line) } cmd.Wait() }
|
这里我们使用exec.Command
调用fping命令,一次扫描一个网段,如果是超时的或者不通的IP,我们忽略,只把存活的IP地址打印出来。
自己实现ICMP扫描
使用fping虽然相比ping对于多主机的探测方式性能更好,但是还是达不到我们的需求,我们想持续扫描全网的IP地址的存活性,这个时候我们就不得不编写我们自己的程序了。
我们可以采用和ping、fping的探测协议一样,发送ICMP探测包,如果有ICMP的Reply回来,我们就认为网络是通的。
我们采用手工构造数据链路层的帧,从底层构造这个发送的数据。
数据链路层需要设置mac地址, 本地主机的地址我们可以获取出来(虽然可能有多个网卡,但是我们要挑选出实际路由使用的那个网卡的Mac地址),根据网络处理的方式,我们不需要知道目标地址的Mac地址,只需要填写我们的网关的Mac地址即可,网关会通过路由协议把探测包传播出去,最终到达目标地址,或者中途被丢掉。(如果ping同一个局域网内的主机,则需要填写目标主机的Mac地址,而不是网关的 Mac地址)
1 2 3 4 5
| eth := layers.Ethernet{ SrcMAC: s.iface.HardwareAddr, DstMAC: *s.gwHardwareAddr, EthernetType: layers.EthernetTypeIPv4, }
|
所以第一部就是我们要把本地和网关的Mac地址找出来。找这个地址的协议是arp协议,我们需要构造arp包,处理arp的返回结果:
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
| // getHwAddr gets the hardware address of the gateway by sending an ARP request. func (s *Scanner) getHwAddr() (net.HardwareAddr, error) { start := time.Now() arpDst := s.gw // prepare the layers to send for an ARP request. eth := layers.Ethernet{ SrcMAC: s.iface.HardwareAddr, DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, EthernetType: layers.EthernetTypeARP, } arp := layers.ARP{ AddrType: layers.LinkTypeEthernet, Protocol: layers.EthernetTypeIPv4, HwAddressSize: 6, ProtAddressSize: 4, Operation: layers.ARPRequest, SourceHwAddress: []byte(s.iface.HardwareAddr), SourceProtAddress: []byte(s.src), DstHwAddress: []byte{0, 0, 0, 0, 0, 0}, DstProtAddress: []byte(arpDst), } // send a single ARP request packet (we never retry a send) if err := s.sendPackets(ð, &arp); err != nil { return nil, err } // wait 3 seconds for an ARP reply. for { if time.Since(start) > time.Second*3 { return nil, errors.New("timeout getting ARP reply") } data, _, err := s.handle.ReadPacketData() if err == pcap.NextErrorTimeoutExpired { continue } else if err != nil { return nil, err } packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy) if arpLayer := packet.Layer(layers.LayerTypeARP); arpLayer != nil { arp := arpLayer.(*layers.ARP) if net.IP(arp.SourceProtAddress).Equal(net.IP(arpDst)) { return net.HardwareAddr(arp.SourceHwAddress), nil } } } } // sendPackets sends a packet with the given layers. func (s *Scanner) sendPackets(l ...gopacket.SerializableLayer) error { if err := gopacket.SerializeLayers(s.buf, s.opts, l...); err != nil { return err } return s.handle.WritePacketData(s.buf.Bytes()) }
|
构造这些底层的网络包(帧),我们常用的是gopacket库,并且利用它发送和接收包。
上面这个函数开始构造了layers.Ethernet
和layers.ARP
的内容,然后调用sendPackets
发送出去,然后调用s.handle.ReadPacketData()
读取arp返回结果,返回结果中就包含了网关的Mac地址。
实际上,我们定义了一个Scanner
,方便我们处理整个逻辑。这个Scanner
初始化的时候,把探测使用的本地IP地址、本地Mac地址、网关Mac地址都准备好:
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
| type Scanner struct { iface *net.Interface gw net.IP gwHardwareAddr *net.HardwareAddr src net.IP handle *pcap.Handle opts gopacket.SerializeOptions buf gopacket.SerializeBuffer } func NewScanner() *Scanner { s := &Scanner{ opts: gopacket.SerializeOptions{ FixLengths: true, ComputeChecksums: true, }, buf: gopacket.NewSerializeBuffer(), } router, err := routing.New() if err != nil { log.Fatal(err) } iface, gw, src, err := router.Route(net.ParseIP("114.114.114.114")) if err != nil { log.Fatal(err) } s.gw, s.src, s.iface = gw, src, iface handle, err := pcap.OpenLive(iface.Name, 100, true, pcap.BlockForever) if err != nil { log.Fatal(err) } s.handle = handle gwHwAddr, err := s.getHwAddr() if err != nil { log.Fatal(err) } s.gwHardwareAddr = &gwHwAddr log.Infof("scanning with interface %v, gateway %v, src %v, hwaddr: %v", iface.Name, gw, src, gwHwAddr) return s }
|
这里有一个技巧,一般服务器可能有多个网卡和更多的Ip地址,那么探测的时候使用哪一个网卡和本地IP地址呢?可以使用router.Route
,得到访问公网锁使用的本地网卡、网关、本地IP等,这个例子中我们访问知名的114.114.114.114
公网地址作为目标地址,把这些信息保存下来备用,接着调用getHwAddr
获取网关的Mac地址。
gopacket
通过pcap.OpenLive
打开一个设备进行读写网络数据,我们在创建Scanner
时候都把这些准备好。
现在万事俱备,只欠东风了,我们需要把要探测的目的IP传给它,让它进行探测。这里我们实现一个Scan
方法:
1 2 3 4 5 6 7 8 9
| func (s *Scanner) Scan(input chan []string) (output chan string) { output = make(chan string, 1024*1024) go s.recv(output) go s.send(input) return output }
|
input
是一个channel, 用户可以把要探测的目标IP传入到这个channel中。它会返回一个output
channel,这个channel中包含活跃的目标IP。
那么整个逻辑都很清晰了:启动一个goroutine发送ICMP包,启动一个goroutine接收ICMP,两者之间并没有阻塞,性能自然非常的好:
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
| func (s *Scanner) send(input chan []string) error { id := uint16(os.Getpid()) seq := uint16(0) for ips := range input { for _, ip := range ips { dstIP := net.ParseIP(ip) if dstIP == nil { continue } dstIP = dstIP.To4() if dstIP == nil { continue } eth := layers.Ethernet{ SrcMAC: s.iface.HardwareAddr, DstMAC: *s.gwHardwareAddr, EthernetType: layers.EthernetTypeIPv4, } ip4 := layers.IPv4{ SrcIP: s.src, DstIP: dstIP.To4(), Version: 4, TTL: 64, Protocol: layers.IPProtocolICMPv4, } icmpLayer := layers.ICMPv4{ TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0), Id: id, Seq: seq, } seq++ err := s.sendPackets(ð, &ip4, &icmpLayer) if err != nil { log.Error(err) } } } return nil }
|
发送数据构造layers.Ethernet
、layers.IPv4
、layers.ICMPv4
包,每个ICMPv4包探测一个目标IP,它值负责发送。
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
| func (s *Scanner) recv(output chan string) { defer close(output) s.handle.SetBPFFilter("dst host " + s.src.To4().String() + " and icmp") for { data, _, err := s.handle.ReadPacketData() if err == pcap.NextErrorTimeoutExpired { continue } else if errors.Is(err, io.EOF) { return } else if err != nil { log.Infof("error reading packet: %v", err) continue } packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy) if net := packet.NetworkLayer(); net == nil { continue } else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil { continue } else if ip, ok := ipLayer.(*layers.IPv4); !ok { continue } else if icmpLayer := packet.Layer(layers.LayerTypeICMPv4); icmpLayer == nil { continue } else if icmp, ok := icmpLayer.(*layers.ICMPv4); !ok { continue } else if icmp.TypeCode.Type() == layers.ICMPv4TypeEchoReply { select { case output <- ip.SrcIP.String(): default: } } else { } } }
|
接收的逻辑有个技巧,它通过BPFFilter对packet在内核态进行过滤,值关注我们的ICMP回包。 格式你参考tcpdump工具的格式就行,一样一样的。
它读取到ICMP的回包后,就把目标地址写入到output
中。
基本上,使用不到一个CPU的资源,在一个半小时左右就把大陆所有的公网IP探测了一遍。
自己实现TCP扫描,并找出暴露Redis端口IP地址
嗯,上面的技术不错,可以很快的扫描全网的公网地址,但是我们还想进一步,使用TCP的方式扫描全网的公网IP,而且,我们扫描特定的·端口,以便看看是否在公网IP上暴露了某个服务。这种扫描经常是安全公司去做扫描,这次我们自己手工实现一把。
这次我们扫描6379端口,这是redis默认的端口。虽然服务器上开了这个端口并不意味着就步数了Redis服务,但是很大可能的确部署了Redis服务。出于安全的考虑,这些暴露的Redis服务应该都设置了AUTH才对,甚至应该通过防火墙设置值允许特定的IP访问才对。
采用TCP方式探测,我们使用三次握手中的前两步:先发送一个syn包,对方可能回一个sync+ack包(如果端口开启了)或者rst包(如果端口没有开启,或者拒绝连接的话),或者没有任何返回。我们值关注前两种。
其实代码逻辑和上面的ICMP程序差不多太多,获取网关Mac地址等方式和上面而例子没啥区别。我们还是定义了一个Scanner
类型,但是多了本地端口和远程端口的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| type Scanner struct { iface *net.Interface gw, src net.IP gwHardwareAddr *net.HardwareAddr srcPort, dstPort int handle *pcap.Handle opts gopacket.SerializeOptions buf gopacket.SerializeBuffer }
|
我们想一次收集可以连通的但是端口未开启的IP地址,和端口已开启的IP地址,所以Scan
方法作了微调,支持两个列表的输出:
1 2 3 4 5 6 7 8 9 10
| func (s *Scanner) Scan(input chan []string) (connOutput, portOpenOutput chan string) { connOutput = make(chan string, 1024*1024) portOpenOutput = make(chan string, 1024*1024) go s.recv(connOutput, portOpenOutput) go s.send(input) return connOutput, portOpenOutput }
|
发送数据时我们要构造TCP的syn包:
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
| func (s *Scanner) send(input chan []string) error { for ips := range input { for _, ip := range ips { dstIP := net.ParseIP(ip) if dstIP == nil { continue } dstIP = dstIP.To4() if dstIP == nil { continue } eth := layers.Ethernet{ SrcMAC: s.iface.HardwareAddr, DstMAC: *s.gwHardwareAddr, EthernetType: layers.EthernetTypeIPv4, } ip4 := layers.IPv4{ SrcIP: s.src, DstIP: dstIP.To4(), Version: 4, TTL: 64, Protocol: layers.IPProtocolTCP, } tcp := layers.TCP{ SrcPort: layers.TCPPort(s.srcPort), DstPort: layers.TCPPort(s.dstPort), SYN: true, } tcp.SetNetworkLayerForChecksum(&ip4) err := s.sendPackets(ð, &ip4, &tcp) if err != nil { log.Error(err) } } } return nil }
|
接收数据的时候,我们区分TCP+ACK包和RST包:
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
| func (s *Scanner) recv(connOutput, portOpenOutput chan string) { defer close(connOutput) defer close(portOpenOutput) s.handle.SetBPFFilter("dst port " + strconv.Itoa(s.srcPort) + " and dst host " + s.src.To4().String()) for { data, _, err := s.handle.ReadPacketData() if err == pcap.NextErrorTimeoutExpired { continue } else if errors.Is(err, io.EOF) { return } else if err != nil { log.Infof("error reading packet: %v", err) continue } packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy) if net := packet.NetworkLayer(); net == nil { continue } else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil { continue } else if ip, ok := ipLayer.(*layers.IPv4); !ok { continue } else if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer == nil { } else if tcp, ok := tcpLayer.(*layers.TCP); !ok { continue } else if tcp.DstPort != layers.TCPPort(s.srcPort) { } else if tcp.RST { select { case connOutput <- ip.SrcIP.String(): default: } } else if tcp.SYN && tcp.ACK { select { case portOpenOutput <- ip.SrcIP.String(): default: } } else { } } }
|
代码结构和ICMP的探测区别不大。
借助于gopacket (libpcap)库,我们可以轻松高效的实现全网公网IP扫描,这对于网络探测、安全检查都非常有意义。 根据上面的例子,你也很容易实现UDP方式的探测,你也不妨练习下。