TCP全连接端口扫描器

原理介绍

TCP全连接端口扫描器是最基础的扫描器,它的原理是调用Socketconnect函数连接到目标IP的特定端口上,如果连接成功说明端口是开放的,如果连接失败,说明端口没有开放。

Go语言的net包提供的DialDialTimeout函数,对传统的socket函数进行了封装,无论想创建什么协议的连接,都只需要调用这两个函数即可。这两个函数的区别是DialTimeout增加了超时时间。

普通扫描器

以下代码片段利用DialTimeout实现了一个Connect方法,可以判断一个端口是否开放

func Connect(ip string,port int)(net.Conn,error)  {
	conn,err := net.DialTimeout("tcp",fmt.Sprintf("%v:%v",ip,port),2*time.Second)
	defer func() {
		if conn != nil{
			_ = conn.Close()
		}
	}()
	return conn,err
}

这里封装一个GetIpList函数,可以根据输入的ipList返回一个[]net.IP的切片

func GetIpList(ips string)([]net.IP,error)  {
	addressList,err := iprange.ParseList(ips)
	if err != nil{
		return nil, err
	}
	list := addressList.Expand()
	return list,err
}

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

func GetPorts(selection string)([]int,error)  {
	ports := []int{}
	if selection == ""{
		return ports,nil
	}
	ranges := strings.Split(selection,",")
	for _,r := range ranges{
		r = strings.TrimSpace(r)
		if strings.Contains(r,"-"){
			parts := strings.Split(r,"-")
			if len(parts) != 2{
				return nil, fmt.Errorf("Invaild port selection sequment: '%s'",r)
			}
			p1,err := strconv.Atoi(parts[0])
			if err != nil{
				return nil,fmt.Errorf("Invaild port number: '%s'",parts[0])
			}
			p2,err := strconv.Atoi(parts[1])
			if err != nil{
				return nil,fmt.Errorf("Invaild port number: '%s'",parts[1])
			}
			if p1 > p2{
				return nil,fmt.Errorf("Invaild port range:%d-%d",p1,p2)
			}
			for i := p1;i <= p2;i++{
				ports = append(ports,i)
			}
		}else{
			if port, err := strconv.Atoi(r);err!=nil{
				return nil,fmt.Errorf("Invaild port number: '%s'",r)
			}else{
				ports = append(ports,port)
			}
		}
	}
	return ports,nil
}

主函数和导入的相应包

func main() {
	if len(os.Args) == 3{
		ipList := os.Args[1]
		portList := os.Args[2]
		ips,err := GetIpList(ipList)
		_ = err
		ports,err := GetPorts(portList)
		for _,ip := range ips{
			for _, port := range ports{
				_, err := Connect(ip.String(),port)
				if err != nil{
					continue
				}
				fmt.Printf("ip: %v,port: %v is open \n",ip,port)
			}
		}
	}else{
		fmt.Printf("%v iplist port \n",os.Args[0])
	}
}
import (
	"fmt"
	"github.com/malfunkt/iprange"
	"net"
	"os"
	"strconv"
	"strings"
	"time"
)

运行结果,因为现在完成的TCP全连接端口扫描器是单线程扫描器,扫描速度非常慢,不适合用在实际的扫描任务中。因此我们需要将它改装为高并发的扫描器,扫描速度可以与Nmap媲美。
在这里插入图片描述

高并发扫描器(一)

Go语言是原生支持并发的语言,它的并发是通过协程实现的。实现的步骤是:

  1. 生成扫描任务列表:首先解析出需要扫描的IP与端口的切片,然后将需要扫描的IP与端口列表放入一个[]map[string]int中,map的key为IP地址,value为端口,[]map[string]int表示所有需要扫描的IP与端口对的切片。
func GenerateTask(ipList []net.IP, ports []int) ([]map[string]int, int) {
	tasks := make([]map[string]int, 0)

	for _, ip := range ipList {
		for _, port := range ports {
			ipPort := map[string]int{ip.String(): port}
			tasks = append(tasks, ipPort)
		}
	}

	return tasks, len(tasks)
}
  1. 分割扫描任务:根据并发数将需要扫描的[]map[string]int切片分割成组,以便按组进行并发扫描。
func AssigningTasks(tasks []map[string]int) {
	scanBatch := len(tasks) / vars.ThreadNum

	for i := 0; i < scanBatch; i++ {
		curTask := tasks[vars.ThreadNum*i : vars.ThreadNum*(i+1)]
		RunTask(curTask)
	}

	if len(tasks)%vars.ThreadNum > 0 {
		lastTasks := tasks[vars.ThreadNum*scanBatch:]
		RunTask(lastTasks)
	}
}

len(tasks)%vars.ThreadNum >0表示len(tasks) / vars.ThreadNum不能整除,还有剩余的任务列表需要进行处理。

  1. 按组执行扫描任务:分别将每组扫描任务传入具体的扫描任务中,扫描任务函数利用sync.WaitGroup实现并发扫描,在扫描的过程中将结果保存到一个并发安全的map中。
func AssigningTasks(tasks []map[string]int) {
	scanBatch := len(tasks) / vars.ThreadNum

	for i := 0; i < scanBatch; i++ {
		curTask := tasks[vars.ThreadNum*i : vars.ThreadNum*(i+1)]
		RunTask(curTask)
	}

	if len(tasks)%vars.ThreadNum > 0 {
		lastTasks := tasks[vars.ThreadNum*scanBatch:]
		RunTask(lastTasks)
	}
}
  1. 展示扫描结果:所有扫描任务完成后,输出保存在并发安全map中的扫描结果。
func SaveResult(ip string, port int, err error) error {
	// fmt.Printf("ip: %v, port: %v,err: %v, goruntineNum: %v\n", ip, port, err, runtime.NumGoroutine())
	if err != nil {
		return err
	}

	v, ok := vars.Result.Load(ip)
	if ok {
		ports, ok1 := v.([]int)
		if ok1 {
			ports = append(ports, port)
			vars.Result.Store(ip, ports)
		}
	} else {
		ports := make([]int, 0)
		ports = append(ports, port)
		vars.Result.Store(ip, ports)
	}
	return err
}

func PrintResult() {
	vars.Result.Range(func(key, value interface{}) bool {
		fmt.Printf("ip:%v\n", key)
		fmt.Printf("ports: %v\n", value)
		fmt.Println(strings.Repeat("-", 100))
		return true
	})
}

在main函数中分别调用任务生成、任务分配与结果展示的函数即可

package main

import (
	"fmt"
	"os"
	"runtime"

	"sec-dev-in-action-src/scanner/tcp-connect-scanner1/scanner"
	"sec-dev-in-action-src/scanner/tcp-connect-scanner1/util"
)

func main() {
	if len(os.Args) == 3 {
		ipList := os.Args[1]
		portList := os.Args[2]
		ips, err := util.GetIpList(ipList)
		ports, err := util.GetPorts(portList)
		_ = err

		task, _ := scanner.GenerateTask(ips, ports)
		scanner.AssigningTasks(task)
		scanner.PrintResult()

	} else {
		fmt.Printf("%v iplist port\n", os.Args[0])
	}
}

func init() {
	runtime.GOMAXPROCS(runtime.NumCPU())
}

vars.go

package vars

import "sync"

var (
	ThreadNum = 5000
	Result    *sync.Map
)

func init() {
	Result = &sync.Map{}
}

util.go

package util

import (
	"fmt"
	"net"
	"strconv"
	"strings"

	"github.com/malfunkt/iprange"
)

func GetPorts(selection string) ([]int, error) {
	ports := []int{}
	if selection == "" {
		return ports, nil
	}

	ranges := strings.Split(selection, ",")
	for _, r := range ranges {
		r = strings.TrimSpace(r)
		if strings.Contains(r, "-") {
			parts := strings.Split(r, "-")
			if len(parts) != 2 {
				return nil, fmt.Errorf("Invalid port selection segment: '%s'", r)
			}

			p1, err := strconv.Atoi(parts[0])
			if err != nil {
				return nil, fmt.Errorf("Invalid port number: '%s'", parts[0])
			}

			p2, err := strconv.Atoi(parts[1])
			if err != nil {
				return nil, fmt.Errorf("Invalid port number: '%s'", parts[1])
			}

			if p1 > p2 {
				return nil, fmt.Errorf("Invalid port range: %d-%d", p1, p2)
			}

			for i := p1; i <= p2; i++ {
				ports = append(ports, i)
			}

		} else {
			if port, err := strconv.Atoi(r); err != nil {
				return nil, fmt.Errorf("Invalid port number: '%s'", r)
			} else {
				ports = append(ports, port)
			}
		}
	}
	return ports, nil
}

func GetIpList(ips string) ([]net.IP, error) {
	addressList, err := iprange.ParseList(ips)
	if err != nil {
		return nil, err
	}

	list := addressList.Expand()
	return list, err
}

运行效果如下,扫描速度甚至比nmap还快
在这里插入图片描述
在这里插入图片描述

高并发扫描器(二)

这个扫描器虽然已经实现了并发扫描,但对协程的控制不够精细,每组扫描任务都会瞬间启动大量的协程,然后逐渐关闭,而不是一个平滑的过程。这种方法可能会瞬间将服务器的CPU占满,我们可以使用sync.WaitGroupchannel配合实现了新的并发方式

func RunTask(tasks []map[string]int) {
	wg := &sync.WaitGroup{}

	//创建一个buffer为vars.ThreadNum*2的channel
	taskChan := make(chan map[string]int,vars.ThreadNum*2)

	//创建vars.ThreadNum个协程
	for i:= 0;i < vars.ThreadNum;i++{
		go Scan(taskChan,wg)
	}

	//生产者,不断地往taskChan channel发送数据,知道channel阻塞
	for _,task := range tasks{
		wg.Add(1)
		taskChan <- task
	}
	close(taskChan)
	wg.Wait()
}

func Scan(taskChan chan map[string]int,wg*sync.WaitGroup)  {
	//每个协程都从channel中读取数据后开始扫描并保存存入数据库
	for task := range taskChan{
		for ip,port := range task{
			err := SaveResult(Connect(ip,port))
			_ = err
			wg.Done()
		}
	}
}

RunTask函数不断地将扫描任务发送到taskChan中,Scan会不断地消费taskChan中的数据。
在这里插入图片描述

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平凡的学者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值