Project

General

Profile

Actions

Bug #8457

closed
AM PA

dcerpc.iface keyword matches any interface if PFC_FIRST_FRAG is missing in the BIND request

Bug #8457: dcerpc.iface keyword matches any interface if PFC_FIRST_FRAG is missing in the BIND request

Added by Alexey Monastyrskiy about 1 month ago. Updated about 17 hours ago.

Status:
Closed
Priority:
Normal
Target version:
Affected Versions:
Effort:
Difficulty:
Label:

Description

Summary

The DCE/RPC detection engine causes the dcerpc.iface keyword to unconditionally return a match if the BIND request did not have the PFC_FIRST_FRAG flag set, unless the signature uses the any_frag modifier.

This means a signature designed to alert on a specific DCE/RPC interface will alert on any DCE/RPC interface if the client omits the first-fragment flag during the BIND phase. Signatures that use the any_frag modifier are not affected.

Affected Code

rust/src/dcerpc/detect.rs, match_backuuid()

Reproduced on 8.0.4. The code is identical in 7.0.15. The behavior is also present on current main and main-8.0.x.

Background

For BIND PDUs with minor version 0, the DCE 1.1: RPC specification explicitly states that "the run-time assumes no fragmentation." Therefore, a BIND PDU with pfc_flags=0x00 is perfectly valid and compliant with the specification.

Windows endpoints accept BIND requests with pfc_flags=0x00 (confirmed with live traffic on port 135), producing a valid BIND_ACK. Similarly, Samba's server-side BIND handler (dcesrv_bind() in librpc/rpc/dcesrv_core.c) calls dcerpc_verify_ncacn_packet_header with required_flags=0.

Note: Our testing used minor version 0 BIND PDUs only. We have not tested Windows behavior for minor version 1 BIND PDUs.

What Happens

In rust/src/dcerpc/detect.rs, match_backuuid() walks the per-flow interface UUID entries and compares them to the signature. The faulty pattern is the same on released branches and on main: at the top of each loop iteration ret is set to 1; when any_frag is off and the first-fragment flag is missing on the entry, the code continue@s without clearing @ret, so a later return can still be a match without a successful UUID comparison.

Released 7.0.x and 8.0.x (example below from 8.0.4): the loop iterates bindack.accepted_uuid_list. Return type was u8.

fn match_backuuid(
    tx: &DCERPCTransaction, state: &mut DCERPCState, if_data: &mut DCEIfaceData,
) -> u8 {
    let mut ret = 0;
    if let Some(ref bindack) = state.bindack {
        for uuidentry in bindack.accepted_uuid_list.iter() {
            ret = 1; // <--- ret is set to 1 here

            // if any_frag is not enabled, we need to match only against the first fragment
            if if_data.any_frag == 0 && (uuidentry.flags & DCERPC_UUID_ENTRY_FLAG_FF == 0) {
                SCLogDebug!("any frag not enabled");
                continue; // <--- skips the UUID check, but ret remains 1
            }

            // ... BIND_ACK result check, UUID comparison, context ID and version checks ...

Current main (state as of commit b0e63a2c9): the loop iterates state.interface_uuids; return type is c_int; rejected/unacked entries can appear in the list (see next section). The same ret = 1 + continue on the first-fragment guard still appears before the !uuidentry.acked || uuidentry.result != 0 guard.

fn match_backuuid(
    tx: &DCERPCTransaction, state: &mut DCERPCState, if_data: &mut DCEIfaceData,
) -> c_int {
    let mut ret = 0;
    if !state.interface_uuids.is_empty() {
        for uuidentry in &state.interface_uuids {
            ret = 1;
            if if_data.any_frag == 0 && (uuidentry.flags & DCERPC_UUID_ENTRY_FLAG_FF == 0) {
                SCLogDebug!("any frag not enabled");
                continue; // ret still 1
            }
            if !uuidentry.acked || uuidentry.result != 0 {
                ret = 0;
                continue;
            }
            // ... UUID, context ID, version ...

If the BIND request was missing the PFC_FIRST_FRAG flag, the DCERPC_UUID_ENTRY_FLAG_FF bit is not set in uuidentry.flags. When the signature does not use any_frag (the default behavior), the if condition evaluates to true, and the code hits continue.

The continue skips the UUID comparison, but ret remains 1 from the top of the iteration. If no subsequent iteration resets ret to 0, the function returns 1 (Match) without ever comparing UUIDs. For example, if the BIND negotiated a single interface, the loop has one iteration: ret is set to 1, the UUID comparison is skipped, and the function returns a match regardless of which interface was requested. If the signature does use any_frag, the if condition evaluates to false, the UUID is checked, and the match is evaluated correctly.

Reproduction

1. Create a Suricata rule targeting a dummy interface without the any_frag modifier:

alert dcerpc any any -> any any (msg:"DCERPC BIND test"; dcerpc.iface:11111111-2222-3333-4444-555555555555; sid:1;)

2. Send a DCE/RPC BIND request for a completely different interface (e.g., 12345678-1234-1234-1234-123456789012) but clear the PFC_FIRST_FRAG flag (pfc_flags = 0x00 instead of 0x03).

3. The server accepts the BIND request and replies with a BIND_ACK.

4. Send a REQUEST PDU.

Expected: No alert, because the interface UUIDs do not match.
Actual: Suricata generates an alert for the rule.

Attached PCAP: repro_remote.pcapng — live capture against a Windows endpoint on port 135. Contains two connections: one where the BIND has pfc_flags=0x00 (frames 28-33; triggers the false positive) and one where the BIND has pfc_flags=0x03 (frames 38-43; control, no false positive).

Impact

This is a false positive issue, not an evasion issue. The bug causes dcerpc.iface to match more broadly than intended — any accepted interface satisfies any dcerpc.iface signature when PFC_FIRST_FRAG is missing. An attacker cannot use this to evade detection; the interface is still correctly bound and traffic is processed normally. The practical impact is spurious alerts: a signature written for one DCE/RPC interface will fire on unrelated interfaces if the client happens to omit the first-fragment flag in the BIND.

This only affects standalone DCE/RPC (typically port 135). DCE/RPC over SMB (typically port 445) is not affected (see below).

Note on Rejected Interfaces

A few lines down in the same function on released 7.0.x / 8.0.x, there is a seemingly identical pattern with rejected binds:

            // if the uuid has been rejected(uuidentry->result == 1), we skip to the next uuid
            if uuidentry.result != 0 {
                SCLogDebug!("Skipping to next UUID");
                continue;
            }

If this code were reached, it would also return 1 (Match) for rejected interfaces due to the same ret = 1 initialization. However, this path seems to be dead code in 8.0.4. In rust/src/dcerpc/dcerpc.rs, the process_bindack_pdu() function explicitly breaks and skips adding the UUID to accepted_uuid_list if ack_result != 0. Because rejected interfaces are never added to the list, the loop in match_backuuid() never processes them.

Note on main and main-8.0.x branches

A recent update on main (commit 05a11e289) and cherry-picked to main-8.0.x (commit 691114e95) changed match_backuuid() to iterate over the interface list that includes rejected and unacked entries, not just accepted ones. The patch added ret = 0 before continue for the !acked || result != 0 check — but the PFC_FIRST_FRAG / any_frag guard still uses the ret = 1 + continue pattern and executes before the acked/result check. This potentially means the false positive on main and main-8.0.x triggers not only for accepted BINDs (as in released versions) but also for rejected and unacked BINDs when PFC_FIRST_FRAG is missing. We have not tested any of these scenarios on a build from main or main-8.0.x.

DCE/RPC over SMB is not affected

This bug only affects standalone DCE/RPC (ALPROTO_DCERPC, typically port 135). DCE/RPC encapsulated in SMB (ALPROTO_SMB, typically port 445) uses a completely different code path.

On released 7.0.x and 8.0.x, the C dispatcher in src/detect-dce-iface.c (DetectDceIfaceMatchRust) branches on f->alproto: ALPROTO_DCERPC calls into Rust match_backuuid() (the buggy function described above), while ALPROTO_SMB uses the SMB-side matcher (historically exposed as SCSmbTxGetDceIface / rs_smb_tx_get_dce_iface depending on branch).

On current main, dcerpc.iface lives entirely in rust/src/dcerpc/detect.rs and src/detect-dce-iface.c is gone (4fc6ca5d6 — Redmine 8391). SMB traffic still does not use match_backuuid().

References


Files

repro_remote.pcapng (7.96 KB) repro_remote.pcapng Alexey Monastyrskiy, 04/07/2026 07:37 PM
dcerpc-bind-no-frag-flag.zip (4.19 KB) dcerpc-bind-no-frag-flag.zip Alexey Monastyrskiy, 05/07/2026 12:29 AM

Subtasks 1 (0 open1 closed)

Bug #8522: dcerpc.iface keyword matches any interface if PFC_FIRST_FRAG is missing in the BIND request (8.0.x backport)ClosedPhilippe AntoineActions

Related issues 2 (2 open0 closed)

Related to Suricata - Bug #8577: dcerpc: bind PDUs with 0 pfc_flags don't match without any_fragNewActions
Related to Suricata - Documentation #8578: doc: dcerpc any_frag option should mention which PDU is checkedTriagedOISF DevActions

AM Updated by Alexey Monastyrskiy 21 days ago Actions #1

  • Description updated (diff)

PA Updated by Philippe Antoine 20 days ago Actions #2

  • Status changed from New to Assigned
  • Assignee set to OISF Dev
  • Target version changed from TBD to 9.0.0-beta1
  • Label Needs backport to 8.0 added

OT Updated by OISF Ticketbot 20 days ago Actions #3

  • Subtask #8522 added

OT Updated by OISF Ticketbot 20 days ago Actions #4

  • Label deleted (Needs backport to 8.0)

PA Updated by Philippe Antoine 19 days ago Actions #5

Thanks for the report @alexey

Would you be able to create a Suricata-verify test/PR ?
https://github.com/OISF/suricata-verify/

AM Updated by Alexey Monastyrskiy 14 days ago Actions #6

I've attached a suricata-verify test here for now. I'll check if I can get approval to submit this as a proper PR on GitHub.

PA Updated by Philippe Antoine 13 days ago Actions #7

  • Status changed from Assigned to In Review
  • Assignee changed from OISF Dev to Philippe Antoine

https://github.com/OISF/suricata/pull/15330

Thanks Alexey for the SV test

I did a change in the PR : I think we only expect one alert for Sid 2 and not 2 : we do not expect one alert for sid 2 for the flow with port 54640 as the bind does not have the first_frag flag set, and the signature does not have any_frag

PA Updated by Philippe Antoine 10 days ago Actions #8

  • Status changed from In Review to Resolved

PA Updated by Philippe Antoine 9 days ago Actions #9

  • Status changed from Resolved to Closed

AM Updated by Alexey Monastyrskiy 9 days ago Actions #10

Hi @Philippe Antoine,

Thanks for creating the PRs and merging the fix!

Regarding your comment on the test expecting 1 alert instead of 2 for SID 2: I agree that this is definitely the correct behavior according to Suricata's current implementation. Since the rule lacks the any_frag modifier, Suricata's code strictly requires the PFC_FIRST_FRAG bit to be set on the BIND PDU to trigger a match.

However, I wanted to point out a slight discrepancy between the implementation and the documentation/protocol spec that this test highlighted:

  1. The dcerpc.iface documentation states: "If it's not [given], the match shall only happen on the first fragment." It doesn't explicitly mandate the PFC_FIRST_FRAG flag itself.
  2. According to the DCE 1.1 specification for minor version 0, "the run-time assumes no fragmentation." Therefore, a BIND PDU with pfc_flags=0x00 is a complete, unfragmented PDU. Conceptually, it is the first (and only) fragment.

Because Suricata's implementation strictly checks the PFC_FIRST_FRAG bit (0x01), it treats a valid, unfragmented minor version 0 BIND as if it's not the first fragment. This means a rule writer's expectation (that a rule without any_frag will match a standard, unfragmented BIND) isn't met if the client omits the optional flag.

This is perfectly fine for the scope of this bug fix (the false positive is resolved!), but I wanted to raise this observation in case you think the any_frag logic should eventually be updated to treat pfc_flags=0x00 as a first fragment, or if the documentation should be clarified.

As a side note, the documentation is also unclear about which PDU is checked for the presence of the flag. Because the interface UUID list is only confirmed upon receiving the BIND_ACK, a rule using dcerpc.iface never matches on the BIND PDU itself; it matches on subsequent PDUs (like the REQUEST). However, the implementation strictly checks the PFC_FIRST_FRAG flag of the BIND PDU (saving it to DCERPC_UUID_ENTRY_FLAG_FF during parsing), rather than the flag of the PDU that actually triggers the match.

PA Updated by Philippe Antoine 8 days ago Actions #11

Thanks for the feedback @alexey

unixia what do you think ?
I find this @any_frag
a bit weird...
Should we consider an unfragmented PDU like a first fragment ?

Do we want the following patch ?

diff --git a/rust/src/dcerpc/dcerpc.rs b/rust/src/dcerpc/dcerpc.rs
index 01296c0cb5..efab37388a 100644
--- a/rust/src/dcerpc/dcerpc.rs
+++ b/rust/src/dcerpc/dcerpc.rs
@@ -543,7 +543,8 @@ impl DCERPCState {
                 let pfcflags = hdr.pfc_flags;
                 // Store the first frag flag in the uuid as pfc_flags will
                 // be overwritten by new packets
-                if pfcflags & PFC_FIRST_FRAG > 0 {
+                // pfcflags == 0 means unfragmented, so we consider it a first fragment
+                if pfcflags & PFC_FIRST_FRAG > 0 || pfcflags == 0 {
                     uuidentry.flags |= DCERPC_UUID_ENTRY_FLAG_FF;
                 }
                 for uuid in self.interface_uuids.iter_mut() {

Then, I think we should have a new (documentation ? ) ticket about this

SB Updated by Shivani Bhardwaj 7 days ago · Edited Actions #12

Thank you for the detailed analysis, Alexey!

According to the DCE 1.1 specification for minor version 0, "the run-time assumes no fragmentation."

Correct.

Therefore, a BIND PDU with pfc_flags=0x00 is a complete, unfragmented PDU. Conceptually, it is the first (and only) fragment.

Curious about this. While I do find the docs saying "the run-time assumes no fragmentation.", I also find the following a bit later.

Each fragment is labelled as such using the PFC_FIRST_FRAG and PFC_LAST_FRAG flags in the header pfc_flags field. If a service request needs only a single fragment, that fragment will have both the PFC_FIRST_FRAG and PFC_LAST_FRAG flags set to TRUE. Since the connection-oriented transport guarantees sequentiality, the receiver will always receive the fragments in order.

This is how I also see the data arrive in the pcaps we have and the one you shared. Please note that we do not have support for fragmentation and reassembly for dcerpc so just header is parsed and which is probably why most of our tests are for minor version 0. pfc_flags=0x00 sounds like an anomaly to me as per the above standard and existing pcaps.

However, I can also find implementation detail docs such as https://winprotocoldoc.z19.web.core.windows.net/MS-RPCE/%5BMS-RPCE%5D-240708-diff.pdf that state:

If a server implementation receives a request PDU without the PFC_FIRST_FRAG flag and there is no
active call for the connection, it SHOULD compare the call_id field from the PDU to the Current
call_id on the Server Connection. If the call_id field is smaller by less than 150, the server
SHOULD ignore the packet. If the call_id field is smaller by 150 or more, the server SHOULD treat
this as a protocol error

So, maybe there are certain implementations where this is possible indeed but as per this detail, the server should deal with such a packet by either ignoring it or erroring out so I am again feeling inclined to this being an anomalous case. Please lmk wdyt?

As a side note, the documentation is also unclear about which PDU is checked for the presence of the flag. Because the interface UUID list is only confirmed upon receiving the BIND_ACK, a rule using dcerpc.iface never matches on the BIND PDU itself; it matches on subsequent PDUs (like the REQUEST). However, the implementation strictly checks the PFC_FIRST_FRAG flag of the BIND PDU (saving it to DCERPC_UUID_ENTRY_FLAG_FF during parsing), rather than the flag of the PDU that actually triggers the match.

Very fair. We must update the docs about this. @Philippe Antoine a new doc ticket indeed makes sense to me.
In the protocol specs, I'm reading that fragmentation can only happen on bind, bind_ack, alter_context, alter_context_response PDUs, so the match should only happen for these but it should be clear as to which PDU if there were multiple in a state.

Philippe, what do you propose we do about any_frag?

PA Updated by Philippe Antoine 7 days ago Actions #13

Philippe, what do you propose we do about any_frag?

I was asking you, and you bounce back to me :-p

I think we should start by better documentation, and add maybe new options like "first_frag", "unfrag"...
I would also expect a match on default behavior with flags=0

AM Updated by Alexey Monastyrskiy 1 day ago Actions #14

@Shivani Bhardwaj

Hi Shivani,

Thanks for looking into this!

1. DCE 1.1 Documentation

The Fragmentation and Reassembly section opens with three paragraphs. The first addresses authentication data fragmentation for association PDUs (bind, bind_ack, alter_context, alter_context_response):

"If the PDU has a minor version number of 0 (zero), the run-time assumes no fragmentation. If the minor version number is 1 and the PFC_LAST_FRAG flag is not set, the PDU is fragmented. [...] Only bind, bind_ack, alter_context and alter_context_response PDUs will be fragmented."

The second introduces stub data fragmentation for request and response PDUs. The third — the one you quoted — says:

"Each fragment is labelled as such using the PFC_FIRST_FRAG and PFC_LAST_FRAG flags in the header pfc_flags field. If a service request needs only a single fragment, that fragment will have both the PFC_FIRST_FRAG and PFC_LAST_FRAG flags set to TRUE."

The documentation is genuinely ambiguous here. The first paragraph's conditional — "if the minor version number is 1 and PFC_LAST_FRAG is not set, the PDU is fragmented" — is probably meant to imply that for minor version 0, not setting PFC_LAST_FRAG is not enough to make the PDU fragmented. But that is never stated explicitly. And the third paragraph's opening "each fragment" is broad enough to apply to all PDU types, while "service request" is not a defined term of art that unambiguously excludes bind.

2. [MS-RPCE] Documentation

The MS-RPCE passage you quoted:

"If a server implementation receives a request PDU without the PFC_FIRST_FRAG flag..."

is describing a specific edge case: a server receiving a request PDU fragment for which there is no active call — i.e. a stale or out-of-order packet. The guidance (ignore or treat as protocol error) is about dropping such orphaned packets, not about the general validity of unfragmented PDUs.

3. Practical perspective for an IDS/IPS

Beyond spec interpretation, there is a dimension that is particularly important from an IDS/IPS standpoint: for a detection system, what matters most is how real-world implementations actually behave on live networks.

I have confirmed empirically — in the PCAP with live Windows port 135 traffic attached to this issue — that Windows endpoints accept BIND requests with pfc_flags=0x00 and respond with a valid BIND_ACK. Samba's dcesrv_bind() passes required_flags=0 to its header verification, suggesting it likewise does not enforce the fragment flags.

If Suricata's default behavior requires PFC_FIRST_FRAG to be present in the BIND PDU for dcerpc.iface to match, any client can evade every default dcerpc.iface-based detection rule (i.e. any rule without any_frag) by simply zeroing out pfc_flags. The server still accepts the connection, the interface is bound, and RPC calls proceed normally — but Suricata stops matching the interface UUID. An attacker does not need to know which interface UUIDs are being monitored — zeroing out pfc_flags on every BIND blindly evades all such rules at once.

SB Updated by Shivani Bhardwaj 1 day ago Actions #15

  • Related to Bug #8577: dcerpc: bind PDUs with 0 pfc_flags don't match without any_frag added

SB Updated by Shivani Bhardwaj 1 day ago Actions #16

thank you very much, Alexey!

I have confirmed empirically — in the PCAP with live Windows port 135 traffic attached to this issue — that Windows endpoints accept BIND requests with pfc_flags=0x00 and respond with a valid BIND_ACK. Samba's dcesrv_bind() passes required_flags=0 to its header verification, suggesting it likewise does not enforce the fragment flags.

that's great! Are you able to provide a PCAP/suricata-verify test for that? Please upload it at https://redmine.openinfosecfoundation.org/issues/8577
thanks a lot!

SB Updated by Shivani Bhardwaj 1 day ago Actions #17

  • Related to Documentation #8578: doc: dcerpc any_frag option should mention which PDU is checked added

AM Updated by Alexey Monastyrskiy about 17 hours ago Actions #18

Shivani Bhardwaj wrote in #note-16:

I have confirmed empirically — in the PCAP with live Windows port 135 traffic attached to this issue — that Windows endpoints accept BIND requests with pfc_flags=0x00 and respond with a valid BIND_ACK. Samba's dcesrv_bind() passes required_flags=0 to its header verification, suggesting it likewise does not enforce the fragment flags.

Are you able to provide a PCAP/suricata-verify test for that?

The PCAP I attached to the original report is exactly that:

Attached PCAP: repro_remote.pcapng — live capture against a Windows endpoint on port 135. Contains two connections: one where the BIND has pfc_flags=0x00 (frames 28-33; triggers the false positive) and one where the BIND has pfc_flags=0x03 (frames 38-43; control, no false positive).

Actions

Also available in: PDF Atom