Bug #8457
opendcerpc.iface keyword matches any interface if PFC_FIRST_FRAG is missing in the BIND request
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, the match_backuuid() function iterates over the accepted UUIDs to check if the interface matches the one specified in the signature (code shown is from 8.0.4):
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 ...
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, 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 check (line 70-72) still has 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.
The C dispatcher in detect-dce-iface.c (DetectDceIfaceMatchRust) branches on f->alproto: ALPROTO_DCERPC calls match_backuuid() (the buggy function described above), while ALPROTO_SMB calls rs_smb_tx_get_dce_iface(). The SMB function uses clean exact-match logic that checks i.acked && i.ack_result 0 && i.uuid if_uuid without any fragment-flag dependency. It does not have the ret = 1 / continue pattern.
References¶
- DCE 1.1: RPC -- Fragmentation and Reassembly
- match_backuuid in 7.0.15, lines 62-72
- match_backuuid in 8.0.4, lines 62-72
- match_backuuid on main, lines 62-72
- Samba dcesrv_core.c -- dcesrv_bind() (see
0passed forrequired_flagswhendcerpc_verify_ncacn_packet_headeris called)
Files
No data to display