基于C语言的端口扫描工具设计与实现

      最后更新:2022-02-20 21:01:47 手机定位技术交流文章

      • lfy:Go实现4种扫描方式及对比
      • jyx:C实现4种扫描方式及对比

      一、技术原理

      端口扫描技术向目标系统的TCP/UDP端口发送探测数据包,记录目标系统的响应,通过分析响应来查看该系统处于监听或运行状态的服务。

      1. TCP扫描

      常见的tcp端口扫描方式有以下三种:

      1.1 全扫描(connect)

      扫描主机尝试(使用三次握手)与目标主机的某个端口建立正规的连接,连接由系统调用connect()开始,如果端口开放,则连接将建立成功,否则,返回-1,则表示端口关闭。全扫描流程图如下:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BMZ32BlH-1645250441735)(img/007S8ZIlgy1geb1onsvvuj30np0ra74n.jpg)]

      • 优点:编程简单,只需要一个API connect(),比较可靠,因为TCP是可靠协议,当丢包的时候,会重传SYN帧。
      • 缺点:
        • 正因为TCP的可靠性,所以当端口不存在的时候,源主机会不断尝试发SYN帧企图得到ack的应答,多次尝试后才会放弃,因此造成了扫描的时间较长。
        • connect的扫描方式可能较容易被目标主机发现。

      1.2 半扫描(SYN)

      在这种技术中,扫描主机向目标主机的选择端口发送SYN数据段:

      • 如果应答是RST,那么说明端口是关闭的,按照设定继续扫描其他端口;
      • 如果应答中包含SYN和ACK,说明目标端口处于监听状态。

      由于SYN扫描时,全连接尚未建立,所以这种技术通常被称为“半连接”扫描,有以下优缺点:

      • 优点:在于即使日志中对于扫描有所记录,但是尝试进行连接的记录均为连接建立未成功记录。
      • 缺点:在大部分操作系统中,发送主机需要构造适用于这种扫描的IP包,实现复杂。并且容易被发现。

      1.3 秘密扫描(FIN)

      TCP FIN扫描技术使用FIN数据包探测端口:

      • 当一个FIN数据包到达一个关闭的端口,数据包会被丢掉,并返回一个RST数据包;
      • 当一个FIN数据包到达一个打开的端口,数据包只是简单丢掉,不返回RST数据包。

      TCP FIN扫描又称作秘密扫描,优缺点如下:

      • 优点:不包含标准的TCP三次握手协议的任何部分,无法被记录,能躲避IDS、防火墙、包过滤器和日志审计,比SYN扫描隐蔽很多。
      • 缺点:
        • 在Windows下,无论端口是否监听,都返回RET数据包,无法判断;
        • 可靠性不高,当收不到应答包时,不确定是端口在监听还是丢包了。

      2.UDP扫描

      对UDP端口扫描时,给一个端口发送UDP报文,如果端口是开放的,则没有响应,如果是关闭的,对方会恢复一个ICMP端口不可达报文。

      • 优点:Linux Windows 都能用
      • 缺点:也是不可靠的,因为返回的是错误信息,所以速度相对于TCP的FIN,SYN扫描要慢一些,如果发送的UDP包太快了,回应的ICMP包会出现大量丢失的现象。

      3. 总结

      以上四种扫描方式的判别方法总结如下:

      扫描方式 端口开放 端口关闭
      TCP connect() connect连接成功 connect()返回-1
      TCP SYN 返回SYN及ACK 返回RST
      TCP FIN 不作应答 返回RST
      UDP扫描 不作应答 返回ICMP不可达报文

      二、工具设计

      我们使用了C和Go两种语言来实现了端口扫描工具,分别由两名组员完成。每种语言分别实现了TCP-connect、SYN、FIN、UDP这四种扫描方式。

      为了提高扫描速度,我们分别利用了两种语言的特色:

      1. Go语言实现

      我们采用了Go的携程+生产者消费者模型,让多个生产者发出消息,并同时让多个消费者监听返回,如果收到了对应的返回,则说明端口开放。这样的模型一方面可以实现并行从而加快扫描速度,另一方面使用异步模型,可以有效减少因为Socket IO等待的时间。

      2. C语言实现

      我们采用了多线程的方式。多线程以实现并行从而加快扫描速度,当轮询多个socket io,并且其中某个socket io有响应时,则表示该端口的扫描报文已返回,这样就可以有效减少因为Socket IO等待的时间。

      三、具体实现

      1. Go语言的具体实现(lfy)

      1.1 整体架构

      生产者和消费者模型的具体实现:

      如上,producer函数是生产者,consumer函数是消费者。jobs是一个channel,channel是go语言中用于协程间通信的管道。整个生产者消费者模型的实现主要通过go协程来实现,同时也通过多个go携程来进行并发扫描。通过将scanJob的任务结构体输入jobs中,使消费者可以对生产的任务进行扫描。consumer是消费者,通过从jobs channel中取出job,并根据scanway进行进行响应的扫描。j.Stop是停止的flag,当channel中收到j.Stop时,则传一个true进入StopChan停止扫描。

      go producer产生一个producer协程生成任务,接下来通过轮询results channel来获得结果。

      1.2 全扫描(connect方式)

      TCP扫描的实现最简单,只需要调用Dial函数在TCP层建立socket连接三次握手即可,成功则表示端口开放。这里为了加速,设置了握手的timeout。

      1.3 SYN、FIN扫描的具体实现

      • SYN和FIN包的制作与发送

      可以看到synscan是直接构造tcp包的,是工作在ip层。而SYN扫描的精髓是手工构造的syn包,因此这里重点关注一下makePkg函数。

      重点看一下TCPHeader这个结构体。TCPHeader结构体定义了TCP报文头的格式,通过TCPHeader生成的Buffer字节流就可以直接封装进IP包中发出去。参考TCP的报文格式:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sN6o7O7Q-1645250441738)(img/007S8ZIlgy1geafta994hj30ru0igabc.jpg)]

      TCPHeader开头是来源端口和目的端口,接下来是序号,flags是各个标志位置01后的值。这里可以看到FIN和SYN扫描的区别就是最后两位,一个是10,一个是01。10代表SYN置1,01代表FIN位置1,分别表示SYN包和FIN包。

      接下来对整个头部进行校验和计算后,将checksum设置到tcp包中,然后转换成字节流即可。

      • 对端口是否开放的判断

      SYN和FIN扫描是通过返回包来判断端口是否开放的,因此这里如何接受返回包也是重中之重。

      recvSynAck和recvRst两个函数分别接受SYN和FIN的返回包,看一下具体实现:

      核心代码如上,recvSynAck在ip层上监听,当收到返回包时,通过ReadFrom将字节流读取到buffer slice中,然后判断第13个字节,如果为0x12,也就是10010,则表示syn和ack位为1,也就表示端口是开放的。recvRst的实现类似。

      1.4 UDP扫描的具体实现

      由于UDP端口并不常见,而且目前比较常见的UDP扫描方式比较慢,因此这里对UDP扫描做了一些改进。
      目前,互联网上开放的UDP端口主要有:53、123、161,因此这里暂时只对常见端口做了定制数据,当发送这些数据时,如果有返回则表示端口开放。

      1.5 过程中踩的坑

      如上图所示,左边的是能收到回显的syn包,右边是不能的,右边的最后0000就是Urgent Pointer,之前不一样的只有checksum,因此两边除了option部分完全相同,但左边收到了ack应答,而右边并没有收到。这个bug让我甚至怀疑tcp三次握手是否和option字段有关。

      后来通过查阅资料发现,这与MTU有关,左边帧长大于64字节可以发送,而右边小于64字节,无法发送。因此我在原来的代码中去掉了option部分,并padding了12个,这样就能成功发送了。

      2. C语言的具体实现(jyx)

      2.1 整体架构

      该程序在linux下用c语言实现TCP的三种端口扫描方式(connect、SYN、FIN)和UDP端口扫描。我用一个函数数组存放四个实现函数,根据选择的端口扫描方式,在创建线程的时候选择不同的函数来执行,主线程挂起等待扫描线程结束,最后打印开放的端口。用一个全局的队列来存放开放的端口,链表实现。除了connect方式,其它的方式需要知道源主机的ip,因此在创建扫描线程之前,先获得本机的ip地址,作为参数传给扫描线程。

      每个扫描线程(tcpXXXScanPort)会建立一个线程池,对每个要扫描的端口都创建一个线程tcpXXXScanEach(线程配置成detach属性),具体的实现每种方式有些差别。

      由于调用线程只能传递一个参数,所以我把要传递的信息放在一个数据结构里面,用指针传给子线程。以下是自己定义的要传递的数据结构:

      2.2 全扫描(connect方式)

      • 整体思路:

        每个扫描线程tcpConScanPort会建立一个线程池,对每个要扫描的端口都创建一个线程tcpConScanEach,线程配置成detach属性。tcpConScanEach负责connect各自的端口,将结果存在全局的一个链表中。

      • 遇到的问题和解决方案:

        1. 参数传递

          这里需要给每个端口扫描线程传递的参数包括目的ip和端口,在主端口扫描线程中,我为每一个要扫描的端口动态分配一个sockarddr_in类型的空间,将指向该空间的指针传向每一个子线程,子线程connect完成后释放这个空间。

          开始的时候是让所有的线程都访问同一块内存来确定地址,没有考虑到各个线程是并行的,因此会出现主线程的for循环中已经把地址改成下一个端口了,当前端口的子线程还没有用到自己的地址。

        2. 不可重入函数与线程安全问题

          线程由于是并发的,所以在访问全局变量和不可重入函数是要加锁的,也就是多个线程不能同时访问。记住printf是不可重入的。所以我这里引入了两个互斥锁,分别用于printf和下面说的计数变量。

        3. 线程池中线程数目太多

          线程池中线程数量太多会影响扫描的效率,所以设置了一个全局变量,为每一个存在的线程计数,当同时扫描的端口数超过100时,暂停创建子线程,当这个全局变量归零时说明扫描结束,可以打印结果了。

      • 代码分析:

        这里就贴出头文件,主要定义全局计数变量和互斥锁以及函数声明

      2.3 半扫描(SYN扫描)

      • 整体思路:

        和全扫描类似,每个扫描线程tcpSynScanPort会建立一个线程池,对每个要扫描的端口都创建一个tcpSynScanEach,该线程负责发送SYN数据包到指定端口,另有一个线程tcpSynScanRecv负责处理所有收到的数据包。有一个要点就是,发送的数据包要自己组装,协议可以使用原始套接字,协议选择TCP。

      • 遇到的问题和解决方案:

        1. 参数传递

          也是住线程里malloc一块,从线程sendto之后就free,只不过不能只像connect一样,传递目的ip和端口,因为构造包的时候要填写本机ip和端口,所以自己定义一个数据结构。

        2. 字节序

          注意网络字节序和主机字节序的转换,主机字节序时小端方式存储的,而网络字节序时按照大端方式传输的,这个要注意。

        3. 校验和

          在填写首部字段时免不了要写校验和字段,需要小心校验的范围,这里做一下总结:

          • IP校验和只检验20字节的IP报头;
          • ICMP校验和覆盖整个报文(ICMP报头+ICMP数据);
          • UDP和TCP校验和不仅覆盖整个报文,而且还有12字节的IP伪首部,包括源IP地址(4字节)、目的IP地址(4字节)、协议(2字节,第一字节补0)和TCP/UDP包长(2字节)。另外UDP、TCP数据报的长度可以为奇数字节,所以在计算校验和时需要在最后增加填充字节0(注意,填充字节只是为了计算校验和,可以不被传送)。
        4. 对丢包的处理

          只发送SYN和接收ACK或RST帧其实是在ip层进行的,还没有建立tcp连接,所以丢包的可能性很大。在这种扫描模式下,无论对方端口是打开还是关闭,都会有应答,所以我用一个全局数组记录每个端口的状态,0表示没有应答,1表示收到ACK,2表示收到RST。

        5. 各线程间的同步

          • main函数调用端口扫描主线程,main函数pthread_join等待扫描过程结束。
          • 扫描主线程调用各个端口扫描线程,各端口扫描线程发送之后就自动结束了,扫描主线程不用等待。
          • 扫描主线程调用接收线程,通过判断全局变量synCnt来确定是否扫描结束,归零后由扫描主线程kill掉接收线程,用pthread_cancel函数。
        6. tcp帧头

          [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTYHAAUZ-1645250441740)(img/007S8ZIlgy1geafta994hj30ru0igabc.jpg)]

      • 代码分析:

        • 这里也是一样贴出头文件,包含了函数和全局变量的声明

        • 实现本模块还需要定义tcp伪首部,伪首部是一个虚拟的数据结构,其中的信息是从数据报所在IP分组头的分组头中提取的,既不向下传送也不向上递交,而仅仅是为计算校验和。这样的校验和,既校验了TCP&UDP用户数据的源端口号和目的端口号以及TCP&UDP用户数据报的数据部分,又检验了IP数据报的源IP地址和目的地址。伪报头保证TCP&UDP数据单元到达正确的目的地址。

        • checksum函数

      2.4 秘密扫描(FIN扫描)

      • 整体思路:

        众所周知,当调用close()时要经历四次挥手的过程FIN-ACK-FIN-ACK。当我们发送FIN帧给一个非监听的端口时,会有RST应答,反之,发给一个正在监听的端口时,不会有任何回应。这中扫描速度快,隐蔽性好,但是对windows系统无效。

      • 具体细节:

        和SYN扫描基本相同,就是构造的数据包有一点差别,标志位FIN设为0。还有对接收数据包的处理不完全相同。

      • 代码分析:

        给出头文件,全局变量定义和函数声明:

      2.5 UDP扫描

      • 整体思路:

        给一个端口发送UDP报文,如果端口是开放的,则没有响应,如果端口是关闭的,对方会回复一个ICMP端口不可达报文。主扫描线程udpIcmpScanPort建立一个线程池,对每个要扫描的端口都创建一个线程udpIcmpScanEach,负责发送UDP包到各端口,另有一个线程udpIcmpScanRecv负责处理所有收到的数据包。发送的数据包要自己组装,协议可以使用原始套接字,协议选择UDP,接收时另外建立一个socket,也是使用原始套接字,协议选择ICMP。

      • 遇到的问题和解决方案:

        1. 发送频率太快会大量丢包

          按照SYN或者FIN的频率发送UDP包的话,会出现大量ICMP或者UDP丢包现象。频率的大小和扫描端口的数量有关系,需要扫描的数量越大,用来记录目前存在线程数的全局变量udpCnt越容易超过上限,程序就卡死在这里了。

          我想到两种解决方法,一个是把频率调低,频率降为原来的1/2时扫描1000个包还行。但是总觉得这种方法有侥幸的感觉。我尝试使用第二种方法,还是原来的频率,加了一个定时器(信号ALARM实现),在规定的时间内(我设置的是30秒)如果程序卡死在这里,则重新发送UDP包,速度可以做到跟FIN差不多,程序也没有卡死。

        2. 选择icmp相关数据结构的问题

          linux提供的有关icmp的数据结构有两种,icmp与icmphdr,我用sizeof测了一下,前者是20字节,后者是8字节。因为抓包来看,接收到的ICMP报文是IP首部(20字节)+ICMP首部(8字节)+发送的IP层的UDP报文,所以其实是IP首部(20字节,目的端口方)+ICMP首部(8字节)+IP首部(20字节,端口扫描方)+UDP首部+UDP数据,所以我用的是icmphdr。

      • 代码分析:

        头文件

      四、实际效果对比

      目前实际应用最广泛的端口扫描工具有nmap和zmap,nmap强在以功能强大、扫描结果精准,zmap的优点主要是扫描速度快,号称44分钟扫遍整个互联网。但是zmap不支持多端口批量扫描,因此,我们将我们写的两款端口扫描器与nmap这款工业界十分出名的产品做了比较。

      Go-Portscan

      1. SYN方式

      可以看到,对于SYN方式,go扫描器的扫描结果与nmap基本没有差别,并且速度快很多。这里快的原因应该主要是nmap对服务指纹进行了确认,因此速度慢了一点。

      2. TCP方式

      nmap中出现了很多filter的端口,nmap的文档指出filter的端口实际上是不开放的,因此这里可以看到我们还是快了一些,并且结果正确。

      3. FIN方式

      4. UDP方式

      这里可以看到,UDP扫描简直慢到了极点。为了扫描的准确性,nmap慢到了不可忍耐。而通过我们的改进,虽然麻烦了一些,但是速度提升了很多。

      C-portscan

      1. connect方式

      对于connect方式,扫描速度还是比较慢的,但是由于多线程加速,还是快了不少

      2. SYN扫描

      相比于全扫描,半扫描的速度大大加快,同时准确度也有所保障

      3. FIN扫描

      理论上来说FIN扫描的速度应该是最快的,但是这里结果并不是这样,是因为FIN扫描不可靠,如果未收到数据包,不能确定是丢包了还是端口开放,所以会对那些未响应的端口多次发送数据包来确认,以减少丢包带来的差错,所以扫描所耗时间较长。

      4. UDP扫描

      可以看到,UDP扫描是非常非常慢的

      五、总结与反思

      ​ 通过这次实验,加深了我们对计算机网络协议的理解与应用,在这个过程中学到了很多知识,对网络编程有所了解。然而,我们所完成的程序与专业的端口扫描软件的准确度和扫描速度还是相差很多,希望能在以后的学习中将其多加改进。

      本文由 在线网速测试 整理编辑,转载请注明出处,原文链接:https://www.wangsu123.cn/news/18727.html

          热门文章

          文章分类