OXID 实习主机探测(包含完整实现代码)
2026/6/5 12:07:55 网站建设 项目流程

目录

前言

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服务

  1. EPM查询(端口135):
    • 客户端问EPM:"Excel应用程序接口在哪里?"
    • EPM回答:"在 192.168.1.100:5001"
  1. OXID解析(端口5001):
    • 客户端连接到5001端口的OXID解析器
    • 问:"我要创建Excel对象,给我一个对象引用"
    • OXID解析器返回:"新对象OXID是0x1234567890ABCDEF,在192.168.1.100:6001"
  1. 对象使用(端口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 }

接受的包类型含义:

包类型值

名称

含义

为什么接受

0x0C

Bind Ack

绑定确认

✅ 服务接受绑定请求

0x0D

Bind Nack

绑定拒绝

✅ 服务存在但拒绝绑定(版本不匹配等)

0x02

Response

正常响应

✅ 服务处理了请求

0x03

Fault

错误响应

✅ 服务存在但处理出错


源代码

直接给出完整源代码

https://github.com/yty0v0/ReconQuiver/blob/main/internal/discovery/oxid_host/oxid.go


其它

在我写完针对多协议端口扫描和主机探测的工具后,希望通过文章整理用到的知识点,非常欢迎各位大佬指正文章内容的错误和工具的问题。

这里附上工具链接 https://github.com/yty0v0/ReconQuiver

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询