目录
前言
ICMP 时间戳探测存活主机原理
基本工作原理
时间戳消息结构
探测逻辑
技术优势
代码设计思路
整体架构设计
请求发送流程
响应处理流程
当前代码的4层验证
是否可以省略IP验证?
运行流程总结
代码分析
创建ICMP连接
构造数据包并发送
监听并分析响应
输出结果
源代码
其它
前言
这里进行第二种常用的icmp探测方法,通过发送时间戳请求进行探测,下面我详细讲解一下。
ICMP 时间戳探测存活主机原理
基本工作原理
ICMP(Internet Control Message Protocol)时间戳探测利用 ICMP 协议中的时间戳请求/回复机制:
- ICMP 时间戳请求(Type 13):向目标主机发送包含当前时间戳的请求包
- ICMP 时间戳回复(Type 14):存活的主机会回复包含三个时间戳的响应包
时间戳消息结构
时间戳消息体包含三个关键时间戳:
- Originate Timestamp:请求发起时间(由发送方填充)
- Receive Timestamp:目标接收时间(由接收方填充)
- Transmit Timestamp:回复发送时间(由接收方填充)
探测逻辑
发送ICMP时间戳请求(Type 13) → 目标主机 → 接收ICMP时间戳回复(Type 14) ↓ ↓ 设置唯一标识符 验证标识符匹配 记录发送时间 提取时间戳信息 等待响应 判断主机存活状态技术优势
- 绕过简单过滤:某些网络可能允许 ICMP 时间戳而禁止 ICMP Echo
- 提供额外信息:可以获得网络延迟和时间同步信息
- 协议层探测:在网络层工作,不依赖传输层端口
代码设计思路
整体架构设计
基于并发生产者-消费者模型:
主线程 ↓ 生成扫描任务 → 协程池(20个并发) → 发送ICMP时间戳请求 ↓ ↓ 等待所有任务完成 接收并解析响应 ↓ ↓ 汇总结果显示 更新存活主机列表请求发送流程
1. 创建原始ICMP套接字 2. 生成唯一标识符(PID + 序列号) 3. 构造时间戳请求数据 4. 序列化ICMP消息 5. 发送到目标IP响应处理流程
1. 设置读取超时等待响应 2. 验证响应来源IP匹配 3. 解析ICMP消息类型(Type 14) 4. 提取并验证标识符匹配 5. 记录存活主机信息当前代码的4层验证
- IP地址匹配:
sourceAddr.String() != Ip - 消息类型验证:
rm.Type != ipv4.ICMPTypeTimestampReply - 数据长度验证:
len(responseData) < 16 - ID/Seq匹配:
responseID == pid && responseSeq == uint16(seq)
是否可以省略IP验证?
可以省略,原因如下:
技术层面:
pid是进程ID的低16位,全局唯一性很高seq是递增序列号,在短时间内重复概率极低- 两者组合的碰撞概率几乎为零
运行流程总结
- 初始化阶段:准备目标列表,初始化数据结构
- 并发探测阶段:启动多个协程并发发送时间戳请求
- 响应处理阶段:异步接收并验证响应数据
- 结果汇总阶段:收集所有探测结果并输出统计信息
这种设计既保证了扫描效率,又确保了程序的稳定性和准确性,能够有效地发现网络中响应 ICMP 时间戳请求的存活主机。
代码分析
创建ICMP连接
// 创建ICMP连接 conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { return } defer conn.Close()构造数据包并发送
// 生成唯一ID和序列号 pid := uint16(os.Getpid() & 0xffff) currentTime := uint32(time.Now().UnixNano() / 1e6) // 毫秒时间戳 // 构造时间戳请求数据 (20字节) timestampData := make([]byte, 20) binary.BigEndian.PutUint16(timestampData[0:2], pid) // ID (2字节) binary.BigEndian.PutUint16(timestampData[2:4], uint16(seq)) // Seq (2字节) binary.BigEndian.PutUint32(timestampData[4:8], currentTime) // Originate (4字节) binary.BigEndian.PutUint32(timestampData[8:12], 0) // Receive (4字节) binary.BigEndian.PutUint32(timestampData[12:16], 0) // Transmit (4字节) // 剩余4字节填充0 // 创建ICMP时间戳请求消息 msg := icmp.Message{ Type: ipv4.ICMPTypeTimestamp, // Type 13 - 时间戳请求 Code: 0, Body: &icmp.RawBody{ Data: timestampData, }, } // 序列化消息 wb, err := msg.Marshal(nil) if err != nil { return } // 解析目标地址 host, err := net.ResolveIPAddr("ip4", Ip) if err != nil { return } // 发送时间戳请求 _, err = conn.WriteTo(wb, host) if err != nil { return }生成PID
pid := uint16(os.Getpid() & 0xffff)作用:生成唯一的 ICMP 消息标识符,用于匹配请求和响应。
分解说明:
os.Getpid() // 获取当前进程的PID(进程ID),例如:12345 & 0xffff // 按位与操作,取低16位(0xffff = 65535) uint16(...) // 转换为16位无符号整数生成时间戳
currentTime := uint32(time.Now().UnixNano() / 1e6)作用:生成当前时间戳,用于 ICMP 时间戳请求的 Originate 字段。
分解说明:
time.Now().UnixNano() // 获取当前时间的纳秒数,例如:1672531200000000000 / 1e6 // 除以1000000,将纳秒转换为毫秒 uint32(...) // 转换为32位无符号整数标识符字段 (0-3字节)
binary.BigEndian.PutUint16(timestampData[0:2], pid) // ID binary.BigEndian.PutUint16(timestampData[2:4], uint16(seq)) // Seq将32位无符号整数pid以大端序(Big-Endian)格式写入字节切片timestampData的第0到第1个位置。
- 位置:字节0-3
- ID字段(2字节):进程标识,用于区分不同扫描进程
- Seq字段(2字节):序列号,用于区分同一进程的不同请求
- 大端序:网络字节序,确保跨平台兼容
时间戳字段 (4-15字节)
binary.BigEndian.PutUint32(timestampData[4:8], currentTime) // Originate binary.BigEndian.PutUint32(timestampData[8:12], 0) // Receive binary.BigEndian.PutUint32(timestampData[12:16], 0) // Transmit三个时间戳的作用:
- Originate(4-7字节):请求发起时间,由发送方填充
- Receive(8-11字节):请求接收时间,由目标主机填充(初始为0)
- Transmit(12-15字节):回复发送时间,由目标主机填充(初始为0)
时间戳格式:
- 32位无符号整数
- 表示从UTC时间1900年1月1日午夜开始的毫秒数
- 实际实现中常用相对时间戳
填充字段 (16-19字节)
剩余4字节自动为0(make([]byte, 20)初始化为0)
ICMP消息封装
msg := icmp.Message{ Type: ipv4.ICMPTypeTimestamp, // Type 13 Code: 0, Body: &icmp.RawBody{ Data: timestampData, }, }ICMP类型和代码
- Type 13:时间戳请求 (Timestamp Request)
- Type 14:时间戳回复 (Timestamp Reply)
- Code 0:时间戳消息的代码字段始终为0
序列化消息
// 序列化消息 wb, err := msg.Marshal(nil) if err != nil { return }msg.Marshal():ICMP消息的序列化方法nil:可选的缓冲区参数(传入nil表示自动创建缓冲区)wb:返回的字节切片(wire bytes - 网络字节)err:错误信息(序列化失败时返回)
监听并分析响应
// 设置总超时 deadline := time.Now().Add(3 * time.Second) for { // 检查总超时 if time.Now().After(deadline) { return } // 设置读取超时 conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) // 读取响应 rb := make([]byte, 1500) n, sourceAddr, err := conn.ReadFrom(rb) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue // 超时继续等待 } return } // 检查响应来源是否匹配目标IP if sourceAddr.String() != Ip { continue } // 解析ICMP消息 rm, err := icmp.ParseMessage(ipv4.ICMPTypeTimestampReply.Protocol(), rb[:n]) if err != nil { continue } // 检查是否为时间戳回复 (Type 14) if rm.Type != ipv4.ICMPTypeTimestampReply { continue } // 提取消息体数据 var responseData []byte switch body := rm.Body.(type) { case *icmp.RawBody: responseData = body.Data default: continue } // 验证响应数据长度 if len(responseData) < 16 { 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(ipv4.ICMPTypeTimestampReply.Protocol(), rb[:n]) //告诉解析器这是ICMP协议的数据,解析出完整的ICMP消息结构 if err != nil { continue } // 检查是否为时间戳回复 (Type 14) if rm.Type != ipv4.ICMPTypeTimestampReply { //检查消息类型,只处理ICMPTypeTimestampReply continue }上面的代码是让程序按照icmp的形式解析数据,下面的代码才是检查消息类型
// 示例 ipv4.ICMPTypeEchoReply.Protocol() // 返回 1 (ICMP协议号) ipv4.ICMPTypeEchoReply // 返回 0 (Echo Reply类型值)消息体数据提取
var responseData []byte switch body := rm.Body.(type) { case *icmp.RawBody: responseData = body.Data default: continue }作用:从解析后的ICMP消息中提取原始数据体
类型断言过程:
rm.Body是interface{}类型,可能是多种消息体类型- 检查是否是
*icmp.RawBody类型(时间戳回复使用RawBody) - 如果是,提取
body.Data包含原始的20字节时间戳数据 - 如果不是,跳过这个响应(可能是其他类型的ICMP消息)
数据长度验证
if len(responseData) < 16 { continue }作用:确保响应数据格式正确
为什么是16字节:
时间戳消息体最小有效结构: 0-1: ID (2字节) 2-3: Seq (2字节) 4-7: Originate (4字节) 8-11: Receive (4字节) 12-15: Transmit (4字节) 总计:16字节输出结果
// 输出结果 fmt.Println("存活主机列表:") fmt.Println("IP地址\t\t状态") j := 0 for k, v := range survival { fmt.Printf("%s\t%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/timestamp.go
其它
在我写完针对多协议端口扫描和主机探测的工具后,希望通过文章整理用到的知识点,非常欢迎各位大佬指正文章内容的错误和工具的问题。
这里附上工具链接 https://github.com/yty0v0/ReconQuiver