news 2026/4/16 10:38:09

OXID 实习主机探测(包含完整实现代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OXID 实习主机探测(包含完整实现代码)

目录

前言

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

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

2025免费降AI率完全指南:从ai查重工具选择到降AI技巧,一步到位!

在论文、报告、内容创作越来越严格的时代&#xff0c;查AI率、检测AI率、降AI率 已经成为学生、写作者、博主的日常需求。很多同学因为 AI率过高被导师指出“AI痕迹太重”&#xff0c;甚至退回重写。本文今天一次性告诉你&#xff1a; 检测AI率应该注意什么 免费查AI率的网站有…

作者头像 李华
网站建设 2026/4/12 16:47:22

2025免费降AI率完全指南:从工具选择到实操技巧,一步到位

在论文、报告、内容创作越来越严格的时代&#xff0c;查AI率、检测AI率、降AI率 已经成为学生、写作者、博主的日常需求。很多同学因为 AI率过高被导师指出“AI痕迹太重”&#xff0c;甚至退回重写。本文今天一次性告诉你&#xff1a; 检测AI率应该注意什么 免费查AI率的网站有…

作者头像 李华
网站建设 2026/4/15 8:48:46

Java 8 JVM动态年龄计算机制详解

本文探讨一下HotSpot JVM开发团队引入动态年龄判断&#xff08;或称“自适应调整”&#xff09;的核心原因和设计哲学。 接下来让让我们深入剖析一下这个机制—— 核心原理&#xff1a;TargetSurvivorRatio 与动态年龄 动态年龄计算并不是直接丢弃MaxTenuringThreshold&#xf…

作者头像 李华