【Go黑帽子】使用Golang编写一个TCP扫描器(基础篇)

【Go黑帽子】使用Golang编写一个TCP扫描器(基础篇)

  • 一、TCP基础知识
  • 二、执行非并发扫描
    • 2.1 一端口非并发扫描
    • 2.2 多端口非并发扫描
  • 三、执行并发扫描
  • 后记

一、TCP基础知识

TCP全称:传输控制协议(Transmission Control Protocol,TCP)

TCP握手机制:
1) 如果端口是开放的,则会进行3次握手。首先,客户端发送一个syn数据包传给服务器,该数据包表示通信开始;然后,服务器以syn-ack进行响应传回客户端,提示客户端以ack作为结束;之后就可以进行数据传输。
2) 如果端口关闭,则当客户端发送syn数据包之后,服务器会以一个rst数据包而不是syn-ack进行响应。
3) 如果流量被防火墙过滤,则客户端通常不会从服务器收到任何响应。

学习测试过程中,我们连接并扫描的地址为scanme.nmap.org

注意! 这是Nmap的创建者Fyodor提供的一项免费服务,但是当你扫描时,请保持礼貌。他要求”尽量不要给服务器太大的压力或者破坏服务器。一天进行几次扫描是可以的,不要扫描超过100次。“

二、执行非并发扫描

2.1 一端口非并发扫描

在这个过程中,我们需要使用到Go的net包:

net.Dial(network,address string)

这句代码的第一个参数是一个字符串,用于标识要启动的连接类型。
第二个参数是我们需要连接的主机。该参数同样只是字符串,而不是字符串+整数。

该函数返回net.Conn和error,如果连接成功,error的值将为nil。因此,通过判断error的值,就可以验证是否连接成功。

函数具体解释如下:

func net.Dial(network string, address string) (net.Conn, error)
Dial connects to the address on the named network.

Known networks are "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".

For TCP and UDP networks, the address has the form "host:port". The host
must be a literal IP address, or a host name that can be resolved to IP 
addresses. The port must be a literal port number or a service name. If 
the host is a literal IPv6 address it must be enclosed in square brackets, 
as in "[2001:db8::1]:80" or "[fe80::1%zone]:80". The zone specifies the 
scope of the literal IPv6 address as defined in RFC 4007. The functions 
JoinHostPort and SplitHostPort manipulate a pair of host and port in this 
form. When using TCP, and the host resolves to multiple IP addresses, Dial
will try each IP address in order until one succeeds.

Examples:

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")
For IP networks, the network must be "ip", "ip4" or "ip6" followed by a 
colon and a literal protocol number or a protocol name, and the address 
has the form "host". The host must be a literal IP address or a literal 
IPv6 address with zone. It depends on each operating system how the operating 
system behaves with a non-well known protocol number such as "0" or "255".

Examples:

Dial("ip4:1", "192.0.2.1")
Dial("ip6:ipv6-icmp", "2001:db8::1")
Dial("ip6:58", "fe80::1%lo0")
For TCP, UDP and IP networks, if the host is empty or a literal unspecified 
IP address, as in ":80", "0.0.0.0:80" or "[::]:80" for TCP and UDP, "", 
"0.0.0.0" or "::" for IP, the local system is assumed.

For Unix networks, the address must be a file system path.

根据上面的知识点,我们可以写出仅扫描一个端口的扫描器:

package main

import (
	"fmt"
	"net"
)

func main() {
	//通过conn和err读取net.Dial()函数的返回值
	conn, err := net.Dial("tcp", "scanme.nmap.org:80")

	//通过err的值检验端口是否连接成功
	if err == nil {
		fmt.Println("Connection successful")

		//如果端口连接成功,则需要主动关闭端口连接
		conn.Close()
	}
}

在此,我们扫描并连接的端口为80。注意,在学习过程中,我们连接完端口之后需要主动关闭连接!上述代码运行结果如下:
【Go黑帽子】使用Golang编写一个TCP扫描器(基础篇)_第1张图片

2.2 多端口非并发扫描

当然,如果只扫描一个端口并没有什么用处,并且效率极低。

TCP端口范围为1~65535。

如果我们需要扫描很多个端口,其一是需要使用for循环,其次是需要更改代码conn, err := net.Dial("tcp", "scanme.nmap.org:80")中的80端口。

更改80端口的过程中,我们首先会想到将for循环中的 i 参数传入到80的位置。但是该函数的第二个参数需要传入的类型为字符串。

在此,我们就需要使用到fmt包中的函数:

// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...any) string {
	p := newPrinter()
	p.doPrintf(format, a)
	s := string(p.buf)
	p.free()
	return s
}

通过使用该函数将整数转换为字符串。

因此,我们进行一些改动,就可以扫描1024个端口:

package main

import (
	"fmt"
	"net"
)

func main() {
	for i := 1; i <= 1024; i++ {
		//将i的值和地址一起转换为字符串
		address := fmt.Sprintf("scanme.nmap.org:%d", i)

		conn, err := net.Dial("tcp", address)

		//如果端口已关闭或已过滤,直接进入下一循环
		if err != nil {
			continue
		}

		//关闭端口连接
		conn.Close()
		fmt.Printf("%d open\n", i)
	}
}

我们使用上述代码即可扫描1024个端口,但是,扫描执行速度极慢!在代码运行几分钟之后,才可以看到有几个打开的端口。

三、执行并发扫描

为了加快端口扫描的速度,我们可以同时扫描多个端口。为此,需要用到goroutine

在Go中,我们可以创建尽可能多的goroutine,其数量仅受到系统处理能力和可用内存的限制。

在代码编写中,为了保持代码的优雅,我们使用匿名函数

匿名函数:就是没有函数名的函数。

我们可以通过如下两种方法来定义和执行匿名函数:

package main

import (
	"fmt"
)

func main() {
	function := func() {
		fmt.Println("Hello,World!")
	}

	function()
}

package main

import (
	"fmt"
)

func main() {
	func() {
		fmt.Println("Hello,World!")
	}()
}

上述两种方法执行的结果一致。

因此,我们可以编写一个多端口并发扫描器:

package main

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

func main() {
	for i := 1; i <= 1024; i++ {

		go func(j int) {
			address := fmt.Sprintf("scanme.nmap.org:%d", j)
			conn, err := net.Dial("tcp", address)

			//如果端口已关闭或已过滤,结束本次循环
			if err != nil {
				return
			}

			conn.Close()
			fmt.Printf("%d open\n", j)
		}(i)

	}
	//强制函数main()暂停1s,以保证匿名函数可以执行完成
	time.Sleep(1 * time.Second)
}

运行上述代码,我们很快就能得到运行结果:
【Go黑帽子】使用Golang编写一个TCP扫描器(基础篇)_第2张图片

后记

我们成功编写了一个简单的TCP扫描器。
下一篇文章将继续介绍使用WaitGroup进行同步扫描、使用人工池进行端口扫描和多通道通信,来优化本文中的扫描器。

你可能感兴趣的:(Go黑帽子,Go语言学习之旅,golang,tcp/ip,网络,网络安全)