像闪电一样快速扫描全网的IP地址

Go高级网络编程第11章

新开一个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.Ethernetlayers.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
// Scanner represents a ICMP scanner. It contains a pcap handle and
// other information that is needed to scan the network.
type Scanner struct {
// iface is the network interface on which to scan.
iface *net.Interface
// gw is the gateway address.
gw net.IP
// gwHardwareAddr is the gateway hardware address.
gwHardwareAddr *net.HardwareAddr
// src is the source IP address.
src net.IP
// handle is the pcap handle.
handle *pcap.Handle
// opts and buf allow us to easily serialize packets in the send() method.
opts gopacket.SerializeOptions
buf gopacket.SerializeBuffer
}
// NewScanner creates a new Scanner.
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)
}
// figure out the route by using the IP.
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
// open the handle for reading/writing.
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
// Scan scans the network and returns a channel that contains the
// IP addresses of the hosts that respond to ICMP echo requests.
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
// send sends a single ICMP echo request packet for each ip in the input channel.
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
}
// construct all the network layers we need.
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.Ethernetlayers.IPv4layers.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
// recv receives ICMP echo reply packets and sends the IP addresses
func (s *Scanner) recv(output chan string) {
defer close(output)
// set the filter to only receive ICMP echo reply packets.
s.handle.SetBPFFilter("dst host " + s.src.To4().String() + " and icmp")
for {
// read in the next packet.
data, _, err := s.handle.ReadPacketData()
if err == pcap.NextErrorTimeoutExpired {
continue
} else if errors.Is(err, io.EOF) {
// log.Infof("error reading packet: %v", err)
return
} else if err != nil {
log.Infof("error reading packet: %v", err)
continue
}
packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy)
// find the packets we care about, and print out logging
// information about them. All others are ignored.
if net := packet.NetworkLayer(); net == nil {
// log.Info("packet has no network layer")
continue
} else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil {
// log.Info("packet has not ip layer")
continue
} else if ip, ok := ipLayer.(*layers.IPv4); !ok {
continue
} else if icmpLayer := packet.Layer(layers.LayerTypeICMPv4); icmpLayer == nil {
// log.Info("packet has not icmp layer")
continue
} else if icmp, ok := icmpLayer.(*layers.ICMPv4); !ok {
// log.Info("packet is not icmp")
continue
} else if icmp.TypeCode.Type() == layers.ICMPv4TypeEchoReply {
// log.Info("packet is not icmp")
select {
case output <- ip.SrcIP.String():
default:
}
} else {
// log.Info("ignoring useless packet")
}
}
}

接收的逻辑有个技巧,它通过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
// Scanner is the main struct that holds all the state for the scanner.
type Scanner struct {
// iface is the interface to send packets on.
iface *net.Interface
// gw and src are the gateway and source IP addresses of the
// interface we're scanning.
gw, src net.IP
gwHardwareAddr *net.HardwareAddr
srcPort, dstPort int
// handle is the pcap handle that we use to receive packets.
handle *pcap.Handle
// opts and buf allow us to easily serialize packets in the send()
// method.
opts gopacket.SerializeOptions
buf gopacket.SerializeBuffer
}

我们想一次收集可以连通的但是端口未开启的IP地址,和端口已开启的IP地址,所以Scan方法作了微调,支持两个列表的输出:

1
2
3
4
5
6
7
8
9
10
// Scan scans the network for open TCP ports.
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
// send sends packets to the network.
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
}
// construct all the network layers we need.
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
// recv receives packets from the network.
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 {
// read in the next packet.
data, _, err := s.handle.ReadPacketData()
if err == pcap.NextErrorTimeoutExpired {
continue
} else if errors.Is(err, io.EOF) {
// log.Infof("error reading packet: %v", err)
return
} else if err != nil {
log.Infof("error reading packet: %v", err)
continue
}
packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy)
// find the packets we care about, and print out logging
// information about them. All others are ignored.
if net := packet.NetworkLayer(); net == nil {
// log.Info("packet has no network layer")
continue
} else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil {
// log.Info("packet has not ip layer")
continue
} else if ip, ok := ipLayer.(*layers.IPv4); !ok {
continue
} else if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer == nil {
// log.Info("packet has not tcp layer")
} else if tcp, ok := tcpLayer.(*layers.TCP); !ok {
continue
} else if tcp.DstPort != layers.TCPPort(s.srcPort) {
// log.Infof("dst port %v does not match", tcp.DstPort)
} 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 {
// log.Printf("ignoring useless packet")
}
}
}

代码结构和ICMP的探测区别不大。

借助于gopacket (libpcap)库,我们可以轻松高效的实现全网公网IP扫描,这对于网络探测、安全检查都非常有意义。 根据上面的例子,你也很容易实现UDP方式的探测,你也不妨练习下。