~/posts/web/dcsync-dcshadow-rpc-context-detection.md

DCSync 与 DCShadow:RPC 上下文决定告警

从接口协商、context ID 和 opnum 还原目录复制请求,解释 flowbit 的状态缺口、DCShadow 反向拉取及字节序边界,并给出协议规则与部署回归矩阵。

date[31:24]
read[23:16]
7 分钟
cat[15:8]
Web 安全
目录
  1. 0x00先定义角色,再定义告警
  2. 0x01两个数据包之间还有协商状态
  3. 0x02从 PDU 起点解释 opnum
  4. 0x03flowbit 是近似状态,不是接口状态机
  5. 0x04DCShadow 的复制方向会翻转
  6. 0x05字节序来自报文,不来自操作系统假设
  7. 0x06协议规则仍需要回归验收
  8. 0x07官方参考

Active Directory 中出现 DsGetNCChanges,既可能是正常域控同步,也可能是持有复制权限的其他主机读取目录。检测的核心不是工具名称,而是谁在已协商的哪个 RPC 接口上调用了什么操作。

以下依据 2021 年 DCSync、DCShadow 的网络记录恢复这条证据链。接口绑定、操作调用、服务响应和实际目录效果是不同层级的证据;只识别到请求,不代表敏感数据已被读取或修改成功。

先定义角色,再定义告警

DCSync 借用目录复制权限请求更新;DCShadow 还涉及临时组织复制来源并推动目录变化。它们不是仅靠网络可达就能获得域权限的协议漏洞,正常域控也广泛使用同一接口。

WORKSTATIONS 与 DCS 应来自维护中的资产集合,而不是临时猜测的网段。对被委派复制任务的非域控服务还要核对业务范围;域控身份本身也不保证一次操作合理。传感器需覆盖内部复制路径,互联网出口抓包通常不足以回答这些问题。

flowchart TD
  A["连接端点与主机角色"] --> B["Bind / Alter Context"]
  B --> C["已接受的 context → 接口"]
  C --> D["Request:context ID + opnum"]
  D --> E["响应与目录效果"]

操作编号只在接口内部有意义。还要区分三种上下文:p_cont_id 选择呈现上下文,call_id 关联 RPC 请求与响应,IDL_DRSBind 返回的 DRS_HANDLE 属于 DRS 业务接口。RPC Bind PDU 与 IDL_DRSBind 调用也不是同一件事。

两个数据包之间还有协商状态

DCSync — TCP stream 4pcap
序号源地址目的地址协议长度信息
28192.0.2.16192.0.2.5DCERPC1794Bind, call_id=2
30192.0.2.5192.0.2.16DCERPC314Bind_ack
31192.0.2.16192.0.2.5DCERPC274Alter_context
32192.0.2.5192.0.2.16DCERPC159Alter_context_resp
41192.0.2.16192.0.2.5DRSUAPI498DsGetNCChanges request, context_id=0, opnum=3
45192.0.2.5192.0.2.16DRSUAPI822DsGetNCChanges response
6 个数据包

地址已替换为文档地址,包号与长度保留。TCP stream 4 的第 28 帧声明接口,第 41 帧请求 DsGetNCChanges,第 45 帧是对应响应;中间还有 Bind_ack 与 Alter_context。检测器应关联提议和接受结果,而不是看到任意 Bind 就视为接口已可用。

接口 UUID 为 e3514235-4b06-11d1-ab04-00c04fc2dcd2,记录显示接口版本 4.0。小端数据表示下,UUID 的字节是:

DRSUAPI UUID — little-endian representation
00000000354251E3064BD111AB0400C04FC2DCD2
  1. Data10x00–0x03
  2. Data20x04–0x05
  3. Data30x06–0x07
  4. Data40x08–0x0F

前三个字段分别按各自字段宽度处理字节序,Data4 保持字节顺序,并非整体倒序。独立核对可用 UUID(...).bytes_le;绑定中还存在 transfer syntax,接口标识和传输语法不要混为一个 UUID。

从 PDU 起点解释 opnum

DCE/RPC request — fixed headerLE
偏移名称类型大小值
0x00rpc_vers15
0x01rpc_vers_minor10
0x02ptype10
0x03pfc_flags1
0x04packed_drep4
0x08frag_length2
0x0aauth_length2
0x0ccall_id4
0x10alloc_hint4
0x14p_cont_id2
0x16opnum23
大小 = 0x18(24 字节)

这是固定请求头布局,value 仅标注此处讨论的版本、PDU 类型和操作号,不表示补齐了整份报文。所有偏移相对于 RPC PDU 起点,不是以太网帧或 TCP 段起点;可选对象 UUID、认证尾部和 stub 数据仍需后续解析。

Bind 前缀是 05 00 0B,Request 前缀是 05 00 00。第 41 帧的 context ID = 0,偏移 22 的小端 03 00 对应 IDL_DRSGetNCChanges,其语义是从服务端命名上下文副本复制更新。Microsoft DRSGetNCChanges

该请求显示 SPNEGO packet privacy。请求头的操作类型仍可见,不代表传感器能读取受保护的全部参数;响应帧存在,也不自动说明返回成功或包含哪些属性。

flowbit 是近似状态,不是接口状态机

raw-drs.rulestext
alert tcp $WORKSTATIONS any -> $DCS any (msg:"DRS bind candidate"; flow:established,to_server; content:"|05 00 0b|"; depth:3; content:"|35 42 51 e3 06 4b d1 11 ab 04 00 c0 4f c2 dc d2|"; depth:100; flowbits:set,drs_seen; flowbits:noalert; sid:9001000; rev:1;)
alert tcp $WORKSTATIONS any -> $DCS any (msg:"DRS GetNCChanges candidate"; flow:established,to_server; flowbits:isset,drs_seen; content:"|05 00 00|"; depth:3; content:"|03 00|"; offset:22; depth:2; sid:9001001; rev:1;)

这组规则仅用于说明原始字节匹配:假设检测缓冲区从 PDU 起点开始,且采用小端表示。第一条设置流标记并抑制自身告警;第二条在同一流已标记后检查请求编号。它们没有经过此环境中的 Suricata 加载或 PCAP 回放测试。

流标记省掉了重复 UUID 搜索,却丢失了重要维度。一个连接可以有多个 presentation context,drs_seen 并不证明之后的每个 opnum=3 都属于 DRSUAPI。完整实现至少维护 连接 + context ID → 已接受接口及传输语法,并处理上下文变更、失败协商和连接结束。

TCP 重分段、多 PDU 合并、RPC 分片和抓包缺失都会影响原始偏移。若只捕获到请求而没看到协商,应记录“上下文未知”,而不是静默当作可信,也不是把 opnum 强行解释成 DRS 操作。

DCShadow 的复制方向会翻转

样本使用两个执行上下文:一个准备本地 RPC 服务,另一个持有所需目录管理权限并推动注册、同步和注销。控制台中可见域控功能级别 WIN2016;这不等于完整 Windows 构建号。

RPC service: selected lineslog
RPC bind registered
RPC Server is waiting!
1 object(s) pushed
RPC bind unregistered
stopping RPC server
RPC server stopped

上方只保留服务状态行。域名、主机、账户标识、进程号以及会话密钥不进入公开记录;1 object(s) pushed 是工具输出,实际属性效果仍应由独立目录查询核对。

DCShadow — selected streamspcap
序号源地址目的地址协议长度信息
74192.0.2.16192.0.2.5DCERPC2026stream=7, Bind
93192.0.2.16192.0.2.5DRSUAPI434stream=7, DRSUAPI_REPLICA_ADD request, opnum=5
118192.0.2.5192.0.2.16DCERPC468stream=10, Bind
125192.0.2.5192.0.2.16DRSUAPI150stream=10, DsGetNCChanges request
127192.0.2.16192.0.2.5DRSUAPI1298stream=10, DsGetNCChanges response
130192.0.2.5192.0.2.16DRSUAPI178stream=7, DRSUAPI_REPLICA_ADD response
131192.0.2.16192.0.2.5DRSUAPI354stream=7, DRSUAPI_REPLICA_DEL request
7 个数据包

第 93 帧从 192.0.2.16 向域控 192.0.2.5 请求添加复制来源,opnum=5。Microsoft 将 IDL_DRSReplicaAdd 定义为给指定命名上下文增加复制来源引用,而不是直接把它等同于一次属性写入。Microsoft DRSReplicaAdd

随后 stream 10 的第 125 帧方向反转:域控向该端点发送 DsGetNCChanges,第 127 帧由端点返回数据。正常 AD 复制本身也是接收方拉取来源的更新,方向应结合角色解释。Microsoft 复制过程

因此,只部署“工作站 → 域控、opnum=3”的规则会遗漏这条反向拉取。应另行关注域控对非基线复制来源的 DRS 请求,并把它与 stream 7 中的来源添加、返回及删除操作关联;这不是把不同流的 flowbit 混用。

字节序来自报文,不来自操作系统假设

packed_drep 第一个字节的高半字节表示整数编码:1 为小端,0 为大端。于是操作号三可表示为 03 00 或 00 03;context、call ID 和其他多字节字段也应遵循同一表示。

request-header.pypython
from uuid import UUID
DRS = "e3514235-4b06-11d1-ab04-00c04fc2dcd2"

def request_header(pdu):
    if len(pdu) < 24 or pdu[:3] != b"\x05\x00\x00":
        raise ValueError("not a complete fixed request header")
    rep = pdu[4] >> 4
    if rep not in (0, 1):
        raise ValueError("unsupported integer representation")
    order = "little" if rep == 1 else "big"
    return (int.from_bytes(pdu[12:16], order),
            int.from_bytes(pdu[20:22], order),
            int.from_bytes(pdu[22:24], order))

这个函数只读完整的固定请求头,未验证整份片段长度、认证数据、对象 UUID 或 TCP 重组,也未执行接口绑定。它是独立字段核对工具,不是生产解析器。

本地以合成请求头分别检查大小端的 opnum 三和五,再对照一个预先声明为“已接受”的 context 映射。错误接口、其他流与无关操作号都不匹配,实际输出如下:

drs-context-model.py
$ python drs-context-model.py
UUID byte order: PASS
request headers: 4 PASS
invalid headers: 4 PASS
context correlation: 5 PASS
scope: synthetic headers and accepted-context map only

只给请求规则增加大端 opnum、而绑定规则仍只搜索小端 UUID,不构成完整的大端支持。字段读取与接口协商应统一处理。

协议规则仍需要回归验收

部署语法对照采用 Suricata 8.0.3 文档中的关键字;这是明确版本的规则示例,不是声称 2021 年抓包使用该版本引擎。只对编号三和五做精确集合匹配,不依赖比较运算符。

drs-semantic.rulestext
alert tcp $WORKSTATIONS any -> $DCS any (msg:"Unexpected DRS replication operation"; flow:established,to_server; dcerpc.iface:e3514235-4b06-11d1-ab04-00c04fc2dcd2; dcerpc.opnum:3,5; sid:9001002; rev:1;)

dcerpc.iface 和 dcerpc.opnum 将接口与操作匹配交给协议解析器;实际支持的封装、可见性与重组仍取决于引擎配置。上面的方向仅覆盖工作站到域控,DCShadow 的反向链应以单独的资产策略补充。OISF 关键字文档

上线前的回归矩阵8 行
输入变化 验收目标
完整协商与请求 能回到具体流、context 和帧号
仅有请求 明确标识缺失上下文
相同 opnum、不同接口 不借用其他 context 的接口身份
被拒绝的 Bind / Alter Context 不建立已接受映射
改变 TCP 切片、合并 PDU 重组后的语义保持一致
改变整数表示 UUID 与请求字段一致解码
域控向非基线端点拉取 不被单向源角色规则遗漏
正常复制及批准的业务例外 保留可解释的基线

目前完成的是 UUID、请求头和关联逻辑的离线模型,没有重放 PCAP,也没有执行目录变更。吞吐、丢包、分片、绑定拒绝及封装回归属于部署验收项,不记作已通过。

最终告警应给出主机角色、接口、context ID、opnum、请求与响应帧,再关联目录审计。识别敏感操作是调查起点,不是操作成功或攻击归因的终点。

官方参考

NORMAL~/posts/web/dcsync-dcshadow-rpc-context-detection.md§--
0%zh-CN