分析某款go端口扫描器之三

一、概述

前两篇主要分析些工具集,已经针对web服务的指纹和端口指纹信息进行识别,并没有真正开始扫描。本篇主要分析如何进行IP存活探测以及tcp扫描实现。

项目来源:https://github.com/XinRoom/go-portScan/blob/main/util/file.go

二、/core/host/ 目录(进行主机的ping探测存活)

/core/host/ping.go

此代码主要是用来进行主机存活的。

首先设置判断变量和常见端口列表

Go 复制代码
var CanIcmp bool // 用于标识是否支持发送 ICMP 包。
var TcpPingPorts = []uint16{80, 22, 445, 23, 443, 81, 161, 3389, 8080, 8081}//是用于 TCP Ping 的默认常见端口列表。

函数列表分析:

  • func IcmpOK(host string) bool

函数尝试直接发送 ICMP 包来检查主机是否存活。

Go 复制代码
// IcmpOK 直接发ICMP包
func IcmpOK(host string) bool {
	pinger, err := ping.NewPinger(host)
	if err != nil {
		return false
	}
	pinger.SetPrivileged(true)
	pinger.Count = 1
	pinger.Timeout = 800 * time.Millisecond
	if pinger.Run() != nil { // Blocks until finished. return err
		return false
	}
	if stats := pinger.Statistics(); stats.PacketsRecv > 0 {
		return true
	}
	return false
}
  • func PingOk(host string) bool

函数尝试通过执行不同操作系统的 Ping 命令来检查主机是否存活。

Go 复制代码
// PingOk Ping命令模式
func PingOk(host string) bool {
	switch runtime.GOOS {
	case "linux":
		cmd := exec.Command("ping", "-c", "1", "-W", "1", host)
		var out bytes.Buffer
		cmd.Stdout = &out
		cmd.Run()
		if strings.Contains(out.String(), "ttl=") {
			return true
		}
	case "windows":
		cmd := exec.Command("ping", "-n", "1", "-w", "500", host)
		var out bytes.Buffer
		cmd.Stdout = &out
		cmd.Run()
		if strings.Contains(out.String(), "TTL=") {
			return true
		}
	case "darwin":
		cmd := exec.Command("ping", "-c", "1", "-t", "1", host)
		var out bytes.Buffer
		cmd.Stdout = &out
		cmd.Run()
		if strings.Contains(out.String(), "ttl=") {
			return true
		}
	}
	return false
}
  • func TcpPing(host string, ports \[\]uint16, timeout time.Duration) (ok bool)

函数使用 TCP 连接在指定端口上对主机进行存活探测,在连接成功或者连接被拒绝时,都会判断主机为存活状态。

Go 复制代码
func TcpPing(host string, ports []uint16, timeout time.Duration) (ok bool) {
	var wg sync.WaitGroup              // 创建一个 WaitGroup 用于等待所有端口的探测完成
	ctx, cancel := context.WithCancel(context.Background()) // 创建一个上下文和取消函数
	d := net.Dialer{                   // 创建一个 Dialer,用于建立 TCP 连接
		Timeout:   timeout + time.Second, // 设置连接超时时间
		KeepAlive: 0,                    // 禁用 KeepAlive
	}
	for _, port := range ports {      // 遍历端口列表
		time.Sleep(10 * time.Millisecond) // 间隔一段时间再探测下一个端口,避免过于频繁的连接
		wg.Add(1)                     // 每个端口探测前增加 WaitGroup 计数
		go func(_port uint16) {       // 并发进行端口探测
			conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", host, _port)) // 尝试建立 TCP 连接
			if conn != nil {          // 如果成功建立连接
				conn.Close()          // 关闭连接
				ok = true             // 设置存活状态为 true
			} else if err != nil && strings.Contains(err.Error(), "refused it") { // 如果连接被拒绝
				ok = true             // 设置存活状态为 true
			}
			if ok {                   // 如果已经确认存活
				cancel()              // 取消其他端口的探测
			}
			wg.Done()                 // 完成当前端口的探测
		}(_port)                      // 传入端口号进行端口探测
	}
	wg.Wait()                        // 等待所有端口的探测完成
	return                            // 返回存活状态
}
  • func init()

函数在包加载时运行,它尝试在本地(127.0.0.1)发送 ICMP 包来检查系统是否支持 ICMP。

Go 复制代码
func init() {
	if IcmpOK("127.0.0.1") {
		CanIcmp = true
	}
}
  • func IsLive(ip string, tcpPing bool, tcpTimeout time.Duration) (ok bool)

IsLive 函数是用来检查主机是否存活的主要函数。如果支持 ICMP,则使用 IcmpOK 函数;否则,使用 PingOk 函数。如果前面的检查未能确定主机存活且 tcpPing 参数为真,则尝试使用 TcpPing 函数进行 TCP 存活探测。

Go 复制代码
func IsLive(ip string, tcpPing bool, tcpTimeout time.Duration) (ok bool) {
	if CanIcmp {
		ok = IcmpOK(ip)
	} else {
		ok = PingOk(ip)
	}
	if !ok && tcpPing {
		ok = TcpPing(ip, TcpPingPorts, tcpTimeout)
	}
	return
}

++ps:总体而言,这些函数提供了多种方法来检查主机是否存活,包括使用 ICMP 包、执行系统命令(如 Ping 命令)以及 TCP 连接到常见端口。这样做可以在不同系统和网络环境下更全面地检查主机的存活状态++

三、port端口解析,以及tcp扫描

1、/core/port/port.go

这个文件代码主要包含了端口扫描相关的逻辑和数据结构。

  • var TopTcpPorts = \[\]uint16{} 常见端口列表
  • 各结构体列表
Go 复制代码
type Scanner interface {//这是一个接口,定义了扫描器的操作,包括关闭、等待、扫描指定 IP 和端口,以及等待速率限制器。
	Close()
	Wait()
	Scan(ip net.IP, dst uint16) error
	WaitLimiter() error
}

// OpenIpPort retChan,这个结构体表示一个开放的 IP 和端口,包含 IP 地址、端口号、服务名和可能的 HTTP 信息。
type OpenIpPort struct {
	Ip       net.IP
	Port     uint16
	Service  string
	HttpInfo *HttpInfo
}


// Option ...这个结构体包含了扫描的参数配置,如速率限制、超时时间、网卡信息和是否进行服务探测等。
type Option struct {
	Rate        int    // 每秒速度限制, 单位: s, 会在1s内平均发送, 相当于每个包之间的延迟
	Timeout     int    // TCP连接响应延迟, 单位: ms
	NextHop     string // pcap dev name
	FingerPrint bool   // 服务探测
	Httpx       bool   // HttpInfo 探测
}

// HttpInfo Http服务基础信息,这个结构体存储了 HTTP 服务的基础信息,包括状态码、响应包大小、URL、重定向路径、标题、服务名、TLS 信息和 Web 指纹
type HttpInfo struct {
	StatusCode int      // 状态码
	ContentLen int      // 相应包大小
	Url        string   // Url
	Location   string   // 302、301重定向路径
	Title      string   // 标题
	Server     string   // 服务名
	TlsCN      string   // tls使用者名称
	TlsDNS     []string // tlsDNS列表
	Fingers    []string // 识别到的web指纹
}
  • func (op OpenIpPort) String() string

这个函数是为了实现 Stringer 接口,这个接口包含一个 String() 方法,用于定义该类型的字符串表示形式。对于 OpenIpPort 类型的实例,这个函数会返回其 IP 地址、端口号以及可能的服务信息的字符串表示形式

Go 复制代码
func (op OpenIpPort) String() string {
	buf := strings.Builder{}
	buf.WriteString(op.Ip.String())
	buf.WriteString(":")
	buf.WriteString(strconv.Itoa(int(op.Port)))
	if op.Service != "" {
		buf.WriteString(" ")
		buf.WriteString(op.Service)
	}
	if op.HttpInfo != nil {
		buf.WriteString("\n")
		buf.WriteString(op.HttpInfo.String())
	}
	return buf.String()
}
  • func (hi *HttpInfo) String() string

这个函数也是实现 Stringer 接口的方法。对于 HttpInfo 类型的实例,这个函数返回其包含的 HTTP 信息的字符串表示形式,包括状态码、响应包大小、URL、重定向路径、标题、服务名和 Web 指纹等。

Go 复制代码
func (hi *HttpInfo) String() string {
	if hi == nil {
		return ""
	}
	var buf strings.Builder
	buf.WriteString(fmt.Sprintf("[HttpInfo]%s StatusCode:%d ContentLen:%d Title:%s ", hi.Url, hi.StatusCode, hi.ContentLen, hi.Title))
	if hi.Location != "" {
		buf.WriteString("Location:" + hi.Location + " ")
	}
	if hi.TlsCN != "" {
		buf.WriteString("TlsCN:" + hi.TlsCN + " ")
	}
	if len(hi.TlsDNS) > 0 {
		buf.WriteString("TlsDNS:" + strings.Join(hi.TlsDNS, ",") + " ")
	}
	if hi.Server != "" {
		buf.WriteString("Server:" + hi.Server + " ")
	}
	if len(hi.Fingers) != 0 {
		buf.WriteString(fmt.Sprintf("Fingers:%s ", hi.Fingers))
	}
	return buf.String()
}
  • func ParsePortRangeStr(portStr string) (out \[\]\[\]uint16, err error)

这个函数用于解析端口字符串,将其转换为端口范围的列表。

Go 复制代码
// ParsePortRangeStr 解析端口字符串
func ParsePortRangeStr(portStr string) (out [][]uint16, err error) {
	portsStrGroup := strings.Split(portStr, ",")//逗号分隔的端口
	var portsStrGroup3 []string
	var portStart, portEnd uint64
	for _, portsStrGroup2 := range portsStrGroup {
		if portsStrGroup2 == "top1000" {
			continue
		}
		portsStrGroup3 = strings.Split(portsStrGroup2, "-")//-作为端口范围的
		portStart, err = strconv.ParseUint(portsStrGroup3[0], 10, 16)//返回端口的整数值,10进制,类型为int16
		if err != nil {
			return
		}
		portEnd = portStart
		if len(portsStrGroup3) == 2 {
			portEnd, err = strconv.ParseUint(portsStrGroup3[1], 10, 16)
		}
		if err != nil {
			return
		}
		out = append(out, []uint16{uint16(portStart), uint16(portEnd)})
	}
	return
}
  • func IsInPortRange(port uint16, portRanges \[\]\[\]uint16) bool

这个函数用于检查指定端口是否在端口范围内

Go 复制代码
// IsInPortRange 判断port是否在端口范围里
func IsInPortRange(port uint16, portRanges [][]uint16) bool {
	for _, portRange := range portRanges {
		if port >= portRange[0] && port <= portRange[1] {
			return true
		}
	}
	return false
}
  • func ShuffleParseAndMergeTopPorts(portStr string) (ports \[\]uint16, err error)

这个函数主要实现了对端口的解析、合并和随机化处理。它会解析传入的端口字符串,根据配置信息选取一些端口,优先使用常见的 TCP 端口,然后从用户指定的端口范围中选择未被选取的端口,并最终随机排序这些端口

Go 复制代码
// ShuffleParseAndMergeTopPorts shuffle parse portStr and merge TopTcpPorts
func ShuffleParseAndMergeTopPorts(portStr string) (ports []uint16, err error) {
	if portStr == "" {
		ports = TopTcpPorts //未指定则用默认top端口
		return
	}
	var portRanges [][]uint16
	portRanges, err = ParsePortRangeStr(portStr)
	if err != nil {
		return
	}
	// 优先发送top端口
	selectTopPort := make(map[uint16]struct{}) // TopPort
	hasTopStr := strings.Contains(portStr, "top1000")
	for _, _port := range TopTcpPorts {
		if hasTopStr || IsInPortRange(_port, portRanges) { //检测端口是否在范围内
			selectTopPort[_port] = struct{}{}
			ports = append(ports, _port)
		}
	}
	selectPort := make(map[uint16]struct{}) // OtherPort
	for _, portRange := range portRanges {
		var ok bool
		for _port := portRange[0]; _port <= portRange[1]; _port++ {
			if _port == 0 {
				continue
			}
			if _, ok = selectTopPort[_port]; ok {
				continue
			} else if _, ok = selectPort[_port]; ok {
				continue
			}
			selectPort[_port] = struct{}{}
			ports = append(ports, _port) //得到所有端口,并将top端口排在前面
			if _port == 65535 {
				break
			}
		}
	}
	if len(ports) == 0 {
		err = errors.New("ports len is 0")
		return
	}
	// 端口随机化
	skip := uint64(len(selectTopPort)) // 跳过Top
	_ports := make([]uint16, len(ports))
	copy(_ports, ports)
	sf := util.NewShuffle(uint64(len(ports)) - skip)
	if sf != nil {
		for i := skip; i < uint64(len(_ports)); i++ {
			ports[i] = _ports[skip+sf.Get(i-skip)]
		}
	}
	return
}

++ps:这些函数主要提供了对端口进行解析、筛选和随机化的功能,以便用于端口扫描和服务探测。它将常见的 TCP 端口列表与用户输入的端口范围合并,随机化排序以减少扫描的可预测性。++

2、/core/port/tcp/tcp.go

整体来说,这个代码文件定义了一个 TCP 端口扫描器,可以根据指定的 IP 地址和端口号对目标进行扫描,并可选地进行服务探测和 HTTP 信息探测。同时,它实现了速率限制以及 goroutine 的管理,确保扫描操作的安全性和效率。

Go 复制代码
var DefaultTcpOption = port.Option{//这里定义了默认的 TCP 扫描选项,包括扫描速率和超时时间等。
	Rate:    1000,
	Timeout: 800,
}

type TcpScanner struct {//管理 TCP 端口扫描器的状态和操作。结构体中包含了需要的字段和方法。
	ports   []uint16             // 指定端口
	retChan chan port.OpenIpPort // 返回值队列
	limiter *limiter.Limiter
	ctx     context.Context
	timeout time.Duration
	isDone  bool
	option  port.Option
	wg      sync.WaitGroup
}
  • func NewTcpScanner(retChan chan port.OpenIpPort, option port.Option) (ts *TcpScanner, err error)

这个函数是一个构造器,用于创建一个新的 TCP 扫描器实例。它接收一个返回值通道 retChan 和扫描选项 option,并返回一个 TcpScanner 的指针。函数首先对传入的选项进行验证,确保速率大于等于 10,超时时间大于 0。然后,它初始化了一个 TcpScanner 结构体实例,设置了返回通道、速率限制器、上下文、超时时间和其他选项,并将该实例赋值给 ts,最后返回该实例和可能的错误。

Go 复制代码
// NewTcpScanner Tcp扫描器
func NewTcpScanner(retChan chan port.OpenIpPort, option port.Option) (ts *TcpScanner, err error) {
	// option verify
	if option.Rate < 10 {
		err = errors.New("rate can not be set less than 10") // 如果速率小于 10,则返回错误
		return
	}
	if option.Timeout <= 0 {
		err = errors.New("timeout can not be set to 0") // 如果超时时间小于等于 0,则返回错误
		return
	}

	// 初始化 TcpScanner 结构体
	ts = &TcpScanner{
		retChan: retChan, // 设置返回通道
		limiter: limiter.NewLimiter(limiter.Every(time.Second/time.Duration(option.Rate)), option.Rate/10), // 设置速率限制器
		ctx:     context.Background(), // 初始化上下文
		timeout: time.Duration(option.Timeout) * time.Millisecond, // 设置超时时间
		option:  option, // 设置选项
	}

	return // 返回 TcpScanner 实例和可能的错误
}
  • func (ts *TcpScanner) Scan(ip net.IP, dst uint16) error

这个函数用于执行对指定 IP 和目标端口进行扫描的操作。它会启动一个 goroutine,在其中进行端口扫描并将结果发送到 retChan 通道中。函数首先检查扫描器是否已关闭,然后将一个任务添加到等待组 wg 中。接着,它初始化了一个 port.OpenIpPort 结构体实例,表示正在扫描的 IP 和端口。接下来的部分涉及服务指纹识别和 HTTP 信息探测的逻辑。如果设置了相应的选项,它将调用相关的函数进行识别并填充 openIpPort 结构体中的信息。最后,如果没有进行服务指纹识别或者 HTTP 信息探测,它将尝试通过 net.DialTimeout 进行连接,判断端口是否开放,并将结果发送到通道中。

Go 复制代码
// Scan 对指定IP和dis port进行扫描
func (ts *TcpScanner) Scan(ip net.IP, dst uint16) error {
	if ts.isDone {
		return errors.New("scanner is closed") // 如果扫描器已关闭,则返回错误
	}
	ts.wg.Add(1) // 增加等待组中的任务数
	go func() {
		defer ts.wg.Done() // 标记任务结束
		//fmt.Println(1)
		openIpPort := port.OpenIpPort{
			Ip:   ip,
			Port: dst,
		}
		var isDailErr bool
		if ts.option.FingerPrint {
			openIpPort.Service, isDailErr = fingerprint.PortIdentify("tcp", ip, dst, 2*time.Second) // 进行服务指纹识别
			if isDailErr {
				return // 如果识别过程出错,直接返回
			}
		}
		if ts.option.Httpx && (openIpPort.Service == "" || openIpPort.Service == "http" || openIpPort.Service == "https") {
			openIpPort.HttpInfo, isDailErr = fingerprint.ProbeHttpInfo(ip, dst, 2*time.Second) // 进行 HTTP 信息探测
			if isDailErr {
				return // 如果探测过程出错,直接返回
			}
			if openIpPort.HttpInfo != nil {
				if strings.HasPrefix(openIpPort.HttpInfo.Url, "https") {
					openIpPort.Service = "https" // 如果是 HTTPS,则标记为 HTTPS 服务
				} else {
					openIpPort.Service = "http" // 如果是 HTTP,则标记为 HTTP 服务
				}
			}
		}
		if !ts.option.FingerPrint && !ts.option.Httpx {
			conn, _ := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, dst), ts.timeout) // 尝试连接端口
			if conn != nil {
				conn.Close() // 如果连接成功,则关闭连接
			} else {
				return // 如果连接失败,直接返回
			}
		}
		ts.retChan <- openIpPort // 将扫描结果发送到通道
	}()
	return nil
}
  • 辅助函数

func (ts *TcpScanner) Wait()

func (ts *TcpScanner) Close()

func (ts *TcpScanner) WaitLimiter() error

Go 复制代码
//这个方法用于等待所有启动的 goroutine 完成扫描操作。它会等待 wg 等待组中的所有 goroutine 完成。
func (ts *TcpScanner) Wait() {
	ts.wg.Wait()
}

// Close chan这个方法用于关闭 retChan 通道,表示扫描已经完成。它还会设置 isDone 标志,表示扫描器已关闭
func (ts *TcpScanner) Close() {
	ts.isDone = true
	close(ts.retChan)
}

// WaitLimiter Waiting for the speed limit这个方法用于等待速率限制器。它会通过 limiter 控制扫描的速率,以确保按照设定的速率发送扫描请求
func (ts *TcpScanner) WaitLimiter() error {
	return ts.limiter.Wait(ts.ctx)
}
相关推荐
长栎1 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode1 小时前
Redis 在生产项目的使用
前端·后端
用户559822481221 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode1 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战1 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha1 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn1 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425911 小时前
ShardingJDBC
后端
行者全栈架构师1 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
Colin草率地做慢慢地改1 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构