没有理想的人不伤心

GO黑帽子 - 端口扫描器

2025/03/07
2
0

TCP、扫描器、代理

本章内容:

  1. 学习 go 中的基本 TCP 通信
  2. 如何构建并发的、经过适当控制的端口扫描器
  3. 创建用于端口转发的TCP 代理
  4. 重新创建Netcat 的“安全巨洞”功能

TCP 的握手机制

三次握手:客户端发 syn 数据包——服务器以 syn-ack 进行响应——客户端发送 ACK 数据包,成功建立连接。

如何判断一个端口的状态(开放、关闭、过滤)?

开放:进行正常的三次握手。

关闭:服务器以 rst 数据包进行响应,而不是 syn-ack。

过滤(流量被防火墙过滤):服务器端没有任何响应。

通过端口转发绕过防火墙

端口转发:使用中间系统代理连接绕过或穿透防火墙

如企业内某客户端,想要访问被防火墙过滤的恶意网址 b.com,那么客户端就可以先访问没有被防火墙过滤的 a.com,在 a.com设置中间代理,将流量代理到 b.com,那么客户端就能够绕过客户端成功访问了 b.com

1710391386608-70604e3e-a11c-42af-9b20-271f1ed25d4e.png

编写端口扫描器

基础

Go 的**net**包:**func Dial(network,address string) (Conn,error)**

第一个参数用于标识要启动的连接类型,是一个字符串,有 "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6"(IPv6-only), "unix", "unixgram" and "unixpacket"

第二个参数是想要连接的主机,对于 TCP/UDP 连接,使用 host:port形式,host 可以是域名、ip 地址、IPv6 地址,port 可以是端口名或服务名,如

    Dial("tcp", "golang.org:http")

    Dial("tcp", "192.0.2.1:http")

    Dial("tcp", "198.51.100.1:80")

    Dial("udp", "[2001:db8::1]:domain")

    Dial("udp", "[fe80::1%lo0]:53")

    Dial("tcp", ":80")

通过检查返回的 error参数来判断连接是否成功。

// 扫描 1-1024 端口
// fmt.Sprintf()格式化字符串
func main() {
	for i:= 1;i <= 1024;i++ {
		addr:= fmt.Sprintf("scanme.nmap.org:%d",i)
		fmt.Println(addr)
		conn,err := net.Dial("tcp",addr)
		if err!= nil {
			fmt.Println("连接失败")
			continue
		}
		fmt.Println(i,"号端口连接成功")
		conn.Close()	// 关闭连接
	}
}

并发扫描

解决 main goroutine 不等待其他 goroutine 的问题

  1. 在 main 中使用 time.sleep()等待函数,等待其他 goroutine
  2. 使用 sync 包中的 WaitGroup

1710407756017-8b66514d-b523-4f60-ac06-f1d9d6249404.png

WaitGroup结构体字段不可导出,该结构体有三个方法

Add(int):该方法会按所提供的数字递增内部计数器

Done():将内部计数器减 1

Wait():阻止调用它的 goroutine 执行,并在内部计数器达到零之前不允许进一步执行(即进入等待状态)

func main() {
	var wg sync.WaitGroup
	for i:= 1;i <= 1024;i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			addr:= fmt.Sprintf("scanme.nmap.org:%d",i)
			//fmt.Println(addr)
			conn,err := net.Dial("tcp",addr)
			if err!= nil {
				return
			}
			fmt.Printf("端口%d:连接成功\n",i)
			conn.Close()
		}(i)
	}
	wg.Wait()
}

首先声明 WaitGroup 变量,在循环中每创建一个 goroutine 就把内部计数器+1,在每个 goroutine 中,若是连接失败立即返回,因此需要使用延迟调用 defer 来-1,main goroutine 中使用等待函数,当其他 goroutine 运行完毕,才结束主程序。

通过这种方法进行扫描会出现结果不一样的问题,通过工人池的方法可以解决

  1. 使用 goroutine 工人池进行端口扫描

使用 for 循环创建一定数量的工人 goroutine 作为资源池,然后在 main goroutine 中使用通道提供工作

注释仔细看!

func worker(ports chan int,wg *sync.WaitGroup) {
    //为什么在此要用循环
    //因为创建的一百个工人 goroutine 只要没有 return 返回就会一直存在
    //若不使用循环,那么工人只会干一次就罢工(还没死)也就是一百个工人只能扫描一百个端口,而 wg.done 也只能触发 100 次,而主程序中 wg.add 执行了 1024 次,程序永远不会停止
    //使用循环才会使工人一直干,直到工程完毕,工人才会放假,wg.wait 才能使 main 结束等待,程序结束
    for i:= range ports {
        addr:= fmt.Sprintf("127.0.0.1:%d",i)
        conn,err := net.Dial("tcp",addr)
        if err!= nil {
            //fmt.Printf("端口%d:连接失败\n",i)
            // 在工人池的 goroutine 中不能 return,因为 return 了,goroutine 就会销毁,而工人池中一共一百个 goroutine,会消耗掉
            //return
        } else {
            fmt.Printf("端口%d:连接成功\n",i)
            // 连接成功了才能关闭,否则 conn 没有值,访问空地址,产生 panic
            conn.Close()
        }
        // 此处的 wg.done 不能用 defer,因为只有函数返回时才会调用 defer,而我们要求工人 goroutine 一直存活,因此 defer 永远不会被调用
        wg.Done()
    }
}

func main() {
    var wg sync.WaitGroup
    ports:= make(chan int,100)
    //创建工人池,其中包含一百个蓄势待发的工人 goroutine
    for i:= 0;i < cap(ports);i++ {
        go worker(ports, &wg)
    }
    //通过管道按需分配 goroutine,让工人工作
    for i:= 1;i <= 1024;i++ {
        wg.Add(1)
        ports <- i
    }
    wg.Wait()
    close(ports)
}

//输出
端口 135:连接成功
端口 445:连接成功
端口 902:连接成功
端口 912:连接成功

以上打印出来的端口是乱序的,如何按照端口号进行顺序输出?

  1. 多通道的端口扫描
func worker(ports chan int,results chan<- result) {
    for i:= range ports {
        addr:= fmt.Sprintf("127.0.0.1:%d",i)
        conn,err := net.Dial("tcp",addr)
        if err!= nil {
            results <- result{i,false}
        } else {
            results <- result{i,true}
            conn.Close()
        }
    }
}

type result struct {
    port   int
    isTrue bool
}

func main() {
    maxPort:= 1024
    results:= make(chan result)
    ports:= make(chan int,100)
    var s []int
    //创建工人池,其中包含一百个蓄势待发的工人 goroutine
    for i:= 0;i < cap(ports);i++ {
        go worker(ports,results)
    }
    //通过管道按需分配 goroutine,让工人工作
    // 这里为什么需要创建 goroutine
    //若不用 goroutine,那么由于 ports 管道容量为 100,而有 1024 个端口,而工人 goroutine 不断往 results 管道装东西,但是没有接收者取东西,results 管道一直是满的状态,所以工人 goroutine 就会陷入阻塞,
    //  导致 ports 中的内容也不能被接收,所以 main goroutine 也会陷入阻塞,程序就卡住了
    //若不使用 goroutine,只要将 results 管道的容量改为>=端口的数量,程序将会正常运行。
    //而使用 goroutine,results 中的内容被 main goroutine 不断地接收,ports 中的内容就能不断被工人接收,所以程序也不会陷入阻塞,就能正常运行
    go func() {
        for i:= 1;i <= maxPort;i++ {
            ports <- i
        }
    }()
    //由于一共 1024 个端口,每扫描一次都会往 results 中发送,所以用 1024 次循环来接收
    for i:= 0;i < maxPort;i++ {
        result:= <-results
        if result.isTrue {
            s = append(s,result.port)
        }
    }
	//对其中的元素进行排序
	sort.Ints(s)
    
    for i:= 0;i < len(s);i++ {
        fmt.Printf("端口%d:连接成功\n",s[i])
    }
    close(ports)
    close(results)
}

以上代码不需要使用 Waitgroup进行同步,因为通过无缓存的 channel 就能完成同步。

通过使用通道将结果传递给切片,并对切片进行处理,最后集中打印,就能使数据有序

不止是无缓存通道会阻塞,若有缓存通道已经满了,而无人接收,那么再发送也会阻塞

构造 TCP 代理

io.Reader io.Writer

//io.Reader
type Reader interface {
    Read(p []byte) (n int,err error)
}
//io.Writer
type Writer interface {
    Write(p []byte) (n int,err error)
}

Reader 和 Writer 是接口类型,不能直接实例化,需要实现其中的方法

Reader 接口用于包装基本的读取方法。

Read 方法读取 len(p)字节数据写入 p。它返回写入的字节数和遇到的任何错误。即使 Read 方法返回值 n < len(p),本方法在被调用时仍可能使用 p 的全部长度作为暂存空间。如果有部分可用数据,但不够 len(p)字节,Read 按惯例会返回可以读取到的数据,而不是等待更多数据。

当 Read 在读取 n > 0 个字节后遭遇错误或者到达文件结尾时,会返回读取的字节数。它可能会在该次调用返回一个非 nil 的错误,或者在下一次调用时返回 0 和该错误。一个常见的例子,Reader 接口会在输入流的结尾返回非 0 的字节数,返回值 err == EOF 或 err == nil。但不管怎样,下一次 Read 调用必然返回(0,EOF)。调用者应该总是先处理读取的 n > 0 字节再处理错误值。这么做可以正确的处理发生在读取部分数据后的 I/O 错误,也能正确处理 EOF 事件。

如果 Read 的某个实现返回 0 字节数和 nil 错误值,表示被阻碍;调用者应该将这种情况视为未进行操作

Writer 接口用于包装基本的写入方法。

Write 方法 len(p) 字节数据从 p 写入底层的数据流。它会返回写入的字节数(0 <= n <= len(p))和遇到的任何导致写入提取结束的错误。Write 必须返回非 nil 的错误,如果它返回的 n < len(p)。Write 不能修改切片 p 中的数据,即使临时修改也不行。

创建回显服务器

Conn接口代表通用的面向流的网络连接

type Conn interface {
    // Read 从连接中读取数据
    // Read 方法可能会在超过某个固定时间限制后超时返回错误,该错误的 Timeout()方法返回真
    Read(b []byte) (n int,err error)
    // Write 从连接中写入数据
    // Write 方法可能会在超过某个固定时间限制后超时返回错误,该错误的 Timeout()方法返回真
    Write(b []byte) (n int,err error)
    // Close 方法关闭该连接
    // 并会导致任何阻塞中的 Read 或 Write 方法不再阻塞并返回错误
    Close()error
    // 返回本地网络地址
    LocalAddr()Addr
    // 返回远端网络地址
    RemoteAddr()Addr
    // 设定该连接的读写 deadline,等价于同时调用 SetReadDeadline 和 SetWriteDeadline
    // deadline 是一个绝对时间,超过该时间后 I/O 操作就会直接因超时失败返回而不会阻塞
    // deadline 对之后的所有 I/O 操作都起效,而不仅仅是下一次的读或写操作
    // 参数 t 为零值表示不设置期限
    SetDeadline(t time.Time)error
    // 设定该连接的读操作 deadline,参数 t 为零值表示不设置期限
    SetReadDeadline(t time.Time)error
    // 设定该连接的写操作 deadline,参数 t 为零值表示不设置期限
    // 即使写入超时,返回值 n 也可能>0,说明成功写入了部分数据
    SetWriteDeadline(t time.Time)error
}

从连接中读取数据 conn.Read() Read(b []byte) (n int,err error)

向连接中写入数据 conn.Write() Write(b []byte) (n int,err error)

net.Listen()监听来自客户端的连接请求 Listener

func Listen(net,laddr string) (Listener,error)

返回在一个本地网络地址 laddr 上监听的 Listener。网络类型参数 net 必须是面向流的网络:“tcp”、“tcp4”、“tcp6”、“unix"或"unixpacket”

监听器 Listener是一个用于面向流的网络协议的公用的网络监听器接口

type Listener interface {
    // Addr 返回该接口的网络地址
    Addr()Addr
    // Accept 等待并返回下一个连接到该接口的连接
    Accept() (c Conn,err error)
    // Close 关闭该接口,并使任何阻塞的 Accept 操作都会不再阻塞并返回错误。
    Close()error
}

接收客户端的连接 Listener.Accept() Accept() (c Conn,err error)

服务器流程:监听连接——接受连接——处理连接

// echo is a handler function that simply echoes received data.
func echo(conn net.Conn) {
    defer conn.Close()

    // Create a buffer to store received data.
    b:= make([]byte,512)
    for {
        // Receive data via conn.Read into a buffer.
        size,err := conn.Read(b[0:])
        if err!= nil && err!= io.EOF {
            log.Println("Unexpected error")
            break
        }

        if err == io.EOF && size == 0 {
            log.Println("Client disconnected")
            break
        }

        log.Printf("Received %d bytes: %s",size,string(b))

        // Send data via conn.Write.
        log.Println("Writing data")
        if _,err := conn.Write(b[0:size]);err != nil {
            log.Fatalln("Unable to write data")
        }
    }
}

func main() {
    // Bind to TCP port 20080 on all interfaces.
    listener,err := net.Listen("tcp", ":20080")
    if err!= nil {
        log.Fatalln("Unable to bind to port")
    }
    log.Println("Listening on 0.0.0.0:20080")
    for {
        // Wait for connection. Create net.Conn on connection established.
        conn,err := listener.Accept()
        log.Println("Received connection")
        if err!= nil {
            log.Fatalln("Unable to accept connection")
        }
        // Handle the connection. Using goroutine for concurrency.
        go echo(conn)
    }
}