目录
前言
NetBIOS
什么是 NetBIOS?
NetBIOS 核心服务
NetBIOS 到底干什么用?
1. 名字解析(主要功能)
2. 服务发现
3. 会话管理
探测原理
存活判断的标准
代码设计思路
两阶段扫描架构
状态机设计
模块1: NetBIOS探测模块
模块2: 协议验证模块
代码分析
构造探测数据
建立连接并发送探测数据
接受并分析响应
源代码
其它
前言
判断存活的标准是服务开启+端口开启,下面进行详细讲解,这种探测方式不太常用。
NetBIOS
什么是 NetBIOS?
网络基本输入/输出系统
NetBIOS(Network Basic Input/Output System)是1983年由IBM开发的网络协议,为局域网应用程序提供统一的命令集。虽然现在逐渐被DNS取代,但在Windows网络中仍然广泛使用。
NetBIOS 核心服务
端口 | 协议 | 服务名称 | 功能 |
137/UDP | NetBIOS名称服务 | NBNS | 主机名解析、名称注册查询 |
138/UDP | NetBIOS数据报服务 | NBDS | 网络广播、消息传递 |
139/TCP | NetBIOS会话服务 | NBSS | 文件/打印机共享连接 |
NetBIOS 到底干什么用?
1.名字解析(主要功能)
你想联系"财务部电脑",但不知道它的IP地址 → 问NetBIOS:"财务部电脑的IP是多少?" → NetBIOS回答:"192.168.1.105"2.服务发现
你想知道网络里有哪些电脑共享了打印机 → 问NetBIOS:"谁共享了打印机?" → NetBIOS回答:"技术部电脑、前台电脑"3.会话管理
你要访问"技术部电脑"的共享文件夹 → NetBIOS帮你建立稳定连接探测原理
基于NetBIOS名称服务协议
- 协议: NetBIOS Name Service (NBNS)
- 端口: UDP 137
- 机制: 通过发送特定的NetBIOS状态查询包,根据响应判断主机存活状态
存活判断的标准
udp的137端口开启且netbios服务开启
代码设计思路
两阶段扫描架构
阶段1: 主机发现 (TCP端口扫描) → 阶段2: NetBIOS服务探测 (UDP 137端口)
设计理念:先找到存活主机,再针对性地探测服务,避免对不存在的主机进行无谓的UDP探测。
状态机设计
定义了清晰的三种状态:
- alive: 收到有效的NetBIOS响应
- filtered: 主机存活但NetBIOS无响应(端口被过滤)
- dead: 主机不存活或NetBIOS服务关闭
模块1: NetBIOS探测模块
// 设计思路:UDP协议状态探测 输入: 存活主机IP 输出: NetBIOS状态结果 策略: 发送标准NetBIOS查询包,根据响应判断服务状态模块2: 协议验证模块
// 设计思路:协议格式验证 输入: 原始网络数据 输出: 是否为有效NetBIOS响应 策略: 检查数据包长度和标志位(QR位)代码分析
构造探测数据
func createNetBIOSQuery() []byte { return []byte{ 0x12, 0x34, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, } }这是一个NetBIOS名称服务状态查询包
这个查询包的作用是:
- 查询类型: NetBIOS节点状态查询 (NBSTAT)
- 目标: 请求目标主机返回其NetBIOS名称表
- 响应内容: 包括主机名、服务类型、MAC地址等
当这个包发送到目标的137端口时:
- 如果主机存在且运行NetBIOS服务,会返回节点状态信息
- 如果主机不存在或服务关闭,会超时或无响应
- 响应包包含详细的NetBIOS名称表和MAC地址
建立连接并发送探测数据
func netbios_scan_survival(ipaddres []string) { // 阶段1: NetBIOS扫描(只对存活主机) fmt.Println("阶段1: NetBIOS扫描...") sem := make(chan struct{}, 50) for _, ip := range aliveHosts { wg.Add(1) go func(ip string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() result := netbiosProbe(ip) mu.Lock() if result.Status != "dead" { results = append(results, result) } mu.Unlock() }(ip) } wg.Wait() ...... } // NetBIOS探测 - 只对已知存活的主机进行 func netbiosProbe(ip string) NetBIOSResult { result := NetBIOSResult{ IP: ip, Status: "dead", Port137: "关闭", } // UDP 137端口探测 conn, err := net.DialTimeout("udp", fmt.Sprintf("%s:%d", ip, 137), 3*time.Second) if err != nil { return result } defer conn.Close() conn.SetDeadline(time.Now().Add(3 * time.Second)) // 发送查询 query := createNetBIOSQuery() if _, err := conn.Write(query); err != nil { return result } ...... return result }接受并分析响应
// NetBIOS探测 - 只对已知存活的主机进行 func netbiosProbe(ip string) NetBIOSResult { result := NetBIOSResult{ IP: ip, Status: "dead", Port137: "关闭", } ...... // 接收响应 buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { // 已知存活的主机 + UDP超时 = open|filtered result.Status = "filtered" result.Port137 = "开放或被过滤" } return result } // 收到有效响应 if n > 0 && validateNetBIOSResponse(buffer[:n]) { result.Status = "alive" result.Port137 = "开放" } return result } // 验证 NetBIOS 响应 func validateNetBIOSResponse(data []byte) bool { if len(data) < 12 { return false } // 检查响应标志位 (第3字节的最高位) flags := binary.BigEndian.Uint16(data[2:4]) isResponse := (flags & 0x8000) != 0 // 检查答案数量 answerCount := binary.BigEndian.Uint16(data[6:8]) //之前的代码没有,新添加的 return isResponse && answerCount > 0 }长度检查
if len(data) < 12 { return false }- 原因: NetBINS响应包头部至少12字节
- 作用: 过滤掉太短的无意义数据包
提取标志位
flags := binary.BigEndian.Uint16(data[2:4])- 位置: 数据包的第3-4字节(0-based索引2:4)
- 格式: 大端序16位无符号整数
- 含义: 提取NetBIOS响应标志字段
源代码
直接给出完整源代码
https://github.com/yty0v0/ReconQuiver/blob/main/internal/discovery/netbios_host/netbios.go
其它
在我写完针对多协议端口扫描和主机探测的工具后,希望通过文章整理用到的知识点,非常欢迎各位大佬指正文章内容的错误和工具的问题。
这里附上工具链接 https://github.com/yty0v0/ReconQuiver