![白帽子安全开发实战](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=1738981161-siYLVTdM3H95pL6vvSKBUgVLwWN3E9zn-0-664532047c300000b7170f1335068f5e)
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=1738981161-HnpJ7yNUwbPIKnjCM972QPSWEPDb4Y27-0-f0976053806385eaee2fb9eb96e75989)
目前为止,已经实现了一个最简单的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=1738981161-tAVzynHtdRJAdzrfdxes20DvlTU30cyb-0-f91b92e685d4d632fb1fb6bb1f2b4eed)
iprange库的完整使用示例如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/34_01.jpg?sign=1738981161-sVFgXODbzUU4o7DN2qYQJ2wMnoPTrSB0-0-5e30721f5360fbd30d553ac3cb7c244e)
这里封装了一个GetIpList函数,可以根据输入的ipList返回一个[]net.IP的切片,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/34_02.jpg?sign=1738981161-FJS3S05omeS9gPVKToFphGtvJPKVGgPw-0-4fbf9245f3128cc6a673167a4da2473c)
多端口的处理需要支持“,”与“-”分割的端口列表,可以使用strings包的Split函数先分割以“,”连接的ipList,然后再分割以“-”连接的ipList,最后返回一个[]int切片,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/34_03.jpg?sign=1738981161-8ZWhAryvVB4lBGeh70PG6mAnAAlI1AI7-0-de64980fa9f28dda0af430fff7610d57)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/35_01.jpg?sign=1738981161-OFqCutPSdmy5QVcwLiRWwhclb9u8z6nM-0-a03402a19b0e981d642acd2f7f610124)
到目前为止,已经实现了支持对多个IP与端口进行扫描的函数,接下来再用main函数调用以上函数,即可实现一个完整的TCP全连接端口扫描器,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/35_02.jpg?sign=1738981161-trdYH4pylLujzwwcSOqAkqJIPQRjbQRq-0-861245acff748d1b2d45ead2fc75726a)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/36_01.jpg?sign=1738981161-Wn4CezAyw2tGvQxr2s2XkvIGUVFHpKlM-0-60936931693734036fe021a451433d3f)
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=1738981161-CKywVDOBJ9PwCqMLY86ZUgbJ6ao2H6V6-0-0cab6904d5727a6c3e798703ad97aa7c)
●图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=1738981161-LLrAurEpYSsZNFjndrBdDiSvKo0w30Bn-0-5b54e4e789e28b79db0dead6b25b0fa3)
2)分割扫描任务,根据并发数分割成组,然后将每组任务传入RunTask函数中执行,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/37_02.jpg?sign=1738981161-qQqEOJuwGVb1eIdps558xDmQjnTwYS3F-0-4495b0c26f9fc2f9d3d98d2609e66668)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/38_01.jpg?sign=1738981161-X9lcQ9mdKiqWmtGnYjmYmbTQSc4dx3oc-0-ec57137bf2bc45a2a85f489f0e4fb498)
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=1738981161-RgDXkFEj4B0BDKvX1TRGgWiCBVLFVZRd-0-defd0c6d14a725038ad7f8217a17904c)
4)展示扫描结果,直接通过sync.map的Range方法枚举出所有结果并展示出来,代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/38_03.jpg?sign=1738981161-TIKLTf4YFv7VHvhbR7AAQQ5y2ESanewV-0-b1cf0cd1bb8871350bab104218591db8)
以上4步全部完成后,在main函数中分别调用任务生成、任务分配与结果展示的函数即可,代码片断如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/39_01.jpg?sign=1738981161-Tu4i2FUzMLZotaEhBIVgBqwkaKGdyD6H-0-1271d2a231e5a276e9574ba2d1455dbc)
接下来用新实现的并发端口扫描器tcp-connect-scanner1与Nmap分别执行一遍刚才的任务,发现tcp-connect-scanner1的扫描速度与Nmap差不多,甚至比Nmap还快了一些,如图2-2所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/39_02.jpg?sign=1738981161-GO9mWFeLYdSVGdhSrjkdEZWqaX9xJaRk-0-bc65dfdfd52332cf154accd72db9f1b7)
●图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=1738981161-uUxirhHfMy3bbgT9R73ntST6xY3DIbpK-0-4ba957cab3fa65a73e8e9e391e4e78fe)
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=1738981161-OGHw7uTRrQQTv4lO2NrVyEXllplgGLax-0-395534b94566fbf492b4c71efe11a567)
●图2-3 tcp-connect-scanner2测试
2.1.3 TCP半连接端口扫描器
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/41_02.jpg?sign=1738981161-TpwFm0mhq4YWityHBmYN0RM6F7vL6PLU-0-cf4d50b667cefe87bb04ec2b9c7da119)
一个完整的TCP连接的建立需要经过三次握手,必须是一方主动打开,另一方被动打开的。客户端主动发起连接的过程如图2-4所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/41_03.jpg?sign=1738981161-sV6Q5AFowYpAnZkLCxH7SHI1VKBMiFsW-0-a322a1ebd1c0a9ee798ab1b85e22650c)
●图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=1738981161-8NNSwbJYgYR8vFJfKimHvgV5rR78UGbb-0-6532ddd629e9d2a5b10e37075eb7760f)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/43_01.jpg?sign=1738981161-Rtpm128E3ppCBvK4FOlJ8Y4nhEVwBmYm-0-1cfdb8a9c2402c4036b059d125cd7bba)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/44_01.jpg?sign=1738981161-voHTOR3RSvaHaGPBQWzuUb9s7VzulXtx-0-0009ce55769b27d90dd90ebbe022cf37)
分别利用刚开发完成的TCP半连接端口扫描器与Nmap的TCP半连接端口扫描模式进行扫描,得出的扫描结果是一致的,且刚开发完成的TCP半连接端口扫描器的速度比Nmap稍快一些,如图2-5所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/44_02.jpg?sign=1738981161-OSJwQWGDbA26XbGHzNhAwByPWjkEL7jT-0-6ee7e55b7209c77cf2cc934756f82f78)
●图2-5 TCP半连接端口扫描器测试
2.1.4 同时支持两种扫描方式的端口扫描器
前面已经开发了TCP全连接与TCP半连接端口扫描器,为了方便使用,接下来将两种扫描器合并,命令行参数如下:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/45_01.jpg?sign=1738981161-hV9uJePbpu7ZUG6THbs1FUxyQG7LFPcw-0-4ef741333ce58dceba751e57aaf97cf8)
● 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=1738981161-wxh4pYS39CJTw0AUJ785MbT6ZaV1wjl0-0-295fc96dc900b705f39b4c3039ca609f)
在扫描器项目的目录下建立一个cmd目录来存放命令处理文件,增加一个变量名为Scan的cli.Command对象,如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/45_03.jpg?sign=1738981161-bThnXZv1Wk8W2SUWQ6HjDBCYzP3R0M4h-0-5796752637146068b288a592ee5cd4cb)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/46_01.jpg?sign=1738981161-hsqS8wihaUe8J3LOKgebf446XlzLtWUf-0-0095be1220f04c2a7becadbd8a069e48)
Scan命令的具体执行代码在util.Scan文件中,详细代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/46_02.jpg?sign=1738981161-z8ejAHpYjLjXBdaCgAcVf0NZKG01wyXv-0-6916b9d4d83e23b71064f67f9b0486bf)
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/47_01.jpg?sign=1738981161-SYKTLkK42r6SwRPV1nFypH2VOFBtHDf7-0-f5c3499c56fd9b3183d9d0344f54bdaf)
以上代码的作用是检查是否在命令行中指定了每个参数的值,如果有指定的值,就会用新的值替换参数的默认值,然后生成待扫描的任务列表,并调用RunTask函数进行扫描。
scanner.RunTask(tasks)会根据扫描的类型调用不同的扫描函数,代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/47_02.jpg?sign=1738981161-hybmyLKFrDoNzuzWNJdVSlQlm0FE36bh-0-865de13afacfcc4d3e8fe76634bb8bd4)
在程序的main函数中,直接使用cli包实现命令行参数功能,代码如下所示:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/47_03.jpg?sign=1738981161-JJlkqDd4b1hDdWi8MXcYDpaynLPxVQOK-0-1499a8379c28da74f9b5836f32e55572)
最终项目的代码结构如图2-6所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/48_01.jpg?sign=1738981161-en8QYUswfPogVlRbDSgSTOAyOQRcHMnY-0-818f7fc3c6dd7980580400dac4bb55a5)
●图2-6 端口扫描器的代码结构
● cmd包为命令行参数的实现。
● scanner包为扫描器的具体实现,其中有TCP全连接与半连接端口扫描器的扫描函数与任务调度函数。
● util包为工具函数。
● vars包包含了项目中定义的所有全局变量。
最后将程序进行编译,直接运行后会显示出命令行参数使用说明,如图2-7所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/48_02.jpg?sign=1738981161-xTQJfF4jOgl0RMQYJgomOO0BsynRkjVg-0-6a66a2bbb37985db6a2f2f50961d0aeb)
●图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=1738981161-usUlfynPwnenIlstlWg78cBtRQiql1ZP-0-4bc2aacefecb405665f078fdae1c80ab)
● 以半连接方式扫描,命令如下:
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/49_02.jpg?sign=1738981161-h1ywv6z2AYu8yNhXHCKH5NxyCkU1WGLF-0-aee102e9f39a45ab0006c74e4648a60d)
通过以上两种模式对目标进行扫描后,得出的扫描结果是一致的,消耗的时间也差不多,都为2s左右,测试结果如图2-8所示。
![](https://epubservercos.yuewen.com/BCD655/19773741601353506/epubprivate/OEBPS/Images/49_03.jpg?sign=1738981161-wqusPTe6jKp3LGwLtclWMjRAZc4rOgPL-0-6ca62b54bd34cd68ac78b6bed8015889)
●图2-8 端口扫描器测试结果