2.1 端口扫描器
位于网络中的计算机,每一个端口就是一个潜在的通信通道,发现这些端口后,可以判断监听这些端口的服务有哪些,然后进一步判断这些服务是否存在安全隐患。
Nmap是端口扫描的“泰山北斗”,支持TCP全连接端口扫描、TCP半连接端口扫描和UDP端口扫描等多种扫描方式,本节将介绍如何实现常用的TCP全连接与半连接扫描器,如何实现高并发。
2.1.1 TCP全连接端口扫描器

TCP全连接端口扫描器是最基础的扫描器,它的原理是调用Socket的connect函数连接到目标IP的特定端口上,如果连接成功说明端口是开放的,如果连接失败,说明端口没有开放。
Go语言的net包提供的Dial与DialTimeout函数,对传统的socket函数进行了封装,无论想创建什么协议的连接,都只需要调用这两个函数即可。这两个函数的区别是DialTimeout增加了超时时间。
以下代码片断利用DialTimeout实现了一个Connect方法,可以判断一个端口是否开放,如下所示:

目前为止,已经实现了一个最简单的TCP全连接端口扫描器,但这个扫描器一次只能检测一个IP的一个端口。接下来实现类似于Nmap那样支持对多个IP与端口进行扫描的扫描器。
要实现对多IP的扫描,需引入一个第三方包github.com/malfunkt/iprange,它实现了类似于Nmap风格对多个IP的解析,支持的格式如下。
● 10.0.0.1。
● 10.0.0.0/24。
● 10.0.0.∗。
● 10.0.0.1-10。
● 10.0.0.1,10.0.0.5-10,192.168.1.∗,192.168.10.0/24。
iprange库会将Nmap风格的IP解析为AddressRange对象,然后调用AddressRange的Expand方法会返回一个[]net.IP,函数原型如下所示:

iprange库的完整使用示例如下所示:

这里封装了一个GetIpList函数,可以根据输入的ipList返回一个[]net.IP的切片,代码片断如下所示:

多端口的处理需要支持“,”与“-”分割的端口列表,可以使用strings包的Split函数先分割以“,”连接的ipList,然后再分割以“-”连接的ipList,最后返回一个[]int切片,代码片断如下所示:


到目前为止,已经实现了支持对多个IP与端口进行扫描的函数,接下来再用main函数调用以上函数,即可实现一个完整的TCP全连接端口扫描器,代码片断如下所示:


TCP全连接端口扫描器已经编写完成,接下来编译出可执行文件并扫描一些IP和端口来进行验证。以下分别用自研的TCP全连接端口扫描器与Nmap扫描45.22.2.156和114.114.114.114的22、23、53、80-100,扫描结果如图2-1所示。

●图2-1 单线程TCP全连接端口扫描器测试
从图2-1可以看出,TCP全连接端口扫描器的扫描结果与Nmap的TCP全连接端口扫描模式得出的结果是相同的,美中不足的是现在完成的TCP全连接端口扫描器是单线程扫描器,扫描速度非常慢,不适合用在实际的扫描任务中。
下一小节将介绍如何将这个单线程的TCP全连接端口扫描器改为高并发的扫描器,达到媲美Nmap扫描器的速度。
2.1.2 支持并发的TCP全连接端口扫描器
Go语言是原生支持并发的语言,它的并发是通过协程实现的。
这里介绍了两个版本的支持并发的TCP全连接端口扫描器,项目工程名分别为tcp-connect-scanner1与tcp-connect-scanner2。
tcp-connect-scanner1的实现步骤如下。
1)生成扫描任务列表:首先解析出需要扫描的IP与端口的切片,然后将需要扫描的IP与端口列表放入一个[]map[string]int中,map的key为IP地址,value为端口,[]map[string]int表示所有需要扫描的IP与端口对的切片。
2)分割扫描任务:根据并发数将需要扫描的[]map[string]int切片分割成组,以便按组进行并发扫描。
3)按组执行扫描任务:分别将每组扫描任务传入具体的扫描任务中,扫描任务函数利用sync.WaitGroup实现并发扫描,在扫描的过程中将结果保存到一个并发安全的map中。
4)展示扫描结果:所有扫描任务完成后,输出保存在并发安全map中的扫描结果。
tcp-connect-scanner1的具体实现过程如下。
1)生成扫描任务列表,代码片断如下所示:

2)分割扫描任务,根据并发数分割成组,然后将每组任务传入RunTask函数中执行,代码片断如下所示:


len(tasks)%vars.ThreadNum > 0表示len(tasks) / vars.ThreadNum不能整除,还有剩余的任务列表需要进行处理。
3)按组执行扫描任务,这个版本的并发是通过sync.WaitGroup来控制的,一次性创建出所有协程,然后等待所有任务完成,代码片断如下所示:

4)展示扫描结果,直接通过sync.map的Range方法枚举出所有结果并展示出来,代码如下所示:

以上4步全部完成后,在main函数中分别调用任务生成、任务分配与结果展示的函数即可,代码片断如下所示:

接下来用新实现的并发端口扫描器tcp-connect-scanner1与Nmap分别执行一遍刚才的任务,发现tcp-connect-scanner1的扫描速度与Nmap差不多,甚至比Nmap还快了一些,如图2-2所示。

●图2-2 tcp-connect-scanner1测试结果
这个扫描器虽然已经实现了并发扫描,但对协程的控制不够精细,每组扫描任务都会瞬间启动大量的协程,然后逐渐关闭,而不是一个平滑的过程。这种方法可能会瞬间将服务器的CPU占满,为了解决此问题,在tcp-connect-scanner2中使用sync.WaitGroup与channel配合实现了新的并发方式,代码片断如下所示:

RunTask函数不断地将扫描任务发送到taskChan中,Scan会不断地消费taskChan中的数据。
接下来对比tcp-connect-scanner2与Nmap扫描相同任务的耗时,发现tcp-connect-scan ner2扫描速度比Nmap默认线程数的扫描速度还快了一些,如图2-3所示。

●图2-3 tcp-connect-scanner2测试
2.1.3 TCP半连接端口扫描器

一个完整的TCP连接的建立需要经过三次握手,必须是一方主动打开,另一方被动打开的。客户端主动发起连接的过程如图2-4所示。

●图2-4 TCP三次握手
三次握手之前主动打开连接的客户端结束CLOSED阶段,被动打开的服务器也结束CLOSED阶段,并进入LISTEN阶段。三次握手的具体过程如下所述。
1)客户端向服务器端发送一段TCP报文,标志位为SYN,表示请求建立新连接。
2)服务器端接收到来自客户端的TCP报文之后,结束LISTEN阶段,并返回一段TCP报文,标志位为SYN和ACK,表示确认客户端的报文seq序号有效,服务器能正常接收客户端发送的数据,并同意建立新连接。
3)客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器端的数据传输是正常的,结束SYN-SENT阶段,并返回最后一段TCP报文,标志位为ACK,表示确认收到服务器端同意连接的信号。
TCP半连接端口扫描器只会向目标端口发送一个SYN包,如果服务器的端口是开放的,会返回SYN/ACK包,如果端口不开放,则会返回RST/ACK包。
TCP半连接端口扫描器可以复用前面开发好的TCP全连接端口扫描器的代码,只需要将执行全连接扫描的Connect(ip string, port int)函数修改为半连接扫描的函数即可,代码片断如下所示:



分别利用刚开发完成的TCP半连接端口扫描器与Nmap的TCP半连接端口扫描模式进行扫描,得出的扫描结果是一致的,且刚开发完成的TCP半连接端口扫描器的速度比Nmap稍快一些,如图2-5所示。

●图2-5 TCP半连接端口扫描器测试
2.1.4 同时支持两种扫描方式的端口扫描器
前面已经开发了TCP全连接与TCP半连接端口扫描器,为了方便使用,接下来将两种扫描器合并,命令行参数如下:

● iplist表示扫描的IP列表。
● port表示扫描的端口列表。
● mode表示扫描模式,全连接或半连接。
● timeout表示每个连接的超时时间。
● concurrency表示扫描器的并发数。
Go语言标准库专门提供了用来处理命令行参数的flag包,但这里不使用这个包,而是使用功能更加强大的第三方包github.com/urfave/cli,它的用法如下所示:

在扫描器项目的目录下建立一个cmd目录来存放命令处理文件,增加一个变量名为Scan的cli.Command对象,如下所示:


Scan命令的具体执行代码在util.Scan文件中,详细代码如下所示:


以上代码的作用是检查是否在命令行中指定了每个参数的值,如果有指定的值,就会用新的值替换参数的默认值,然后生成待扫描的任务列表,并调用RunTask函数进行扫描。
scanner.RunTask(tasks)会根据扫描的类型调用不同的扫描函数,代码如下所示:

在程序的main函数中,直接使用cli包实现命令行参数功能,代码如下所示:

最终项目的代码结构如图2-6所示。

●图2-6 端口扫描器的代码结构
● cmd包为命令行参数的实现。
● scanner包为扫描器的具体实现,其中有TCP全连接与半连接端口扫描器的扫描函数与任务调度函数。
● util包为工具函数。
● vars包包含了项目中定义的所有全局变量。
最后将程序进行编译,直接运行后会显示出命令行参数使用说明,如图2-7所示。

●图2-7 端口扫描器命令行参数
2.1.5 端口扫描器测试
前面已经开发了支持全连接与半连接模式的端口扫描器,假设目标IP列表为45.33.32.156、114.114.114.114,目标端口列表为22、23、25、53和80-139,分别测试以TCP全连接模式与TCP半连接模式扫描目标服务器的端口的效果。
● 以全连接方式扫描,命令如下:

● 以半连接方式扫描,命令如下:

通过以上两种模式对目标进行扫描后,得出的扫描结果是一致的,消耗的时间也差不多,都为2s左右,测试结果如图2-8所示。

●图2-8 端口扫描器测试结果