目录
前言
地址掩码
基本概念
常见的子网掩码
ICMP Address Mask探测存活主机原理
基本原理
工作流程
ICMP报文构造模块
响应验证模块
主探测流程
代码分析
定义ICMP地址掩码相关常量
创建ICMP连接
构造并发送数据
监听并分析响应
输出结果
源代码
其它
前言
这里讲解icmp的第三种常用探测方法,下面我进行详细讲解,从最根本的原理入手,最后给出源代码。
地址掩码
基本概念
地址掩码(也叫子网掩码)是用来区分IP地址中网络部分和主机部分的32位二进制数。
常见的子网掩码
CIDR表示法 子网掩码 可用主机数 /24 255.255.255.0 254 /16 255.255.0.0 65,534 /8 255.0.0.0 16,777,214ICMP Address Mask探测存活主机原理
基本原理
ICMP地址掩码请求/回复是ICMP协议的一种类型,用于查询主机的子网掩码信息:
- ICMP类型17:地址掩码请求(Address Mask Request)
- ICMP类型18:地址掩码回复(Address Mask Reply)
//请求:向目标发送地址掩码请求,掩码字段为0 请求数据: [ID][Seq][00000000] = 8字节 //响应:目标主机回复自己的子网掩码 响应数据: [ID][Seq][实际掩码] = 8字节 //示例:如果目标掩码是255.255.255.0 addressMask := binary.BigEndian.Uint32(responseData[4:8]) //addressMask = 0xFFFFFF00 (255.255.255.0)工作流程
- 请求发送:向目标主机发送ICMP地址掩码请求包
- 存活判断:如果目标主机存活且支持该功能,会回复地址掩码信息
- 响应验证:通过匹配ID和序列号确认响应有效性
ICMP报文构造模块
// 地址掩码请求数据包结构 type AddressMaskRequest struct { ID uint16 // 进程ID标识 Seq uint16 // 序列号 Mask uint32 // 请求时为0,回复时为子网掩码 } // 使用RawBody构造ICMP消息体 msg := icmp.Message{ Type: ipv4.ICMPType(17), // 地址掩码请求类型 Code: 0, Body: &icmp.RawBody{ Data: addressMaskData, // 8字节数据 },响应验证模块
// 四层验证机制 1. 来源IP验证:确认响应来自目标IP 2. ICMP类型验证:必须是类型18(地址掩码回复) 3. 数据长度验证:响应数据至少8字节 4. ID/序列号匹配:确保响应与请求对应主探测流程
开始 ↓ 遍历IP地址列表 ↓ 启动并发探测(最多20个) ↓ 对于每个IP: ├─ 构造ICMP地址掩码请求 ├─ 发送请求包 ├─ 等待响应(总超时3秒) ├─ 验证响应有效性 └─ 记录存活主机 ↓ 等待所有探测完成 ↓ 输出结果统计 结束代码分析
定义ICMP地址掩码相关常量
// 定义ICMP地址掩码相关常量 const ( ICMPTypeAddressMaskRequest = 17 // 地址掩码请求 ICMPTypeAddressMaskReply = 18 // 地址掩码回复 )因为Go的icmp包中没有预定义地址掩码相关的常量
// 在 golang.org/x/net/icmp/message.go 中只有: const ( ICMPTypeEchoReply = 0 ICMPTypeDestinationUnreachable = 3 ICMPTypeEcho = 8 ICMPTypeTimeExceeded = 11 // 没有 ICMPTypeAddressMaskRequest/Reply )创建ICMP连接
// 创建ICMP连接 conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { fmt.Printf("创建连接失败 %s: %v\n", Ip, err) return } defer conn.Close()构造并发送数据
详细分析参考探测存活主机7的部分
// 生成唯一ID pid := uint16(os.Getpid() & 0xffff) // 构造地址掩码请求数据 (8字节) addressMaskData := make([]byte, 8) binary.BigEndian.PutUint16(addressMaskData[0:2], pid) // ID (2字节) binary.BigEndian.PutUint16(addressMaskData[2:4], uint16(seq)) // Seq (2字节) binary.BigEndian.PutUint32(addressMaskData[4:8], 0) // Address Mask (4字节,请求时为0) // 创建ICMP地址掩码请求消息 - 使用 RawBody msg := icmp.Message{ Type: ipv4.ICMPType(ICMPTypeAddressMaskRequest), Code: 0, Body: &icmp.RawBody{ Data: addressMaskData, }, } // 序列化消息 wb, err := msg.Marshal(nil) if err != nil { fmt.Printf("序列化失败 %s: %v\n", Ip, err) return } // 解析目标地址 host, err := net.ResolveIPAddr("ip4", Ip) if err != nil { fmt.Printf("解析地址失败 %s: %v\n", Ip, err) return } // 发送地址掩码请求 _, err = conn.WriteTo(wb, host) if err != nil { fmt.Printf("发送失败 %s: %v\n", Ip, err) return }监听并分析响应
// 设置总超时 deadline := time.Now().Add(3 * time.Second) for { // 检查总超时 if time.Now().After(deadline) { return } // 设置读取超时 conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) // 读取响应 rb := make([]byte, 1500) n, peer, err := conn.ReadFrom(rb) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue // 超时继续等待 } return } // 验证响应来源IP peerIP, ok := peer.(*net.IPAddr) if !ok { continue } if peerIP.String() != Ip { continue // 不是目标IP的响应 } // 解析ICMP消息 rm, err := icmp.ParseMessage(1, rb[:n]) //告诉解析器这是ICMP协议的数据,解析出完整的ICMP消息结构 if err != nil { continue } // 检查是否为地址掩码回复 if rm.Type != ipv4.ICMPType(ICMPTypeAddressMaskReply) { fmt.Printf("非地址掩码回复 %s: Type=%d\n", Ip, rm.Type) continue } // 提取消息体数据 var responseData []byte switch body := rm.Body.(type) { case *icmp.RawBody: responseData = body.Data default: continue } // 验证响应数据长度 if len(responseData) < 8 { continue } // 解析响应中的ID和序列号 responseID := binary.BigEndian.Uint16(responseData[0:2]) responseSeq := binary.BigEndian.Uint16(responseData[2:4]) // 检查ID和序列号是否匹配 if responseID == pid && responseSeq == uint16(seq) { mu.Lock() survival[Ip] = "up" mu.Unlock() return // 收到响应,退出循环 } }// 解析ICMP消息 rm, err := icmp.ParseMessage(1, rb[:n]) //告诉解析器这是ICMP协议的数据,解析出完整的ICMP消息结构 if err != nil { continue }这里的1表示告诉解析器这是ICMP协议的数据,解析出完整的ICMP消息结构。
//icmp ping rm, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), rb[:n]) //icmp timestamp rm, err := icmp.ParseMessage(ipv4.ICMPTypeTimestampReply.Protocol(), rb[:n]) // 它们最终都是:icmp.ParseMessage(1, rb[:n]) // 在源码中是这样定义的 func (ICMPType) Protocol() int { return 1 }总结:
方法不依赖于具体的ICMPType值,都是告诉解析器这是ICMP协议的数据,解析出完整的ICMP消息结构的意思。
任何 ipv4.ICMPType 实例调用 Protocol() 都返回 1
甚至零值 ipv4.ICMPType(0).Protocol() 也返回 1
当然在ping里写成ICMPTypeEchoReply可读性更好,这里写1是因为Go的icmp包中没有预定义地址掩码相关的常量
// 在 golang.org/x/net/icmp/message.go 中只有: const ( ICMPTypeEchoReply = 0 ICMPTypeDestinationUnreachable = 3 ICMPTypeEcho = 8 ICMPTypeTimeExceeded = 11 // 没有 ICMPTypeAddressMaskRequest/Reply )// 验证响应数据长度 if len(responseData) < 8 { continue }长度必须>=8的原因:
// responseData 的8字节结构: // 字节 0-1: Identifier (2字节) // 字节 2-3: Sequence Number (2字节) // 字节 4-7: Address Mask (4字节) responseData := make([]byte, 8) // [0:2] - ID // [2:4] - 序列号 // [4:8] - 地址掩码值输出结果
// 输出结果 fmt.Println("存活主机列表:") fmt.Println("IP地址\t\t状态") j := 0 for k, v := range survival { fmt.Printf("%s %s\n", k, v) j++ } usetime := time.Since(start) fmt.Printf("\n存活主机数量:%d\n", j) fmt.Printf("运行时间: %v\n", usetime)源代码
直接给出最终源代码
https://github.com/yty0v0/ReconQuiver/blob/main/internal/discovery/icmp_host/addressmask.go
其它
在我写完针对多协议端口扫描和主机探测的工具后,希望通过文章整理用到的知识点,非常欢迎各位大佬指正文章内容的错误和工具的问题。
这里附上工具链接 https://github.com/yty0v0/ReconQuiver