Go编写工具教程第一课 高并发端口扫描

2021-02-04 18:05:39 浏览数 (1)

声明

本文作者:TheNorth

本文字数:3194

阅读时长:20分钟

附件/链接:点击查看原文下载

声明:请勿用作违法用途,否则后果自负

本文属于WgpSec原创奖励计划,未经许可禁止转载

前言

        今天我们一起来学习下如何用GO 编写一个高并发端口扫描工具,本教学文章持续连载,后面会接连着实现主机发现,漏洞探测,远程执行,暴力破解等等的教学,有兴趣的师傅可关注公众号回复加群一起讨论~

一、

理论知识

    在本文开始之前,我们介绍一些理论知识。

1.1 

TCP三次握手

TCP握手有三个过程。首先,客户端发送一个SYN探测包,如果客户端收到连接超时,说明该端口可能在防火墙后面。如果服务端应答syn-ack 包,意味着这个端口是打开的,否则会返回rst包,最后,客户端需要另外发送一个ack包。从这时起,客户端与服务端就已经建立连接。

1.2 

GO中的协程

    1.2.1 Goroutine

Go语言中的Goroutine类似与线程,但是goroutine是由Go的运行时调度和管理的。Go程序会智能的讲goroutine中的任务合理的分配给每个CPU。

在Go语言中,你不用自己编写额外的进程、线程、协程。当你需要某个任务并发执行时,我们只需要把这个任务封装成一个函数,开启一个goroutine去执行这个函数就可以了,十分简单。

    1.2.2 Goroutine 中的使用

只需要在调用函数的时候前面加上go这个关键字就可以了

    1.2.3 启动多个Goroutine如何同步

当我们进行某一IP对应的端口扫描,我们可能会开启多个goroutine来帮我们完成扫描任务,但是我们必须等待所有的goroutine执行完对应的任务,我们才会知道相应的结果,此时就需要考虑到多个goroutine如何同步。

在Go中,如果我们开启多个Goroutine,我们使用sync.WaitGroup来实现goroutine的同步

代码语言:javascript复制
var wg sync.WautGroup

func Task(){
	defer wg.Done() //wg.Done()表明当前goroutine结束,defer关键字会让wg.Done()操作在Task函数中所有操作执行完再执wg.Done()
	fmt.Printf("任务开始%dn", i)
	time.Sleep(time.Second) // 模拟运行的时间
	fmt.Printf("任务完成%dn", i)
}

func DoTask() {
	for i := 0; i < 10; i   {
		wg.Add(1) //每要启动一个goroutine,就加1
		go Task(i)
	}
	wg.Wait() //阻塞,等待所有goroutine完成
}
注意:在使用goroutine的时候,如果主线程结束了,其他协程也没法执行

1.3 

Channel

        在我们实际开发中,我们需要函数与函数之间的数据交换,使用共享内存进行数据交换时,在共享内存存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,我们需要加锁,但是加锁必然会导致性能降低。

因此,Go中提出了一种特殊的类型(channel),channel类似于一个队列,遵循先进先出,保证收发数据的顺序。

代码语言:javascript复制
ch1 := make(chan int,[缓冲大小]) //如果使用无缓冲区的的channel,必须有接收者,否则报deadlock错误

//channel基本操作
//(1)发送
ch1 <- 10
//(2)接收
val := <- ch //从ch中接受之,并赋给XX
<-ch //从ch中接收值,忽略结果
//(3)关闭
close(ch)

关闭后的通道有以下特点:

代码语言:javascript复制
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。

二、

编写端口扫描工具(TCP协议)

上面的基础知识,只是会涉及的内容,每一个基础知识都有很多知识点,师傅们可以根据个人需求,进行进一步的学习。

2.1 简单的非并发端口扫描

代码语言:javascript复制
package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	defaultports := [...]int{21, 22, 23, 25, 80, 443, 8080,
		110, 135, 139, 445, 389, 489, 587, 1433, 1434,
		1521, 1522, 1723, 2121, 3306, 3389, 4899, 5631,
		5632, 5800, 5900, 7071, 43958, 65500, 4444, 8888,
		6789, 4848, 5985, 5986, 8081, 8089, 8443, 10000,
		6379, 7001, 7002}
	var res []int = make([]int,0)
	start := time.Now()
	for _, port := range defaultports {
        //参数1,扫描使用的协议,参数2,IP 端口号,参数3,设置连接超时的时间
		_, err := net.DialTimeout("tcp", "127.0.0.1:" strconv.Itoa(port), time.Second)
		if err == nil {
			fmt.Printf("端口开放%dn", port)
			res = append(res, port)
		} else {
			fmt.Printf("端口关闭%dn", port)
		}
	}
	end := time.Since(start)
	fmt.Println("花费的总时间:", end)
	fmt.Println("开放的端口", res)
}

通过上面的例子,我们可以看到如果不使用并发进行端口扫描,扫描花费的时间较高

2.2 并发式端口扫描工具

ScanTask.go

代码语言:javascript复制
package PortScan

import (
	"fmt"
	"net"
	"strconv"
	"sync"
	"time"
)

//taskschan中存储要扫描的端口,reschan存储开放的端口号,exitchan存储当前的goroutine是否完成的状态,wgscan 同步goroutine
func Scan(ip string, taskschan chan int, reschan chan int, exitchan chan bool, wgscan *sync.WaitGroup) {
	defer func() {
		fmt.Println("任务完成")
		exitchan <- true
		wgscan.Done()
	}()
	fmt.Println("开始任务")
	for {
		port, ok := <-taskschan
		if !ok {
			break
		}
		_, err := net.DialTimeout("tcp", ip ":" strconv.Itoa(port), time.Second)
		if err == nil {
			reschan <- port
			fmt.Println("开放的端口", port)
		}
	}
}

main.go

代码语言:javascript复制
package main

import (
	"AstaGo/Tools/PortScan"
	"fmt"
	"sync"
	"time"
)


func main() {
	defaultports := [...]int{21, 22, 23, 25, 80, 443, 8080,
		110, 135, 139, 445, 389, 489, 587, 1433, 1434,
		1521, 1522, 1723, 2121, 3306, 3389, 4899, 5631,
		5632, 5800, 5900, 7071, 43958, 65500, 4444, 8888,
		6789, 4848, 5985, 5986, 8081, 8089, 8443, 10000,
		6379, 7001, 7002}
	taskschan := make(chan int, len(defaultports))
	reschan := make(chan int, len(defaultports))
	exitchan := make(chan bool, 4)
	var wgp sync.WaitGroup
	for _, value := range defaultports {
		taskschan <- value
	}
    //向taskschan中写完数据时,就需要关闭taskschan,否则goroutine会一直认为该channel会写入数据,会一直等待
	close(taskschan)
	start := time.Now()
    //开启四个goroutine执行扫描任务
	for i := 0; i < 4; i   {
		wgp.Add(1)
		go PortScan.Scan("127.0.0.1", taskschan, reschan, exitchan, &wgp)
	}
	wgp.Wait()
    //判断4个goroutine是否都执行完了,当他们都执行完了写入到reschan,rechan才可以被关闭
	for i := 0; i < 4; i   {
		<-exitchan
	}
	end := time.Since(start)
	close(exitchan)
	close(reschan)
	for {
		openport, ok := <-reschan
		if !ok {
			break
		}
		fmt.Println("开放的端口:", openport)
	}
	fmt.Println("花费的时间:", end)
}

我们可以看到,我们开启4个goroutine扫描相同的端口,花费的时间缩小为原来的1/4。

三、

优化工具

需要优化的指标

(1)用户可以自己输入IP,端口号

(2)用户可以指定goroutine的数量

go开发者给我们提供了flag包,可以方便的解析命令行参数,而且参数顺序可以随意

ScanTask.go

代码语言:javascript复制
package PortScan

import (
	"net"
	"sync"
	"time"
)

func Scan(ip string, taskschan chan string, reschan chan string, exitchan chan bool, wgscan *sync.WaitGroup) {
	defer func() {
		exitchan <- true
		wgscan.Done()
	}()
	for {
		port, ok := <-taskschan
		if !ok {
			break
		}
		_, err := net.DialTimeout("tcp", ip ":" port, time.Second)
		if err == nil {
			reschan <- port
		}
	}
}

main.go

代码语言:javascript复制
package main

import (
	"AstaGo/Tools/PortScan"
	"flag"
	"fmt"
	"strings"
	"sync"
	"time"
)



func main() {
	var scanports []string
	defaultports := [...]string{"21", "22", "23", "25", "80", "443", "8080",
		"110", "135", "139", "445", "389", "489", "587", "1433", "1434",
		"1521", "1522", "1723", "2121", "3306", "3389", "4899", "5631",
		"5632", "5800", "5900", "7071", "43958", "65500", "4444", "8888",
		"6789", "4848", "5985", "5986", "8081", "8089", "8443", "10000",
		"6379", "7001", "7002"}
	var ip string
	var ports string
	var gonum int
	flag.StringVar(&ip, "i", "127.0.0.1", "扫描的IP地址,默认本机地址")
	flag.StringVar(&ports, "p", "", "扫描的端口地址,默认使用默认端口")
	flag.IntVar(&gonum, "g", 4, "开启的goroutine的数量,默认为4")
	flag.Parse()
	if len(ports) != 0 {
		scanports = strings.Split(ports, ",")
	} else {
		scanports = defaultports[:]
	}
	taskschan := make(chan string, len(scanports))
	reschan := make(chan string, len(scanports))
	exitchan := make(chan bool, 4)
	var wgp sync.WaitGroup
	for _, value := range scanports {
		taskschan <- value
	}
	close(taskschan)
	start := time.Now()
	for i := 0; i < gonum; i   {
		wgp.Add(1)
		go PortScan.Scan("127.0.0.1", taskschan, reschan, exitchan, &wgp)
	}
	wgp.Wait()
	for i := 0; i < gonum; i   {
		<-exitchan
	}
	end := time.Since(start)
	close(exitchan)
	close(reschan)
	for {
		openport, ok := <-reschan
		if !ok {
			break
		}
		fmt.Println("开放的端口:", openport)
	}
	fmt.Println("花费的时间:", end)
}

后记

目前这个端口扫描只支持TCP协议,由于UDP协议无连接的特性,导致,针对使用UDP协议的端口识别存在问题。如果师傅们有比较好的建议,可以留言。同时Go开发一个自动化的扫描工具,端口扫描只是其中的一部分,后面会接连着实现主机发现,漏洞探测,远程执行,暴力破解等等的教学。

0 人点赞