From fdd9f8a9a50ffb51dbde702eb7c369790cae8e50 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Fri, 5 Jun 2026 14:19:31 -0600 Subject: [PATCH 01/18] github-ci: consolidate new authors check into a single workflow Replace the two-workflow pattern (authors.yml + authors-done.yml) with a single pull_request_target workflow. I'm not sure this was possible when this job was originally created, but apparently it is now. --- .github/workflows/authors-done.yml | 53 ------------------------------ .github/workflows/authors.yml | 31 +++++++++++------ 2 files changed, 21 insertions(+), 63 deletions(-) delete mode 100644 .github/workflows/authors-done.yml diff --git a/.github/workflows/authors-done.yml b/.github/workflows/authors-done.yml deleted file mode 100644 index 6c677d06ed0e..000000000000 --- a/.github/workflows/authors-done.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: New Authors Report - -on: - workflow_run: - workflows: [New Authors Check] - types: [completed] - -jobs: - comment: - runs-on: ubuntu-latest - steps: - - run: echo "Author check is complete" - - - name: Download artifact new authors - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "new-authors"; - })[0]; - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/new-authors.zip`, Buffer.from(download.data)); - - run: unzip new-authors.zip - - run: | - if test -s new-authors.txt; then - echo new_authors=yes >> $GITHUB_ENV - fi - - name: Comment on PR - if: ${{ env.new_authors == 'yes' }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - let fs = require('fs'); - let issue_number = Number(fs.readFileSync('./pr-number.txt')); - let msg = 'NOTE: This PR may contain new authors.'; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - body: msg - }); diff --git a/.github/workflows/authors.yml b/.github/workflows/authors.yml index d250e9243d4a..16c843184fc7 100644 --- a/.github/workflows/authors.yml +++ b/.github/workflows/authors.yml @@ -1,9 +1,16 @@ name: New Authors Check +# pull_request_target runs with base-repo permissions (including write) even +# for fork PRs, allowing us to post a comment directly without the artifact +# hand-off to a second workflow_run workflow. +# +# Only git history is read here — no PR code is built or executed — so +# checking out the PR head SHA is safe under pull_request_target. on: - pull_request: + pull_request_target: -permissions: read-all +permissions: + pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -36,13 +43,17 @@ jobs: echo has_new_authors="yes" >> $GITHUB_ENV fi done < commit-authors.txt - - run: mkdir new-authors - - run: cp new-authors.txt new-authors - - run: echo ${{ github.event.number }} > new-authors/pr-number.txt - - run: ls -l - - name: Upload new authors - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + - name: Comment on PR + if: ${{ env.has_new_authors == 'yes' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 with: - name: new-authors - path: new-authors + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + let msg = 'NOTE: This PR may contain new authors.'; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: msg + }); From e5e56062485f6ec0ae2702b0df9d5f3e92ab6c10 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sun, 7 Jun 2026 22:21:56 +0200 Subject: [PATCH 02/18] rust: format snmp files Ticket: 3836 --- rust/src/snmp/mod.rs | 4 +- rust/src/snmp/snmp.rs | 175 ++++++++++++++++++++++-------------------- scripts/rustfmt.sh | 3 +- 3 files changed, 96 insertions(+), 86 deletions(-) diff --git a/rust/src/snmp/mod.rs b/rust/src/snmp/mod.rs index 7c6ceb35884b..b8c769f45855 100644 --- a/rust/src/snmp/mod.rs +++ b/rust/src/snmp/mod.rs @@ -21,6 +21,6 @@ extern crate snmp_parser; -pub mod snmp; -pub mod log; pub mod detect; +pub mod log; +pub mod snmp; diff --git a/rust/src/snmp/snmp.rs b/rust/src/snmp/snmp.rs index 0064997b43e4..c037e3d8c1de 100644 --- a/rust/src/snmp/snmp.rs +++ b/rust/src/snmp/snmp.rs @@ -17,25 +17,26 @@ // written by Pierre Chifflier +use super::detect::detect_snmp_register; +use super::log::snmp_log_json_response; +use crate::applayer::{self, *}; +use crate::core::{self, *}; use crate::direction::Direction; use crate::flow::Flow; use crate::snmp::snmp_parser::*; -use crate::core::{self, *}; -use crate::applayer::{self, *}; -use super::log::snmp_log_json_response; -use super::detect::detect_snmp_register; use std; use std::ffi::CString; use asn1_rs::Oid; use der_parser::ber::BerObjectContent; use der_parser::der::parse_der_sequence; +use nom7::error::{make_error, ErrorKind}; use nom7::{Err, IResult}; -use nom7::error::{ErrorKind, make_error}; use suricata_sys::sys::{ AppLayerParserState, AppProto, AppProtoNewProtoFromString, EveJsonTxLoggerRegistrationData, - SCAppLayerParserRegisterLogger, SCAppLayerProtoDetectConfProtoDetectionEnabled, - SCOutputEvePreRegisterLogger, SCOutputJsonLogDirection, SCSigTablePreRegister, SCAppLayerParserConfParserEnabled, + SCAppLayerParserConfParserEnabled, SCAppLayerParserRegisterLogger, + SCAppLayerProtoDetectConfProtoDetectionEnabled, SCOutputEvePreRegisterLogger, + SCOutputJsonLogDirection, SCSigTablePreRegister, }; #[derive(AppLayerEvent)] @@ -64,7 +65,7 @@ pub(super) struct SNMPPduInfo<'a> { pub err: ErrorStatus, - pub trap_type: Option<(TrapType,Oid<'a>,NetworkAddress)>, + pub trap_type: Option<(TrapType, Oid<'a>, NetworkAddress)>, pub vars: Vec>, } @@ -120,11 +121,11 @@ impl<'a> SNMPState<'a> { impl<'a> Default for SNMPPduInfo<'a> { fn default() -> SNMPPduInfo<'a> { - SNMPPduInfo{ + SNMPPduInfo { pdu_type: PduType(0), err: ErrorStatus::NoError, trap_type: None, - vars: Vec::new() + vars: Vec::new(), } } } @@ -148,11 +149,10 @@ impl<'a> SNMPState<'a> { match *pdu { SnmpPdu::Generic(ref pdu) => { pdu_info.err = pdu.err; - }, - SnmpPdu::Bulk(_) => { - }, - SnmpPdu::TrapV1(ref t) => { - pdu_info.trap_type = Some((t.generic_trap,t.enterprise.clone(),t.agent_addr)); + } + SnmpPdu::Bulk(_) => {} + SnmpPdu::TrapV1(ref t) => { + pdu_info.trap_type = Some((t.generic_trap, t.enterprise.clone(), t.agent_addr)); } } @@ -166,7 +166,11 @@ impl<'a> SNMPState<'a> { let mut tx = self.new_tx(_direction); // in the message, version is encoded as 0 (version 1) or 1 (version 2) if self.version != msg.version + 1 { - SCLogDebug!("SNMP version mismatch: expected {}, received {}", self.version, msg.version+1); + SCLogDebug!( + "SNMP version mismatch: expected {}, received {}", + self.version, + msg.version + 1 + ); self.set_event_tx(&mut tx, SNMPEvent::VersionMismatch); } self.add_pdu_info(&msg.pdu, &mut tx); @@ -178,22 +182,26 @@ impl<'a> SNMPState<'a> { fn handle_snmp_v3(&mut self, msg: SnmpV3Message<'a>, _direction: Direction) -> AppLayerResult { let mut tx = self.new_tx(_direction); if self.version != msg.version { - SCLogDebug!("SNMP version mismatch: expected {}, received {}", self.version, msg.version); + SCLogDebug!( + "SNMP version mismatch: expected {}, received {}", + self.version, + msg.version + ); self.set_event_tx(&mut tx, SNMPEvent::VersionMismatch); } match msg.data { ScopedPduData::Plaintext(pdu) => { self.add_pdu_info(&pdu.data, &mut tx); - }, - _ => { + } + _ => { tx.encrypted = true; } } match msg.security_params { SecurityParameters::USM(usm) => { tx.usm = Some(usm.msg_user_name); - }, - _ => { + } + _ => { self.set_event_tx(&mut tx, SNMPEvent::UnknownSecurityModel); } } @@ -211,14 +219,15 @@ impl<'a> SNMPState<'a> { } } match parse_snmp_generic_message(i) { - Ok((_rem,SnmpGenericMessage::V1(msg))) | - Ok((_rem,SnmpGenericMessage::V2(msg))) => self.handle_snmp_v12(msg, direction), - Ok((_rem,SnmpGenericMessage::V3(msg))) => self.handle_snmp_v3(msg, direction), + Ok((_rem, SnmpGenericMessage::V1(msg))) | Ok((_rem, SnmpGenericMessage::V2(msg))) => { + self.handle_snmp_v12(msg, direction) + } + Ok((_rem, SnmpGenericMessage::V3(msg))) => self.handle_snmp_v3(msg, direction), Err(_e) => { SCLogDebug!("parse_snmp failed: {:?}", _e); self.set_event(SNMPEvent::MalformedData); AppLayerResult::err() - }, + } } } @@ -234,7 +243,10 @@ impl<'a> SNMPState<'a> { } fn get_tx_by_id(&mut self, tx_id: u64) -> Option<&SNMPTransaction<'_>> { - self.transactions.iter().rev().find(|&tx| tx.id == tx_id + 1) + self.transactions + .iter() + .rev() + .find(|&tx| tx.id == tx_id + 1) } fn free_tx(&mut self, tx_id: u64) { @@ -273,7 +285,9 @@ impl<'a> SNMPTransaction<'a> { } /// Returns *mut SNMPState -extern "C" fn snmp_state_new(_orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto) -> *mut std::os::raw::c_void { +extern "C" fn snmp_state_new( + _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, +) -> *mut std::os::raw::c_void { let state = SNMPState::new(); let boxed = Box::new(state); return Box::into_raw(boxed) as *mut _; @@ -282,113 +296,108 @@ extern "C" fn snmp_state_new(_orig_state: *mut std::os::raw::c_void, _orig_proto /// Params: /// - state: *mut SNMPState as void pointer extern "C" fn snmp_state_free(state: *mut std::os::raw::c_void) { - let mut snmp_state = unsafe{ Box::from_raw(state as *mut SNMPState) }; + let mut snmp_state = unsafe { Box::from_raw(state as *mut SNMPState) }; snmp_state.free(); } -unsafe extern "C" fn snmp_parse_request(_flow: *mut Flow, - state: *mut std::os::raw::c_void, - _pstate: *mut AppLayerParserState, - stream_slice: StreamSlice, - _data: *mut std::os::raw::c_void, - ) -> AppLayerResult { - let state = cast_pointer!(state,SNMPState); +unsafe extern "C" fn snmp_parse_request( + _flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState, + stream_slice: StreamSlice, _data: *mut std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, SNMPState); state.parse(stream_slice.as_slice(), Direction::ToServer) } -unsafe extern "C" fn snmp_parse_response(_flow: *mut Flow, - state: *mut std::os::raw::c_void, - _pstate: *mut AppLayerParserState, - stream_slice: StreamSlice, - _data: *mut std::os::raw::c_void, - ) -> AppLayerResult { - let state = cast_pointer!(state,SNMPState); +unsafe extern "C" fn snmp_parse_response( + _flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState, + stream_slice: StreamSlice, _data: *mut std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, SNMPState); state.parse(stream_slice.as_slice(), Direction::ToClient) } -unsafe extern "C" fn snmp_state_get_tx(state: *mut std::os::raw::c_void, - tx_id: u64) - -> *mut std::os::raw::c_void -{ - let state = cast_pointer!(state,SNMPState); +unsafe extern "C" fn snmp_state_get_tx( + state: *mut std::os::raw::c_void, tx_id: u64, +) -> *mut std::os::raw::c_void { + let state = cast_pointer!(state, SNMPState); match state.get_tx_by_id(tx_id) { Some(tx) => tx as *const _ as *mut _, - None => std::ptr::null_mut(), + None => std::ptr::null_mut(), } } -unsafe extern "C" fn snmp_state_get_tx_count(state: *mut std::os::raw::c_void) - -> u64 -{ - let state = cast_pointer!(state,SNMPState); +unsafe extern "C" fn snmp_state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { + let state = cast_pointer!(state, SNMPState); state.tx_id } -unsafe extern "C" fn snmp_state_tx_free(state: *mut std::os::raw::c_void, - tx_id: u64) -{ - let state = cast_pointer!(state,SNMPState); +unsafe extern "C" fn snmp_state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { + let state = cast_pointer!(state, SNMPState); state.free_tx(tx_id); } -extern "C" fn snmp_tx_get_alstate_progress(_tx: *mut std::os::raw::c_void, - _direction: u8) - -> std::os::raw::c_int -{ +extern "C" fn snmp_tx_get_alstate_progress( + _tx: *mut std::os::raw::c_void, _direction: u8, +) -> std::os::raw::c_int { 1 } -pub(super) static mut ALPROTO_SNMP : AppProto = ALPROTO_UNKNOWN; +pub(super) static mut ALPROTO_SNMP: AppProto = ALPROTO_UNKNOWN; // Read PDU sequence and extract version, if similar to SNMP definition -fn parse_pdu_envelope_version(i:&[u8]) -> IResult<&[u8],u32> { +fn parse_pdu_envelope_version(i: &[u8]) -> IResult<&[u8], u32> { match parse_der_sequence(i) { - Ok((_,x)) => { + Ok((_, x)) => { #[allow(clippy::single_match)] match x.content { BerObjectContent::Sequence(ref v) => { if v.len() == 3 { - match v[0].as_u32() { - Ok(0) => { return Ok((i,1)); }, // possibly SNMPv1 - Ok(1) => { return Ok((i,2)); }, // possibly SNMPv2c - _ => () + match v[0].as_u32() { + Ok(0) => { + return Ok((i, 1)); + } // possibly SNMPv1 + Ok(1) => { + return Ok((i, 2)); + } // possibly SNMPv2c + _ => (), } } else if v.len() == 4 && v[0].as_u32() == Ok(3) { - return Ok((i,3)); // possibly SNMPv3 + return Ok((i, 3)); // possibly SNMPv3 } - }, - _ => () + } + _ => (), }; Err(Err::Error(make_error(i, ErrorKind::Verify))) - }, + } Err(Err::Incomplete(i)) => Err(Err::Incomplete(i)), - Err(Err::Failure(_)) | - Err(Err::Error(_)) => Err(Err::Error(make_error(i,ErrorKind::Verify))) + Err(Err::Failure(_)) | Err(Err::Error(_)) => { + Err(Err::Error(make_error(i, ErrorKind::Verify))) + } } } -unsafe extern "C" fn snmp_probing_parser(_flow: *const Flow, - _direction: u8, - input:*const u8, - input_len: u32, - _rdir: *mut u8) -> AppProto { +unsafe extern "C" fn snmp_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { if input.is_null() { return ALPROTO_UNKNOWN; } - let slice = build_slice!(input,input_len as usize); + let slice = build_slice!(input, input_len as usize); let alproto = ALPROTO_SNMP; - if slice.len() < 4 { return ALPROTO_UNKNOWN; } + if slice.len() < 4 { + return ALPROTO_UNKNOWN; + } match parse_pdu_envelope_version(slice) { - Ok((_,_)) => alproto, + Ok((_, _)) => alproto, Err(Err::Incomplete(_)) => ALPROTO_UNKNOWN, - _ => ALPROTO_FAILED, + _ => ALPROTO_FAILED, } } export_tx_data_get!(snmp_get_tx_data, SNMPTransaction); export_state_data_get!(snmp_get_state_data, SNMPState); -const PARSER_NAME : &[u8] = b"snmp\0"; +const PARSER_NAME: &[u8] = b"snmp\0"; #[no_mangle] pub unsafe extern "C" fn SCRegisterSnmpParser() { diff --git a/scripts/rustfmt.sh b/scripts/rustfmt.sh index 8e63cbbb6df2..64c7a37cec85 100755 --- a/scripts/rustfmt.sh +++ b/scripts/rustfmt.sh @@ -40,4 +40,5 @@ rustfmt --check rust/src/dns/*.rs rust/src/applayertemplate/*.rs rust/src/asn1/* rust/src/dhcp/*.rs rust/src/krb/*.rs rust/src/mdns/*.rs rust/src/pop3/*.rs \ rust/src/http2/*.rs rust/src/ike/*.rs rust/src/modbus/*.rs rust/src/mqtt/*.rs \ rust/src/nfs/*.rs rust/src/pgsql/*.rs rust/src/rdp/*.rs rust/src/sdp/*.rs \ - rust/src/sip/*.rs rust/src/telnet/*.rs rust/src/tftp/*.rs rust/src/x509/*.rs + rust/src/sip/*.rs rust/src/telnet/*.rs rust/src/tftp/*.rs rust/src/x509/*.rs \ + rust/src/snmp/*.rs From d3a8a06d1071b379eef1bb073186f7dac8818ce4 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Sun, 7 Jun 2026 22:22:23 +0200 Subject: [PATCH 03/18] rust: check llmnr format Ticket: 3836 --- scripts/rustfmt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rustfmt.sh b/scripts/rustfmt.sh index 64c7a37cec85..67a5f2558b4e 100755 --- a/scripts/rustfmt.sh +++ b/scripts/rustfmt.sh @@ -41,4 +41,4 @@ rustfmt --check rust/src/dns/*.rs rust/src/applayertemplate/*.rs rust/src/asn1/* rust/src/http2/*.rs rust/src/ike/*.rs rust/src/modbus/*.rs rust/src/mqtt/*.rs \ rust/src/nfs/*.rs rust/src/pgsql/*.rs rust/src/rdp/*.rs rust/src/sdp/*.rs \ rust/src/sip/*.rs rust/src/telnet/*.rs rust/src/tftp/*.rs rust/src/x509/*.rs \ - rust/src/snmp/*.rs + rust/src/snmp/*.rs rust/src/llmnr/*.rs From cad95a49616f1af7dd9cc64402c0100ba2fa980e Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Fri, 6 Mar 2026 10:04:52 +0100 Subject: [PATCH 04/18] 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 c08ee1001156..a9d6afdd3065 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 1524c1df5692..785b25b26999 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 303802c22329..b4abaf6128d8 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 000000000000..72ebebad7a93 --- /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 32ce23dcda6b..3f37713dd3be 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 dc958c9c45f8..45602235d58c 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 48d62028431f..311fcb952812 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 f83a4434ef05..97d219dd03d4 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 ce452930aa21..593f212f6fe5 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 5b6e2a1220a6..b44906d06a5b 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 4ce39159d9da..e53b79a1e3fa 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(); From c941b0c09c32b7d5f2d9b7ca8e386f7fe00b76cc Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Tue, 24 Mar 2026 10:02:57 +0100 Subject: [PATCH 05/18] detect/sctp: add sctp.hdr sticky buffer Implement a sticky buffer to match the raw SCTP header (common header + chunks) Ticket #4251 --- src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-sctphdr.c | 111 +++++++++++++++++++++++++++++++++++ src/detect-sctphdr.h | 27 +++++++++ 5 files changed, 143 insertions(+) create mode 100644 src/detect-sctphdr.c create mode 100644 src/detect-sctphdr.h diff --git a/src/Makefile.am b/src/Makefile.am index 44ff227012d1..d5bbb60c1c0a 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -228,6 +228,7 @@ noinst_HEADERS = \ detect-id.h \ detect-igmphdr.h \ detect-igmp-type.h \ + detect-sctphdr.h \ detect-ipaddr.h \ detect-ipopts.h \ detect-ipproto.h \ @@ -804,6 +805,7 @@ libsuricata_c_a_SOURCES = \ detect-id.c \ detect-igmphdr.c \ detect-igmp-type.c \ + detect-sctphdr.c \ detect-ipaddr.c \ detect-ipopts.c \ detect-ipproto.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 1b441fc29675..cf8de45b3501 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -155,6 +155,7 @@ #include "detect-icmpv4hdr.h" #include "detect-igmphdr.h" #include "detect-igmp-type.h" +#include "detect-sctphdr.h" #include "detect-urilen.h" #include "detect-bsize.h" #include "detect-detection-filter.h" @@ -676,6 +677,7 @@ void SigTableSetup(void) DetectIcmpv4HdrRegister(); DetectIGMPHdrRegister(); DetectIGMPTypeRegister(); + DetectSCTPHdrRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 0538d998a930..92a57c89002d 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -51,6 +51,7 @@ enum DetectKeywordId { DETECT_ICMPV4HDR, DETECT_IGMPHDR, DETECT_IGMP_TYPE, + DETECT_SCTPHDR, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-sctphdr.c b/src/detect-sctphdr.c new file mode 100644 index 000000000000..72ae60dbc87b --- /dev/null +++ b/src/detect-sctphdr.c @@ -0,0 +1,111 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * Implements sctp.hdr sticky buffer + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" + +#include "detect.h" +#include "detect-engine.h" +#include "detect-engine-buffer.h" +#include "detect-engine-mpm.h" +#include "detect-sctphdr.h" +#include "detect-engine-prefilter.h" + +static int DetectSCTPHdrSetup(DetectEngineCtx *, Signature *, const char *); + +static int g_sctphdr_buffer_id = 0; + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Packet *p, const int list_id); + +void DetectSCTPHdrRegister(void) +{ + sigmatch_table[DETECT_SCTPHDR].name = "sctp.hdr"; + sigmatch_table[DETECT_SCTPHDR].desc = "sticky buffer to match on the SCTP header"; + sigmatch_table[DETECT_SCTPHDR].url = "/rules/sctp-keywords.html#sctp-hdr"; + sigmatch_table[DETECT_SCTPHDR].Setup = DetectSCTPHdrSetup; + sigmatch_table[DETECT_SCTPHDR].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; + + g_sctphdr_buffer_id = DetectBufferTypeRegister("sctp.hdr"); + BUG_ON(g_sctphdr_buffer_id < 0); + + DetectBufferTypeSupportsPacket("sctp.hdr"); + + DetectPktMpmRegister("sctp.hdr", 2, PrefilterGenericMpmPktRegister, GetData); + + DetectPktInspectEngineRegister("sctp.hdr", GetData, DetectEngineInspectPktBufferGeneric); +} + +/** + * \brief setup sctp.hdr sticky buffer + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the current Signature + * \param _unused unused + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectSCTPHdrSetup(DetectEngineCtx *de_ctx, Signature *s, const char *_unused) +{ + if (!(DetectProtoContainsProto(s->proto, IPPROTO_SCTP))) + return -1; + + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + if (SCDetectBufferSetActiveList(de_ctx, s, g_sctphdr_buffer_id) < 0) + return -1; + + return 0; +} + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Packet *p, const int list_id) +{ + SCEnter(); + + if (!PacketIsSCTP(p)) { + SCReturnPtr(NULL, "InspectionBuffer"); + } + + InspectionBuffer *buffer = SCInspectionBufferGet(det_ctx, list_id); + if (buffer->inspect == NULL) { + const SCTPHdr *sctph = PacketGetSCTP(p); + const uint16_t hlen = p->l4.vars.sctp.hlen; + if (((uint8_t *)sctph + (ptrdiff_t)hlen) > + ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))) { + SCLogDebug("data out of range: %p > %p", ((uint8_t *)sctph + (ptrdiff_t)hlen), + ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))); + SCReturnPtr(NULL, "InspectionBuffer"); + } + + const uint32_t data_len = hlen; + const uint8_t *data = (const uint8_t *)sctph; + + SCInspectionBufferSetupAndApplyTransforms( + det_ctx, list_id, buffer, data, data_len, transforms); + } + + SCReturnPtr(buffer, "InspectionBuffer"); +} diff --git a/src/detect-sctphdr.h b/src/detect-sctphdr.h new file mode 100644 index 000000000000..02d47157364d --- /dev/null +++ b/src/detect-sctphdr.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTPHDR_H +#define SURICATA_DETECT_SCTPHDR_H + +void DetectSCTPHdrRegister(void); + +#endif /* SURICATA_DETECT_SCTPHDR_H */ From f73c26fef86de90e682b6eeea90a971fc7ab8c61 Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Tue, 24 Mar 2026 10:17:44 +0100 Subject: [PATCH 06/18] detect/sctp: add sctp.chunk_type keyword Add a U8 numeric keyword to match the first SCTP chunk type in a packet with prefilter support. Ticket #4251 --- rust/src/lib.rs | 1 + rust/src/sctp/detect.rs | 146 +++++++++++++++++++++++++++++++++++ rust/src/sctp/mod.rs | 20 +++++ src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-sctp-chunk-type.c | 140 +++++++++++++++++++++++++++++++++ src/detect-sctp-chunk-type.h | 27 +++++++ 8 files changed, 339 insertions(+) create mode 100644 rust/src/sctp/detect.rs create mode 100644 rust/src/sctp/mod.rs create mode 100644 src/detect-sctp-chunk-type.c create mode 100644 src/detect-sctp-chunk-type.h diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 45f1688d9157..10c65e66a2b7 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -136,6 +136,7 @@ pub mod lzma; pub mod util; pub mod ffi; pub mod feature; +pub mod sctp; pub mod sdp; pub mod ldap; pub mod flow; diff --git a/rust/src/sctp/detect.rs b/rust/src/sctp/detect.rs new file mode 100644 index 000000000000..c1067c5eb79b --- /dev/null +++ b/rust/src/sctp/detect.rs @@ -0,0 +1,146 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Giuseppe Longo + +use crate::detect::uint::{detect_parse_uint_enum, DetectUintData}; + +use std::ffi::CStr; + +/// SCTP chunk types (RFC 4960 sec 3.2) +#[repr(u8)] +#[derive(EnumStringU8)] +pub enum SctpChunkType { + Data = 0x00, + Init = 0x01, + InitAck = 0x02, + Sack = 0x03, + Heartbeat = 0x04, + HbAck = 0x05, + Abort = 0x06, + Shutdown = 0x07, + ShutdownAck = 0x08, + Error = 0x09, + CookieEcho = 0x0A, + CookieAck = 0x0B, + Ecne = 0x0C, + Cwr = 0x0D, + ShutdownComplete = 0x0E, + ForwardTsn = 0xC0, +} + +#[no_mangle] +pub unsafe extern "C" fn SCSctpDetectChunkTypeParse( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = detect_parse_uint_enum::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +/// Returns the string name for a chunk type value, or NULL for unknown types. +#[no_mangle] +pub extern "C" fn SCSctpChunkTypeToString(val: u8) -> *const std::os::raw::c_char { + let s: &[u8] = match val { + 0x00 => b"data\0", + 0x01 => b"init\0", + 0x02 => b"init_ack\0", + 0x03 => b"sack\0", + 0x04 => b"heartbeat\0", + 0x05 => b"hb_ack\0", + 0x06 => b"abort\0", + 0x07 => b"shutdown\0", + 0x08 => b"shutdown_ack\0", + 0x09 => b"error\0", + 0x0A => b"cookie_echo\0", + 0x0B => b"cookie_ack\0", + 0x0C => b"ecne\0", + 0x0D => b"cwr\0", + 0x0E => b"shutdown_complete\0", + 0xC0 => b"forward_tsn\0", + _ => return std::ptr::null(), + }; + s.as_ptr() as *const std::os::raw::c_char +} + +#[cfg(test)] +mod test { + use super::*; + use crate::detect::uint::DetectUintMode; + + #[test] + fn parse_numeric() { + let ctx = detect_parse_uint_enum::("0").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_enum::("1").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("192").unwrap(); + assert_eq!(ctx.arg1, 0xC0); + } + + #[test] + fn parse_named() { + let ctx = detect_parse_uint_enum::("data").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_enum::("init").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("init_ack").unwrap(); + assert_eq!(ctx.arg1, 2); + let ctx = detect_parse_uint_enum::("sack").unwrap(); + assert_eq!(ctx.arg1, 3); + let ctx = detect_parse_uint_enum::("heartbeat").unwrap(); + assert_eq!(ctx.arg1, 4); + let ctx = detect_parse_uint_enum::("hb_ack").unwrap(); + assert_eq!(ctx.arg1, 5); + let ctx = detect_parse_uint_enum::("abort").unwrap(); + assert_eq!(ctx.arg1, 6); + let ctx = detect_parse_uint_enum::("shutdown").unwrap(); + assert_eq!(ctx.arg1, 7); + let ctx = detect_parse_uint_enum::("cookie_echo").unwrap(); + assert_eq!(ctx.arg1, 0x0A); + let ctx = detect_parse_uint_enum::("forward_tsn").unwrap(); + assert_eq!(ctx.arg1, 0xC0); + } + + #[test] + fn parse_case_insensitive() { + let ctx = detect_parse_uint_enum::("INIT").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("Init").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("INIT_ACK").unwrap(); + assert_eq!(ctx.arg1, 2); + } + + #[test] + fn parse_negation() { + let ctx = detect_parse_uint_enum::("!init").unwrap(); + assert_eq!(ctx.arg1, 1); + assert_eq!(ctx.mode, DetectUintMode::DetectUintModeNe); + } + + #[test] + fn parse_invalid() { + assert!(detect_parse_uint_enum::("foo").is_none()); + assert!(detect_parse_uint_enum::("").is_none()); + } +} diff --git a/rust/src/sctp/mod.rs b/rust/src/sctp/mod.rs new file mode 100644 index 000000000000..b772cdd0ebd0 --- /dev/null +++ b/rust/src/sctp/mod.rs @@ -0,0 +1,20 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Giuseppe Longo + +pub mod detect; diff --git a/src/Makefile.am b/src/Makefile.am index d5bbb60c1c0a..35d5ce145f48 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -229,6 +229,7 @@ noinst_HEADERS = \ detect-igmphdr.h \ detect-igmp-type.h \ detect-sctphdr.h \ + detect-sctp-chunk-type.h \ detect-ipaddr.h \ detect-ipopts.h \ detect-ipproto.h \ @@ -806,6 +807,7 @@ libsuricata_c_a_SOURCES = \ detect-igmphdr.c \ detect-igmp-type.c \ detect-sctphdr.c \ + detect-sctp-chunk-type.c \ detect-ipaddr.c \ detect-ipopts.c \ detect-ipproto.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index cf8de45b3501..6bc14dfe0bed 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -156,6 +156,7 @@ #include "detect-igmphdr.h" #include "detect-igmp-type.h" #include "detect-sctphdr.h" +#include "detect-sctp-chunk-type.h" #include "detect-urilen.h" #include "detect-bsize.h" #include "detect-detection-filter.h" @@ -678,6 +679,7 @@ void SigTableSetup(void) DetectIGMPHdrRegister(); DetectIGMPTypeRegister(); DetectSCTPHdrRegister(); + DetectSCTPChunkTypeRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 92a57c89002d..ca77a229c876 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -52,6 +52,7 @@ enum DetectKeywordId { DETECT_IGMPHDR, DETECT_IGMP_TYPE, DETECT_SCTPHDR, + DETECT_SCTP_CHUNK_TYPE, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-sctp-chunk-type.c b/src/detect-sctp-chunk-type.c new file mode 100644 index 000000000000..dda9d91cca12 --- /dev/null +++ b/src/detect-sctp-chunk-type.c @@ -0,0 +1,140 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * Implements sctp.chunk_type keyword support + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine-prefilter-common.h" +#include "detect-engine-build.h" + +#include "detect-sctp-chunk-type.h" +#include "detect-engine-uint.h" + +#include "util-byte.h" +#include "util-debug.h" + +static int DetectSCTPChunkTypeMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static int DetectSCTPChunkTypeSetup(DetectEngineCtx *, Signature *, const char *); +void DetectSCTPChunkTypeFree(DetectEngineCtx *, void *); + +static int PrefilterSetupSCTPChunkType(DetectEngineCtx *de_ctx, SigGroupHead *sgh); +static bool PrefilterSCTPChunkTypeIsPrefilterable(const Signature *s); + +void DetectSCTPChunkTypeRegister(void) +{ + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].name = "sctp.chunk_type"; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].desc = "match on any SCTP chunk type in the packet"; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].url = "/rules/sctp-keywords.html#sctp-chunk-type"; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].Match = DetectSCTPChunkTypeMatch; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].Setup = DetectSCTPChunkTypeSetup; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].Free = DetectSCTPChunkTypeFree; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].flags = + SIGMATCH_INFO_UINT8 | SIGMATCH_INFO_MULTI_UINT | SIGMATCH_INFO_ENUM_UINT; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].SupportsPrefilter = + PrefilterSCTPChunkTypeIsPrefilterable; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].SetupPrefilter = PrefilterSetupSCTPChunkType; +} + +static int DetectSCTPChunkTypeMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!PacketIsSCTP(p)) { + return 0; + } + + const DetectU8Data *data = (const DetectU8Data *)ctx; + const uint8_t cnt = p->l4.vars.sctp.tracked_chunk_cnt; + for (uint8_t i = 0; i < cnt; i++) { + if (DetectU8Match(p->l4.vars.sctp.chunk_types[i], data)) { + return 1; + } + } + return 0; +} + +static int DetectSCTPChunkTypeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + DetectU8Data *data = SCSctpDetectChunkTypeParse(str); + if (data == NULL) + return -1; + + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_SCTP_CHUNK_TYPE, (SigMatchCtx *)data, + DETECT_SM_LIST_MATCH) == NULL) { + DetectSCTPChunkTypeFree(de_ctx, data); + return -1; + } + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + return 0; +} + +void DetectSCTPChunkTypeFree(DetectEngineCtx *de_ctx, void *ptr) +{ + DetectU8Data *data = (DetectU8Data *)ptr; + SCDetectU8Free(data); +} + +static void PrefilterPacketSCTPChunkTypeMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!PacketIsSCTP(p)) { + return; + } + + const PrefilterPacketU8HashCtx *h = pectx; + const uint8_t cnt = p->l4.vars.sctp.tracked_chunk_cnt; + /* bitmap to dedup repeated chunk types within a single packet */ + 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))) { + continue; + } + seen[val >> 5] |= 1U << (val & 0x1F); + const SigsArray *sa = h->array[val]; + if (sa) { + PrefilterAddSids(&det_ctx->pmq, sa->sigs, sa->cnt); + } + } +} + +static int PrefilterSetupSCTPChunkType(DetectEngineCtx *de_ctx, SigGroupHead *sgh) +{ + return PrefilterSetupPacketHeaderU8Hash(de_ctx, sgh, DETECT_SCTP_CHUNK_TYPE, + SIG_MASK_REQUIRE_REAL_PKT, PrefilterPacketU8Set, PrefilterPacketU8Compare, + PrefilterPacketSCTPChunkTypeMatch); +} + +static bool PrefilterSCTPChunkTypeIsPrefilterable(const Signature *s) +{ + return PrefilterIsPrefilterableById(s, DETECT_SCTP_CHUNK_TYPE); +} diff --git a/src/detect-sctp-chunk-type.h b/src/detect-sctp-chunk-type.h new file mode 100644 index 000000000000..b3d2a874fbee --- /dev/null +++ b/src/detect-sctp-chunk-type.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_CHUNK_TYPE_H +#define SURICATA_DETECT_SCTP_CHUNK_TYPE_H + +void DetectSCTPChunkTypeRegister(void); + +#endif /* SURICATA_DETECT_SCTP_CHUNK_TYPE_H */ From 25b8db34ef778831eb8608219430dfdb65cde2fe Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Tue, 24 Mar 2026 10:35:30 +0100 Subject: [PATCH 07/18] detect/sctp: add sctp.chunk_cnt keyword Add a U8 numeric keyword to match the number of SCTP chunks parsed in a packet with prefilter support. Ticket #4251 --- src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-sctp-chunk-cnt.c | 122 +++++++++++++++++++++++++++++++++++ src/detect-sctp-chunk-cnt.h | 27 ++++++++ 5 files changed, 154 insertions(+) create mode 100644 src/detect-sctp-chunk-cnt.c create mode 100644 src/detect-sctp-chunk-cnt.h diff --git a/src/Makefile.am b/src/Makefile.am index 35d5ce145f48..2bc3c58d5073 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -229,6 +229,7 @@ noinst_HEADERS = \ detect-igmphdr.h \ detect-igmp-type.h \ detect-sctphdr.h \ + detect-sctp-chunk-cnt.h \ detect-sctp-chunk-type.h \ detect-ipaddr.h \ detect-ipopts.h \ @@ -807,6 +808,7 @@ libsuricata_c_a_SOURCES = \ detect-igmphdr.c \ detect-igmp-type.c \ detect-sctphdr.c \ + detect-sctp-chunk-cnt.c \ detect-sctp-chunk-type.c \ detect-ipaddr.c \ detect-ipopts.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 6bc14dfe0bed..135795d3584f 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -157,6 +157,7 @@ #include "detect-igmp-type.h" #include "detect-sctphdr.h" #include "detect-sctp-chunk-type.h" +#include "detect-sctp-chunk-cnt.h" #include "detect-urilen.h" #include "detect-bsize.h" #include "detect-detection-filter.h" @@ -680,6 +681,7 @@ void SigTableSetup(void) DetectIGMPTypeRegister(); DetectSCTPHdrRegister(); DetectSCTPChunkTypeRegister(); + DetectSCTPChunkCntRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index ca77a229c876..814643b04e33 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -53,6 +53,7 @@ enum DetectKeywordId { DETECT_IGMP_TYPE, DETECT_SCTPHDR, DETECT_SCTP_CHUNK_TYPE, + DETECT_SCTP_CHUNK_CNT, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-sctp-chunk-cnt.c b/src/detect-sctp-chunk-cnt.c new file mode 100644 index 000000000000..cd54e6fda46f --- /dev/null +++ b/src/detect-sctp-chunk-cnt.c @@ -0,0 +1,122 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * Implements sctp.chunk_cnt keyword + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine-prefilter-common.h" +#include "detect-engine-build.h" + +#include "detect-sctp-chunk-cnt.h" +#include "detect-engine-uint.h" + +#include "util-byte.h" +#include "util-debug.h" + +static int DetectSCTPChunkCntMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static int DetectSCTPChunkCntSetup(DetectEngineCtx *, Signature *, const char *); +void DetectSCTPChunkCntFree(DetectEngineCtx *, void *); + +static int PrefilterSetupSCTPChunkCnt(DetectEngineCtx *de_ctx, SigGroupHead *sgh); +static bool PrefilterSCTPChunkCntIsPrefilterable(const Signature *s); + +void DetectSCTPChunkCntRegister(void) +{ + sigmatch_table[DETECT_SCTP_CHUNK_CNT].name = "sctp.chunk_cnt"; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].desc = "match on the SCTP chunk count"; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].url = "/rules/sctp-keywords.html#sctp-chunk-cnt"; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].Match = DetectSCTPChunkCntMatch; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].Setup = DetectSCTPChunkCntSetup; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].Free = DetectSCTPChunkCntFree; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].flags = SIGMATCH_INFO_UINT8; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].SupportsPrefilter = PrefilterSCTPChunkCntIsPrefilterable; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].SetupPrefilter = PrefilterSetupSCTPChunkCnt; +} + +static int DetectSCTPChunkCntMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!PacketIsSCTP(p)) { + return 0; + } + + uint8_t val = p->l4.vars.sctp.chunk_cnt; + const DetectU8Data *data = (const DetectU8Data *)ctx; + return DetectU8Match(val, data); +} + +static int DetectSCTPChunkCntSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + DetectU8Data *data = DetectU8Parse(str); + if (data == NULL) + return -1; + + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_SCTP_CHUNK_CNT, (SigMatchCtx *)data, + DETECT_SM_LIST_MATCH) == NULL) { + DetectSCTPChunkCntFree(de_ctx, data); + return -1; + } + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + return 0; +} + +void DetectSCTPChunkCntFree(DetectEngineCtx *de_ctx, void *ptr) +{ + DetectU8Data *data = (DetectU8Data *)ptr; + SCDetectU8Free(data); +} + +static void PrefilterPacketSCTPChunkCntMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (PacketIsSCTP(p)) { + uint8_t val = p->l4.vars.sctp.chunk_cnt; + const PrefilterPacketU8HashCtx *h = pectx; + const SigsArray *sa = h->array[val]; + if (sa) { + PrefilterAddSids(&det_ctx->pmq, sa->sigs, sa->cnt); + } + } +} + +static int PrefilterSetupSCTPChunkCnt(DetectEngineCtx *de_ctx, SigGroupHead *sgh) +{ + return PrefilterSetupPacketHeaderU8Hash(de_ctx, sgh, DETECT_SCTP_CHUNK_CNT, + SIG_MASK_REQUIRE_REAL_PKT, PrefilterPacketU8Set, PrefilterPacketU8Compare, + PrefilterPacketSCTPChunkCntMatch); +} + +static bool PrefilterSCTPChunkCntIsPrefilterable(const Signature *s) +{ + return PrefilterIsPrefilterableById(s, DETECT_SCTP_CHUNK_CNT); +} diff --git a/src/detect-sctp-chunk-cnt.h b/src/detect-sctp-chunk-cnt.h new file mode 100644 index 000000000000..1c9e7620b60b --- /dev/null +++ b/src/detect-sctp-chunk-cnt.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_CHUNK_CNT_H +#define SURICATA_DETECT_SCTP_CHUNK_CNT_H + +void DetectSCTPChunkCntRegister(void); + +#endif /* SURICATA_DETECT_SCTP_CHUNK_CNT_H */ From cdb79dc2520af969e8b3f645ff5cc49a23cab78e Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Tue, 24 Mar 2026 10:41:02 +0100 Subject: [PATCH 08/18] detect/sctp: add sctp.vtag keyword Add a U32 numeric keyword to match the SCTP verification tag from the common header with prefilter support. Ticket #4251 --- src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-sctp-vtag.c | 129 +++++++++++++++++++++++++++++++++++ src/detect-sctp-vtag.h | 27 ++++++++ 5 files changed, 161 insertions(+) create mode 100644 src/detect-sctp-vtag.c create mode 100644 src/detect-sctp-vtag.h diff --git a/src/Makefile.am b/src/Makefile.am index 2bc3c58d5073..325d6d12d01e 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -231,6 +231,7 @@ noinst_HEADERS = \ detect-sctphdr.h \ detect-sctp-chunk-cnt.h \ detect-sctp-chunk-type.h \ + detect-sctp-vtag.h \ detect-ipaddr.h \ detect-ipopts.h \ detect-ipproto.h \ @@ -810,6 +811,7 @@ libsuricata_c_a_SOURCES = \ detect-sctphdr.c \ detect-sctp-chunk-cnt.c \ detect-sctp-chunk-type.c \ + detect-sctp-vtag.c \ detect-ipaddr.c \ detect-ipopts.c \ detect-ipproto.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 135795d3584f..d73076ae9fd7 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -158,6 +158,7 @@ #include "detect-sctphdr.h" #include "detect-sctp-chunk-type.h" #include "detect-sctp-chunk-cnt.h" +#include "detect-sctp-vtag.h" #include "detect-urilen.h" #include "detect-bsize.h" #include "detect-detection-filter.h" @@ -682,6 +683,7 @@ void SigTableSetup(void) DetectSCTPHdrRegister(); DetectSCTPChunkTypeRegister(); DetectSCTPChunkCntRegister(); + DetectSCTPVtagRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 814643b04e33..d8e3e1151539 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -54,6 +54,7 @@ enum DetectKeywordId { DETECT_SCTPHDR, DETECT_SCTP_CHUNK_TYPE, DETECT_SCTP_CHUNK_CNT, + DETECT_SCTP_VTAG, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-sctp-vtag.c b/src/detect-sctp-vtag.c new file mode 100644 index 000000000000..f0a1983f8fff --- /dev/null +++ b/src/detect-sctp-vtag.c @@ -0,0 +1,129 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * Implements sctp.vtag keyword + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-prefilter.h" +#include "detect-engine-prefilter-common.h" +#include "detect-engine-build.h" +#include "detect-engine-uint.h" + +#include "detect-sctp-vtag.h" + +#include "util-debug.h" + +static int DetectSCTPVtagSetup(DetectEngineCtx *, Signature *, const char *); +static int DetectSCTPVtagMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static void DetectSCTPVtagFree(DetectEngineCtx *, void *); +static int PrefilterSetupSCTPVtag(DetectEngineCtx *de_ctx, SigGroupHead *sgh); +static bool PrefilterSCTPVtagIsPrefilterable(const Signature *s); + +#ifdef UNITTESTS +void DetectSCTPVtagRegisterTests(void); +#endif + +void DetectSCTPVtagRegister(void) +{ + sigmatch_table[DETECT_SCTP_VTAG].name = "sctp.vtag"; + sigmatch_table[DETECT_SCTP_VTAG].desc = "match on the SCTP verification tag"; + sigmatch_table[DETECT_SCTP_VTAG].url = "/rules/sctp-keywords.html#sctp-vtag"; + sigmatch_table[DETECT_SCTP_VTAG].Match = DetectSCTPVtagMatch; + sigmatch_table[DETECT_SCTP_VTAG].Setup = DetectSCTPVtagSetup; + sigmatch_table[DETECT_SCTP_VTAG].Free = DetectSCTPVtagFree; + sigmatch_table[DETECT_SCTP_VTAG].flags = SIGMATCH_INFO_UINT32; + sigmatch_table[DETECT_SCTP_VTAG].SupportsPrefilter = PrefilterSCTPVtagIsPrefilterable; + sigmatch_table[DETECT_SCTP_VTAG].SetupPrefilter = PrefilterSetupSCTPVtag; +} + +static int DetectSCTPVtagMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU32Data *data = (const DetectU32Data *)ctx; + + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!(PacketIsSCTP(p))) { + return 0; + } + + return DetectU32Match(SCTP_GET_RAW_VTAG(PacketGetSCTP(p)), data); +} + +static int DetectSCTPVtagSetup(DetectEngineCtx *de_ctx, Signature *s, const char *optstr) +{ + DetectU32Data *data = SCDetectU32Parse(optstr); + if (data == NULL) + return -1; + + if (SCSigMatchAppendSMToList( + de_ctx, s, DETECT_SCTP_VTAG, (SigMatchCtx *)data, DETECT_SM_LIST_MATCH) == NULL) { + DetectSCTPVtagFree(de_ctx, data); + return -1; + } + s->flags |= SIG_FLAG_REQUIRE_PACKET; + return 0; +} + +static void DetectSCTPVtagFree(DetectEngineCtx *de_ctx, void *ptr) +{ + SCDetectU32Free(ptr); +} + +static void PrefilterPacketSCTPVtagMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + const PrefilterPacketHeaderCtx *ctx = pectx; + + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + if (!PrefilterPacketHeaderExtraMatch(ctx, p)) + return; + + if (p->proto == IPPROTO_SCTP && PacketIsSCTP(p)) { + DetectU32Data du32; + du32.mode = ctx->v1.u8[0]; + du32.arg1 = ctx->v1.u32[1]; + du32.arg2 = ctx->v1.u32[2]; + if (DetectU32Match(SCTP_GET_RAW_VTAG(PacketGetSCTP(p)), &du32)) { + SCLogDebug("packet matches SCTP vtag %u", ctx->v1.u32[0]); + PrefilterAddSids(&det_ctx->pmq, ctx->sigs_array, ctx->sigs_cnt); + } + } +} + +static int PrefilterSetupSCTPVtag(DetectEngineCtx *de_ctx, SigGroupHead *sgh) +{ + return PrefilterSetupPacketHeader(de_ctx, sgh, DETECT_SCTP_VTAG, SIG_MASK_REQUIRE_REAL_PKT, + PrefilterPacketU32Set, PrefilterPacketU32Compare, PrefilterPacketSCTPVtagMatch); +} + +static bool PrefilterSCTPVtagIsPrefilterable(const Signature *s) +{ + return PrefilterIsPrefilterableById(s, DETECT_SCTP_VTAG); +} diff --git a/src/detect-sctp-vtag.h b/src/detect-sctp-vtag.h new file mode 100644 index 000000000000..a9decaa1224b --- /dev/null +++ b/src/detect-sctp-vtag.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_VTAG_H +#define SURICATA_DETECT_SCTP_VTAG_H + +void DetectSCTPVtagRegister(void); + +#endif /* SURICATA_DETECT_SCTP_VTAG_H */ From a856a085248161711bfefa0a29a50e3820ac3593 Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Tue, 24 Mar 2026 13:39:02 +0100 Subject: [PATCH 09/18] output/json: add sctp metadata to alerts Log SCTP-specific fields in the EVE JSON "sctp" object for alert events. Ticket #4251 --- etc/schema.json | 53 +++++++++++++++++++++++++++++++++++++++++++++++ src/output-json.c | 26 +++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/etc/schema.json b/etc/schema.json index a9d6afdd3065..c3ec340679b4 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -5641,6 +5641,59 @@ } } }, + "sctp": { + "type": "object", + "description": "SCTP protocol information", + "additionalProperties": false, + "properties": { + "chunk_cnt": { + "type": "integer", + "description": "Number of SCTP chunks in the packet", + "suricata": { + "keywords": [ + "sctp.chunk_cnt" + ] + } + }, + "chunk_types": { + "type": "array", + "description": "Array of SCTP chunk type names present in the packet", + "items": { + "type": "string" + }, + "suricata": { + "keywords": [ + "sctp.chunk_type" + ] + } + }, + "has_abort": { + "type": "boolean", + "description": "Whether the packet contains an ABORT chunk" + }, + "has_data": { + "type": "boolean", + "description": "Whether the packet contains a DATA chunk" + }, + "has_init": { + "type": "boolean", + "description": "Whether the packet contains an INIT chunk" + }, + "has_init_ack": { + "type": "boolean", + "description": "Whether the packet contains an INIT_ACK chunk" + }, + "vtag": { + "type": "integer", + "description": "SCTP verification tag", + "suricata": { + "keywords": [ + "sctp.vtag" + ] + } + } + } + }, "sip": { "type": "object", "additionalProperties": false, diff --git a/src/output-json.c b/src/output-json.c index f1a4a29cecc9..b6c6436ec938 100644 --- a/src/output-json.c +++ b/src/output-json.c @@ -925,6 +925,32 @@ SCJsonBuilder *CreateEveHeader(const Packet *p, enum SCOutputJsonLogDirection di } } break; + case IPPROTO_SCTP: + if (PacketIsSCTP(p)) { + 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 = p->l4.vars.sctp.tracked_chunk_cnt; + SCJbOpenArray(js, "chunk_types"); + for (uint8_t i = 0; i < cnt; i++) { + const char *name = SCSctpChunkTypeToString(p->l4.vars.sctp.chunk_types[i]); + if (name) { + SCJbAppendString(js, name); + } else { + char unknown[16]; + snprintf(unknown, sizeof(unknown), "unknown(%u)", + p->l4.vars.sctp.chunk_types[i]); + SCJbAppendString(js, unknown); + } + } + SCJbClose(js); + SCJbSetBool(js, "has_init", p->l4.vars.sctp.has_init); + SCJbSetBool(js, "has_init_ack", p->l4.vars.sctp.has_init_ack); + SCJbSetBool(js, "has_data", p->l4.vars.sctp.has_data); + SCJbSetBool(js, "has_abort", p->l4.vars.sctp.has_abort); + SCJbClose(js); + } + break; } SCJbSetString(js, "pkt_src", PktSrcToString(p->pkt_src)); From 8744750092fc523846201572816a61266af82321 Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Tue, 24 Mar 2026 16:59:58 +0100 Subject: [PATCH 10/18] decode/sctp: set p->payload to data chunk 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 #4251 --- src/decode-sctp.c | 219 ++++++++++++++++++++++++++++++++++++++++++++++ src/decode-sctp.h | 9 ++ 2 files changed, 228 insertions(+) diff --git a/src/decode-sctp.c b/src/decode-sctp.c index 311fcb952812..48df9e1b4c93 100644 --- a/src/decode-sctp.c +++ b/src/decode-sctp.c @@ -64,6 +64,7 @@ static int DecodeSCTPChunks(Packet *p, const uint8_t *pkt, uint16_t len) bool has_init_ack = false; bool has_data = false; bool has_abort = false; + uint8_t data_chunk_cnt = 0; int ret = 0; while (offset < len) { @@ -112,6 +113,15 @@ static int DecodeSCTPChunks(Packet *p, const uint8_t *pkt, uint16_t len) 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) { + p->l4.vars.sctp.data_offsets[data_chunk_cnt] = + (uint16_t)(SCTP_HEADER_LEN + offset + SCTP_DATA_CHUNK_HDR_LEN); + p->l4.vars.sctp.data_lens[data_chunk_cnt] = chunk_len - SCTP_DATA_CHUNK_HDR_LEN; + data_chunk_cnt++; + } else if (data_chunk_cnt == SCTP_MAX_DATA_CHUNKS && + chunk_len >= SCTP_DATA_CHUNK_HDR_LEN) { + ENGINE_SET_EVENT(p, SCTP_TOO_MANY_DATA_CHUNKS); + } has_data = true; /* DATA chunks must not have vtag == 0 */ if (vtag == 0) { @@ -143,6 +153,7 @@ static int DecodeSCTPChunks(Packet *p, const uint8_t *pkt, uint16_t len) 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.data_chunk_cnt = data_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; @@ -173,6 +184,16 @@ static int DecodeSCTPPacket(ThreadVars *tv, Packet *p, const uint8_t *pkt, uint1 p->payload_len = 0; return -1; } + + if (p->l4.vars.sctp.data_chunk_cnt > 0) { + /* Point p->payload at the user data of the first DATA chunk, + * past the SCTP common header and the 16-byte DATA chunk + * header. This is what content will inspect. */ + p->payload = (uint8_t *)pkt + p->l4.vars.sctp.data_offsets[0]; + p->payload_len = p->l4.vars.sctp.data_lens[0]; + } else { + p->payload_len = 0; + } } else { p->l4.vars.sctp.hlen = SCTP_HEADER_LEN; } @@ -463,6 +484,199 @@ static int SCTPDecodeInitNotAloneTest07(void) PASS; } +/** \test DATA chunk payload extraction - p->payload points to user data */ +static int SCTPDecodeDataPayloadTest08(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 (16 hdr + 4 data) */ + 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* TSN=1 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + }; + // 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->payload_len != 4); + FAIL_IF(memcmp(p->payload, "ABCD", 4) != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test INIT-only packet - payload_len must be 0 (no application data) */ +static int SCTPDecodeNoDataPayloadTest09(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 */ + 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->payload_len != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Verify data_offset and data_len in SCTPVars */ +static int SCTPDecodeDataOffsetTest10(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 (16 hdr + 4 data) */ + 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* TSN=1 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + }; + // 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)); + + /* data_offsets[0] = SCTP_HEADER_LEN(12) + chunk_offset(0) + DATA_CHUNK_HDR_LEN(16) = 28 */ + FAIL_IF(p->l4.vars.sctp.data_chunk_cnt != 1); + FAIL_IF(p->l4.vars.sctp.data_offsets[0] != 28); + FAIL_IF(p->l4.vars.sctp.data_lens[0] != 4); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Two DATA chunks - both tracked, p->payload is first DATA's user data */ +static int SCTPDecodeMultiDataTest11(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 1: type=0x00, flags=0x02, len=20 (16 hdr + 4 data) */ + 0x00, 0x02, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* TSN=1 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + /* DATA chunk 2: type=0x00, flags=0x01, len=20 (16 hdr + 4 data) */ + 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, /* TSN=2 */ + 0x00, 0x01, 0x00, 0x01, /* stream_id=1, stream_seq=1 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x45, 0x46, 0x47, 0x48, /* data="EFGH" */ + }; + // 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.data_chunk_cnt != 2); + /* first DATA chunk: offset = 12 + 0 + 16 = 28, len = 4 */ + FAIL_IF(p->l4.vars.sctp.data_offsets[0] != 28); + FAIL_IF(p->l4.vars.sctp.data_lens[0] != 4); + /* second DATA chunk: offset = 12 + 20 + 16 = 48, len = 4 */ + FAIL_IF(p->l4.vars.sctp.data_offsets[1] != 48); + FAIL_IF(p->l4.vars.sctp.data_lens[1] != 4); + /* p->payload still points to first DATA's user data */ + FAIL_IF(p->payload_len != 4); + FAIL_IF(memcmp(p->payload, "ABCD", 4) != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test DATA chunk with chunk_len < 16 - malformed, payload_len must be 0 */ +static int SCTPDecodeSmallDataChunkTest12(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=0x00, len=8 (< 16, malformed) */ + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x00, /* 4 bytes of value (not enough for DATA header) */ + }; + // 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.data_chunk_cnt != 0); + FAIL_IF(p->payload_len != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + #endif /* UNITTESTS */ void DecodeSCTPRegisterTests(void) @@ -475,6 +689,11 @@ void DecodeSCTPRegisterTests(void) UtRegisterTest("SCTPDecodeInitNonZeroVtagTest05", SCTPDecodeInitNonZeroVtagTest05); UtRegisterTest("SCTPDecodeMultiChunkTest06", SCTPDecodeMultiChunkTest06); UtRegisterTest("SCTPDecodeInitNotAloneTest07", SCTPDecodeInitNotAloneTest07); + UtRegisterTest("SCTPDecodeDataPayloadTest08", SCTPDecodeDataPayloadTest08); + UtRegisterTest("SCTPDecodeNoDataPayloadTest09", SCTPDecodeNoDataPayloadTest09); + UtRegisterTest("SCTPDecodeDataOffsetTest10", SCTPDecodeDataOffsetTest10); + UtRegisterTest("SCTPDecodeMultiDataTest11", SCTPDecodeMultiDataTest11); + UtRegisterTest("SCTPDecodeSmallDataChunkTest12", SCTPDecodeSmallDataChunkTest12); #endif } /** diff --git a/src/decode-sctp.h b/src/decode-sctp.h index 97d219dd03d4..c6e6b45323d5 100644 --- a/src/decode-sctp.h +++ b/src/decode-sctp.h @@ -38,6 +38,12 @@ * though in practice most packets contain only a few chunks. */ #define SCTP_MAX_TRACKED_CHUNKS 16 +/** max number of DATA chunk payloads tracked per packet */ +#define SCTP_MAX_DATA_CHUNKS 16 + +/** DATA chunk overhead before user data (chunk hdr + TSN + SID + SSN + PPID) */ +#define SCTP_DATA_CHUNK_HDR_LEN 16 + /* SCTP chunk types (RFC 4960 sec 3.2) */ #define SCTP_CHUNK_TYPE_DATA 0x00 #define SCTP_CHUNK_TYPE_INIT 0x01 @@ -74,10 +80,13 @@ typedef struct SCTPVars_ { 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 */ + uint8_t data_chunk_cnt; /**< number of DATA chunk payloads tracked */ bool has_init : 1; bool has_init_ack : 1; bool has_data : 1; bool has_abort : 1; + uint16_t data_offsets[SCTP_MAX_DATA_CHUNKS]; /**< offsets of DATA user data from L4 start */ + uint16_t data_lens[SCTP_MAX_DATA_CHUNKS]; /**< lengths of DATA user data */ } SCTPVars; #define SCTP_GET_RAW_SRC_PORT(sctph) SCNtohs((sctph)->sh_sport) From c7c83020198b44a43b7deb36ca2c6370b1b306ad Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Thu, 26 Mar 2026 11:11:07 +0100 Subject: [PATCH 11/18] detect/sctp: add sctp.chunk_data sticky buffer Add a sctp.data sticky multi-buffer that allows content matching on the bytes inside any of the SCTP DATA chunks extracted. Ticket #4251 --- src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-sctp-chunk-data.c | 220 +++++++++++++++++++++++++++++++++++ src/detect-sctp-chunk-data.h | 27 +++++ 5 files changed, 252 insertions(+) create mode 100644 src/detect-sctp-chunk-data.c create mode 100644 src/detect-sctp-chunk-data.h diff --git a/src/Makefile.am b/src/Makefile.am index 325d6d12d01e..7d6d0ebf0c02 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -231,6 +231,7 @@ noinst_HEADERS = \ detect-sctphdr.h \ detect-sctp-chunk-cnt.h \ detect-sctp-chunk-type.h \ + detect-sctp-chunk-data.h \ detect-sctp-vtag.h \ detect-ipaddr.h \ detect-ipopts.h \ @@ -811,6 +812,7 @@ libsuricata_c_a_SOURCES = \ detect-sctphdr.c \ detect-sctp-chunk-cnt.c \ detect-sctp-chunk-type.c \ + detect-sctp-chunk-data.c \ detect-sctp-vtag.c \ detect-ipaddr.c \ detect-ipopts.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index d73076ae9fd7..b37cd61f8466 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -159,6 +159,7 @@ #include "detect-sctp-chunk-type.h" #include "detect-sctp-chunk-cnt.h" #include "detect-sctp-vtag.h" +#include "detect-sctp-chunk-data.h" #include "detect-urilen.h" #include "detect-bsize.h" #include "detect-detection-filter.h" @@ -684,6 +685,7 @@ void SigTableSetup(void) DetectSCTPChunkTypeRegister(); DetectSCTPChunkCntRegister(); DetectSCTPVtagRegister(); + DetectSCTPChunkDataRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index d8e3e1151539..822b56a6e407 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -55,6 +55,7 @@ enum DetectKeywordId { DETECT_SCTP_CHUNK_TYPE, DETECT_SCTP_CHUNK_CNT, DETECT_SCTP_VTAG, + DETECT_SCTP_CHUNK_DATA, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-sctp-chunk-data.c b/src/detect-sctp-chunk-data.c new file mode 100644 index 000000000000..3d1cbbb5fe82 --- /dev/null +++ b/src/detect-sctp-chunk-data.c @@ -0,0 +1,220 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * Implements sctp.chunk_data multi-buffer sticky buffer. + * + * Each SCTP DATA chunk payload in the packet is inspected as a + * separate buffer instance (not reassembled). + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" + +#include "detect.h" +#include "detect-engine.h" +#include "detect-engine-buffer.h" +#include "detect-engine-content-inspection.h" +#include "detect-engine-inspect-buffer.h" +#include "detect-engine-mpm.h" +#include "detect-engine-prefilter.h" +#include "detect-sctp-chunk-data.h" +#include "util-mpm.h" +#include "util-profiling.h" + +static int DetectSCTPChunkDataSetup(DetectEngineCtx *, Signature *, const char *); + +static int g_buffer_id = 0; + +/** + * \brief Get a multi-instance inspection buffer for a specific DATA chunk. + * + * \param det_ctx detection engine thread context + * \param transforms transforms to apply + * \param p packet + * \param list_id buffer list id + * \param local_id multi-instance buffer index and index into SCTPVars data_offsets/data_lens + * + * \retval buffer or NULL + */ +static InspectionBuffer *GetBuffer(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Packet *p, const int list_id, + const uint32_t local_id) +{ + DEBUG_VALIDATE_BUG_ON(local_id >= SCTP_MAX_DATA_CHUNKS); + + InspectionBuffer *buffer = InspectionBufferMultipleForListGet(det_ctx, list_id, local_id); + if (buffer == NULL) + return NULL; + if (buffer->initialized) + return buffer; + + const uint16_t offset = p->l4.vars.sctp.data_offsets[(uint8_t)local_id]; + const uint16_t len = p->l4.vars.sctp.data_lens[(uint8_t)local_id]; + if (len == 0) { + InspectionBufferSetupMultiEmpty(buffer); + return NULL; + } + + const uint8_t *data = (const uint8_t *)PacketGetSCTP(p) + offset; + if ((data + (ptrdiff_t)len) > ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))) { + SCLogDebug("data out of range: %p > %p", (data + (ptrdiff_t)len), + ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))); + InspectionBufferSetupMultiEmpty(buffer); + return NULL; + } + + InspectionBufferSetupMulti(det_ctx, buffer, transforms, data, len); + return buffer; +} + +/** + * \brief Custom packet inspection callback for sctp.chunk_data. + * + * Loops over all tracked DATA chunks, inspecting each as a separate buffer. + */ +static int DetectEngineInspectSCTPChunkData(DetectEngineThreadCtx *det_ctx, + const DetectEnginePktInspectionEngine *engine, const Signature *s, Packet *p, + uint8_t *_alert_flags) +{ + if (!PacketIsSCTP(p)) + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + + const uint8_t cnt = p->l4.vars.sctp.data_chunk_cnt; + if (cnt == 0) + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + + const int list_id = engine->sm_list; + const DetectEngineTransforms *transforms = NULL; + if (!engine->mpm) { + transforms = engine->v1.transforms; + } + + for (uint8_t i = 0; i < cnt; i++) { + InspectionBuffer *buffer = GetBuffer(det_ctx, transforms, p, list_id, (uint32_t)i); + if (buffer == NULL || buffer->inspect == NULL) + continue; + + if (DetectEngineContentInspectionBuffer(det_ctx->de_ctx, det_ctx, s, engine->smd, p, + p->flow, buffer, DETECT_ENGINE_CONTENT_INSPECTION_MODE_HEADER)) { + return DETECT_ENGINE_INSPECT_SIG_MATCH; + } + } + + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; +} + +typedef struct PrefilterMpmSCTPChunkData { + int list_id; + const MpmCtx *mpm_ctx; + const DetectEngineTransforms *transforms; +} PrefilterMpmSCTPChunkData; + +/** + * \brief Prefilter callback: run MPM on each DATA chunk buffer. + */ +static void PrefilterMpmSCTPChunkDataPkt( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + if (!PacketIsSCTP(p)) + return; + + const uint8_t cnt = p->l4.vars.sctp.data_chunk_cnt; + if (cnt == 0) + return; + + const PrefilterMpmSCTPChunkData *ctx = (const PrefilterMpmSCTPChunkData *)pectx; + const MpmCtx *mpm_ctx = ctx->mpm_ctx; + const int list_id = ctx->list_id; + + for (uint8_t i = 0; i < cnt; i++) { + InspectionBuffer *buffer = GetBuffer(det_ctx, ctx->transforms, p, list_id, (uint32_t)i); + if (buffer == NULL || buffer->inspect == NULL) + continue; + + if (buffer->inspect_len >= mpm_ctx->minlen) { + (void)mpm_table[mpm_ctx->mpm_type].Search( + mpm_ctx, &det_ctx->mtc, &det_ctx->pmq, buffer->inspect, buffer->inspect_len); + PREFILTER_PROFILING_ADD_BYTES(det_ctx, buffer->inspect_len); + } + } +} + +static void PrefilterMpmSCTPChunkDataFree(void *ptr) +{ + SCFree(ptr); +} + +static int PrefilterSCTPChunkDataRegister(DetectEngineCtx *de_ctx, SigGroupHead *sgh, + MpmCtx *mpm_ctx, const DetectBufferMpmRegistry *mpm_reg, int list_id) +{ + PrefilterMpmSCTPChunkData *pectx = SCCalloc(1, sizeof(*pectx)); + if (pectx == NULL) + return -1; + pectx->list_id = list_id; + pectx->mpm_ctx = mpm_ctx; + pectx->transforms = &mpm_reg->transforms; + + return PrefilterAppendEngine(de_ctx, sgh, PrefilterMpmSCTPChunkDataPkt, 0, + SIGNATURE_HOOK_PKT_NOT_SET, pectx, PrefilterMpmSCTPChunkDataFree, mpm_reg->pname); +} + +void DetectSCTPChunkDataRegister(void) +{ + sigmatch_table[DETECT_SCTP_CHUNK_DATA].name = "sctp.chunk_data"; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].desc = + "sticky buffer to match on each SCTP DATA chunk payload"; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].url = "/rules/sctp-keywords.html#sctp-chunk-data"; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].Setup = DetectSCTPChunkDataSetup; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; + + g_buffer_id = DetectBufferTypeRegister("sctp.chunk_data"); + BUG_ON(g_buffer_id < 0); + + DetectBufferTypeSupportsPacket("sctp.chunk_data"); + DetectBufferTypeSupportsMultiInstance("sctp.chunk_data"); + + DetectPktMpmRegister("sctp.chunk_data", 2, PrefilterSCTPChunkDataRegister, NULL); + + DetectPktInspectEngineRegister("sctp.chunk_data", NULL, DetectEngineInspectSCTPChunkData); +} + +/** + * \brief setup sctp.chunk_data sticky buffer + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the current Signature + * \param _unused unused + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectSCTPChunkDataSetup(DetectEngineCtx *de_ctx, Signature *s, const char *_unused) +{ + if (!(DetectProtoContainsProto(s->proto, IPPROTO_SCTP))) + return -1; + + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + if (SCDetectBufferSetActiveList(de_ctx, s, g_buffer_id) < 0) + return -1; + + return 0; +} diff --git a/src/detect-sctp-chunk-data.h b/src/detect-sctp-chunk-data.h new file mode 100644 index 000000000000..e66d012ec0b7 --- /dev/null +++ b/src/detect-sctp-chunk-data.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_CHUNK_DATA_H +#define SURICATA_DETECT_SCTP_CHUNK_DATA_H + +void DetectSCTPChunkDataRegister(void); + +#endif /* SURICATA_DETECT_SCTP_CHUNK_DATA_H */ From 4b3d97cfd5ac92e5b00c5b462dcd8c7e2a5ecc60 Mon Sep 17 00:00:00 2001 From: Giuseppe Longo Date: Thu, 26 Mar 2026 11:25:21 +0100 Subject: [PATCH 12/18] doc/sctp: add sctp keywords Add documentation for all sctp keywords. Ticket #4251 --- doc/userguide/rules/index.rst | 1 + doc/userguide/rules/sctp-keywords.rst | 150 ++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 doc/userguide/rules/sctp-keywords.rst diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index 04ea736c9d23..b8d21f0c5916 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -33,6 +33,7 @@ Suricata Rules base64-keywords sip-keywords sdp-keywords + sctp-keywords rfb-keywords mqtt-keywords ike-keywords diff --git a/doc/userguide/rules/sctp-keywords.rst b/doc/userguide/rules/sctp-keywords.rst new file mode 100644 index 000000000000..2220c4363f6e --- /dev/null +++ b/doc/userguide/rules/sctp-keywords.rst @@ -0,0 +1,150 @@ +.. role:: example-rule-emphasis + +SCTP Keywords +============= + +Suricata supports sticky buffers and keywords for matching on SCTP +packet headers, chunks, and metadata. + +Sticky buffers are expected to be followed by one or more +:doc:`payload-keywords`. + +sctp.hdr +-------- + +Sticky buffer to match on the raw SCTP header and all chunks. + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP header match"; :example-rule-emphasis:`sctp.hdr; content:"|01|"; offset:8; depth:1;` sid:1; rev:1;) + +``sctp.hdr`` is a 'sticky buffer'. + +``sctp.hdr`` can be used as ``fast_pattern``. + +sctp.chunk_data +--------------- + +Sticky buffer to match on any SCTP DATA chunk user payload. + +When a packet contains DATA chunks, the packet payload (``p->payload``) +is set to the user data of the first DATA chunk. A bare ``content`` +match (without a sticky buffer) therefore inspects the first DATA +chunk's payload. Use ``sctp.chunk_data`` to inspect all DATA chunks +independently. + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP DATA payload match"; :example-rule-emphasis:`sctp.chunk_data; content:"test";` sid:2; rev:1;) + +``sctp.chunk_data`` is a 'sticky buffer'. + +``sctp.chunk_data`` can be used as ``fast_pattern``. + +sctp.vtag +--------- + +Match on the SCTP verification tag field in the common header. + +sctp.vtag uses an :ref:`unsigned 32-bit integer `. + +Syntax:: + + sctp.vtag:[op] + +The verification tag can be matched exactly, or compared using the _op_ setting:: + + sctp.vtag:12345 # exactly 12345 + sctp.vtag:>0 # greater than 0 + sctp.vtag:100-200 # range 100 to 200 + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP vtag match"; :example-rule-emphasis:`sctp.vtag:0;` sid:3; rev:1;) + +sctp.chunk_type +--------------- + +Match on the type of any SCTP chunk in the packet. + +sctp.chunk_type uses an :ref:`unsigned 8-bit integer `. + +Syntax:: + + sctp.chunk_type:[!] + sctp.chunk_type:[op] + +Values can be specified by name or by numeric value. The following +named chunk types are supported: + +================= ===== +Name Value +================= ===== +data 0 +init 1 +init_ack 2 +sack 3 +heartbeat 4 +hb_ack 5 +abort 6 +shutdown 7 +shutdown_ack 8 +error 9 +cookie_echo 10 +cookie_ack 11 +ecne 12 +cwr 13 +shutdown_complete 14 +forward_tsn 192 +================= ===== + +Named values are case-insensitive and can be negated with ``!``:: + + sctp.chunk_type:init # INIT chunk + sctp.chunk_type:init_ack # INIT ACK chunk + sctp.chunk_type:!data # any chunk that is not DATA + +Numeric values support comparison operators and ranges:: + + sctp.chunk_type:1 # INIT chunk (type 1) + sctp.chunk_type:0-4 # range 0 to 4 + +Example rules: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP INIT chunk detected"; :example-rule-emphasis:`sctp.chunk_type:init;` sid:4; rev:1;) + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP INIT chunk detected"; :example-rule-emphasis:`sctp.chunk_type:1;` sid:5; rev:1;) + +sctp.chunk_cnt +-------------- + +Match on the number of SCTP chunks in the packet. + +sctp.chunk_cnt uses an :ref:`unsigned 8-bit integer `. + +Syntax:: + + sctp.chunk_cnt:[op] + +The chunk count can be matched exactly, or compared using the _op_ setting:: + + sctp.chunk_cnt:1 # exactly 1 chunk + sctp.chunk_cnt:>3 # more than 3 chunks + sctp.chunk_cnt:2-5 # range 2 to 5 + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP packet with multiple chunks"; :example-rule-emphasis:`sctp.chunk_cnt:>1;` sid:5; rev:1;) + From 7f982973a58c0175612086cc6bc0ec4847c89a7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:38:25 +0000 Subject: [PATCH 13/18] github-actions: bump github/codeql-action from 4.36.1 to 4.36.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.1 to 4.36.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Commits](https://github.com/github/codeql-action/compare/v4.36.1...v4.36.2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.36.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/codeqlpy.yml | 4 ++-- .github/workflows/scorecards-analysis.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5fc9cafb3d1f..3a33778b582a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4.36.1 + uses: github/codeql-action/init@v4.36.2 with: languages: ${{ matrix.language }} queries: security-extended @@ -62,4 +62,4 @@ jobs: ./configure --enable-warnings --enable-unittests make - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.36.1 + uses: github/codeql-action/analyze@v4.36.2 diff --git a/.github/workflows/codeqlpy.yml b/.github/workflows/codeqlpy.yml index db021863e8a2..1afecfbd3558 100644 --- a/.github/workflows/codeqlpy.yml +++ b/.github/workflows/codeqlpy.yml @@ -43,7 +43,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4.36.1 + uses: github/codeql-action/init@v4.36.2 with: languages: ${{ matrix.language }} queries: security-extended @@ -64,4 +64,4 @@ jobs: ./configure --enable-warnings make - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.36.1 + uses: github/codeql-action/analyze@v4.36.2 diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 7bee6933b456..37c81d8b3722 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload SARIF results" - uses: github/codeql-action/upload-sarif@8ed7f7c384ef65d96d422e33fe592d3572522558 # v1 + uses: github/codeql-action/upload-sarif@eda5730a8bfb740e03a28087a958444c646e5842 # v1 with: sarif_file: results.sarif From c5c1ede34c1a9b52623b676fb3ee73c46923b1a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:38:33 +0000 Subject: [PATCH 14/18] github-actions: bump codecov/codecov-action from 6.0.1 to 7.0.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6.0.1 to 7.0.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/e79a6962e0d4c0c17b229090214935d2e33f8354...fb8b3582c8e4def4969c97caa2f19720cb33a72f) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/builds.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index d7f44017b529..f565fdfe923c 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -918,7 +918,7 @@ jobs: name: coverage-lcov-${{ github.job }} path: coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: fail_ci_if_error: false files: coverage.lcov @@ -1631,7 +1631,7 @@ jobs: name: coverage-lcov-${{ github.job }} path: coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: fail_ci_if_error: false files: coverage.lcov @@ -1743,7 +1743,7 @@ jobs: name: coverage-lcov-${{ github.job }} path: coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: fail_ci_if_error: false files: coverage.lcov @@ -1894,7 +1894,7 @@ jobs: name: coverage-lcov-${{ github.job }} path: coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: fail_ci_if_error: false files: coverage.lcov @@ -2138,7 +2138,7 @@ jobs: name: coverage-lcov-${{ github.job }} path: coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: fail_ci_if_error: true files: coverage.lcov @@ -2356,7 +2356,7 @@ jobs: name: coverage-lcov-${{ github.job }} path: coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: fail_ci_if_error: false files: coverage.lcov From 0e3dd86ebd646d94b886aed93cabbce8a92f27eb Mon Sep 17 00:00:00 2001 From: Denis Balashov Date: Tue, 9 Jun 2026 11:32:25 +0300 Subject: [PATCH 15/18] detect/parse: assert HashListTableLookup results in duplicate sig check Add DEBUG_VALIDATE_BUG_ON() at four sites in DetectEngineSignatureIsDuplicate() where HashListTableLookup() is assumed to return non-NULL. Documents the invariant that every Signature in sig_list has a dup_sig_hash_table entry and catches violations in debug builds. Flagged by Svace static analyzer. Ticket: 8635 --- src/detect-parse.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/detect-parse.c b/src/detect-parse.c index 7300bd2d1502..328ca712796f 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -3390,7 +3390,9 @@ static inline int DetectEngineSignatureIsDuplicate(DetectEngineCtx *de_ctx, sw_tmp.s = de_ctx->sig_list; sw_old = HashListTableLookup(de_ctx->dup_sig_hash_table, (void *)&sw_tmp, 0); - /* sw_old == NULL case is impossible */ + /* sw_old == NULL case is impossible: every sig in sig_list + * must have a corresponding dup_sig_hash_table entry */ + DEBUG_VALIDATE_BUG_ON(sw_old == NULL); sw_old->s_prev = sig; } @@ -3425,6 +3427,7 @@ static inline int DetectEngineSignatureIsDuplicate(DetectEngineCtx *de_ctx, if (sw_temp.s != NULL) { sw_next = HashListTableLookup(de_ctx->dup_sig_hash_table, (void *)&sw_temp, 0); + DEBUG_VALIDATE_BUG_ON(sw_next == NULL); sw_next->s_prev = sw_dup->s_prev; } SigFree(de_ctx, sw_dup->s); @@ -3455,6 +3458,7 @@ static inline int DetectEngineSignatureIsDuplicate(DetectEngineCtx *de_ctx, if (sw_temp.s != NULL) { sw_next = HashListTableLookup(de_ctx->dup_sig_hash_table, (void *)&sw_temp, 0); + DEBUG_VALIDATE_BUG_ON(sw_next == NULL); sw_next->s_prev = sw_dup->s_prev; } SigFree(de_ctx, sw_dup->s); @@ -3470,6 +3474,7 @@ static inline int DetectEngineSignatureIsDuplicate(DetectEngineCtx *de_ctx, sw_tmp.s = de_ctx->sig_list; SigDuplWrapper *sw_old = HashListTableLookup(de_ctx->dup_sig_hash_table, (void *)&sw_tmp, 0); + DEBUG_VALIDATE_BUG_ON(sw_old == NULL); if (sw_old->s != sw_dup->s) { // Link on top of the list if there was another element sw_old->s_prev = sig; From 9cb7fb282735773ceb49959c7ce52cb3ed33d081 Mon Sep 17 00:00:00 2001 From: Denis Balashov Date: Tue, 9 Jun 2026 11:33:05 +0300 Subject: [PATCH 16/18] detect: guard rate filter callback registration, return bool on failure SCDetectEngineRegisterRateFilterCallback() dereferences the result of DetectEngineGetCurrent() unconditionally. Add a NULL guard with SCLogError and return false so callers can detect registration failure. Flagged by Svace and confirmed by gcc -fanalyzer. Ticket: 8560 --- src/detect-engine.c | 7 ++++++- src/detect.h | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/detect-engine.c b/src/detect-engine.c index c526154027b2..850f221256d7 100644 --- a/src/detect-engine.c +++ b/src/detect-engine.c @@ -5203,12 +5203,17 @@ void DetectLowerSetupCallback( } } -void SCDetectEngineRegisterRateFilterCallback(SCDetectRateFilterFunc fn, void *arg) +bool SCDetectEngineRegisterRateFilterCallback(SCDetectRateFilterFunc fn, void *arg) { DetectEngineCtx *de_ctx = DetectEngineGetCurrent(); + if (de_ctx == NULL) { + SCLogError("no detection engine available for rate filter callback registration"); + return false; + } de_ctx->RateFilterCallback = fn; de_ctx->rate_filter_callback_arg = arg; DetectEngineDeReference(&de_ctx); + return true; } int DetectEngineThreadCtxGetJsonContext(DetectEngineThreadCtx *det_ctx) diff --git a/src/detect.h b/src/detect.h index 9f1f5e0bfb59..37e923424dc9 100644 --- a/src/detect.h +++ b/src/detect.h @@ -1214,7 +1214,7 @@ typedef struct DetectEngineCtx_ { * This callback is added to the current detection engine and will be * copied to all future detection engines over rule reloads. */ -void SCDetectEngineRegisterRateFilterCallback(SCDetectRateFilterFunc cb, void *arg); +bool SCDetectEngineRegisterRateFilterCallback(SCDetectRateFilterFunc cb, void *arg); /* Engine groups profiles (low, medium, high, custom) */ enum { From cf10c01aa8b6a5669f0eade22274a2f34a087cb5 Mon Sep 17 00:00:00 2001 From: Denis Balashov Date: Tue, 9 Jun 2026 11:33:26 +0300 Subject: [PATCH 17/18] examples: check rate filter callback registration result Check DetectEngineEnabled() before registering, and warn if the registration call returns false. --- examples/lib/custom/main.c | 8 +++++++- examples/lib/live/main.c | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/lib/custom/main.c b/examples/lib/custom/main.c index b11b6e01d7e6..ea513cdeef07 100644 --- a/examples/lib/custom/main.c +++ b/examples/lib/custom/main.c @@ -253,7 +253,13 @@ int main(int argc, char **argv) * ThreadVars will be ready. */ SuricataInit(); - SCDetectEngineRegisterRateFilterCallback(RateFilterCallback, NULL); + if (DetectEngineEnabled()) { + if (!SCDetectEngineRegisterRateFilterCallback(RateFilterCallback, NULL)) { + SCLogWarning("rate filter callback registration failed"); + } + } else { + SCLogWarning("detection engine not enabled, rate filter callback not registered"); + } /* Spawn our worker threads. */ pthread_t worker; diff --git a/examples/lib/live/main.c b/examples/lib/live/main.c index 572a7dde241a..5a58fe5ce4de 100644 --- a/examples/lib/live/main.c +++ b/examples/lib/live/main.c @@ -306,7 +306,13 @@ int main(int argc, char **argv) * ThreadVars will be ready. */ SuricataInit(); - SCDetectEngineRegisterRateFilterCallback(RateFilterCallback, NULL); + if (DetectEngineEnabled()) { + if (!SCDetectEngineRegisterRateFilterCallback(RateFilterCallback, NULL)) { + SCLogWarning("rate filter callback registration failed"); + } + } else { + SCLogWarning("detection engine not enabled, rate filter callback not registered"); + } /* Spawn our worker threads, one for each interface. */ pthread_t workers[MAX_INTERFACES]; From 0cd9cc642d52929d19c3cc91f9cb95895c8cc4cd Mon Sep 17 00:00:00 2001 From: Shivani Bhardwaj Date: Wed, 10 Jun 2026 11:53:01 +0530 Subject: [PATCH 18/18] rust: exclude DETECT_BYTEMATH_ENDIAN_DEFAULT from bindings to deal with the failure due to cbindgen updates and mismatches in generated bindings. detect-bytemath.c:61: error: "DETECT_BYTEMATH_ENDIAN_DEFAULT" redefined [-Werror] 61 | #define DETECT_BYTEMATH_ENDIAN_DEFAULT (uint8_t) BigEndian | In file included from rust.h:34, from detect-bytemath.c:32: ./../rust/gen/rust-bindings.h:5071: note: this is the location of the previous definition 5071 | #define DETECT_BYTEMATH_ENDIAN_DEFAULT BigEndian | --- rust/cbindgen.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/cbindgen.toml b/rust/cbindgen.toml index 350f215f2e79..7ed656e5282d 100644 --- a/rust/cbindgen.toml +++ b/rust/cbindgen.toml @@ -86,6 +86,7 @@ include = [ # default: [] exclude = [ "CLuaState", + "DETECT_BYTEMATH_ENDIAN_DEFAULT", ] # Types of items that we'll generate. If empty, then all types of item are emitted.