From 21d35f6b37380736f7a90e6155d28856f597d6a8 Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Fri, 6 Mar 2026 10:04:52 +0100 Subject: [PATCH] decoder/sctp: extend decoder 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 #4251 --- etc/schema.json | 58 +++++- rules/Makefile.am | 1 + rules/README.md | 1 + rules/sctp-events.rules | 13 ++ src/decode-events.c | 28 +++ src/decode-events.h | 9 +- src/decode-sctp.c | 420 +++++++++++++++++++++++++++++++++++++++- src/decode-sctp.h | 66 ++++++- src/decode.c | 5 + src/decode.h | 6 + src/runmode-unittests.c | 1 + 11 files changed, 589 insertions(+), 19 deletions(-) create mode 100644 rules/sctp-events.rules diff --git a/etc/schema.json b/etc/schema.json index c08ee10011..a9d6afdd30 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -7459,8 +7459,37 @@ "type": "object", "additionalProperties": false, "properties": { + "chunk_len_invalid": { + "type": "integer", + "description": "SCTP chunk length < 4 or exceeds remaining packet" + }, + "chunk_too_small": { + "type": "integer", + "description": "Remaining data too small for SCTP chunk header" + }, + "data_with_zero_vtag": { + "type": "integer", + "description": "SCTP DATA chunk with verification tag == 0" + }, + "init_chunk_bundled": { + "type": "integer", + "description": "RFC 4960 sec 6.10 violation: INIT/INIT_ACK bundled with other chunks" + }, + "init_with_non_zero_vtag": { + "type": "integer", + "description": "SCTP INIT with verification tag != 0" + }, "pkt_too_small": { - "type": "integer" + "type": "integer", + "description": "SCTP packet smaller than minimum size" + }, + "too_many_chunks": { + "type": "integer", + "description": "More chunks than SCTP_MAX_TRACKED_CHUNKS" + }, + "too_many_data_chunks": { + "type": "integer", + "description": "More DATA chunks than SCTP_MAX_DATA_CHUNKS" } } }, @@ -8547,6 +8576,33 @@ } } }, + "sctp": { + "type": "object", + "description": "Statistics on SCTP chunk types", + "additionalProperties": false, + "properties": { + "abort": { + "type": "integer", + "description": "Number of SCTP packets with ABORT chunk" + }, + "data": { + "type": "integer", + "description": "Number of SCTP packets with DATA chunk" + }, + "init": { + "type": "integer", + "description": "Number of SCTP packets with INIT chunk" + }, + "init_ack": { + "type": "integer", + "description": "Number of SCTP packets with INIT_ACK chunk" + }, + "shutdown": { + "type": "integer", + "description": "Number of SCTP packets with SHUTDOWN chunk" + } + } + }, "stream": { "type": "object", "description": "Observational statistics on TCP stream events", diff --git a/rules/Makefile.am b/rules/Makefile.am index 1524c1df56..785b25b269 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -24,6 +24,7 @@ pgsql-events.rules \ pop3-events.rules \ quic-events.rules \ rfb-events.rules \ +sctp-events.rules \ smb-events.rules \ smtp-events.rules \ snmp-events.rules \ diff --git a/rules/README.md b/rules/README.md index 303802c223..b4abaf6128 100644 --- a/rules/README.md +++ b/rules/README.md @@ -34,6 +34,7 @@ signature IDs. | POP3 | 2236000 | 2236999 | | LDAP | 2237000 | 2237999 | | SNMP | 2238000 | 2238999 | +| SCTP | 2239000 | 2239999 | | DNS | 2240000 | 2240999 | | PGSQL | 2241000 | 2241999 | | mDNS | 2242000 | 2242999 | diff --git a/rules/sctp-events.rules b/rules/sctp-events.rules new file mode 100644 index 0000000000..72ebebad7a --- /dev/null +++ b/rules/sctp-events.rules @@ -0,0 +1,13 @@ +# SCTP decoder event rules. +# SID's fall in the 2239000+ range. See rules/README.md + +alert sctp any any -> any any (msg:"SURICATA SCTP packet too small"; decode-event:sctp.pkt_too_small; classtype:protocol-command-decode; sid:2239001; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP chunk too small"; decode-event:sctp.chunk_too_small; classtype:protocol-command-decode; sid:2239002; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP chunk length invalid"; decode-event:sctp.chunk_len_invalid; classtype:protocol-command-decode; sid:2239003; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP INIT chunk bundled"; decode-event:sctp.init_chunk_bundled; classtype:protocol-command-decode; sid:2239004; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP INIT with non-zero vtag"; decode-event:sctp.init_with_non_zero_vtag; classtype:protocol-command-decode; sid:2239005; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP DATA with zero vtag"; decode-event:sctp.data_with_zero_vtag; classtype:protocol-command-decode; sid:2239006; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP too many chunks"; decode-event:sctp.too_many_chunks; classtype:protocol-command-decode; sid:2239007; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP too many data chunks"; decode-event:sctp.too_many_data_chunks; classtype:protocol-command-decode; sid:2239008; rev:1;) + +#next sid is 2239009 diff --git a/src/decode-events.c b/src/decode-events.c index 32ce23dcda..3f37713dd3 100644 --- a/src/decode-events.c +++ b/src/decode-events.c @@ -467,6 +467,34 @@ const struct DecodeEvents_ DEvents[] = { "decoder.sctp.pkt_too_small", SCTP_PKT_TOO_SMALL, }, + { + "decoder.sctp.chunk_too_small", + SCTP_CHUNK_TOO_SMALL, + }, + { + "decoder.sctp.chunk_len_invalid", + SCTP_CHUNK_LEN_INVALID, + }, + { + "decoder.sctp.init_chunk_bundled", + SCTP_INIT_CHUNK_BUNDLED, + }, + { + "decoder.sctp.init_with_non_zero_vtag", + SCTP_INIT_WITH_NON_ZERO_VTAG, + }, + { + "decoder.sctp.data_with_zero_vtag", + SCTP_DATA_WITH_ZERO_VTAG, + }, + { + "decoder.sctp.too_many_chunks", + SCTP_TOO_MANY_CHUNKS, + }, + { + "decoder.sctp.too_many_data_chunks", + SCTP_TOO_MANY_DATA_CHUNKS, + }, /* ESP EVENTS */ { diff --git a/src/decode-events.h b/src/decode-events.h index dc958c9c45..45602235d5 100644 --- a/src/decode-events.h +++ b/src/decode-events.h @@ -172,7 +172,14 @@ enum { LTNULL_UNSUPPORTED_TYPE, /**< pkt has a type that the decoder doesn't support */ /* SCTP EVENTS */ - SCTP_PKT_TOO_SMALL, /**< sctp packet smaller than minimum size */ + SCTP_PKT_TOO_SMALL, /**< sctp packet smaller than minimum size */ + SCTP_CHUNK_TOO_SMALL, /**< remaining data too small for chunk header */ + SCTP_CHUNK_LEN_INVALID, /**< chunk length < 4 or exceeds remaining packet */ + SCTP_INIT_CHUNK_BUNDLED, /**< RFC 4960 sec 6.10: INIT/INIT_ACK bundled with other chunks */ + SCTP_INIT_WITH_NON_ZERO_VTAG, /**< INIT with vtag != 0 */ + SCTP_DATA_WITH_ZERO_VTAG, /**< DATA chunk with vtag == 0 */ + SCTP_TOO_MANY_CHUNKS, /**< more chunks than SCTP_MAX_TRACKED_CHUNKS */ + SCTP_TOO_MANY_DATA_CHUNKS, /**< more DATA chunks than SCTP_MAX_DATA_CHUNKS */ /* ESP EVENTS */ ESP_PKT_TOO_SMALL, /**< esp packet smaller than minimum size */ diff --git a/src/decode-sctp.c b/src/decode-sctp.c index 48d6202843..311fcb9528 100644 --- a/src/decode-sctp.c +++ b/src/decode-sctp.c @@ -21,7 +21,6 @@ * @{ */ - /** * \file * @@ -41,6 +40,117 @@ #include "util-optimize.h" #include "flow.h" +/** + * \brief Parse SCTP chunks after the common header. + * + * Iterates over chunks, validates each chunk header, and populates + * SCTPVars with chunk metadata. + * + * \param p Packet to decode + * \param pkt Pointer to the start of chunk data (after 12-byte common header) + * \param len Length of chunk data remaining + * + * \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, uint16_t len) +{ + const SCTPHdr *sctph = PacketGetSCTP(p); + const uint32_t vtag = SCTP_GET_RAW_VTAG(sctph); + uint32_t offset = 0; + uint8_t chunk_cnt = 0; + uint8_t tracked_chunk_cnt = 0; + bool has_init = false; + bool has_init_ack = false; + bool has_data = false; + bool has_abort = false; + int ret = 0; + + while (offset < len) { + /* need at least a chunk header */ + if (len - offset < SCTP_CHUNK_HDR_LEN) { + ENGINE_SET_INVALID_EVENT(p, SCTP_CHUNK_TOO_SMALL); + ret = -1; + break; + } + + SCTPChunkHdr chunk; + memcpy(&chunk, pkt + offset, sizeof(chunk)); + const uint16_t chunk_len = SCNtohs(chunk.length); + + /* RFC 4960 sec 3.2: chunk length includes the header and must be >= 4 */ + if (chunk_len < SCTP_CHUNK_HDR_LEN) { + ENGINE_SET_INVALID_EVENT(p, SCTP_CHUNK_LEN_INVALID); + ret = -1; + break; + } + + /* chunk must not extend beyond available data */ + if (chunk_len > (len - offset)) { + ENGINE_SET_INVALID_EVENT(p, SCTP_CHUNK_LEN_INVALID); + ret = -1; + break; + } + + if (chunk_cnt < SCTP_MAX_TRACKED_CHUNKS) { + p->l4.vars.sctp.chunk_types[chunk_cnt] = chunk.type; + tracked_chunk_cnt++; + } else if (chunk_cnt == SCTP_MAX_TRACKED_CHUNKS) { + ENGINE_SET_EVENT(p, SCTP_TOO_MANY_CHUNKS); + } + chunk_cnt++; + + switch (chunk.type) { + case SCTP_CHUNK_TYPE_INIT: + has_init = true; + /* RFC 4960 sec 8.5.1: INIT must have vtag == 0 */ + if (vtag != 0) { + ENGINE_SET_EVENT(p, SCTP_INIT_WITH_NON_ZERO_VTAG); + } + break; + case SCTP_CHUNK_TYPE_INIT_ACK: + has_init_ack = true; + break; + case SCTP_CHUNK_TYPE_DATA: + has_data = true; + /* DATA chunks must not have vtag == 0 */ + if (vtag == 0) { + ENGINE_SET_EVENT(p, SCTP_DATA_WITH_ZERO_VTAG); + } + break; + case SCTP_CHUNK_TYPE_ABORT: + has_abort = true; + break; + default: + break; + } + + /* advance to next chunk: padded to 4-byte boundary (RFC 4960 sec 3.2) */ + uint32_t padded_len = (chunk_len + 3) & ~3U; + /* guard against infinite loop with zero-padding overshoot */ + if (padded_len < SCTP_CHUNK_HDR_LEN) { + padded_len = SCTP_CHUNK_HDR_LEN; + } + offset += padded_len; + DEBUG_VALIDATE_BUG_ON(offset > (uint32_t)len + 3); + } + + /* RFC 4960 sec 6.10: INIT/INIT_ACK must be the only chunk in the packet */ + if ((has_init || has_init_ack) && chunk_cnt > 1) { + ENGINE_SET_EVENT(p, SCTP_INIT_CHUNK_BUNDLED); + } + + p->l4.vars.sctp.hlen = (uint16_t)(SCTP_HEADER_LEN + MIN(offset, len)); + p->l4.vars.sctp.chunk_cnt = chunk_cnt; + p->l4.vars.sctp.tracked_chunk_cnt = tracked_chunk_cnt; + p->l4.vars.sctp.has_init = has_init; + p->l4.vars.sctp.has_init_ack = has_init_ack; + p->l4.vars.sctp.has_data = has_data; + p->l4.vars.sctp.has_abort = has_abort; + + return ret; +} + static int DecodeSCTPPacket(ThreadVars *tv, Packet *p, const uint8_t *pkt, uint16_t len) { DEBUG_VALIDATE_BUG_ON(pkt == NULL); @@ -50,31 +160,323 @@ static int DecodeSCTPPacket(ThreadVars *tv, Packet *p, const uint8_t *pkt, uint1 return -1; } - SCTPHdr *sctph = PacketSetSCTP(p, pkt); - p->sp = SCNtohs(sctph->sh_sport); - p->dp = SCNtohs(sctph->sh_dport); - p->payload = (uint8_t *)pkt + sizeof(SCTPHdr); - p->payload_len = len - sizeof(SCTPHdr); + const SCTPHdr *sctph = PacketSetSCTP(p, pkt); + + p->sp = SCTP_GET_RAW_SRC_PORT(sctph); + p->dp = SCTP_GET_RAW_DST_PORT(sctph); + p->payload = (uint8_t *)pkt + SCTP_HEADER_LEN; + p->payload_len = len - SCTP_HEADER_LEN; p->proto = IPPROTO_SCTP; + + if (p->payload_len > 0) { + if (DecodeSCTPChunks(p, p->payload, p->payload_len) < 0) { + p->payload_len = 0; + return -1; + } + } else { + p->l4.vars.sctp.hlen = SCTP_HEADER_LEN; + } + return 0; } -int DecodeSCTP(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, - const uint8_t *pkt, uint16_t len) +int DecodeSCTP(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, const uint8_t *pkt, uint16_t len) { StatsCounterIncr(&tv->stats, dtv->counter_sctp); - if (unlikely(DecodeSCTPPacket(tv, p,pkt,len) < 0)) { + if (unlikely(DecodeSCTPPacket(tv, p, pkt, len) < 0)) { PacketClearL4(p); return TM_ECODE_FAILED; } SCLogDebug("SCTP sp: %u -> dp: %u", p->sp, p->dp); + if (p->l4.vars.sctp.has_init) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_init); + } + if (p->l4.vars.sctp.has_init_ack) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_init_ack); + } + if (p->l4.vars.sctp.has_data) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_data); + } + if (p->l4.vars.sctp.has_abort) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_abort); + } + for (uint8_t i = 0; i < p->l4.vars.sctp.tracked_chunk_cnt; i++) { + if (p->l4.vars.sctp.chunk_types[i] == SCTP_CHUNK_TYPE_SHUTDOWN) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_shutdown); + break; + } + } + FlowSetupPacket(p); return TM_ECODE_OK; } + +#ifdef UNITTESTS + +/** \test Valid SCTP packet with INIT chunk */ +static int SCTPDecodeValidInitTest01(void) +{ + /* SCTP common header: sport=1234 dport=80 vtag=0 checksum=0 + * followed by INIT chunk: type=0x01 flags=0x00 length=20 + * with 16 bytes of INIT-specific data (initiate_tag, a_rwnd, etc.) */ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x00, /* vtag=0 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, 0x00, 0x14, /* chunk: INIT, flags=0, len=20 */ + 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->sp != 1234); + FAIL_IF(p->dp != 80); + FAIL_IF(p->l4.vars.sctp.chunk_types[0] != SCTP_CHUNK_TYPE_INIT); + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 1); + FAIL_IF(!p->l4.vars.sctp.has_init); + FAIL_IF(p->l4.vars.sctp.has_data); + FAIL_IF(p->l4.vars.sctp.has_abort); + + /* no protocol violation events expected */ + FAIL_IF(ENGINE_ISSET_EVENT(p, SCTP_INIT_WITH_NON_ZERO_VTAG)); + FAIL_IF(ENGINE_ISSET_EVENT(p, SCTP_INIT_CHUNK_BUNDLED)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Packet too small (< 12 bytes) */ +static int SCTPDecodePktTooSmallTest02(void) +{ + uint8_t raw_sctp[] = { 0x04, 0xd2, 0x00, 0x50, 0x00, 0x00 }; + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + int ret = DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF(ret != TM_ECODE_FAILED); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_PKT_TOO_SMALL)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Chunk too small - header + 2 bytes garbage (not enough for chunk header) */ +static int SCTPDecodeChunkTooSmallTest03(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, /* only 2 bytes of chunk data */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + int ret = DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF(ret != TM_ECODE_FAILED); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_CHUNK_TOO_SMALL)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Invalid chunk length (chunk_len < 4) */ +static int SCTPDecodeChunkLenInvalidTest04(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x00, 0x00, 0x00, 0x02, /* chunk: DATA, flags=0, len=2 (invalid < 4) */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + int ret = DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF(ret != TM_ECODE_FAILED); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_CHUNK_LEN_INVALID)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test INIT with non-zero verification tag */ +static int SCTPDecodeInitNonZeroVtagTest05(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x42, /* vtag=0x42 (non-zero, invalid for INIT) */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, 0x00, 0x14, /* chunk: INIT, flags=0, len=20 */ + 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_INIT_WITH_NON_ZERO_VTAG)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Multiple chunks: DATA + SACK */ +static int SCTPDecodeMultiChunkTest06(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* DATA chunk: type=0x00, flags=0x03, len=20 */ + 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, /* TSN=0 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + /* SACK chunk: type=0x03, flags=0x00, len=16 */ + 0x03, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, /* cumulative_tsn_ack=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x00, 0x00, 0x00, /* num_gap_blocks=0, num_dup_tsns=0 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 2); + FAIL_IF(p->l4.vars.sctp.chunk_types[0] != SCTP_CHUNK_TYPE_DATA); + FAIL_IF(p->l4.vars.sctp.chunk_types[1] != SCTP_CHUNK_TYPE_SACK); + FAIL_IF(!p->l4.vars.sctp.has_data); + FAIL_IF(p->l4.vars.sctp.has_init); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test INIT bundled with another chunk (violates RFC 4960 sec 6.10) */ +static int SCTPDecodeInitNotAloneTest07(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x00, /* vtag=0 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* INIT chunk: type=0x01, flags=0, len=20 */ + 0x01, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + /* DATA chunk: type=0x00, flags=0, len=16 (bundled illegally) */ + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_INIT_CHUNK_BUNDLED)); + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 2); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +#endif /* UNITTESTS */ + +void DecodeSCTPRegisterTests(void) +{ +#ifdef UNITTESTS + UtRegisterTest("SCTPDecodeValidInitTest01", SCTPDecodeValidInitTest01); + UtRegisterTest("SCTPDecodePktTooSmallTest02", SCTPDecodePktTooSmallTest02); + UtRegisterTest("SCTPDecodeChunkTooSmallTest03", SCTPDecodeChunkTooSmallTest03); + UtRegisterTest("SCTPDecodeChunkLenInvalidTest04", SCTPDecodeChunkLenInvalidTest04); + UtRegisterTest("SCTPDecodeInitNonZeroVtagTest05", SCTPDecodeInitNonZeroVtagTest05); + UtRegisterTest("SCTPDecodeMultiChunkTest06", SCTPDecodeMultiChunkTest06); + UtRegisterTest("SCTPDecodeInitNotAloneTest07", SCTPDecodeInitNotAloneTest07); +#endif +} /** * @} */ diff --git a/src/decode-sctp.h b/src/decode-sctp.h index f83a4434ef..97d219dd03 100644 --- a/src/decode-sctp.h +++ b/src/decode-sctp.h @@ -25,16 +25,66 @@ #define SURICATA_DECODE_SCTP_H /** size of the packet header without any chunk headers */ -#define SCTP_HEADER_LEN 12 - -typedef struct SCTPHdr_ -{ - uint16_t sh_sport; /* source port */ - uint16_t sh_dport; /* destination port */ - uint32_t sh_vtag; /* verification tag, defined per flow */ - uint32_t sh_sum; /* checksum, computed via crc32 */ +#define SCTP_HEADER_LEN 12 + +/** size of a chunk header (type + flags + length) */ +#define SCTP_CHUNK_HDR_LEN 4 + +/** max number of chunks tracked per packet for detection/logging */ +/** value chosen 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. */ +#define SCTP_MAX_TRACKED_CHUNKS 16 + +/* SCTP chunk types (RFC 4960 sec 3.2) */ +#define SCTP_CHUNK_TYPE_DATA 0x00 +#define SCTP_CHUNK_TYPE_INIT 0x01 +#define SCTP_CHUNK_TYPE_INIT_ACK 0x02 +#define SCTP_CHUNK_TYPE_SACK 0x03 +#define SCTP_CHUNK_TYPE_HEARTBEAT 0x04 +#define SCTP_CHUNK_TYPE_HB_ACK 0x05 +#define SCTP_CHUNK_TYPE_ABORT 0x06 +#define SCTP_CHUNK_TYPE_SHUTDOWN 0x07 +#define SCTP_CHUNK_TYPE_SHUTDOWN_ACK 0x08 +#define SCTP_CHUNK_TYPE_ERROR 0x09 +#define SCTP_CHUNK_TYPE_COOKIE_ECHO 0x0A +#define SCTP_CHUNK_TYPE_COOKIE_ACK 0x0B +#define SCTP_CHUNK_TYPE_ECNE 0x0C +#define SCTP_CHUNK_TYPE_CWR 0x0D +#define SCTP_CHUNK_TYPE_SHUTDOWN_COMPLETE 0x0E +#define SCTP_CHUNK_TYPE_FORWARD_TSN 0xC0 + +typedef struct SCTPHdr_ { + uint16_t sh_sport; /* source port */ + uint16_t sh_dport; /* destination port */ + uint32_t sh_vtag; /* verification tag, defined per flow */ + uint32_t sh_sum; /* checksum, computed via crc32 */ } __attribute__((__packed__)) SCTPHdr; +typedef struct SCTPChunkHdr_ { + uint8_t type; + uint8_t flags; + uint16_t length; +} __attribute__((__packed__)) SCTPChunkHdr; + +typedef struct SCTPVars_ { + uint16_t hlen; /**< total header length (common header + chunks) */ + uint8_t chunk_cnt; /**< number of chunks parsed */ + uint8_t tracked_chunk_cnt; /**< number of chunks tracked (capped at SCTP_MAX_TRACKED_CHUNKS) */ + uint8_t chunk_types[SCTP_MAX_TRACKED_CHUNKS]; /**< types of first N chunks */ + bool has_init : 1; + bool has_init_ack : 1; + bool has_data : 1; + bool has_abort : 1; +} SCTPVars; + +#define SCTP_GET_RAW_SRC_PORT(sctph) SCNtohs((sctph)->sh_sport) +#define SCTP_GET_RAW_DST_PORT(sctph) SCNtohs((sctph)->sh_dport) +#define SCTP_GET_RAW_VTAG(sctph) SCNtohl((sctph)->sh_vtag) +#define SCTP_GET_RAW_SUM(sctph) SCNtohl((sctph)->sh_sum) + void DecodeSCTPRegisterTests(void); #endif /* SURICATA_DECODE_SCTP_H */ diff --git a/src/decode.c b/src/decode.c index ce452930aa..593f212f6f 100644 --- a/src/decode.c +++ b/src/decode.c @@ -656,6 +656,11 @@ void DecodeRegisterPerfCounters(DecodeThreadVars *dtv, ThreadVars *tv) dtv->counter_udp = StatsRegisterCounter("decoder.udp", &tv->stats); dtv->counter_sctp = StatsRegisterCounter("decoder.sctp", &tv->stats); + dtv->counter_sctp_init = StatsRegisterCounter("sctp.init", &tv->stats); + dtv->counter_sctp_init_ack = StatsRegisterCounter("sctp.init_ack", &tv->stats); + dtv->counter_sctp_data = StatsRegisterCounter("sctp.data", &tv->stats); + dtv->counter_sctp_abort = StatsRegisterCounter("sctp.abort", &tv->stats); + dtv->counter_sctp_shutdown = StatsRegisterCounter("sctp.shutdown", &tv->stats); dtv->counter_esp = StatsRegisterCounter("decoder.esp", &tv->stats); dtv->counter_icmpv4 = StatsRegisterCounter("decoder.icmpv4", &tv->stats); dtv->counter_icmpv6 = StatsRegisterCounter("decoder.icmpv6", &tv->stats); diff --git a/src/decode.h b/src/decode.h index 5b6e2a1220..b44906d06a 100644 --- a/src/decode.h +++ b/src/decode.h @@ -491,6 +491,7 @@ struct PacketL4 { ICMPV4Vars icmpv4; ICMPV6Vars icmpv6; IGMPVars igmp; + SCTPVars sctp; } vars; }; @@ -1025,6 +1026,11 @@ typedef struct DecodeThreadVars_ StatsCounterId counter_raw; StatsCounterId counter_null; StatsCounterId counter_sctp; + StatsCounterId counter_sctp_init; + StatsCounterId counter_sctp_init_ack; + StatsCounterId counter_sctp_data; + StatsCounterId counter_sctp_abort; + StatsCounterId counter_sctp_shutdown; StatsCounterId counter_esp; StatsCounterId counter_ppp; StatsCounterId counter_geneve; diff --git a/src/runmode-unittests.c b/src/runmode-unittests.c index 4ce39159d9..e53b79a1e3 100644 --- a/src/runmode-unittests.c +++ b/src/runmode-unittests.c @@ -167,6 +167,7 @@ static void RegisterUnittests(void) DecodeTCPRegisterTests(); DecodeUDPV4RegisterTests(); DecodeGRERegisterTests(); + DecodeSCTPRegisterTests(); DecodeESPRegisterTests(); DecodeMPLSRegisterTests(); DecodeNSHRegisterTests();