SCTP decoder and sticky buffers v5 - #15497
Conversation
Extend the SCTP decoder to parse chunk headers after the 12-byte common header. Each chunk is validated for minimum header size and length consistency per RFC 4960 sec 3.2. Add SCTPChunkHdr and SCTPVars structs to track per-packet chunk metadata Add five new decoder events for protocol violations: - SCTP_CHUNK_TOO_SMALL: insufficient data for a chunk header - SCTP_CHUNK_LEN_INVALID: chunk length < 4 or exceeds packet - SCTP_INIT_CHUNK_NOT_ALONE: INIT/INIT_ACK bundled (RFC 4960 sec 6.10) - SCTP_INIT_WITH_NON_ZERO_VTAG: INIT with vtag != 0 (RFC 4960 sec 8.5.1) - SCTP_DATA_WITH_ZERO_VTAG: DATA chunk with vtag == 0 Ticket OISF#4251
Implement a sticky buffer to match the raw SCTP header (common header + chunks) Ticket OISF#4251
Add a U8 numeric keyword to match the first SCTP chunk type in a packet with prefilter support. Ticket OISF#4251
Add a U8 numeric keyword to match the number of SCTP chunks parsed in a packet with prefilter support. Ticket OISF#4251
Add a U32 numeric keyword to match the SCTP verification tag from the common header with prefilter support. Ticket OISF#4251
Log SCTP-specific fields in the EVE JSON "sctp" object for alert events. Ticket OISF#4251
Track the first DATA chunk's data offset and length during chunk iteration, then reassign p->payload to point at the user data. When no DATA chunk is present (INIT, SACK, HEARTBEAT, etc.), payload_len is set to 0 since there is no application data. Ticket OISF#4251
Add a sctp.data sticky multi-buffer that allows content matching on the bytes inside any of the SCTP DATA chunks extracted. Ticket OISF#4251
Add documentation for all sctp keywords. Ticket OISF#4251
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #15497 +/- ##
========================================
Coverage 82.75% 82.76%
========================================
Files 998 1005 +7
Lines 272550 273301 +751
========================================
+ Hits 225562 226195 +633
- Misses 46988 47106 +118
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
|
WARNING:
Pipeline = 31783 |
victorjulien
left a comment
There was a problem hiding this comment.
Almost there I think, see inline comments.
| @@ -0,0 +1,8 @@ | |||
| # SCTP decoder event rules. | |||
| # SID's fall in the 2239000-2239999 range. See http://doc.emergingthreats.net/bin/view/Main/SidAllocation | |||
There was a problem hiding this comment.
nit: we recently updated these for other files
| break; | ||
| } | ||
|
|
||
| if (chunk_cnt < SCTP_MAX_TRACKED_CHUNKS) { |
There was a problem hiding this comment.
should there be an event when there are more chunks than we track?
There was a problem hiding this comment.
It would be useful for sure.
| has_init_ack = true; | ||
| break; | ||
| case SCTP_CHUNK_TYPE_DATA: | ||
| if (data_chunk_cnt < SCTP_MAX_DATA_CHUNKS && chunk_len >= SCTP_DATA_CHUNK_HDR_LEN) { |
There was a problem hiding this comment.
same, should there be an event if max is exceeded?
| #define SCTP_CHUNK_HDR_LEN 4 | ||
|
|
||
| /** max number of chunks tracked per packet for detection/logging */ | ||
| #define SCTP_MAX_TRACKED_CHUNKS 16 |
There was a problem hiding this comment.
how was 16 chosen? Add as a comment in the code
There was a problem hiding this comment.
It's a choice I made to keep per-packet overhead low while still allowing some room to track chunks.
SCTP has no hard limit on the number of chunks per packet. A packet can carry as many chunks as fit within the MTU, though in practice most packets contain only a few chunks.
There was a problem hiding this comment.
Thinking about the maximum packet size, which is probably close to 64k, how many iterations of the loop will there be at max? So max size / smallest chunk record size I suppose?
| #define SCTP_MAX_TRACKED_CHUNKS 16 | ||
|
|
||
| /** max number of DATA chunk payloads tracked per packet */ | ||
| #define SCTP_MAX_DATA_CHUNKS 16 |
| */ | ||
| static InspectionBuffer *GetBuffer(DetectEngineThreadCtx *det_ctx, | ||
| const DetectEngineTransforms *transforms, Packet *p, const int list_id, | ||
| const uint32_t local_id, const uint8_t chunk_idx) |
There was a problem hiding this comment.
since local_id == chunk_idx every time, could just pass one
| } | ||
|
|
||
| const DetectU8Data *data = (const DetectU8Data *)ctx; | ||
| const uint8_t cnt = MIN(p->l4.vars.sctp.chunk_cnt, SCTP_MAX_TRACKED_CHUNKS); |
There was a problem hiding this comment.
this has been validated by the decoder already, right?
There was a problem hiding this comment.
No, the decoder stores the raw count at line 146:
p->l4.vars.sctp.chunk_cnt = chunk_cnt;
And chunk_cnt is incremented unconditionally for every chunk, even after SCTP_MAX_TRACKED_CHUNKS is reached.
| } | ||
|
|
||
| const PrefilterPacketU8HashCtx *h = pectx; | ||
| const uint8_t cnt = MIN(p->l4.vars.sctp.chunk_cnt, SCTP_MAX_TRACKED_CHUNKS); |
| uint32_t seen[8] = { 0 }; | ||
| for (uint8_t i = 0; i < cnt; i++) { | ||
| const uint8_t val = p->l4.vars.sctp.chunk_types[i]; | ||
| if (seen[val >> 5] & (1U << (val & 0x1F))) { |
There was a problem hiding this comment.
can you explain the shift & mask logic here?
There was a problem hiding this comment.
The purpose is to skip calling PrefilterAddSids for duplicate chunk types within the same packet.
If two chunks both have type DATA, the sigs for that type are only added once.
So, line 119 is used to check if val has been seen.
| SCJbOpenObject(js, "sctp"); | ||
| SCJbSetUint(js, "vtag", SCTP_GET_RAW_VTAG(PacketGetSCTP(p))); | ||
| SCJbSetUint(js, "chunk_cnt", p->l4.vars.sctp.chunk_cnt); | ||
| const uint8_t cnt = MIN(p->l4.vars.sctp.chunk_cnt, SCTP_MAX_TRACKED_CHUNKS); |
There was a problem hiding this comment.
same, I think we should be able to trust the decoder. To double check we can add debug validate bug on's
| break; | ||
| } | ||
|
|
||
| const SCTPChunkHdr *chunk = (const SCTPChunkHdr *)(pkt + offset); |
There was a problem hiding this comment.
can this be an unaligned access? Should probably be a memcpy.
There was a problem hiding this comment.
Isn't this line following the same pattern as, for instance, PacketSetTCP?
buf is cast directly to a TCPHdr *:
p->l4.hdrs.tcph = (TCPHdr *)buf;
There was a problem hiding this comment.
Yes but we're slowing going to update these.
| p->proto = IPPROTO_SCTP; | ||
|
|
||
| if (p->payload_len > 0) { | ||
| if (DecodeSCTPChunks(p, p->payload, p->payload_len) < 0) { |
There was a problem hiding this comment.
we pass in payload_len, which is uint16_t
| * \retval 0 on success (even if some events were set) | ||
| * \retval -1 on fatal error (packet should be rejected) | ||
| */ | ||
| static int DecodeSCTPChunks(Packet *p, const uint8_t *pkt, uint32_t len) |
There was a problem hiding this comment.
do we need to use uint32_t here? We pass in uint16_t
| } | ||
|
|
||
| /* chunk must not extend beyond available data */ | ||
| if (chunk_len > (len - offset)) { |
There was a problem hiding this comment.
can we also have an absolute max value?
There was a problem hiding this comment.
I've tried to use UINT16_MAX as absolute value but, as expected, the compiler is complaining:
decode-sctp.c:91:23: warning: comparison is always false due to limited range of data type [-Wtype-limits]
91 | if (chunk_len > UINT16_MAX || chunk_len > (len - offset)) {
I wonder if I should use a different value or I can remove this check.
| p->l4.vars.sctp.data_lens[data_chunk_cnt] = chunk_len - SCTP_DATA_CHUNK_HDR_LEN; | ||
| data_chunk_cnt++; | ||
| } | ||
| has_data = true; |
There was a problem hiding this comment.
is there a scenario where we set this but didn't track the chunks? If so, what would happen?
There was a problem hiding this comment.
There are two cases:
chunk_len < SCTP_DATA_CHUNK_HDR_LEN(Malformed data chunk)data_chunk_cnt >= SCTP_MAX_DATA_CHUNKS)(more DATA chunks than the array can hold)
The only effect is that counter_sctp_data is incremented, see DecodeSCTP line 210:
if (p->l4.vars.sctp.has_data) {
StatsCounterIncr(&tv->stats, dtv->counter_sctp_data);
}
| if (padded_len < SCTP_CHUNK_HDR_LEN) { | ||
| padded_len = SCTP_CHUNK_HDR_LEN; | ||
| } | ||
| offset += padded_len; |
There was a problem hiding this comment.
can we add a DEBUG_VALIDATE_BUG_ON(offset > len) here, as we shouldn't advance offset beyond len
There was a problem hiding this comment.
Yes but note that offset can legitimately exceed len after padding.
The padding rounds up to a 4-byte boundary, so if the last chunk ends at len but isn't 4-byte aligned, padded_len > chunk_len and offset overshoots len.
We'd need to check only for the case where offset exceeds len by more than 3 bytes (the max padding).
|
Replaced with #15534 |
Link to ticket: https://redmine.openinfosecfoundation.org/issues/4251
Describe changes:
sctp.has_init,sctp.has_data,sctp.has_abortkeywords removedPrevious PR: #15482
SV_BRANCH=OISF/suricata-verify#2999