A DsGetNCChanges request in Active Directory may be ordinary domain-controller replication or a directory read from another host holding replication rights. Detection hinges on who invoked which operation on which negotiated RPC interface, not on a tool name.
The following analysis reconstructs that chain from 2021 DCSync and DCShadow network observations. Interface negotiation, an operation request, a response, and the actual directory effect are different levels of evidence. Seeing the request alone does not prove that sensitive data was read or changed.
Define roles before alerts
DCSync uses directory replication privileges to request updates. DCShadow additionally arranges a temporary replication source and initiates directory changes. Neither is a protocol flaw that grants domain privileges merely through network reachability; legitimate controllers use the same interface.
WORKSTATIONS and DCS should be maintained asset sets, not guessed subnets. Non-controller services with delegated replication tasks need business-scope review, while controller status does not make every operation appropriate. A sensor needs coverage of internal replication paths; an Internet-egress capture usually is not enough.
flowchart TD A["Connection endpoints and host roles"] --> B["Bind / Alter Context"] B --> C["Accepted context → interface"] C --> D["Request: context ID + opnum"] D --> E["Response and directory effect"]
Operation numbers have meaning only within an interface. Distinguish three contexts: p_cont_id selects a presentation context, call_id correlates RPC requests and responses, and the DRS_HANDLE returned by IDL_DRSBind belongs to the DRS application interface. An RPC Bind PDU is not an IDL_DRSBind method call.
Negotiation sits between the decisive packets
| No. | Source | Destination | Protocol | Length | Info |
|---|---|---|---|---|---|
| 28 | 192.0.2.16 | 192.0.2.5 | DCERPC | 1794 | Bind, call_id=2 |
| 30 | 192.0.2.5 | 192.0.2.16 | DCERPC | 314 | Bind_ack |
| 31 | 192.0.2.16 | 192.0.2.5 | DCERPC | 274 | Alter_context |
| 32 | 192.0.2.5 | 192.0.2.16 | DCERPC | 159 | Alter_context_resp |
| 41 | 192.0.2.16 | 192.0.2.5 | DRSUAPI | 498 | DsGetNCChanges request, context_id=0, opnum=3 |
| 45 | 192.0.2.5 | 192.0.2.16 | DRSUAPI | 822 | DsGetNCChanges response |
Addresses are replaced with documentation addresses; packet numbers and lengths are retained. In TCP stream 4, frame 28 proposes the interface, frame 41 requests DsGetNCChanges, and frame 45 is its response. Bind_ack and Alter_context also occur in between. A detector should correlate proposals with acceptance, not treat every Bind as a usable interface.
The UUID is e3514235-4b06-11d1-ab04-00c04fc2dcd2, with interface version 4.0 shown in the record. Its little-endian representation is:
Data10x00–0x03Data20x04–0x05Data30x06–0x07Data40x08–0x0F
The first three fields follow their respective field widths, while Data4 retains its byte sequence; this is not a reversal of the whole UUID. UUID(...).bytes_le provides an independent check. Binding also includes a transfer syntax, whose identifier must not be confused with the interface UUID.
Interpret opnum from the PDU boundary
| Offset | Name | Type | Size | Value |
|---|---|---|---|---|
| 0x00 | rpc_vers | 1 | 5 | |
| 0x01 | rpc_vers_minor | 1 | 0 | |
| 0x02 | ptype | 1 | 0 | |
| 0x03 | pfc_flags | 1 | ||
| 0x04 | packed_drep | 4 | ||
| 0x08 | frag_length | 2 | ||
| 0x0a | auth_length | 2 | ||
| 0x0c | call_id | 4 | ||
| 0x10 | alloc_hint | 4 | ||
| 0x14 | p_cont_id | 2 | ||
| 0x16 | opnum | 2 | 3 |
This is the fixed request-header layout. Values mark only the version, PDU type, and operation discussed here; they do not reconstruct a complete packet. Offsets start at the RPC PDU, not at the Ethernet frame or TCP segment. Optional object UUIDs, authentication trailers, and stub data require additional parsing.
Bind starts with 05 00 0B and Request with 05 00 00. Frame 41 uses context ID = 0; little-endian 03 00 at offset 22 identifies IDL_DRSGetNCChanges, which replicates updates from a naming-context replica on the server. Microsoft DRSGetNCChanges
This request shows SPNEGO packet privacy. The visible request header identifies the operation but does not expose all protected arguments. A response frame also does not, by itself, establish successful completion or identify the returned attributes.
A flowbit approximates state, not interface negotiation
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;)These rules illustrate raw-byte matching only: they assume a detection buffer aligned to the PDU start and little-endian representation. The first sets a flow marker without alerting; the second checks the operation after that marker exists on the same flow. They have not been loaded into Suricata or tested through PCAP replay in this environment.
The marker avoids repeated UUID searches but discards important dimensions. A connection can have multiple presentation contexts, and drs_seen does not prove that every later opnum=3 belongs to DRSUAPI. A complete design at least maintains connection + context ID → accepted interface and transfer syntax, handling context changes, rejected negotiation, and connection termination.
TCP segmentation, combined PDUs, RPC fragmentation, and capture loss affect raw offsets. A request observed without its negotiation should carry an unknown-context status, rather than being silently trusted or assigned a DRS meaning from its opnum alone.
DCShadow reverses the replication direction
The sample uses two execution contexts: one prepares a local RPC service, while another has the directory-management rights needed for registration, synchronization, and removal. The console reports WIN2016 controller functionality, which is not a complete Windows build number.
RPC bind registered
RPC Server is waiting!
1 object(s) pushed
RPC bind unregistered
stopping RPC server
RPC server stoppedOnly service-state lines are retained above. Domains, hosts, account identifiers, process IDs, and the session key are omitted from the public record. 1 object(s) pushed is tool output; an independent directory query is still needed to verify the attribute effect.
| No. | Source | Destination | Protocol | Length | Info |
|---|---|---|---|---|---|
| 74 | 192.0.2.16 | 192.0.2.5 | DCERPC | 2026 | stream=7, Bind |
| 93 | 192.0.2.16 | 192.0.2.5 | DRSUAPI | 434 | stream=7, DRSUAPI_REPLICA_ADD request, opnum=5 |
| 118 | 192.0.2.5 | 192.0.2.16 | DCERPC | 468 | stream=10, Bind |
| 125 | 192.0.2.5 | 192.0.2.16 | DRSUAPI | 150 | stream=10, DsGetNCChanges request |
| 127 | 192.0.2.16 | 192.0.2.5 | DRSUAPI | 1298 | stream=10, DsGetNCChanges response |
| 130 | 192.0.2.5 | 192.0.2.16 | DRSUAPI | 178 | stream=7, DRSUAPI_REPLICA_ADD response |
| 131 | 192.0.2.16 | 192.0.2.5 | DRSUAPI | 354 | stream=7, DRSUAPI_REPLICA_DEL request |
Frame 93 goes from 192.0.2.16 to controller 192.0.2.5 and requests a replication source with opnum=5. Microsoft defines IDL_DRSReplicaAdd as adding a replication-source reference for the specified naming context, not as an attribute write in itself. Microsoft DRSReplicaAdd
Frame 125 in stream 10 reverses direction: the controller sends DsGetNCChanges to that endpoint, which returns data in frame 127. Normal AD replication likewise has the receiving controller pull changes from a source, so direction must be interpreted with host roles. Microsoft replication behavior
A rule covering only workstation-to-controller opnum=3 misses that pull. Also monitor controller DRS requests to non-baseline replication sources, correlating them with source-addition, response, and deletion activity in stream 7. This is cross-flow event correlation, not reuse of one flow's flowbit on another.
Byte order comes from the packet
The high nibble of the first packed_drep byte selects integer representation: 1 means little-endian and 0 big-endian. Operation three can therefore appear as 03 00 or 00 03. Context, call ID, and other multibyte fields follow the same representation.
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))
This function reads only a complete fixed request header. It does not validate the entire fragment, authentication data, optional object UUID, or TCP reassembly, and it does not negotiate interfaces. It is a field-checking helper, not a production parser.
Local tests use synthetic request headers for operations three and five in both byte orders, then consult a context map explicitly designated as already accepted. The wrong interface, another flow, and an unrelated operation do not match. Actual output:
$ 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 onlyAdding big-endian opnum bytes to the request rule while retaining only a little-endian UUID in the bind rule does not provide complete big-endian support. Field decoding and interface negotiation need consistent treatment.
Protocol rules still need regression testing
The deployment syntax example uses keywords documented for Suricata 8.0.3. This is an explicit rule-reference version, not a claim that the 2021 capture used that engine. It matches the exact set of operations three and five without relying on comparison operators.
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 and dcerpc.opnum delegate interface and operation matching to the protocol parser. Supported encapsulation, visibility, and reassembly still depend on configuration. This direction covers only workstation-to-controller traffic; a separate asset policy must cover DCShadow's reverse path. OISF keyword documentation
| Input variation | Acceptance target |
|---|---|
| Full negotiation and request | Traceable flow, context, and packet numbers |
| Request only | Explicit missing-context status |
| Same opnum on a different interface | No identity borrowed from another context |
| Rejected Bind / Alter Context | No accepted mapping created |
| Different TCP splits or combined PDUs | Equivalent meaning after reassembly |
| Different integer representation | Consistent UUID and request-field decoding |
| Controller pull from a non-baseline endpoint | Not lost to a one-way source-role rule |
| Legitimate replication and approved exceptions | An explainable baseline |
Completed work covers offline UUID, request-header, and correlation models. No PCAP replay or directory changes were performed. Throughput, loss, fragmentation, rejected-binding, and encapsulation regressions remain deployment acceptance tasks, not reported passes.
An actionable alert should retain host roles, interface, context ID, opnum, and request/response frames, then correlate directory auditing. Identifying a sensitive operation starts an investigation; it does not finish proof of success or attribution.