news 2026/1/16 6:23:39

ICMP Address Mask 探测存活主机(包含完整实现代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ICMP Address Mask 探测存活主机(包含完整实现代码)

目录

前言

地址掩码

基本概念

常见的子网掩码

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,214

ICMP 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)

工作流程

  1. 请求发送:向目标主机发送ICMP地址掩码请求包
  2. 存活判断:如果目标主机存活且支持该功能,会回复地址掩码信息
  3. 响应验证:通过匹配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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!