Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented-udp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Tests the dcerpc.is_fragmented keyword over DCE/RPC-over-UDP (connectionless).

For UDP the keyword treats a transaction whose flags1 has the fragment bit
(0x04, PFCL1_FRAG) set as fragmented. The pcap (see
dcerpc_is_fragmented_udp_scapy.py) has two REQUEST PDUs: the first with
flags1=0x04 (fragmented) and the second with flags1=0x00 (not fragmented). The
rules confirm is_fragmented:true matches the first and is_fragmented:false the
second.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python
"""
Generate a DCE/RPC-over-UDP (connectionless) pcap for the dcerpc.is_fragmented
keyword test.

Two REQUEST PDUs on the same UDP flow, each its own transaction:

1. flags1 = 0x04 (PFCL1_FRAG set) -> fragmented (is_fragmented:yes)
2. flags1 = 0x00 (no frag flag) -> not fragmented (is_fragmented:no)

For UDP the fragment flag lives in flags1, stored in the low byte of the
per-direction flags of the transaction.
"""

import struct

from scapy.all import Ether, IP, UDP, Raw, wrpcap

CLIENT_IP = "192.168.0.1"
SERVER_IP = "192.168.0.2"


def cl_header(ptype, flags1, flags2, activity, seqnum):
"""Build an 80-byte connectionless DCE/RPC header."""
h = bytearray(80)
h[0] = 0x04 # rpc_vers (connectionless == 4)
h[1] = ptype # 0 = request
h[2] = flags1
h[3] = flags2
h[4:7] = b"\x10\x00\x00" # drep (little-endian)
h[40:56] = activity # activity UUID
struct.pack_into("<I", h, 64, seqnum) # seqnum
struct.pack_into("<H", h, 74, 0) # fraglen (no body)
struct.pack_into("<H", h, 76, 0) # fragnum (first fragment)
return bytes(h)


def pkt(payload):
return (
Ether(dst="00:00:00:00:00:02", src="00:00:00:00:00:01")
/ IP(src=CLIENT_IP, dst=SERVER_IP)
/ UDP(sport=1025, dport=135) / Raw(load=payload)
)


def create_pcap():
frag = cl_header(0x00, 0x04, 0x00, bytes(range(1, 17)), 0)
nofrag = cl_header(0x00, 0x00, 0x00, bytes(range(17, 33)), 1)
return [pkt(frag), pkt(nofrag)]


wrpcap("input.pcap", create_pcap())
Binary file added tests/dcerpc/dcerpc-is-fragmented-udp/input.pcap
Binary file not shown.
2 changes: 2 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented-udp/test.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alert dcerpc any any -> any any (msg:"dcerpc.is_fragmented udp true (frag flag)"; dcerpc.is_fragmented:true; sid:1;)
alert dcerpc any any -> any any (msg:"dcerpc.is_fragmented udp false"; dcerpc.is_fragmented:false; sid:2;)
21 changes: 21 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented-udp/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
requires:
min-version: 9

pcap: input.pcap

args:
- -k none

checks:
# first request has the fragment flag (flags1 0x04) set -> fragmented
- filter:
count: 1
match:
event_type: alert
alert.signature_id: 1
# second request has no fragment flag -> not fragmented
- filter:
count: 1
match:
event_type: alert
alert.signature_id: 2
18 changes: 18 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Tests the dcerpc.is_fragmented keyword over DCE/RPC-over-TCP with a real
multi-PDU fragmented request.

The pcap (see dcerpc_fragmented_tcp_scapy.py) is a single TCP connection with
two client -> server calls:

- call_id 1: one REQUEST fragmented across TWO PDUs, each in its own TCP
segment: PFC_FIRST_FRAG (0x01) then PFC_LAST_FRAG (0x02). These reassemble
into a single request transaction; per C706 it is fragmented because no
single PDU sets both FIRST and LAST.
- call_id 2: a single complete REQUEST PDU with both PFC_FIRST_FRAG and
PFC_LAST_FRAG set (0x03) -> not fragmented.

dcerpc.is_fragmented:true matches the fragmented request (call_id 1, one
transaction) and dcerpc.is_fragmented:false matches the complete request
(call_id 2). This exercises genuine multi-PDU fragmentation and reassembly
rather than flipping flag bits on a single PDU. A DCE/RPC connection whose first
PDU is a request is detected as DCERPC to_server, so no bind is needed.
90 changes: 90 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented/dcerpc_fragmented_tcp_scapy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python
"""
Build a real multi-PDU fragmented DCE/RPC-over-TCP pcap for the
dcerpc.is_fragmented keyword test.

A single TCP connection carrying two calls, both client -> server:

Call 1 (call_id 1): a REQUEST fragmented across TWO PDUs, each its own TCP
segment, labelled with the connection-oriented fragmentation flags:
PDU 1: PFC_FIRST_FRAG (0x01) -- first fragment
PDU 2: PFC_LAST_FRAG (0x02) -- last fragment
The RPC run-time reassembles these into a single request transaction; per
C706 it is fragmented because no single PDU sets both FIRST and LAST.

Call 2 (call_id 2): a single complete REQUEST PDU with both PFC_FIRST_FRAG and
PFC_LAST_FRAG set (0x03) -- not fragmented.

So dcerpc.is_fragmented:yes matches call 1 and dcerpc.is_fragmented:no matches
call 2. A DCE/RPC connection whose first PDU is a request (05 00 00 ...) is
detected as DCERPC to_server, so no bind is required.

Connection-oriented request PDU layout (little-endian data representation):
header (16): 05 00 | ptype(00) | pfc_flags | 10 00 00 00 | frag_length(2) |
auth_length(2)=0 | call_id(4)
body: alloc_hint(4) | ctx_id(2) | opnum(2) | stub...
"""

import struct

from scapy.all import Ether, IP, TCP, Raw, wrpcap

CLIENT_IP = "192.168.0.1"
SERVER_IP = "192.168.0.2"
CLIENT_PORT = 1025
SERVER_PORT = 135
CMAC = "00:00:00:00:00:01"
SMAC = "00:00:00:00:00:02"

PFC_FIRST_FRAG = 0x01
PFC_LAST_FRAG = 0x02


def co_request(pfc_flags, call_id, ctx_id, opnum, stub):
body = struct.pack("<I", len(stub)) # alloc_hint
body += struct.pack("<H", ctx_id) # p_cont_id
body += struct.pack("<H", opnum) # opnum
body += stub
frag_length = 16 + len(body)
hdr = bytes([0x05, 0x00, 0x00, pfc_flags, 0x10, 0x00, 0x00, 0x00])
hdr += struct.pack("<H", frag_length) # frag_length
hdr += struct.pack("<H", 0) # auth_length
hdr += struct.pack("<I", call_id) # call_id
return hdr + body


# Call 1: request fragmented across two PDUs (first then last) -> one tx.
frag1 = co_request(PFC_FIRST_FRAG, 1, 0, 0, b"\xaa" * 24)
frag2 = co_request(PFC_LAST_FRAG, 1, 0, 0, b"\xbb" * 24)
# Call 2: single complete request (both first and last set).
whole = co_request(PFC_FIRST_FRAG | PFC_LAST_FRAG, 2, 0, 1, b"\xcc" * 24)


def c2s(**kw):
return Ether(src=CMAC, dst=SMAC) / IP(src=CLIENT_IP, dst=SERVER_IP) \
/ TCP(sport=CLIENT_PORT, dport=SERVER_PORT, **kw)


def s2c(**kw):
return Ether(src=SMAC, dst=CMAC) / IP(src=SERVER_IP, dst=CLIENT_IP) \
/ TCP(sport=SERVER_PORT, dport=CLIENT_PORT, **kw)


cseq = 1000
sseq = 5000
pkts = []

# 3-way handshake
pkts.append(c2s(flags="S", seq=cseq))
pkts.append(s2c(flags="SA", seq=sseq, ack=cseq + 1))
cseq += 1
sseq += 1
pkts.append(c2s(flags="A", seq=cseq, ack=sseq))

# client -> server data segments, each a full DCE/RPC PDU
for payload in (frag1, frag2, whole):
pkts.append(c2s(flags="PA", seq=cseq, ack=sseq) / Raw(load=payload))
cseq += len(payload)
pkts.append(s2c(flags="A", seq=sseq, ack=cseq))

wrpcap("input.pcap", pkts)
Binary file added tests/dcerpc/dcerpc-is-fragmented/input.pcap
Binary file not shown.
2 changes: 2 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented/test.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alert dcerpc any any -> any any (msg:"dcerpc.is_fragmented true: multi-PDU fragmented request"; flow:to_server; dcerpc.is_fragmented:true; sid:1;)
alert dcerpc any any -> any any (msg:"dcerpc.is_fragmented false: complete request"; flow:to_server; dcerpc.is_fragmented:false; sid:2;)
24 changes: 24 additions & 0 deletions tests/dcerpc/dcerpc-is-fragmented/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
requires:
min-version: 9

pcap: input.pcap

args:
- -k none

checks:
# call_id 1 is one request fragmented across two PDUs (FIRST then LAST), which
# reassemble into a single transaction; no PDU sets both PFC_FIRST_FRAG and
# PFC_LAST_FRAG -> fragmented
- filter:
count: 1
match:
event_type: alert
alert.signature_id: 1
# call_id 2 is a single complete request (both PFC_FIRST_FRAG and
# PFC_LAST_FRAG set) -> not fragmented
- filter:
count: 1
match:
event_type: alert
alert.signature_id: 2
26 changes: 26 additions & 0 deletions tests/dcerpc/dcerpc-smb-is-fragmented/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Tests the dcerpc.is_fragmented keyword on DCE/RPC carried over SMB, per
direction, with genuine multi-PDU fragmentation.

Reuses the filestore-filecontainer-smb capture, a busy SMB2 session doing many
registry/service (WINREG/SVCCTL) operations. Three of its DCE/RPC responses are
fragmented at the DCE/RPC layer across multiple PDUs over SMB:

- call_id 14: first + last (2 fragments, WINREG response)
- call_id 3: first + middle + last (3 fragments, SVCCTL response)
- call_id 573: first + middle + last (3 fragments, SVCCTL response)

Each reassembles into a single SMB DCE/RPC transaction whose response side is
fragmented (resp_is_fragmented = true, derived from the record's
first_frag/last_frag). All other DCE/RPC PDUs are single complete PDUs (both
PFC_FIRST_FRAG and PFC_LAST_FRAG set) and thus not fragmented.

The rules confirm:

- is_fragmented:true on to_client matches exactly the three fragmented
responses (via smb_tx_match_dce_is_fragmented);
- is_fragmented:true on to_server matches nothing (no request is fragmented);
- is_fragmented:false matches every complete request (to_server) and every
complete response (to_client).

This exercises both the fragmented and not-fragmented cases, in both directions,
using real multi-PDU fragmented DCE/RPC-over-SMB traffic.
4 changes: 4 additions & 0 deletions tests/dcerpc/dcerpc-smb-is-fragmented/test.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
alert smb any any -> any any (msg:"dcerpc.is_fragmented true: fragmented response (to_client)"; flow:to_client; dcerpc.is_fragmented:true; sid:1;)
alert smb any any -> any any (msg:"dcerpc.is_fragmented true: no fragmented request (to_server)"; flow:to_server; dcerpc.is_fragmented:true; sid:2;)
alert smb any any -> any any (msg:"dcerpc.is_fragmented false: complete response (to_client)"; flow:to_client; dcerpc.is_fragmented:false; sid:3;)
alert smb any any -> any any (msg:"dcerpc.is_fragmented false: complete request (to_server)"; flow:to_server; dcerpc.is_fragmented:false; sid:4;)
37 changes: 37 additions & 0 deletions tests/dcerpc/dcerpc-smb-is-fragmented/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
requires:
min-version: 9

pcap: ../../filestore-filecontainer-smb/filecontainer-smb.pcap

args:
- -k none

checks:
# This capture contains three DCE/RPC responses that are genuinely fragmented
# across multiple PDUs over SMB (call_id 14: first+last; call_id 3 and 573:
# first+middle+last). Each reassembles into one transaction whose response
# side is fragmented, so is_fragmented:true matches them on to_client.
- filter:
count: 3
match:
event_type: alert
alert.signature_id: 1
# No request in this capture is fragmented, so is_fragmented:true matches
# nothing on to_server.
- filter:
count: 0
match:
event_type: alert
alert.signature_id: 2
# Every other (complete) response is not fragmented -> is_fragmented:false.
- filter:
count: 1507
match:
event_type: alert
alert.signature_id: 3
# All requests are single complete PDUs -> is_fragmented:false.
- filter:
count: 1507
match:
event_type: alert
alert.signature_id: 4
Loading