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

一、概述

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

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

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

/core/host/ping.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 包来检查主机是否存活。

// 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 命令来检查主机是否存活。

// 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 连接在指定端口上对主机进行存活探测,在连接成功或者连接被拒绝时,都会判断主机为存活状态。

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。

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 存活探测。

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{} 常见端口列表

  • 各结构体列表
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 地址、端口号以及可能的服务信息的字符串表示形式

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 指纹等。

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)

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

// 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

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

// 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 端口,然后从用户指定的端口范围中选择未被选取的端口,并最终随机排序这些端口

// 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 的管理,确保扫描操作的安全性和效率。

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,最后返回该实例和可能的错误。

// 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 进行连接,判断端口是否开放,并将结果发送到通道中。

// 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

//这个方法用于等待所有启动的 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)
}

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月26日
下一篇 2023年12月26日

相关推荐