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

资源下载地址:https://download.csdn.net/download/sheziqiong/85638489
资源下载地址:https://download.csdn.net/download/sheziqiong/85638489

一、技术原理

端口扫描技术向目标系统的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 整体架构

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

func producer(jobs chan *scanJob, ports []uint16) {
	for _, p := range ports {
		s := scanJob{
			Laddr: LAddr,
			Raddr: RAddr,
			SPort: uint16(random(10000, 65535)),
			DPort: p,
		}
		jobs <- &s
	}
	jobs <- &scanJob{Stop: true}
	close(jobs)
}
http://www.biyezuopin.vip
func consumer(jobs <-chan *scanJob, results chan<- *scanResult) {
	for {
		if j, ok := <-jobs; ok {
			if j.Stop == true {
				time.Sleep(time.Duration(*TimeOut) * time.Second)
				StopChan <- true
			} else if *ScanWay == "SYN" {
				SynScan(j)
				time.Sleep(1e7)
			} else if *ScanWay == "TCP" {
				TcpScan(j, results)
			} else if *ScanWay == "FIN" {
				FinScan(j)
				time.Sleep(1e7)
			} else if *ScanWay == "UDP" {
				UdpScan(j, results)
				time.Sleep(1e7)
		  }
		}
	}
}

如上,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(jobs, ports)

for {
	select {
	case res := <-results:
		fmt.Println("Open: ", res.Port)
	case <-StopChan:
		if *ScanWay == "FIN" {
			for _, v := range ports {
				if vis[v] == false {
					fmt.Println("Open: ", v)
				}
			}
		}
		eTime := time.Now().Unix()
		fmt.Println("Time: ", eTime-sTime, "s")
		os.Exit(0)
	}
}

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

1.2 全扫描(connect方式)

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

func TcpScan(j *scanJob, result chan<- *scanResult) {
	target := fmt.Sprintf("%s:%d", j.Raddr, j.DPort)
	conn, err := net.DialTimeout("tcp", target, time.Duration(*TimeOut)*time.Second)
	if err == nil {
		result <- &scanResult{
			Port: j.DPort,
		}
		defer conn.Close()
	}
}
1.3 SYN、FIN扫描的具体实现
  • SYN和FIN包的制作与发送

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

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个\0,这样就能成功发送了。

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

2.1 整体架构

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

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

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

struct ScanSock
{
    unsigned short portStart;
    unsigned short portEnd;
    char destIP[16];
    char sourIP[16];
};

struct ScanParam
{
    unsigned short sourPort;
    unsigned short destPort;
    char destIP[16];
    char sourIP[16];
};
  • 代码分析:

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

      #ifndef TCPSYNSCAN_H_H
      #define TCPSYNSCAN_H_H
       
      #include "mysock.h"
       
      int synCnt;
      static pthread_mutex_t syn_printf_mutex = PTHREAD_MUTEX_INITIALIZER;
      static pthread_mutex_t syn_num_mutex = PTHREAD_MUTEX_INITIALIZER;
       
      void* tcpSynScanPort(void *arg);
      void* tcpSynScanEach(void *arg);
      void* tcpSynScanRecv(void *arg);
       
      #endif
      
    • 实现本模块还需要定义tcp伪首部,伪首部是一个虚拟的数据结构,其中的信息是从数据报所在IP分组头的分组头中提取的,既不向下传送也不向上递交,而仅仅是为计算校验和。这样的校验和,既校验了TCP&UDP用户数据的源端口号和目的端口号以及TCP&UDP用户数据报的数据部分,又检验了IP数据报的源IP地址和目的地址。伪报头保证TCP&UDP数据单元到达正确的目的地址。

      struct PseudoHdr
      {
          unsigned int    sIP;
          unsigned int    dIP;
          char            useless;
          char            protocol;
          unsigned short  length;
      };
      
    • checksum函数

      unsigned short checksum(unsigned char*buf, unsigned int len)
      {//对每16位进行反码求和(高位溢出位会加到低位),即先对每16位求和,在将得到的和转为反码
          unsigned long sum = 0;
          unsigned short *pbuf;
          pbuf = (unsigned short*)buf;//转化成指向16位的指针
          while(len > 1)//求和
          {
              sum+=*pbuf++;
              len-=2;
          }
          if(len)//如果len为奇数,则最后剩一位要求和
              sum += *(unsigned char*)pbuf;
          sum = (sum>>16)+(sum & 0xffff);
          sum += (sum>>16);//上一步可能产生溢出
          return (unsigned short)(~sum);
      }
      
2.4 秘密扫描(FIN扫描)
  • 整体思路:

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

  • 具体细节:

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

  • 代码分析:

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

    #ifndef TCPFINSCAN_H_H
    #define TCPFINSCAN_H_H
     
    #include "mysock.h"
     
    int finCnt;
    static pthread_mutex_t fin_printf_mutex = PTHREAD_MUTEX_INITIALIZER;
    static pthread_mutex_t fin_num_mutex = PTHREAD_MUTEX_INITIALIZER;
     
    void* tcpFinScanPort(void *arg);
    void* tcpFinScanEach(void *arg);
    void* tcpFinScanRecv(void *arg);
     
    #endif
    
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。

  • 代码分析:

    头文件

    #ifndef UDPICMPSCAN_H_H
    #define UDPICMPSCAN_H_H
     
    #include "mysock.h"
     
    int udpCnt;
    static pthread_mutex_t udp_printf_mutex = PTHREAD_MUTEX_INITIALIZER;
    static pthread_mutex_t udp_num_mutex = PTHREAD_MUTEX_INITIALIZER;
     
    void* udpIcmpScanPort(void *arg);
    void* udpIcmpScanEach(void *arg);
    void* udpIcmpScanRecv(void *arg);
    void alarm_udp(int signo);
     
    #endif
    

四、实际效果对比

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

nmap及zmap的各种扫描方式对应的参数:
-sS/sT/sA/sW/sM: TCP SYN/Connect()/ACK/Window/Maimon scans
-sU: UDP Scan
-sN/sF/sX: TCP Null, FIN, and Xmas scans

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://download.csdn.net/download/sheziqiong/85638489
资源下载地址:https://download.csdn.net/download/sheziqiong/85638489

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值