目录
前言
OXID 解析器基础原理
RPC 架构基础
总体架构关系
OXID 解析流程
多方法探测设计思路
存活判断依据
分层探测策略
核心模块设计
扫描流程
多层验证
严格响应验证
误报防护
代码分析
构造多种探测数据
EPM绑定探测数据
基本RPC验证数据
空请求探测数据
建立连接并发送探测数据
分析响应包
源代码
其它
前言
这里来讲解oxid实现主机探测,判断标准是135端口开启+RPC服务开启,下面我进行详细讲解。
OXID 解析器基础原理
OXID(Object Exporter ID)解析是 Windows RPC(远程过程调用)服务的一项功能,用于解析远程对象的绑定信息。通过向目标主机的 135 端口发送 OXID 解析请求,可以判断主机是否存活。
RPC 架构基础
- DCE/RPC: 分布式计算环境远程过程调用
- OXID 解析器: 对象导出标识符解析器,负责将对象引用解析为网络地址
- Endpoint Mapper: 端点映射器,运行在135端口,管理RPC服务端点信息
总体架构关系
RPC (远程过程调用) │ ├── EPM (端点映射器) - "服务发现系统" │ │ │ └── 管理:哪个RPC接口在哪个端口运行 │ └── OXID解析器 - "对象发现系统" │ └── 管理:哪个COM对象在哪个端点运行OXID 解析流程
客户端 → 135端口 → EPM服务 → OXID解析器 → 返回对象绑定信息
场景:客户端要使用远程Excel服务
- EPM查询(端口135):
- 客户端问EPM:"Excel应用程序接口在哪里?"
- EPM回答:"在 192.168.1.100:5001"
- OXID解析(端口5001):
- 客户端连接到5001端口的OXID解析器
- 问:"我要创建Excel对象,给我一个对象引用"
- OXID解析器返回:"新对象OXID是0x1234567890ABCDEF,在192.168.1.100:6001"
- 对象使用(端口6001):
- 客户端连接到6001端口
- 使用OXID 0x1234567890ABCDEF来调用Excel方法
组件 | 端口 | 作用 |
EPM | 135 | 服务发现- 告诉你某个接口在哪个端口 |
OXID解析器 | 动态端口 | 对象发现- 告诉你特定对象在哪个端口 |
酒店系统类比
- RPC= 整个酒店管理系统
- EPM= 酒店"前台"(固定位置:大堂)
- 告诉你各种服务在哪里:餐厅在3楼,健身房在5楼
- OXID解析器= "客房服务调度中心"
- 告诉你具体客人(对象)在哪个房间
多方法探测设计思路
存活判断依据
确认135端口开放且RPC服务运行
分层探测策略
采用三层递进式探测架构,从精准到宽松:
第一层:EPM绑定探测(最准确) 第二层:基本RPC验证(中等准确) 第三层:空请求检查(最宽松)核心模块设计
设计三种不同特性的RPC请求包:
EPM绑定请求
- 用途:精准识别Windows EPM服务
- 特点:使用标准EPM接口UUID (
e1afab1d-c911-9fe8-0800-2b1048600200) - 预期响应:Bind Ack (0x0C) - 绑定成功
基本RPC验证请求
- 用途:测试RPC服务错误处理机制
- 特点:使用全零UUID但版本不为零
- 预期响应:Bind Nack (0x0D) - 绑定拒绝
空请求
- 用途:最轻量级服务存活检测
- 特点:最小化的合法RPC请求
- 预期响应:Fault (0x03) - 操作错误
扫描流程
初始化信号量 (容量50) 对于每个IP: │ ├─ 获取信号量 ├─ 启动goroutine │ │ │ ├─ 方法1探测 │ ├─ 成功 → 记录结果 │ ├─ 失败 → 方法2探测 │ ├─ 失败 → 方法3探测 │ └─ 释放信号量 │ └─ 等待所有goroutine完成多层验证
// 三重保障机制 EPM绑定 → 确认Windows EPM服务 基本RPC → 确认RPC协议栈 空请求 → 确认服务基本存活严格响应验证
接受的有效响应类型:
0x0CBind Ack - 绑定接受0x0DBind Nack - 绑定拒绝0x02Response - 正常响应0x03Fault - 错误响应
误报防护
- RPC版本强制验证
- 包类型范围限制
- 最小长度检查
代码分析
构造多种探测数据
EPM绑定探测数据
// 使用更通用的 RPC 绑定请求 - EPM (Endpoint Mapper) var epmBindRequest = []byte{ // RPC Bind Header 0x05, 0x00, // RPC 版本 5.0 0x0b, // 包类型: Bind (11) 0x03, // 包标志 0x10, 0x00, 0x00, 0x00, // 数据表示 0x48, 0x00, // 分片长度: 72 0x00, 0x00, // 认证长度: 0 0x01, 0x00, 0x00, 0x00, // 调用ID: 1 // Bind Data 0xd0, 0x16, 0xd0, 0x16, // 最大传输大小: 5840 0x00, 0x00, 0x00, 0x00, // 关联组ID: 0 0x01, 0x00, // 上下文项数量: 1 // Context Item 0x00, 0x00, 0x00, 0x00, // 上下文ID: 0 0x01, 0x00, // 抽象语法数量: 1 // Abstract Syntax: EPM (Endpoint Mapper) - 这是135端口的标准服务 0xe1, 0xaf, 0xab, 0x1d, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, // 版本: 2.0 0x01, 0x00, // 传输语法数量: 1 // Transfer Syntax: NDR 0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60, 0x02, 0x00, 0x00, 0x00, // NDR 版本: 2.0 }这个请求的含义
用自然语言解释就是:
"你好,RPC服务器!我是RPC 5.0客户端,我想绑定到你的端点映射器服务(e1afab1d...版本2.0)。我使用NDR数据格式,最大能处理5840字节的数据包。我不需要认证,请为这次会话分配调用ID 1。"
期望的服务器响应
如果服务器正常运行,应该返回:
- 包类型:
0x0C(Bind Ack - 绑定确认) - 状态码:
0x00000000(成功) - 关联组ID: 新的关联标识
- 传输语法: 服务器选择的传输语法(通常是NDR)
实际应用场景
当你的代码发送这个数据包时,相当于在问:
"135端口,你运行的是Windows EPM服务吗?如果是,请确认我们的连接。"
这就是为什么这个数据包能用来检测Windows RPC服务 - 它使用了EPM服务的标准UUID,只有真正的Windows EPM服务才会正确响应。
基本RPC验证数据
var simpleRPCRequest = []byte{ // RPC头部 (16字节) 0x05, 0x00, 0x0b, 0x03, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // 绑定数据头部 (8字节) 0xd0, 0x16, 0xd0, 0x16, 0x00, 0x00, 0x00, 0x00, // 上下文项 (40字节) 0x01, 0x00, 0x00, 0x00, // 上下文项数量: 1 0x00, 0x00, 0x00, 0x00, // 上下文ID: 0 0x01, 0x00, // 抽象语法数量: 1 // 使用一个简单的UUID(全零但版本不为零) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // 版本: 1.0 0x01, 0x00, // 传输语法数量: 1 // NDR传输语法 0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60, 0x02, 0x00, 0x00, 0x00, // NDR版本: 2.0 }简单来说:
这个数据包就像在敲门测试,但它故意敲一扇不存在的门。
它发送的信息是:
"你好,RPC服务!我想绑定到一个接口,这个接口的编号是:00000000-0000-0000-0000-000000000000"
总结:这个包的目的不是真的要连接什么,而是用"问一个不存在的问题"的方式来验证RPC服务是否还在正常运行。
使用全零UUID的巧妙设计
// 这不是bug,而是有意设计! UUID: 00000000-0000-0000-0000-000000000000 版本: 1.0设计目的:
- 测试RPC服务的错误处理机制
- 验证服务对无效接口的响应行为
- 作为EPM绑定的补充探测方法
合法RPC服务的标准响应:
预期响应: Bind Nack (0x0D) - 绑定拒绝 原因: "不支持的接口"或"接口不存在"为什么这是有效的探测:
// 即使绑定被拒绝,也说明: 1. 服务收到了请求 ✅ 2. 服务理解RPC协议 ✅ 3. 服务进行了协议处理 ✅ 4. 只是不支持这个特定接口 ❌空请求探测数据
// 方法3: 空请求,只检查是否有响应 var nullRequest = []byte{ 0x05, 0x00, 0x00, 0x03, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }RPC 头部 (16字节)
05 00 // RPC版本5.0 - 正确 00 // 包类型: Request (0) - 请求包 03 // 包标志: FirstFrag | LastFrag - 完整数据包 10 00 00 00 // 数据表示: NDR - 正确 18 00 // 分片长度: 24字节 - 正确(整个包长度) 00 00 // 认证长度: 0 - 无认证 00 00 00 00 // 调用ID: 0 - 空请求常用0请求体 (8字节)
00 00 00 00 // 全部为零的请求体 00 00 00 00 // 没有具体操作和数据这个请求的含义
用自然语言解释:
"你好RPC服务,我是一个空的请求包(调用ID=0),没有任何具体操作,请给我一个响应。"
建立连接并发送探测数据
success, method := simpleRPCProbe(ip) //success表示主机是否存活,method是探测成功使用的方法 // 使用多种方法进行探测 func simpleRPCProbe(ip string) (bool, string) { // 方法1: EPM 绑定 if success := probe(ip, epmBindRequest); success { return true, "EPM绑定成功" } // 方法2: 基本RPC验证 if success := probe(ip, simpleRPCRequest); success { return true, "基本RPC响应" } // 方法3: 空请求检查 if success := probe(ip, nullRequest); success { return true, "空请求响应" } return false, "" } // 发包进行探测 func probe(ip string, checkRequest []byte) bool { //先建立tcp连接 conn, err := net.DialTimeout("tcp", ip+":135", 3*time.Second) if err != nil { return false } defer conn.Close() conn.SetDeadline(time.Now().Add(5 * time.Second)) _, err = conn.Write(checkRequest) if err != nil { return false } buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { return false } return validateRPCResponse(buffer[:n]) //调用检查函数来分析响应 }分析响应包
func probe(ip string, checkRequest []byte) bool { ...... return validateRPCResponse(buffer[:n]) //调用检查函数来分析响应 } // 分析响应包 func validateRPCResponse(data []byte) bool { if len(data) < 16 { return false } // 检查基本的 RPC 头部 if data[0] != 0x05 || data[1] != 0x00 { return false } // 接受 Bind Ack (0x0C) 或 Bind Nack (0x0D) 或其他有效响应 packetType := data[2] if packetType == 0x0C || packetType == 0x0D || packetType == 0x02 || packetType == 0x03 { return true } return false }这个函数的核心思想是:只要服务有响应,就认为 RPC 服务存在
基础长度检查
if len(data) < 16 { return false }作用:确保响应数据至少包含完整的 RPC 头部
原因:RPC 头部固定为 16 字节,包含版本、类型、长度等关键信息
对应协议结构:
Offset 0-1: RPC版本 (2字节) Offset 2: 包类型 (1字节) Offset 3: 包标志 (1字节) Offset 4-7: 数据表示 (4字节) Offset 8-9: 分片长度 (2字节) Offset 10-11:认证长度 (2字节) Offset 12-15:调用ID (4字节)RPC 版本验证
if data[0] != 0x05 || data[1] != 0x00 { return false }验证内容:检查是否为 RPC 版本 5.0
协议要求:RPC 版本必须是0x05 0x00
重要性:版本不匹配说明不是标准的 Windows RPC 响应
包类型验证(核心逻辑)
packetType := data[2] if packetType == 0x0C || packetType == 0x0D || packetType == 0x02 || packetType == 0x03 { return true }接受的包类型含义:
包类型值 | 名称 | 含义 | 为什么接受 |
| Bind Ack | 绑定确认 | ✅ 服务接受绑定请求 |
| Bind Nack | 绑定拒绝 | ✅ 服务存在但拒绝绑定(版本不匹配等) |
| Response | 正常响应 | ✅ 服务处理了请求 |
| Fault | 错误响应 | ✅ 服务存在但处理出错 |
源代码
直接给出完整源代码
https://github.com/yty0v0/ReconQuiver/blob/main/internal/discovery/oxid_host/oxid.go
其它
在我写完针对多协议端口扫描和主机探测的工具后,希望通过文章整理用到的知识点,非常欢迎各位大佬指正文章内容的错误和工具的问题。
这里附上工具链接 https://github.com/yty0v0/ReconQuiver