欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Go 中的网络扫描:使用 goroutines 和 channels 等语言特性进行 TCP 端口探测,可以极大地帮助我们加快工具和系统的运行速度。

最编程 2024-03-26 13:56:27
...

在 Go 中编写 TCP 扫描器以获取乐趣和学习。从网络扫描到服务指纹识别。了解goroutines和channels。

在我们开始之前——不要违反规则

修补我们不拥有的设备是不好的。绝对是个坏主意。始终牢记一些好的建议:

  • 不要扫描任何您没有权限的网络
  • 不要扫描任何您没有权限的主机
  • 此处代码仅供学习

识别哪些设备连接到网络以及它们提供哪些服务本身就是一个完整的主题。

端口扫描

端口扫描是一种机制,用于探测和识别远程计算机上打开的端口以及它提供的指纹服务(如果可能)。这包括 SSH、FTP、Web 服务器或 SQL Server 等。

端口扫描和主机发现通常提到三种技术。这些与ICMP、TCP和UDP相关。所有这些识别网络上主机的技术都有效,每一种技术都各有利弊。

使用 nmap 扫描一千个端口

此示例中的代码不是nmap的替代品。我只想说清楚。只是试图了解如何在golang中编写 TCP 端口扫描程序。

要在nmap中对前 100 个端口进行 TCP 端口扫描,我们需要传递标志-sT,-p然后传递端口列表。这些应该是从 1 到 1024 的端口

 time nmap -sT -p 1-1024 192.168.0.42
Starting Nmap 7.91 ( https://nmap.org ) at 2023-01-15 06:40 GMT
Nmap scan report for rpi (192.168.0.42)for rpi (192.168.0.42)
Host is up (0.0062s latency).
Not shown: 1021 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
53/tcp open  domain
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 2.68 seconds
nmap -sT -p 1-1024 192.168.0.42  0.14s user 0.20s system 12% cpu 2.725 total

扫描1024个端口一共耗时2.68秒。我们可以将其作为扫描仪速度的基准。

现在我们将介绍一些关于如何在 Go 中编写 TCP 扫描器的示例。

TCP 端口扫描器

net在 Go 中对单个端口执行 TCP 扫描可以通过尝试使用包连接到端口来完成。一些可以使用的功能是:

func Dial(network, address string) (Conn, error)
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)

使用DialTimeout()我认为可以知道的更多信息。该脚本将采用两个参数,即目标和端口,以使其知道需要扫描的内容。它看起来像这样:

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "time"
)

var target = flag.String("target", "", "target to scan")
var port = flag.Int("port", 80, "port to scan on remote host")

func main() {
    log.Println("start")
    defer log.Println("end")

    flag.Parse()

    if *target == "" {
        log.Fatalf("no --target parameter provided")
    }

    log.Printf("Connecting to %s:%d", *target, *port)
    conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", *target, *port), 2*time.Second)
    if err != nil {
        if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
            log.Println("Connection timed out")
        } else {
            log.Println("Connection refused", neterr)

        }
    } else {
        log.Println("Connection successful to", conn.RemoteAddr().String())
        conn.Close()
    }
}

首先,我们检查是否target已提供。我们尝试连接到端口,如果出现任何错误,我们会检查两个可能的情况:

  • 连接超时
  • 连接被拒绝

这是线性的,可以改进,但对于第一个版本,它可以完成工作。


 time go run tcpscanport.go --target 192.168.0.42 --port 80 
2023/01/14 14:11:45 start
2023/01/14 14:11:45 Connecting to 192.168.0.42:80
2023/01/14 14:11:45 Connection successful to 192.168.0.42:80
2023/01/14 14:11:45 end
go run tcpscanport.go --target 192.168.0.42 --port 80  0.24s user 0.15s system 82% cpu 0.467 total
time go run tcpscanport.go --target 192.168.0.42 --port 443
2023/01/14 14:11:42 start
2023/01/14 14:11:42 Connecting to 192.168.0.42:443
2023/01/14 14:11:42 Connection refused dial tcp 192.168.0.42:443: connect: connection refused
2023/01/14 14:11:42 end
go run tcpscanport.go --target 192.168.0.42 --port 443  0.24s user 0.16s system 83% cpu 0.474 total

扫描端口需要多长时间?对于这两种情况,只用了不到半秒。还不错,但是加起来扫描前 1024 个端口的速度会和 nmap 一样快吗?

扫描多个端口

可以通过不同的方式将单个端口扩展为多个。为了让事情足够简单,我修改了之前的脚本以扫描前 1024 个端口。

并非所有这些端口都是相关的,但它是一种从单一端口扫描扩展到一系列端口的方法。这是更新的版本:

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "time"
)

var target = flag.String("target", "", "target to scan ports")

func main() {

    log.Println("start")
    defer log.Println("end")

    var open_ports []int

    flag.Parse()

    if *target == "" {
        log.Fatalln("no --target parameter providing for scanning")
    }

    for port := 1; port <= 1024; port++ {
        address := fmt.Sprintf("%s:%d", *target, port)
        conn, err := net.DialTimeout("tcp", address, 2*time.Second)
        if err != nil {
            if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
                log.Println("Connection timed out")
            } else {
                log.Println("Connection refused", neterr)
            }
        } else {
            conn.Close()
            log.Printf("Port %d is open on %s\n", port, address)
            open_ports = append(open_ports, port)
        }
    }

    for _, port := range open_ports {
        fmt.Printf("Address %s has Port:%d opened\n", *target, port)
    }
}

输出非常嘈杂。对于每个端口,如果它可以连接,它将打印在屏幕上。最后它会列出所有打开的端口target

go run tcpscan.go --target 192.168.0.42           
2023/01/14 13:23:08 start
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:1: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:2: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:3: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:4: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:5: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:6: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:7: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:8: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:9: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:10: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:11: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:12: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:13: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:14: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:15: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:16: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:17: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:18: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:19: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:20: connect: connection refused
2023/01/14 13:23:08 Connection refused dial tcp 192.168.0.42:21: connect: connection refused
2023/01/14 13:23:08 Port 22 is open on 192.168.0.42:22
...
Address 192.168.0.42 has Port:22 opened
Address 192.168.0.42 has Port:53 opened
Address 192.168.0.42 has Port:80 opened
2023/01/14 13:23:17 end
go run tcpscan.go --target 192.168.0.42  0.41s user 0.54s system 14% cpu 6.396 total

用于测试的主机似乎打开了22、53和80端口。

总扫描时间为 6.3 秒。一点也不差。

此脚本存在性能瓶颈。它遍历 1024 个端口并一次命中一个。如果在任何时候这都必须扫描网络,那是不理想的。我们在速度方面并不接近。让我们再次迭代。

这是 goroutines 和 channels 派上用场的地方。

使用 goroutines 和 channels 扫描多个端口

Go 并发:goroutines 和 channels
几个定义:

goroutine:是由 Go runtime 管理的轻量级线程
channel:一个类型化的管道,您可以通过它使用通道运算符发送和接收值,<-

使用 goroutines 和 channels 进行 TCP 端口扫描

脚本的最终版本将依靠这些 golang 特性来提高其速度。

扫描将在 goroutine 上执行的端口列表的新功能,一个通道(缓冲通道)接收要扫描的端口列表,另一个通道接收每个端口扫描的结果。

更新后的脚本如下所示:

package main

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

var target = flag.String("target", "", "target to scan ports")

func scanner(target string, ports, results chan int) {
    for port := range ports {
        address := fmt.Sprintf("%s:%d", target, port)
        conn, err := net.DialTimeout("tcp", address, 2*time.Second)
        if err != nil {
            neterr, ok := err.(net.Error)
            if ok && neterr.Timeout() {
                log.Println("Connection timed out")
            } else {
                log.Println("Connection refused", neterr)
            }
            results <- 0
        } else {
            conn.Close()
            results <- port
        }
    }
}

func main() {
    log.Println("start")
    defer log.Println("end")

    flag.Parse()

    if *target == "" {
        log.Fatalf("no --target parameter provided for scanning - aborting")
    }

    ports := make(chan int, 100)
    results := make(chan int)

    var open_ports []int

    for port := 1; port <= cap(ports); port++ {
        go scanner(*target, ports, results)
    }

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

    for port := 1; port <= 1024; port++ {
        port_status := <-results
        if port_status != 0 {
            open_ports = append(open_ports, port_status)
        }
    }

    close(ports)
    close(results)
    sort.Ints(open_ports)
    for _, port := range open_ports {
        fmt.Printf("Address %s has Port:%d opened\n", *target, port)
    }
}

该ports通道是一个缓冲通道。我们将一次扫描多达 100 个端口。这可能会很嘈杂,因此它开启了关于速度、隐身性和需求之间平衡的讨论。

接下来,脚本将通过通道发送前 1024 个端口,供每个goroutine接收它们。

最后,遍历通道results将处理每个端口扫描的结果并将结果存储在数组中。这将打印在输出中:

time go run tcpscan_chan.go --target 192.168.0.4
...
2023/01/14 14:04:22 Connection refused dial tcp 192.168.0.42:33: connect: connection refused
2023/01/14 14:04:22 Connection refused dial tcp 192.168.0.42:35: connect: connection refused
2023/01/14 14:04:22 Connection refused dial tcp 192.168.0.42:38: connect: connection refused
2023/01/14 14:04:22 Connection refused dial tcp 192.168.0.42:39: connect: connection refused
2023/01/14 14:04:22 Connection refused dial tcp 192.168.0.42:41: connect: connection refused
Address 192.168.0.42 has Port:22 opened
Address 192.168.0.42 has Port:53 opened
Address 192.168.0.42 has Port:80 opened
2023/01/14 14:04:22 end
go run tcpscan_chan.go --target 192.168.0.42  0.36s user 0.30s system 31% cpu 2.100 total

扫描 100 个端口的总时间略多于 2 秒。虽然这可能不是线性的,但它显示的速度是线性速度的三倍。

goroutines和channels的一个很好的用例,也是 golang 的优势之一。

结论:学习和例子

简单的特性和功能是很好的学习练习。我们可以将 TCP 端口扫描视为微不足道的事情,并寻找示例来学习编排工具的技术和方法。

在golang中,使用goroutines和通道等语言功能可以极大地帮助我们的工具和系统的速度。我正在寻找一些例子来尝试这个,而网络扫描就在我的小巷里。

我总是建议学习关键概念,网络扫描是信息安全领域的基础,很多时候都被忽视了。

在 golang 中扫描端口可以像在经过实战测试的工具(如nmap)中一样快,并且测试这些东西非常有趣。