GO黑帽子学习笔记- 端口扫描

admin 2022年11月9日11:18:26评论55 views字数 7889阅读26分17秒阅读模式

零基础学go

GO黑帽子学习笔记- 端口扫描

TCP基础知识

TCP中最重要的一点即三次握手,所谓tcp全连接扫描,tcp半连接扫描等都是依据三次握手的基础进行的,只不过是在握手的次数上有区别。简单来说三次握手分为下面三步:

第一次握手:客户端发送syn包,表示通信开始

第二次握手:服务端回复syn-ack作为相应,提示客户端以ack结束

第三次握手:客户端发送ack,通信开始

GO黑帽子学习笔记- 端口扫描
image-20220812140116599

而如果端口被关闭,则服务器会发送一个rst数据包而不是syn-ack进行响应。最后,如果流量被防火墙过滤,那么客户端不会从服务器收到任何响应。

一个最简单的扫描器

package main

import (
"fmt"
"net"
)

func main() {
_, err := net.Dial("tcp", "www.haochen1204.com:80")
if err == nil {
fmt.Println("Connection successful!")
} else {
fmt.Println("Connection failed!")
}
}

此段代码即使用go建立一个最简单的tcp扫描器,为了实现tcp连接功能,需要使用net包,我们使用net.Dial函数,可以创建UNIX套接字、UDP和第4层协议的连接。该函数需要两个参数,一个为我们使用的连接类型,比如我使用tcp,那么久传入字符串tcp,然后第二个参数为我们的连接地址和端口,比如127.0.0.1:80或我们上面所写的www.haochen1204.com:80。

那么我们如何知道连接是否成功了呢,Dial函数会返回给我们两个参数,conn和error,如果连接成功,error的值为nil。所以,根据Dial的返回值我们可以我们判断是否连接成功,通过判断error的值是否为nil就可以了。

GO黑帽子学习笔记- 端口扫描
image-20220812141118431

非并发扫描

我们刚刚实现了对一个端口的扫描,但是在实际测试中,我们往往需要对大多数的端口进行扫描,那么其实,我们如果想对一定范围的端口进行扫描,我们只需要在上面的扫描外套一个for循环即可。

package main

import (
"fmt"
"net"
)

func main() {
for i := 1; i <= 1024; i++ {
address := fmt.Sprintf("www.haochen1204.com:%d", i)
//fmt.Println(address)
conn, err := net.Dial("tcp", address)
if err != nil {
// 端口被关闭
//fmt.Printf("%d closen", i)
continue
}
conn.Close()
fmt.Printf("%d openn", i)
}
}

这里需要注意的只有一点,因为我们在DIal中address参数需要为我们的"ip/域名:端口"的格式,而不像python,我们可以在这里直接使用address +':'+str(i)的方式进行拼接,我们需要使用fmt.Sprintf函数,来对我们的地址进行拼接。而fmt包中的Sprintf函数就是这样一个用来格式化字符串的函数,首先我们需要一个需要被格式化的字符串作为参数,其中通过%s %d等(c中学过)来代表需要加入参数的位置,然后后面依次跟上需要增加的参数即可。剩下的就和前面一样了,直接扫描,输出结果,重新进行循环即可。(慢一批,不想等了...)

GO黑帽子学习笔记- 端口扫描
image-20220812143953683

并发扫描

并发扫描,即使用多线程扫描,在go语言中,多线程被称为goroutine,其数量仅受系统的处理能力和可用内存的限制。通过多线程的方式,我们可以极大的加快我们端口扫描的效率。

package main

import (
"fmt"
"net"
"sync"
)

func main() {
for i := 1; i <= 1024; i++ {
go func(j int) {
address := fmt.Sprintf("www.haochen1204.com:%d", j)
//fmt.Println(address)
cnn, err := net.Dial("tcp", address)
if err != nil {
//fmt.Println("close")
return
}
cnn.Close()
fmt.Printf("%d openn", j)
}(i)
}
}

当然,其实这个代码是不对的,在跟着书上敲完运行的时候直接懵了,为什么程序秒结束,而这里就是主线程和子线程的问题,我们在命令行中运行的是我们的main()函数,而这就是我们的主线程,而我们使用go创建的线程被称作为main()的子线程,当我们的主线程结束时,我们的子线程不论有没有执行完成,都会随着主线程的结束一并被结束,所以我们上面的代码,我们创建了1024个子线程后,主线程就立刻结束了,并没有等待子线程执行完成,所以这里会出现程序秒结束的情况。

而这段代码中还有一个知识点,匿名函数,可以看到,我们在main函数中又写了一个函数,而这个函数是没有函数名的,我们在函数结束的位置加上"(参数)"即可将其运行,当然,我们使用了go,那便是创建一个子线程来执行这个函数中的内容了,和我们在外面创建一个scan(j int),然后在主函数中go scan(i)的效果是一样的。

回到正题,我们程序秒结束的问题该怎么解决,在python中,我曾通过thread.active_count()来解决这个问题,当active_count()的返回值为1时,即只剩一个主线程在运行时来判断子线程运行全部完成,可以退出主线程了。但是在go黑帽子这本书中,针对go语言,作者给出了另一种解法,使用sync包中的WaitGroup,原文中写道:有几种方法可以解决这个问题。一种是使用sync包中的WaitGroup,这是一种控制并发的线程安全的方法WaitGroup是一种结构体类型...

那么下面便通过WaitGroup来完成主线程、子线程的同步扫描。

package main

import (
"fmt"
"net"
"sync"
)

func main() {
// 创建同步计数器
var wg sync.WaitGroup
for i := 1; i <= 1024; i++ {
// 计数器加一
wg.Add(1)
go func(j int) {
// 在函数执行结束时计数器减一
defer wg.Done()
address := fmt.Sprintf("www.haochen1204.com:%d", j)
//fmt.Println(address)
cnn, err := net.Dial("tcp", address)
if err != nil {
//fmt.Println("close")
return
}
cnn.Close()
fmt.Printf("%d openn", j)
}(i)
}
// 阻塞 等待计数器归零
wg.Wait()
}

在11行,我们首先创建了同步扫描器,然后每当for循环循环一次,我们给我们的同步计数器使用Add函数加1,然后在我们的子线程的函数中,我们使用了defer,使用这个代表着,无论我们defer后面的内容写在哪里,那么在函数执行结束时,都会去将defer的内容执行完,才会退出,所以在函数结束时,我们使用Done()函数,将我们的计数器减1,代表一个端口扫描已经完成了。然后回到我们的主函数,这里是最关键的,我们使用了Wait()将主函数阻塞,只有当计数器为0时,该函数才会解除阻塞,也就是说,只有当我们的扫描1024个端口的函数全部执行完成,计数器归0时,阻塞才会停止,我们的主函数也就运行结束,程序执行完成。

GO黑帽子学习笔记- 端口扫描

单管道通信

在python使用多线程时,总是有一个很麻烦的问题不好解决,在我们使用thread进行多线程操作时,我们无法通过thread得到函数的返回值,虽然go的goroutine也同样无法得倒返回值,但是可以通过管道的方式去解决。(python好像也有管道,但是太菜了,没有没使用过。)

package main

import (
"fmt"
"sync"
)

func worker(ports chan int, wg *sync.WaitGroup) {
for p := range ports {
fmt.Println(p)
wg.Done()
}
}

func main() {
ports := make(chan int, 100)
var wg sync.WaitGroup
for i := 0; i < cap(ports); i++ {
go worker(ports, &wg)
}
for i := 0; i <= 1024; i++ {
wg.Add(1)
ports <- i
}
wg.Wait()
close(ports)
}

其实书上的这段代码确实偷懒了,他并没有将扫描的内容加进去,导致第一眼看上去直接懵了,不知道这是在干嘛,说好的扫描呢?扫描呢?...

所以这里我也偷懒下,其实扫描的内容应该是在我们的worker函数中。我们首先在16行创建了一个管道,管道中存放的数据类型为int,管道的容量为100,也就是可以存放100个数据,当然,这个参数我们是可以省略不写的,如果不写,那么其默认容量为1,也就时当我们管道中有一个数据时,我们仍想向其中存放数据,那么这一步便会阻塞,等待我们将管道中的这个数据取出后,才会停止阻塞,接着向其中添加数据。我们可以在23行看到,我们向管道中存放了一个int型的i。然后在我们的子函数中,使用for循环循环读取管道中的内容,并将其打印。当然这里面有个小的逻辑,我们的子线程数量是固定的,也就是18行中读取的管道的容量,即100。然后我们其实子线程是一直都不会结束的,如果我们管道中没有数据了,那么它就会阻塞等待我们的管道中的数据,而我们1024个端口都已经添加完成了,没有数据了,那么这是,计数器会归零,直接关闭管道,然后我们的9行中的for循环判断管道关闭,也会退出循环,结束子线程。

这里有个知识点,如下两种写法其实是相同的,也就是我们在从管道中取值的时候,不仅会给我们返回一个值,还会给我们返回一个bool类型,该类型用来判断管道是否已经关闭。

	for p := range ports {
fmt.Println(p)
}

for{
_, ok := <-numChan
fmt.Println("ok的值", ok)
if !ok {
fmt.Println("管道已经关闭了,准备退出")
break
}
}

多管道通信

使用上面单管道通信的方法时,我们只要在上面的代码中加入我们的扫描代码,即可进行扫描,但是会出现一个问题,因为我们的扫描是不断的将需要扫描的端口发送给不同的线程,所以没个线程扫描到的端口是不一样的,可能一个线程在扫描20端口,而另一已经在扫描1020端口了,所以这样我们的输出的结果的顺便便无法保证。(书上是这么说的,但是其实我觉得乱点也无所谓...)而另一个好处就是我们可以消除对WaitGroup的依赖。因为我们知道我们要扫描1024次,那么我们接受结果的代码,只要循环从管道读取到1024次数据,那么是否我们便可以说扫描完成了呢。因为管道在接受数据时是阻塞的,所以在管道关闭之前,只要我们只要接收1024次扫描的结果,便可以证明扫描完成了。

package main

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

func worker(ports, results chan int) {
for p := range ports {
address := fmt.Sprintf("www.haochen1204.com:%d", p)
conn, err := net.Dial("tcp", address)
if err != nil {
//fmt.Println(p)
results <- 0
continue
}
conn.Close()
//fmt.Println(p)
results <- p
}
}

func main() {
ports := make(chan int, 100)
results := make(chan int)
var openports []int

for i := 0; i < cap(ports); i++ {
go worker(ports, results)
}

go func() {
for i := 1; i <= 1024; i++ {
ports <- i
}
}()

for i := 0; i < 1024; i++ {
port := <-results
if port != 0 {
openports = append(openports, port)
}
}

close(ports)
close(results)
sort.Ints(openports)
for _, port := range openports {
fmt.Printf("%d openn", port)
}
}

和上面的代码一样,我们首先创建了worker函数,在该函数中我们循环等待管道中给我们的端口,然后对该端口进行扫描,如果端口关闭,那么在我们的结果管道中给一个0,结束本次循环重新从管道中接受端口数据。如果端口开放,那么把这个端口发送到结果管道中。并且关闭本次端口的连接。(书:打开了端口而不关上是不礼貌的行为~

然后在看主函数,首先创建了两个管道,然后根据管道的数量创建了相应数量的线程,然后开启一个线程用来循环向管道中加入1-1024端口。(这里如果不创建一个新线程用来向管道中发送数据,那么前面创建的100个线程已经运行完成,但是会堵塞在向我们接受用的管道中放结果的这一步,因为我们接受的代码还在主函数的下面,那么这时,我们无法向结果管道中添加数据,那么我们的扫描函数就会阻塞着,这就导致我们放端口的管道也会阻塞着,然后导致我们的主函数无法继续运行,更无法到用来从results管道中接收结果的代码,导致一直阻塞着...)然后我们创建一个for循环,这个循环只循环1024次,因为我们知道我们只有1024个端口等待扫描,那么我们只需要从管道中接受1024次数据即可。最后关闭管道,因为我们刚刚在上面的代码中也说了,我们从多线程中接受到的结果是无序的,那么我们想要有序的结果,便可以使用sort.Ints对其进行排序,最后对其进行输出即可。

最后还有一个点需要注意,为什么从分片(给我感觉就是python中的列表,暂时是这么理解的)中获取了2个返回值,在go中呢,"_"代表不在乎的数据,可以抛弃,而在我们上面的for循环中,获取了2个返回值,一个为当前的序列号,一个为其的值,而我们要的是端口号,也就是他的值,所以只需要第二个参数就可以了。

但是,这个多通道扫描,给我的感觉就很不爽,因为它是等待全部扫描完一起打印的,这样不能知道扫描的进度,就令我很头疼。

其实代码到这里就结束了。但是我在扫描的时候遇到一个问题,就是超时,python中使用requests库对http流量可以设置超时时间,但是在go中的Dail函数如何设置呢。经过查询,我发现了net.DialTimeout可以设置超时。

下面是DialTimeout的说明文档,他说了他是类似于Dial的,只不过增加了超时时间。

GO黑帽子学习笔记- 端口扫描
image-20220812164159923

那么我们便可以修改扫描的代码为:

conn, err := net.DialTimeout("tcp", address, 1*time.Second)

如图,第一次运行是增加了超时的情况下,第二次运行是没有增加超时,可以看到运行速度快了很多倍。(当然,这里设置1秒可能是不合理的,我仅仅是做测试用,实际中应该根据实际的网络情况进行设置,或者给用户让用户自行设置。)

GO黑帽子学习笔记- 端口扫描
截屏2022-08-12 16.48.22

最后假如我们想要根据端口来获取端口所对应的服务呢,书中也给了参考文档,但是弟弟技术太菜了,没有研究懂,也希望有大佬看到可以给弟弟讲解下...

书中给的参考代码地址:https://github.com/blackhat-go/bhg/blob/master/ch-2/scanner-port-format/portformat.go

package portformat

import (
"errors"
"strconv"
"strings"
)

const (
porterrmsg = "Invalid port specification"
)

func dashSplit(sp string, ports *[]int) error {
dp := strings.Split(sp, "-")
if len(dp) != 2 {
return errors.New(porterrmsg)
}
start, err := strconv.Atoi(dp[0])
if err != nil {
return errors.New(porterrmsg)
}
end, err := strconv.Atoi(dp[1])
if err != nil {
return errors.New(porterrmsg)
}
if start > end || start < 1 || end > 65535 {
return errors.New(porterrmsg)
}
for ; start <= end; start++ {
*ports = append(*ports, start)
}
return nil
}

func convertAndAddPort(p string, ports *[]int) error {
i, err := strconv.Atoi(p)
if err != nil {
return errors.New(porterrmsg)
}
if i < 1 || i > 65535 {
return errors.New(porterrmsg)
}
*ports = append(*ports, i)
return nil
}

// Parse turns a string of ports separated by '-' or ',' and returns a slice of Ints.
func Parse(s string) ([]int, error) {
ports := []int{}
if strings.Contains(s, ",") && strings.Contains(s, "-") {
sp := strings.Split(s, ",")
for _, p := range sp {
if strings.Contains(p, "-") {
if err := dashSplit(p, &ports); err != nil {
return ports, err
}
} else {
if err := convertAndAddPort(p, &ports); err != nil {
return ports, err
}
}
}
} else if strings.Contains(s, ",") {
sp := strings.Split(s, ",")
for _, p := range sp {
convertAndAddPort(p, &ports)
}
} else if strings.Contains(s, "-") {
if err := dashSplit(s, &ports); err != nil {
return ports, err
}
} else {
if err := convertAndAddPort(s, &ports); err != nil {
return ports, err
}
}
return ports, nil
}
全狗的自我修养使全狗的自我修养


其它学习教程。

GO黑帽子学习笔记- 端口扫描GO黑帽子学习笔记- 端口扫描



GO黑帽子学习笔记- 端口扫描


原文始发于微信公众号(安全狗的自我修养):GO黑帽子学习笔记- 端口扫描

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年11月9日11:18:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   GO黑帽子学习笔记- 端口扫描http://cn-sec.com/archives/1399707.html

发表评论

匿名网友 填写信息