学习编写安全工具 with Golang之端口扫描篇
前言
友情提示1: 本系列(坑)文章参考书籍为Black Hat Go
,基本代码也是照搬里面的内容,但似乎没有中文版,所以就发发自己的理解,文章的顺序也是我阅读书籍的顺序,还可以顺带熟悉一下Go语言,理解不到位的地方还请各位见谅.
友情提示2: 本系列博文请先知道了Go语言语法再阅读
<!--more-->
三个基础
我们都知道对于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() 记得最后关闭通道
}
返回结果为:
从返回结果来看:
- 通道是阻塞的,这里我们创建了4个需要读取通道数据的进程,而这个时候通道并没有数据,实际上在通道没有数据但却需要读取时,通道阻塞等待写入,还有一种情况则是通道的缓存已经写满了,在读出数据前阻塞.
- 多线程无法保证线程执行的顺序,我们发现结果实际上是乱序,而非顺序的
0-150
- 通道也可以同步进程,因为最后我们的结果是完成了返回,即没有线程提前退出的情况
(可能不太严谨),
一个标准库
测试端口是否开放,我们使用的是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)
}
这就是一个简单的端口扫描模块了,我们可以将这些代码进行封装,然后主程序接入用户输入,从而将该代码作为一个调用模块.