• 1. Stay - Post
  • 2. Circles - Post
  • 3. Hollywood's_Bleeding - Post
  • 4. A_Thousand_Bad_Times - Post

学习编写安全工具 with Golang之端口扫描篇

前言

友情提示1: 本系列(坑)文章参考书籍为Black Hat Go,基本代码也是照搬里面的内容,但似乎没有中文版,所以就发发自己的理解,文章的顺序也是我阅读书籍的顺序,还可以顺带熟悉一下Go语言,理解不到位的地方还请各位见谅.

友情提示2: 本系列博文请先知道了Go语言语法再阅读

三个基础

我们都知道对于TCP协议来说,开放端口总共有65535个端口,因此单线程跑,其速度是可想而知的慢,自然我们需要多线程来扫描TCP端口.

多线程创建

在Go语言里面多线程的创建非常简单,语法很简单,比如:

go func(){
    fmt.Println("hello world")
}()

整体的结构是go关键词加上函数的形式,这里和JS中的匿名函数类似,后面的()是为了调用这个匿名函数,这样就相当于我们创建好了一个线程,形式上非常简单,但这段代码并不会有回显

原因在于我们创建的线程不会和main同步结束,因为Go中多线程是非阻塞的,从而导致程序提前退出,避免这一步的方法一般是让线程main进行sleep操作,或者使用WorkGroup同步线程.

同步线程

Golang中可以使用WorkGroup进行线程间的同步,具体用法如下:

package main

import (
    "fmt"
    "sync"
)

var wg = sync.WaitGroup{}

func main(){
    wg.Add(1)
    go func() {
        fmt.Println("hello")
        wg.Done()
    }()
    wg.Wait()
}

解释一下,Add()需要传入一个Int型参数,简单理解就是告诉程序我要加多少个线程,然后Done()则是告诉编译器这个线程已经执行完毕了(这个操作会使Add()中存储的值-1),最后用Wait()方法进行等待,当Add()中的值为0,则退出等待,所有线程一起返回.这样就不会造成有线程提前结束,导致结果出错的情况了.

通道

Golang中提倡使用通道来进行线程间的通信,而非多个线程一同访问一个全局变量(这样会导致条件竞争,也称竞态)

所以我们需要使用通道来进行进程间的通信,通道一般使用make()函数进行定义,定义形式为变量名 := make(chan [存储的数据类型], [int缓存大小])例程为:

package main

import (
    "fmt"
)

func PrintNum(p chan int){
    for i := range p{
        fmt.Println(i) //range 对通道中的数据进行了读取
    }
}

func main(){
    p := make(chan int,10)
    for i:=0;i<=3;i++{
        go PrintNum(p)
    }
    for i:=0;i<=150;i++{
        p <- i 
    }
    // p.Close() 记得最后关闭通道
}

返回结果为:

从返回结果来看:

  1. 通道是阻塞的,这里我们创建了4个需要读取通道数据的进程,而这个时候通道并没有数据,实际上在通道没有数据但却需要读取时,通道阻塞等待写入,还有一种情况则是通道的缓存已经写满了,在读出数据前阻塞.
  2. 多线程无法保证线程执行的顺序,我们发现结果实际上是乱序,而非顺序的0-150
  3. 通道也可以同步进程,因为最后我们的结果是完成了返回,即没有线程提前退出的情况(可能不太严谨),

一个标准库

测试端口是否开放,我们使用的是net标准库中自带的Dial([链接方式-string],'[连接地址(url或ip)-string]')函数,这个函数返回一个net包中定义好的数据结构和error类型的数据,例程如下:

package main

import (
    "fmt"
    "net"
)

func main(){
    _,err := net.Dial("tcp","scanme.nmap.org:80")
    if err == nil {
        fmt.Println("Port:80 is open")
    }
}

返回结果:

因此我们可以以这种方式,来测试目标网站端口是否开放,由于使用WorkGroup我们可能需要访问一个全局变量,因此我们这里使用通道进行通信.

分部分编程

首先理清楚思路,我们需要一个通道存储端口号,然后将测试结果反馈给另一个变量进行存储,由于多线程无法保证执行顺序,因此我们最后还需要对保存结果的变量进行排序,最后打印出结果,这里只是测试只扫描1000个端口:

扫描端口函数

func ConnectPort(port chan int,result *[]int){
    for p := range port{
        url := fmt.Sprintf("scanme.nmap.org:%d",p)
        connect,err := net.Dial("tcp",url)
        if err != nil {
            continue
        }
        *result = append(*result,p)
        // 用切片作为接受返回结果的数据类型
        // Go自带的排序标准库也是需要切片作为传入参数
        connect.Close()
    }
}

拼接一下:

package main

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

func ConnectPort(port chan int,result *[]int){
    for p := range port{
        url := fmt.Sprintf("scanme.nmap.org:%d",p)
        _,err := net.Dial("tcp",url)
        if err != nil {
            continue
        }
        *result = append(*result,p)
    }
}

func main(){
    t := time.Now()
    ports := make(chan int,400)
    result := make([]int,0)
    for i:=1;i<=cap(ports);i++{
        go ConnectPort(ports,&result)
    } //创建ports的容量个线程数
    for i := 1;i<=1000;i++{
        ports <- i
    }
    close(ports)
    sort.Ints(result) 
    //对切片进行从小到大的排序
    for _,value := range result{
        fmt.Printf("Port:%d is Open\n",value)
    }
    fmt.Println(time.Since(t))
    //记录程序运行的时间
    //运行时间和线程数以及ports容量数有关,但也不能太大,否则直接退出...
}

最后运行结果:


然后可以添加一段显示进度的代码,并将其改成扫描全部端口,注意ports通道请适当增大,经过测试通道为2048大小下大概扫描全端口需要3分23秒,4096大小下大约2分13秒,8192大小下大约1分25秒左右,10240大小下大约1分10秒左右:

for i:=1;i<=65535 ;i++  {
        ports <- i
        fmt.Fprintf(os.Stdout,"Port:%d is scaning\r",i)
    }

这就是一个简单的端口扫描模块了,我们可以将这些代码进行封装,然后主程序接入用户输入,从而将该代码作为一个调用模块.


除非注明,ebounce文章均为原创,转载请以链接形式标明本文地址

本文地址:http://ebounce.cn/code/56.html

新评论

captcha
请输入验证码