![白帽子安全开发实战](https://wfqqreader-1252317822.image.myqcloud.com/cover/529/37323529/b_37323529.jpg)
2.1 端口扫描器
位于网络中的计算机,每一个端口就是一个潜在的通信通道,发现这些端口后,可以判断监听这些端口的服务有哪些,然后进一步判断这些服务是否存在安全隐患。
Nmap是端口扫描的“泰山北斗”,支持TCP全连接端口扫描、TCP半连接端口扫描和UDP端口扫描等多种扫描方式,本节将介绍如何实现常用的TCP全连接与半连接扫描器,如何实现高并发。
2.1.1 TCP全连接端口扫描器
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/33_01.jpg?sign=1738884801-3ZwHco81AyiWrlwta93qgz8EKeCD2A2C-0-99b318894336e762b12221adc687b198)
TCP全连接端口扫描器是最基础的扫描器,它的原理是调用Socket的connect函数连接到目标IP的特定端口上,如果连接成功说明端口是开放的,如果连接失败,说明端口没有开放。
Go语言的net包提供的Dial与DialTimeout函数,对传统的socket函数进行了封装,无论想创建什么协议的连接,都只需要调用这两个函数即可。这两个函数的区别是DialTimeout增加了超时时间。
以下代码片断利用DialTimeout实现了一个Connect方法,可以判断一个端口是否开放,如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/33_02.jpg?sign=1738884801-GybV5b7ByfUJsQjEHXsaswKuTZ5txviU-0-dcf3944c7f9a7c5d38ebcf24c653f775)
目前为止,已经实现了一个最简单的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,函数原型如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/33_03.jpg?sign=1738884801-LTXD5eBO1EipUtfFD9Sl28lkuZH1uEoc-0-bf20813135dba67723dd8c495cd61564)
iprange库的完整使用示例如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/34_01.jpg?sign=1738884801-H0PjvKq0FNYyBP9ulH2l8e7zmf7denon-0-cbedb8a199249dbd9246b70ee8feb5a2)
这里封装了一个GetIpList函数,可以根据输入的ipList返回一个[]net.IP的切片,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/34_02.jpg?sign=1738884801-JxaUgVPM1Vj1gGWAx8bOmNdi6tgU5V16-0-820d718b8e6c02482b6c717d93d829a9)
多端口的处理需要支持“,”与“-”分割的端口列表,可以使用strings包的Split函数先分割以“,”连接的ipList,然后再分割以“-”连接的ipList,最后返回一个[]int切片,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/34_03.jpg?sign=1738884801-lCyhAj0dnLuBpRT2ByylfK4ZZgOOdGxZ-0-62b17cc77646b9d194011a522645acf6)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/35_01.jpg?sign=1738884801-nw5sxYOZAzGKuIf7qwVtUgGVCTUbQ3aV-0-0e631ea4878450dc6c486f20c4da9ea5)
到目前为止,已经实现了支持对多个IP与端口进行扫描的函数,接下来再用main函数调用以上函数,即可实现一个完整的TCP全连接端口扫描器,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/35_02.jpg?sign=1738884801-R3dmtJIzfXqlqrzbYetuYzoGCHlXpTAn-0-6419d987e7e67e1d5d5d1ace043a35d7)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/36_01.jpg?sign=1738884801-Jvw7XJr9DRrLlpPF6j6aFMkSZAeNI1dt-0-1ee7af77e542c3105f0592f4ee8696ff)
TCP全连接端口扫描器已经编写完成,接下来编译出可执行文件并扫描一些IP和端口来进行验证。以下分别用自研的TCP全连接端口扫描器与Nmap扫描45.22.2.156和114.114.114.114的22、23、53、80-100,扫描结果如图2-1所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/36_02.jpg?sign=1738884801-McPOvd0l7KsPnqz3gSqrDzXksguN8Py3-0-859ad3455e028e323179114b3a07a292)
●图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)生成扫描任务列表,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/37_01.jpg?sign=1738884801-yO47YxdbLOQPVNGqzDm30Gy4r7k6CQE2-0-c26f5ad8ab255ddff35e6a853d0bdb02)
2)分割扫描任务,根据并发数分割成组,然后将每组任务传入RunTask函数中执行,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/37_02.jpg?sign=1738884801-K2M0a4mAftJOXuKYFrAS6tmzwhGaBWmi-0-c462e693229500d37a5872ef0445223e)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/38_01.jpg?sign=1738884801-T6pzhpvwPhXOfVEwdzjM3vTSv6RUPgeU-0-269cd6361f7aab7b7fbb7f5acad8ab90)
len(tasks)%vars.ThreadNum > 0表示len(tasks) / vars.ThreadNum不能整除,还有剩余的任务列表需要进行处理。
3)按组执行扫描任务,这个版本的并发是通过sync.WaitGroup来控制的,一次性创建出所有协程,然后等待所有任务完成,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/38_02.jpg?sign=1738884801-c5XHxI9rMO5WOpBC5tSgbSJSneJExHAH-0-d239de1b19d1a3c95fa4fd6925b58cb7)
4)展示扫描结果,直接通过sync.map的Range方法枚举出所有结果并展示出来,代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/38_03.jpg?sign=1738884801-sH6BVwG37IcbtUJ3CGIQwGRCuREbXlgC-0-81e749a4566c62c13689245451cb511e)
以上4步全部完成后,在main函数中分别调用任务生成、任务分配与结果展示的函数即可,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/39_01.jpg?sign=1738884801-Fa1WPKmqdxSNO0i6JmKFp1bvVaRUK7dO-0-2e430f605f31249fd418fd2ceff11272)
接下来用新实现的并发端口扫描器tcp-connect-scanner1与Nmap分别执行一遍刚才的任务,发现tcp-connect-scanner1的扫描速度与Nmap差不多,甚至比Nmap还快了一些,如图2-2所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/39_02.jpg?sign=1738884801-FpHR59ns5JzNMK0zotW7b1mQexX1P23X-0-d158d27ef9ce00daac36aff28dc027ed)
●图2-2 tcp-connect-scanner1测试结果
这个扫描器虽然已经实现了并发扫描,但对协程的控制不够精细,每组扫描任务都会瞬间启动大量的协程,然后逐渐关闭,而不是一个平滑的过程。这种方法可能会瞬间将服务器的CPU占满,为了解决此问题,在tcp-connect-scanner2中使用sync.WaitGroup与channel配合实现了新的并发方式,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/40_01.jpg?sign=1738884801-TnV0GY2F5mvPz1INq7J8fdfeLkpmkapi-0-ab61375e08898098e549aaca01bd7cf1)
RunTask函数不断地将扫描任务发送到taskChan中,Scan会不断地消费taskChan中的数据。
接下来对比tcp-connect-scanner2与Nmap扫描相同任务的耗时,发现tcp-connect-scan ner2扫描速度比Nmap默认线程数的扫描速度还快了一些,如图2-3所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/41_01.jpg?sign=1738884801-gufJxzxLqVz1aYKtrKrYJElaceybrPLX-0-1e23ffba5af97d4eea41706ea03dee04)
●图2-3 tcp-connect-scanner2测试
2.1.3 TCP半连接端口扫描器
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/41_02.jpg?sign=1738884801-qOuZut1Y1jczl1CcDfk53pAzDgmHPMMg-0-14a92440fe5225f7434c792efa4657d2)
一个完整的TCP连接的建立需要经过三次握手,必须是一方主动打开,另一方被动打开的。客户端主动发起连接的过程如图2-4所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/41_03.jpg?sign=1738884801-RSfZKrg39jQW1O00OuxyfHRscoUwvOZW-0-be8be51b0452fba958c3e37f9c9882b4)
●图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)函数修改为半连接扫描的函数即可,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/42_01.jpg?sign=1738884801-aw5il8bO9FjA2jo0Htc6Xo5Jl1p9fFmS-0-32cbfe9bff29e30a7f1a13573189165b)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/43_01.jpg?sign=1738884801-3KRHVGyaLlpVelrhECckmFd3syfbBsqD-0-e8e273ae8bbc7cbbe9f370800bcf544c)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/44_01.jpg?sign=1738884801-GQfKvqDpA6AfMEuHlZGnVtUQTSjqZzgY-0-0a052ec57682f43b803c08c084bd5b46)
分别利用刚开发完成的TCP半连接端口扫描器与Nmap的TCP半连接端口扫描模式进行扫描,得出的扫描结果是一致的,且刚开发完成的TCP半连接端口扫描器的速度比Nmap稍快一些,如图2-5所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/44_02.jpg?sign=1738884801-vzV83vWdPgYnVqwX8EpHdCib3BGWEi1a-0-aee97cd51d7335fde842b7977ddbcfb1)
●图2-5 TCP半连接端口扫描器测试
2.1.4 同时支持两种扫描方式的端口扫描器
前面已经开发了TCP全连接与TCP半连接端口扫描器,为了方便使用,接下来将两种扫描器合并,命令行参数如下:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/45_01.jpg?sign=1738884801-aBcUijfaj6GkiHDbLLvAV0gSuQCRSOSI-0-22d421c03626078516bd3d301edfd904)
● iplist表示扫描的IP列表。
● port表示扫描的端口列表。
● mode表示扫描模式,全连接或半连接。
● timeout表示每个连接的超时时间。
● concurrency表示扫描器的并发数。
Go语言标准库专门提供了用来处理命令行参数的flag包,但这里不使用这个包,而是使用功能更加强大的第三方包github.com/urfave/cli,它的用法如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/45_02.jpg?sign=1738884801-mP4r69344W1ED7QO7XLaoy05rAIFieVk-0-5f3a0b6a0f33f2a556c9231ba8af3b9b)
在扫描器项目的目录下建立一个cmd目录来存放命令处理文件,增加一个变量名为Scan的cli.Command对象,如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/45_03.jpg?sign=1738884801-O17VT6zNgJAQWVIIc8nzOwblCtcaY9R2-0-ce2c67b5eb76f0a994530117aaf4f227)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/46_01.jpg?sign=1738884801-9w9MgvQ05Pgkh31PKKQ2oQjtgCXPoyNz-0-0c31a1fb115fbcaa24e3763d1159466a)
Scan命令的具体执行代码在util.Scan文件中,详细代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/46_02.jpg?sign=1738884801-Oj0opva2G2Hx70dwmAmDjiCwNMCT9ITJ-0-b659efec7f09b5f18bba52ad351999f4)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/47_01.jpg?sign=1738884801-EO67Z8kPysNqgaamGmVsQYz5JvOKfA4m-0-8295f8660c4e051afb72145c7b85cd99)
以上代码的作用是检查是否在命令行中指定了每个参数的值,如果有指定的值,就会用新的值替换参数的默认值,然后生成待扫描的任务列表,并调用RunTask函数进行扫描。
scanner.RunTask(tasks)会根据扫描的类型调用不同的扫描函数,代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/47_02.jpg?sign=1738884801-BtURFzTcou2zPTTR7MUpLeDSUhuDnaAE-0-63c66123057752c27fb402c8b0fa079f)
在程序的main函数中,直接使用cli包实现命令行参数功能,代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/47_03.jpg?sign=1738884801-gUdCDQmgWdEU20KW5DUSMELAX4anCtK0-0-049de6612527675677b1dbb2749de5c6)
最终项目的代码结构如图2-6所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/48_01.jpg?sign=1738884801-ldKRfLqDXl4XsfufTSCIyDTVgP1SWEub-0-184877415f8ee2b4c4eeb96b832835ec)
●图2-6 端口扫描器的代码结构
● cmd包为命令行参数的实现。
● scanner包为扫描器的具体实现,其中有TCP全连接与半连接端口扫描器的扫描函数与任务调度函数。
● util包为工具函数。
● vars包包含了项目中定义的所有全局变量。
最后将程序进行编译,直接运行后会显示出命令行参数使用说明,如图2-7所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/48_02.jpg?sign=1738884801-vUPr80z6jCU4JE7Gh2GZqzdPRag5VvVH-0-0ff5821bbb522af3ef5c8e57a76398f1)
●图2-7 端口扫描器命令行参数
2.1.5 端口扫描器测试
前面已经开发了支持全连接与半连接模式的端口扫描器,假设目标IP列表为45.33.32.156、114.114.114.114,目标端口列表为22、23、25、53和80-139,分别测试以TCP全连接模式与TCP半连接模式扫描目标服务器的端口的效果。
● 以全连接方式扫描,命令如下:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/49_01.jpg?sign=1738884801-f30vYRF9CWzLtDKO1aHtYuMpX39tRRJR-0-2b4a2f8b45e7b2dd1046089c96e95d4b)
● 以半连接方式扫描,命令如下:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/49_02.jpg?sign=1738884801-aS1pugHlt6uLU9BW67S63Tc0i79RPQ4b-0-09b207fce0e824b1d180057c10f6120c)
通过以上两种模式对目标进行扫描后,得出的扫描结果是一致的,消耗的时间也差不多,都为2s左右,测试结果如图2-8所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/49_03.jpg?sign=1738884801-nPjcPNXgnoW3laUDgkdyKtdVDNhbDi57-0-3ecede6ab92a2718c7943a81d011e64a)
●图2-8 端口扫描器测试结果