fscan/Core/PortInfo.go
2025-03-19 10:31:08 +00:00

476 lines
13 KiB
Go

package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"io"
"net"
"strings"
"time"
)
// ServiceInfo defines the service identification result information
type ServiceInfo struct {
Name string // Service name, such as http, ssh, etc.
Banner string // Banner information returned by the service
Version string // Service version number
Extras map[string]string // Other additional information, such as operating system, product name, etc.
}
// Result defines the result of a single detection
type Result struct {
Service Service // Identified service information
Banner string // Service banner
Extras map[string]string // Additional information
Send []byte // Probe data sent
Recv []byte // Response data received
}
// Service defines the basic information of a service
type Service struct {
Name string // Service name
Extras map[string]string // Additional service attributes
}
// Info defines the context information for a single port probe
type Info struct {
Address string // Target IP address
Port int // Target port
Conn net.Conn // Network connection
Result Result // Detection result
Found bool // Whether the service was successfully identified
}
// PortInfoScanner defines a port service identifier
type PortInfoScanner struct {
Address string // Target IP address
Port int // Target port
Conn net.Conn // Network connection
Timeout time.Duration // Timeout duration
info *Info // Detection context
}
// Predefined basic probes
var (
null = new(Probe) // Empty probe, used for basic protocol identification
common = new(Probe) // Common probe, used for common service identification
)
// NewPortInfoScanner creates a new port service identifier instance
func NewPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration) *PortInfoScanner {
return &PortInfoScanner{
Address: addr,
Port: port,
Conn: conn,
Timeout: timeout,
info: &Info{
Address: addr,
Port: port,
Conn: conn,
Result: Result{
Service: Service{},
},
},
}
}
// Identify performs service identification and returns the result
func (s *PortInfoScanner) Identify() (*ServiceInfo, error) {
Common.LogDebug(fmt.Sprintf("Starting to identify service %s:%d", s.Address, s.Port))
s.info.PortInfo()
// Construct the return result
serviceInfo := &ServiceInfo{
Name: s.info.Result.Service.Name,
Banner: s.info.Result.Banner,
Version: s.info.Result.Service.Extras["version"],
Extras: make(map[string]string),
}
// Copy additional information
for k, v := range s.info.Result.Service.Extras {
serviceInfo.Extras[k] = v
}
Common.LogDebug(fmt.Sprintf("Service identification completed %s:%d => %s", s.Address, s.Port, serviceInfo.Name))
return serviceInfo, nil
}
// PortInfo executes the main logic of port service identification
func (i *Info) PortInfo() {
// 1. First try to read the initial response from the service
if response, err := i.Read(); err == nil && len(response) > 0 {
Common.LogDebug(fmt.Sprintf("Received initial response: %d bytes", len(response)))
// Check the response using basic probes
Common.LogDebug("Attempting to check response using basic probes (null/common)")
if i.tryProbes(response, []*Probe{null, common}) {
Common.LogDebug("Basic probe matching successful")
return
}
Common.LogDebug("Basic probes did not match")
} else if err != nil {
Common.LogDebug(fmt.Sprintf("Failed to read initial response: %v", err))
}
// Record used probes to avoid duplication
usedProbes := make(map[string]struct{})
// 2. Try to use port-specific probes
Common.LogDebug(fmt.Sprintf("Attempting to use dedicated probes for port %d", i.Port))
if i.processPortMapProbes(usedProbes) {
Common.LogDebug("Port-specific probe matching successful")
return
}
Common.LogDebug("Port-specific probes did not match")
// 3. Use the default probe list
Common.LogDebug("Attempting to use default probe list")
if i.processDefaultProbes(usedProbes) {
Common.LogDebug("Default probe matching successful")
return
}
Common.LogDebug("Default probes did not match")
// 4. If all probes fail, mark as unknown service
if strings.TrimSpace(i.Result.Service.Name) == "" {
Common.LogDebug("Service not recognized, marking as unknown")
i.Result.Service.Name = "unknown"
}
}
// tryProbes attempts to use the specified probe list to check the response
func (i *Info) tryProbes(response []byte, probes []*Probe) bool {
for _, probe := range probes {
Common.LogDebug(fmt.Sprintf("Attempting probe: %s", probe.Name))
i.GetInfo(response, probe)
if i.Found {
Common.LogDebug(fmt.Sprintf("Probe %s matched successfully", probe.Name))
return true
}
}
return false
}
// processPortMapProbes processes dedicated probes in the port mapping
func (i *Info) processPortMapProbes(usedProbes map[string]struct{}) bool {
// Check if port-specific probes exist
if len(Common.PortMap[i.Port]) == 0 {
Common.LogDebug(fmt.Sprintf("Port %d has no dedicated probes", i.Port))
return false
}
// Iterate through port-specific probes
for _, name := range Common.PortMap[i.Port] {
Common.LogDebug(fmt.Sprintf("Attempting port-specific probe: %s", name))
usedProbes[name] = struct{}{}
probe := v.ProbesMapKName[name]
// Decode probe data
probeData, err := DecodeData(probe.Data)
if err != nil || len(probeData) == 0 {
Common.LogDebug(fmt.Sprintf("Failed to decode probe data for %s", name))
continue
}
// Send probe data and get response
Common.LogDebug(fmt.Sprintf("Sending probe data: %d bytes", len(probeData)))
if response := i.Connect(probeData); len(response) > 0 {
Common.LogDebug(fmt.Sprintf("Received response: %d bytes", len(response)))
// Check response with current probe
i.GetInfo(response, &probe)
if i.Found {
return true
}
// Perform additional checks based on probe type
switch name {
case "GenericLines":
if i.tryProbes(response, []*Probe{null}) {
return true
}
case "NULL":
continue
default:
if i.tryProbes(response, []*Probe{common}) {
return true
}
}
}
}
return false
}
// processDefaultProbes processes the default probe list
func (i *Info) processDefaultProbes(usedProbes map[string]struct{}) bool {
failCount := 0
const maxFailures = 10 // Maximum failure count
// Iterate through the default probe list
for _, name := range Common.DefaultMap {
// Skip already used probes
if _, used := usedProbes[name]; used {
continue
}
probe := v.ProbesMapKName[name]
probeData, err := DecodeData(probe.Data)
if err != nil || len(probeData) == 0 {
continue
}
// Send probe data and get response
response := i.Connect(probeData)
if len(response) == 0 {
failCount++
if failCount > maxFailures {
return false
}
continue
}
// Check response with current probe
i.GetInfo(response, &probe)
if i.Found {
return true
}
// Perform additional checks based on probe type
switch name {
case "GenericLines":
if i.tryProbes(response, []*Probe{null}) {
return true
}
case "NULL":
continue
default:
if i.tryProbes(response, []*Probe{common}) {
return true
}
}
// Try to use other probes in the port mapping
if len(Common.PortMap[i.Port]) > 0 {
for _, mappedName := range Common.PortMap[i.Port] {
usedProbes[mappedName] = struct{}{}
mappedProbe := v.ProbesMapKName[mappedName]
i.GetInfo(response, &mappedProbe)
if i.Found {
return true
}
}
}
}
return false
}
// GetInfo analyzes response data and extracts service information
func (i *Info) GetInfo(response []byte, probe *Probe) {
Common.LogDebug(fmt.Sprintf("Starting to analyze response data, length: %d", len(response)))
// Check response data validity
if len(response) <= 0 {
Common.LogDebug("Response data is empty")
return
}
result := &i.Result
var (
softMatch Match
softFound bool
)
// Process main matching rules
Common.LogDebug(fmt.Sprintf("Processing main matching rules for probe %s", probe.Name))
if matched, match := i.processMatches(response, probe.Matchs); matched {
Common.LogDebug("Hard match found")
return
} else if match != nil {
Common.LogDebug("Soft match found")
softFound = true
softMatch = *match
}
// Process fallback matching rules
if probe.Fallback != "" {
Common.LogDebug(fmt.Sprintf("Attempting fallback match: %s", probe.Fallback))
if fbProbe, ok := v.ProbesMapKName[probe.Fallback]; ok {
if matched, match := i.processMatches(response, fbProbe.Matchs); matched {
Common.LogDebug("Fallback match successful")
return
} else if match != nil {
Common.LogDebug("Fallback soft match found")
softFound = true
softMatch = *match
}
}
}
// Handle case when no match is found
if !i.Found {
Common.LogDebug("No hard match found, handling no match case")
i.handleNoMatch(response, result, softFound, softMatch)
}
}
// processMatches processes the set of matching rules
func (i *Info) processMatches(response []byte, matches *[]Match) (bool, *Match) {
Common.LogDebug(fmt.Sprintf("Starting to process matching rules, total %d rules", len(*matches)))
var softMatch *Match
for _, match := range *matches {
if !match.MatchPattern(response) {
continue
}
if !match.IsSoft {
Common.LogDebug(fmt.Sprintf("Hard match found: %s", match.Service))
i.handleHardMatch(response, &match)
return true, nil
} else if softMatch == nil {
Common.LogDebug(fmt.Sprintf("Soft match found: %s", match.Service))
tmpMatch := match
softMatch = &tmpMatch
}
}
return false, softMatch
}
// handleHardMatch handles hard match results
func (i *Info) handleHardMatch(response []byte, match *Match) {
Common.LogDebug(fmt.Sprintf("Processing hard match result: %s", match.Service))
result := &i.Result
extras := match.ParseVersionInfo(response)
extrasMap := extras.ToMap()
result.Service.Name = match.Service
result.Extras = extrasMap
result.Banner = trimBanner(response)
result.Service.Extras = extrasMap
// Special handling for microsoft-ds service
if result.Service.Name == "microsoft-ds" {
Common.LogDebug("Special handling for microsoft-ds service")
result.Service.Extras["hostname"] = result.Banner
}
i.Found = true
Common.LogDebug(fmt.Sprintf("Service identification result: %s, Banner: %s", result.Service.Name, result.Banner))
}
// handleNoMatch handles the case when no match is found
func (i *Info) handleNoMatch(response []byte, result *Result, softFound bool, softMatch Match) {
Common.LogDebug("Handling no match case")
result.Banner = trimBanner(response)
if !softFound {
// Try to identify HTTP service
if strings.Contains(result.Banner, "HTTP/") ||
strings.Contains(result.Banner, "html") {
Common.LogDebug("Identified as HTTP service")
result.Service.Name = "http"
} else {
Common.LogDebug("Unknown service")
result.Service.Name = "unknown"
}
} else {
Common.LogDebug("Using soft match result")
extras := softMatch.ParseVersionInfo(response)
result.Service.Extras = extras.ToMap()
result.Service.Name = softMatch.Service
i.Found = true
Common.LogDebug(fmt.Sprintf("Soft match service: %s", result.Service.Name))
}
}
// Connect sends data and gets response
func (i *Info) Connect(msg []byte) []byte {
i.Write(msg)
reply, _ := i.Read()
return reply
}
const WrTimeout = 5 // Default read/write timeout (seconds)
// Write writes data to the connection
func (i *Info) Write(msg []byte) error {
if i.Conn == nil {
return nil
}
// Set write timeout
i.Conn.SetWriteDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
// Write data
_, err := i.Conn.Write(msg)
if err != nil && strings.Contains(err.Error(), "close") {
i.Conn.Close()
// Retry when connection is closed
i.Conn, err = net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second)
if err == nil {
i.Conn.SetWriteDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
_, err = i.Conn.Write(msg)
}
}
// Record sent data
if err == nil {
i.Result.Send = msg
}
return err
}
// Read reads response from the connection
func (i *Info) Read() ([]byte, error) {
if i.Conn == nil {
return nil, nil
}
// Set read timeout
i.Conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
// Read data
result, err := readFromConn(i.Conn)
if err != nil && strings.Contains(err.Error(), "close") {
return result, err
}
// Record received data
if len(result) > 0 {
i.Result.Recv = result
}
return result, err
}
// readFromConn helper function to read data from connection
func readFromConn(conn net.Conn) ([]byte, error) {
size := 2 * 1024 // Read buffer size
var result []byte
for {
buf := make([]byte, size)
count, err := conn.Read(buf)
if count > 0 {
result = append(result, buf[:count]...)
}
if err != nil {
if len(result) > 0 {
return result, nil
}
if err == io.EOF {
return result, nil
}
return result, err
}
if count < size {
return result, nil
}
}
}