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 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 diff --git a/doc/userguide/rules/datasets.rst b/doc/userguide/rules/datasets.rst index eeb391251942..b174922f76da 100644 --- a/doc/userguide/rules/datasets.rst +++ b/doc/userguide/rules/datasets.rst @@ -81,7 +81,7 @@ Syntax:: dataset:, \ [, type , save , load , state , memcap , hashsize , format , context_key , value_key , array_key , - remove_key]; + remove_key, match subdomain]; type the data type: string, md5, sha256, ipv4, ip @@ -112,7 +112,11 @@ array_key remove_key if set, the JSON object pointed by value key will be removed from the alert event - +match subdomain + if set to ``subdomain``, enables hierarchical domain matching. + On lookup, the dataset walks up the domain label hierarchy until + a match is found. Only valid with ``isset``/``isnotset`` commands + and ``type string``. Best used with the ``dotprefix`` transform. .. note:: 'type' is mandatory and needs to be set. @@ -137,6 +141,28 @@ on domain names to find TLDs in the dataset ``dns-tld-seen``: .. image:: dataset-examples/detect-unique-tlds.png +3. Block domains and all their subdomains using a blocklist dataset: + +.. container:: example-rule + + reject dns any any -> any any (msg:"Blocked domain"; dns.query; dotprefix; dataset:isset,blocked-domains, type string, match subdomain, load blocked-domains.lst; sid:8000003; rev:1;) + +The ``match subdomain`` option walks up the domain hierarchy on each +lookup. Combined with ``dotprefix``, a query for ``mail.evil.com`` +becomes ``.mail.evil.com`` and is checked against the dataset as: +``.mail.evil.com``, ``.evil.com``, ``.com``. If ``.evil.com`` is in the +dataset, the rule matches. + +The dataset file should contain entries with a leading dot:: + + LmV2aWwuY29tCg== + +which is the base64 encoding of ``.evil.com``. + +When using ``ndjson`` format, use the raw dotted value in the JSON:: + + {"domain": ".evil.com"} + Notice how it is not possible to do certain operations alone with datasets (example 2 above), but, it is possible to use a combination of other rule keywords. Keep in mind the cost of additional keywords though e.g. in the @@ -184,7 +210,7 @@ Syntax:: dataset:, \ [, type , load , format , memcap , hashsize , context_key \ - , value_key , array_key ]; + , value_key , array_key , match subdomain]; Example rules could look like:: 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;) + diff --git a/etc/schema.json b/etc/schema.json index c08ee1001156..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, @@ -7459,8 +7512,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 +8629,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/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]; diff --git a/qa/coccinelle/malloc-error-check.cocci b/qa/coccinelle/malloc-error-check.cocci index ce4d0cc47f96..d7d1bae8cf5b 100644 --- a/qa/coccinelle/malloc-error-check.cocci +++ b/qa/coccinelle/malloc-error-check.cocci @@ -1,7 +1,7 @@ @malloced@ expression x; position p1; -identifier func =~ "(SCMalloc|SCStrdup|SCCalloc|SCMallocAligned|SCRealloc)"; +identifier func =~ "SCMalloc\|SCStrdup\|SCCalloc\|SCMallocAligned\|SCRealloc"; @@ x@p1 = func(...) @@ -10,7 +10,7 @@ x@p1 = func(...) expression x, E; statement S; position malloced.p1; -identifier func =~ "(SCMalloc|SCStrdup|SCCalloc|SCMallocAligned|SCRealloc)"; +identifier func =~ "SCMalloc\|SCStrdup\|SCCalloc\|SCMallocAligned\|SCRealloc"; @@ ( @@ -22,7 +22,7 @@ if (E && (x@p1 = func(...)) == NULL) S @realloc exists@ position malloced.p1; expression x, E1; -identifier func =~ "(SCMalloc|SCCalloc|SCMallocAligned)"; +identifier func =~ "SCMalloc\|SCCalloc\|SCMallocAligned"; @@ x@p1 = func(...) @@ -33,7 +33,7 @@ x = SCRealloc(x, E1) expression x, E1; position malloced.p1; statement S1, S2; -identifier func =~ "(SCMalloc|SCStrdup|SCCalloc|SCMallocAligned|SCRealloc)"; +identifier func =~ "SCMalloc\|SCStrdup\|SCCalloc\|SCMallocAligned\|SCRealloc"; @@ x@p1 = func(...) 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/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. 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/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..67a5f2558b4e 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 rust/src/llmnr/*.rs diff --git a/src/Makefile.am b/src/Makefile.am index 44ff227012d1..7d6d0ebf0c02 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -228,6 +228,11 @@ noinst_HEADERS = \ detect-id.h \ detect-igmphdr.h \ detect-igmp-type.h \ + 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 \ detect-ipproto.h \ @@ -804,6 +809,11 @@ libsuricata_c_a_SOURCES = \ detect-id.c \ detect-igmphdr.c \ detect-igmp-type.c \ + 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 \ detect-ipproto.c \ 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..48df9e1b4c93 100644 --- a/src/decode-sctp.c +++ b/src/decode-sctp.c @@ -21,7 +21,6 @@ * @{ */ - /** * \file * @@ -41,6 +40,128 @@ #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; + uint8_t data_chunk_cnt = 0; + 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: + 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) { + 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.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; + 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 +171,531 @@ 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; + } + + 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; + } + 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; +} + +/** \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) +{ +#ifdef UNITTESTS + UtRegisterTest("SCTPDecodeValidInitTest01", SCTPDecodeValidInitTest01); + UtRegisterTest("SCTPDecodePktTooSmallTest02", SCTPDecodePktTooSmallTest02); + UtRegisterTest("SCTPDecodeChunkTooSmallTest03", SCTPDecodeChunkTooSmallTest03); + UtRegisterTest("SCTPDecodeChunkLenInvalidTest04", SCTPDecodeChunkLenInvalidTest04); + 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 f83a4434ef05..c6e6b45323d5 100644 --- a/src/decode-sctp.h +++ b/src/decode-sctp.h @@ -25,16 +25,75 @@ #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 + +/** 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 +#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 */ + 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) +#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..e6800d80063a 100644 --- a/src/decode.c +++ b/src/decode.c @@ -143,10 +143,7 @@ ExceptionPolicyStatsSetts flow_memcap_eps_stats = { */ PacketAlert *PacketAlertCreate(void) { - PacketAlert *pa_array = SCCalloc(packet_alert_max, sizeof(PacketAlert)); - DEBUG_VALIDATE_BUG_ON(pa_array == NULL); - - return pa_array; + return SCCalloc(packet_alert_max, sizeof(PacketAlert)); } void PacketAlertRecycle(PacketAlert *pa_array, uint16_t cnt) @@ -267,7 +264,10 @@ Packet *PacketGetFromAlloc(void) if (unlikely(p == NULL)) { return NULL; } - PacketInit(p); + if (!PacketInit(p)) { + SCFree(p); + return NULL; + } p->ReleasePacket = PacketFree; SCLogDebug("allocated a new packet only using alloc..."); @@ -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/defrag.c b/src/defrag.c index ff785434592c..397041aa3f81 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -1158,7 +1158,10 @@ static Packet *BuildIpv4TestPacket( if (unlikely(p == NULL)) return NULL; - PacketInit(p); + if (!PacketInit(p)) { + SCFree(p); + return NULL; + } struct timeval tval; gettimeofday(&tval, NULL); @@ -1222,7 +1225,7 @@ static int BuildIpv4TestPacketWithContent(Packet **packet, uint8_t proto, uint16 p = SCCalloc(1, sizeof(*p) + default_packet_size); FAIL_IF_NULL(p); - PacketInit(p); + FAIL_IF(!PacketInit(p)); struct timeval tval; gettimeofday(&tval, NULL); @@ -1277,7 +1280,10 @@ static Packet *BuildIpv6TestPacket( if (unlikely(p == NULL)) return NULL; - PacketInit(p); + if (!PacketInit(p)) { + SCFree(p); + return NULL; + } struct timeval tval; gettimeofday(&tval, NULL); @@ -1347,7 +1353,10 @@ static Packet *BuildIpv6TestPacketWithContent( if (unlikely(p == NULL)) return NULL; - PacketInit(p); + if (!PacketInit(p)) { + SCFree(p); + return NULL; + } struct timeval tval; gettimeofday(&tval, NULL); diff --git a/src/detect-dataset.c b/src/detect-dataset.c index bed1e0de552e..4dabbd6c2c80 100644 --- a/src/detect-dataset.c +++ b/src/detect-dataset.c @@ -49,6 +49,8 @@ #define DETECT_DATASET_CMD_ISNOTSET 2 #define DETECT_DATASET_CMD_ISSET 3 +#define DATASET_SUBDOMAIN_MAX_LOOKUPS 126 + static int DetectDatasetSetup (DetectEngineCtx *, Signature *, const char *); void DetectDatasetFree (DetectEngineCtx *, void *); @@ -62,6 +64,26 @@ void DetectDatasetRegister (void) sigmatch_table[DETECT_DATASET].flags = SIGMATCH_SUPPORT_FIREWALL; } +/** \brief walk up the domain hierarchy looking for a match in a JSON dataset */ +static DataJsonResultType DatajsonLookupSubdomain( + Dataset *set, const uint8_t *data, const uint32_t data_len) +{ + DataJsonResultType r = { + .found = false, .json = { .value = NULL, .len = 0 }, .hashdata = NULL + }; + int lookups = 0; + for (uint32_t i = 1; i < data_len; i++) { + if (data[i] == '.' && data[i - 1] != '.') { + if (++lookups > DATASET_SUBDOMAIN_MAX_LOOKUPS) + break; + r = DatajsonLookup(set, data + i, data_len - i); + if (r.found) + return r; + } + } + return r; +} + /* 1 match 0 no match @@ -77,6 +99,9 @@ static int DetectDatajsonBufferMatch(DetectEngineThreadCtx *det_ctx, const Detec // PrintRawDataFp(stdout, data, data_len); DataJsonResultType r = DatajsonLookup(sd->set, data, data_len); SCLogDebug("r found: %d, len: %u", r.found, r.json.len); + if (!r.found && sd->match_subdomain) { + r = DatajsonLookupSubdomain(sd->set, data, data_len); + } if (!r.found) return 0; if (r.json.len > 0) { @@ -102,6 +127,9 @@ static int DetectDatajsonBufferMatch(DetectEngineThreadCtx *det_ctx, const Detec // PrintRawDataFp(stdout, data, data_len); DataJsonResultType r = DatajsonLookup(sd->set, data, data_len); SCLogDebug("r found: %d, len: %u", r.found, r.json.len); + if (!r.found && sd->match_subdomain) { + r = DatajsonLookupSubdomain(sd->set, data, data_len); + } if (r.found) { DatajsonUnlockElt(&r); return 0; @@ -114,6 +142,22 @@ static int DetectDatajsonBufferMatch(DetectEngineThreadCtx *det_ctx, const Detec return 0; } +/** \brief walk up the domain hierarchy looking for a match in a dataset */ +static int DatasetLookupSubdomain(Dataset *set, const uint8_t *data, const uint32_t data_len) +{ + int lookups = 0; + for (uint32_t i = 1; i < data_len; i++) { + if (data[i] == '.' && data[i - 1] != '.') { + if (++lookups > DATASET_SUBDOMAIN_MAX_LOOKUPS) + break; + int r = DatasetLookup(set, data + i, data_len - i); + if (r == 1) + return 1; + } + } + return 0; +} + /* 1 match 0 no match @@ -133,6 +177,9 @@ int DetectDatasetBufferMatch(DetectEngineThreadCtx *det_ctx, case DETECT_DATASET_CMD_ISSET: { //PrintRawDataFp(stdout, data, data_len); int r = DatasetLookup(sd->set, data, data_len); + if (r != 1 && sd->match_subdomain) { + r = DatasetLookupSubdomain(sd->set, data, data_len); + } SCLogDebug("r %d", r); if (r == 1) return 1; @@ -141,6 +188,9 @@ int DetectDatasetBufferMatch(DetectEngineThreadCtx *det_ctx, case DETECT_DATASET_CMD_ISNOTSET: { //PrintRawDataFp(stdout, data, data_len); int r = DatasetLookup(sd->set, data, data_len); + if (r != 1 && sd->match_subdomain) { + r = DatasetLookupSubdomain(sd->set, data, data_len); + } SCLogDebug("r %d", r); if (r < 1) return 1; @@ -169,7 +219,7 @@ static int DetectDatasetParse(const char *str, char *cmd, int cmd_len, char *nam enum DatasetTypes *type, char *load, size_t load_size, char *save, size_t save_size, uint64_t *memcap, uint32_t *hashsize, DatasetFormats *format, char *value_key, size_t value_key_size, char *array_key, size_t array_key_size, char *enrichment_key, - size_t enrichment_key_size, bool *remove_key) + size_t enrichment_key_size, bool *remove_key, bool *match_subdomain) { bool cmd_set = false; bool name_set = false; @@ -221,6 +271,13 @@ static int DetectDatasetParse(const char *str, char *cmd, int cmd_len, char *nam *remove_key = true; } else return -1; + } else if (strcmp(key, "match") == 0) { + if (strcmp(val, "subdomain") == 0) { + *match_subdomain = true; + } else { + SCLogError("unknown match mode '%s'", val); + return -1; + } } else if (strcmp(key, "type") == 0) { SCLogDebug("type %s", val); @@ -474,6 +531,7 @@ int DetectDatasetSetup (DetectEngineCtx *de_ctx, Signature *s, const char *rawst char array_key[SIG_JSON_CONTENT_KEY_LEN] = ""; char enrichment_key[SIG_JSON_CONTENT_KEY_LEN] = ""; bool remove_key = false; + bool match_subdomain = false; if (DetectBufferGetActiveList(de_ctx, s) == -1) { SCLogError("datasets are only supported for sticky buffers"); @@ -489,7 +547,7 @@ int DetectDatasetSetup (DetectEngineCtx *de_ctx, Signature *s, const char *rawst if (!DetectDatasetParse(rawstr, cmd_str, sizeof(cmd_str), name, sizeof(name), &type, load, sizeof(load), save, sizeof(save), &memcap, &hashsize, &format, value_key, sizeof(value_key), array_key, sizeof(array_key), enrichment_key, - sizeof(enrichment_key), &remove_key)) { + sizeof(enrichment_key), &remove_key, &match_subdomain)) { return -1; } @@ -514,6 +572,17 @@ int DetectDatasetSetup (DetectEngineCtx *de_ctx, Signature *s, const char *rawst return -1; } + if (match_subdomain) { + if (cmd != DETECT_DATASET_CMD_ISSET && cmd != DETECT_DATASET_CMD_ISNOTSET) { + SCLogError("'match subdomain' only supports isset/isnotset commands"); + return -1; + } + if (type != DATASET_TYPE_STRING) { + SCLogError("'match subdomain' only supports type string"); + return -1; + } + } + if ((format == DATASET_FORMAT_JSON) || (format == DATASET_FORMAT_NDJSON)) { if (strlen(save) != 0) { SCLogError("json format is not supported with 'save' or 'state' option"); @@ -579,6 +648,7 @@ int DetectDatasetSetup (DetectEngineCtx *de_ctx, Signature *s, const char *rawst cd->set = set; cd->cmd = cmd; cd->format = format; + cd->match_subdomain = match_subdomain; if ((format == DATASET_FORMAT_JSON) || (format == DATASET_FORMAT_NDJSON)) { strlcpy(cd->json_key, enrichment_key, sizeof(cd->json_key)); } diff --git a/src/detect-dataset.h b/src/detect-dataset.h index 3907648985a9..7022b6084ff4 100644 --- a/src/detect-dataset.h +++ b/src/detect-dataset.h @@ -30,6 +30,7 @@ typedef struct DetectDatasetData_ { Dataset *set; uint8_t cmd; + bool match_subdomain; DatasetFormats format; DataJsonType json; char json_key[SIG_JSON_CONTENT_KEY_LEN]; diff --git a/src/detect-engine-alert.c b/src/detect-engine-alert.c index 5bf150115f53..eb5373d59116 100644 --- a/src/detect-engine-alert.c +++ b/src/detect-engine-alert.c @@ -360,6 +360,9 @@ static inline int PacketAlertSetContext( } } current_json->json_string = SCStrdup(det_ctx->json_content[i].json_content); + if (current_json->json_string == NULL) { + return -1; + } SCLogDebug("json content %u, value '%s' (%p)", (unsigned int)i, current_json->json_string, s); } diff --git a/src/detect-engine-build.c b/src/detect-engine-build.c index c9d27911e181..6abea2658359 100644 --- a/src/detect-engine-build.c +++ b/src/detect-engine-build.c @@ -1534,16 +1534,22 @@ static DetectPort *RulesGroupByPorts(DetectEngineCtx *de_ctx, uint8_t ipproto, u } else { /* Protocol does not match the Signature protocol and is non of IP, pkthdr */ if (!DetectProtoContainsProto(&s->init_data->proto, ipproto)) { - SCLogDebug("skip"); + SCLogDebug("skip s:%u for proto:%u", s->id, ipproto); goto next; } /* Direction does not match Signature direction */ if (direction == SIG_FLAG_TOSERVER) { - if (!(s->flags & SIG_FLAG_TOSERVER)) + if (!(s->flags & SIG_FLAG_TOSERVER)) { + SCLogDebug( + "skip s:%u for proto:%u direction SIG_FLAG_TOSERVER", s->id, ipproto); goto next; + } } else if (direction == SIG_FLAG_TOCLIENT) { - if (!(s->flags & SIG_FLAG_TOCLIENT)) + if (!(s->flags & SIG_FLAG_TOCLIENT)) { + SCLogDebug( + "skip s:%u for proto:%u direction SIG_FLAG_TOCLIENT", s->id, ipproto); goto next; + } } /* see if we want to exclude directionless sigs that really care only for @@ -1585,6 +1591,7 @@ static DetectPort *RulesGroupByPorts(DetectEngineCtx *de_ctx, uint8_t ipproto, u size_unique_port_arr = SetUniquePortPoints(tmp2, unique_port_points, size_unique_port_arr); } + SCLogDebug("s:%u added to group (proto:%u)", s->id, ipproto); p = p->next; } @@ -2214,8 +2221,10 @@ static int SigMatchPrepare(DetectEngineCtx *de_ctx) Signature *s = de_ctx->sig_list; for (; s != NULL; s = s->next) { + SCLogDebug("s:%u: prepare", s->id); /* set up inspect engines */ - DetectEngineAppInspectionEngine2Signature(de_ctx, s); + if (DetectEngineAppInspectionEngine2Signature(de_ctx, s) != 0) + SCReturnInt(-1); /* built-ins */ for (int type = 0; type < DETECT_SM_LIST_MAX; type++) { diff --git a/src/detect-engine-prefilter.c b/src/detect-engine-prefilter.c index cb3dc6a2488b..289ba93bcfdf 100644 --- a/src/detect-engine-prefilter.c +++ b/src/detect-engine-prefilter.c @@ -317,6 +317,7 @@ int PrefilterAppendEngine(DetectEngineCtx *de_ctx, SigGroupHead *sgh, PrefilterP e->name = name; e->gid = PrefilterStoreGetId(de_ctx, e->name, e->Free); + SCLogDebug("sgh->init->pkt_engines %p", sgh->init->pkt_engines); return 0; } @@ -944,10 +945,11 @@ static int SetupNonPrefilter(DetectEngineCtx *de_ctx, SigGroupHead *sgh) for (uint8_t state = 0; state < s->app_progress_hook; state++) { SCLogDebug("handle HOOK %u LTE", state); const int dir = (s->flags & SIG_FLAG_TOSERVER) ? 0 : 1; - const char *pname = AppLayerParserGetStateNameById(IPPROTO_TCP, // TODO + const char *pname = DetectEngineAppHookToName( s->alproto, state, dir == 0 ? STREAM_TOSERVER : STREAM_TOCLIENT); - if (pname == NULL) + if (pname == NULL) { goto error; + } const int sm_list = DetectEngineAppHookToSmlist( s->alproto, state, dir == 0 ? STREAM_TOSERVER : STREAM_TOCLIENT); if (TxNonPFAddSig(de_ctx, tx_engines_hash, s->alproto, dir, (int16_t)state, sm_list, @@ -1089,6 +1091,7 @@ static int SetupNonPrefilter(DetectEngineCtx *de_ctx, SigGroupHead *sgh) pkt_mask = s->mask; pkt_mask_init = true; } + SCLogDebug("s->id %u added", s->id); } } @@ -1131,6 +1134,7 @@ static int SetupNonPrefilter(DetectEngineCtx *de_ctx, SigGroupHead *sgh) tx_engines_hash = NULL; if (pkt_non_pf_array_size) { + SCLogDebug("pkt_non_pf_array_size %u", pkt_non_pf_array_size); struct PrefilterNonPFData *data = SCCalloc(1, sizeof(*data) + pkt_non_pf_array_size * sizeof(data->array[0])); if (data == NULL) @@ -1224,10 +1228,12 @@ int PrefilterSetupRuleGroup(DetectEngineCtx *de_ctx, SigGroupHead *sgh) * match arrays */ PrefilterEngineList *el; if (sgh->init->pkt_engines != NULL) { + SCLogDebug("for %p we have %p", sgh, sgh->init->pkt_engines); uint32_t cnt = 0; for (el = sgh->init->pkt_engines ; el != NULL; el = el->next) { cnt++; } + SCLogDebug("cnt %u", cnt); sgh->pkt_engines = SCMallocAligned(cnt * sizeof(PrefilterEngine), CLS); if (sgh->pkt_engines == NULL) { return -1; diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 1b441fc29675..b37cd61f8466 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -155,6 +155,11 @@ #include "detect-icmpv4hdr.h" #include "detect-igmphdr.h" #include "detect-igmp-type.h" +#include "detect-sctphdr.h" +#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" @@ -676,6 +681,11 @@ void SigTableSetup(void) DetectIcmpv4HdrRegister(); DetectIGMPHdrRegister(); DetectIGMPTypeRegister(); + DetectSCTPHdrRegister(); + DetectSCTPChunkTypeRegister(); + DetectSCTPChunkCntRegister(); + DetectSCTPVtagRegister(); + DetectSCTPChunkDataRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 0538d998a930..822b56a6e407 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -51,6 +51,11 @@ enum DetectKeywordId { DETECT_ICMPV4HDR, DETECT_IGMPHDR, DETECT_IGMP_TYPE, + DETECT_SCTPHDR, + DETECT_SCTP_CHUNK_TYPE, + DETECT_SCTP_CHUNK_CNT, + DETECT_SCTP_VTAG, + DETECT_SCTP_CHUNK_DATA, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-engine.c b/src/detect-engine.c index c526154027b2..4c0abddb0aac 100644 --- a/src/detect-engine.c +++ b/src/detect-engine.c @@ -707,8 +707,19 @@ static void AppendAppInspectEngine(DetectEngineCtx *de_ctx, { if (t->alproto == ALPROTO_UNKNOWN) { /* special case, inspect engine applies to all protocols */ - } else if (s->alproto != ALPROTO_UNKNOWN && !AppProtoEquals(s->alproto, t->alproto)) - return; + } else if (s->alproto != ALPROTO_UNKNOWN) { + if (s->init_data->hook.type == SIGNATURE_HOOK_TYPE_APP) { + /* SIGNATURE_HOOK_TYPE_APP rules are exact about their protocol */ + if (t->alproto != s->alproto) { + return; + } + } else { + /* other rules use the more relax AppProtoEquals logic */ + if (!AppProtoEquals(s->alproto, t->alproto)) { + return; + } + } + } if (s->flags & SIG_FLAG_TOSERVER && !(s->flags & SIG_FLAG_TOCLIENT)) { if (t->dir == 1) @@ -794,6 +805,39 @@ static void AppendAppInspectEngine(DetectEngineCtx *de_ctx, s->init_data->init_flags |= SIG_FLAG_INIT_STATE_MATCH; } +/** + * \param direction STREAM_TOSERVER or STREAM_TOCLIENT + */ +const char *DetectEngineAppHookToName( + const AppProto p, const uint8_t state, const uint8_t direction) +{ + if (!((direction & (STREAM_TOSERVER | STREAM_TOCLIENT)) == STREAM_TOSERVER) && + !((direction & (STREAM_TOSERVER | STREAM_TOCLIENT)) == STREAM_TOCLIENT)) + return NULL; + + const char *pname = AppLayerParserGetStateNameById(IPPROTO_TCP, // TODO + p, state, direction); + if (pname == NULL) { + if (state == 0) { + if (direction == STREAM_TOSERVER) { + pname = "request_started"; + } else { + pname = "response_started"; + } + } else { + const int complete = AppLayerParserGetStateProgressCompletionStatus(p, direction); + if (state == complete) { + if (direction == STREAM_TOSERVER) { + pname = "request_complete"; + } else { + pname = "response_complete"; + } + } + } + } + return pname; +} + /** \brief get the sm_list for a app hook */ int DetectEngineAppHookToSmlist(const AppProto p, const uint8_t state, const int direction) { @@ -805,10 +849,11 @@ int DetectEngineAppHookToSmlist(const AppProto p, const uint8_t state, const int if (strcmp(app_proto, "http") == 0) app_proto = "http1"; - const char *name = AppLayerParserGetStateNameById( - IPPROTO_TCP, p, state, direction & (STREAM_TOSERVER | STREAM_TOCLIENT)); - if (name == NULL) + const char *name = + DetectEngineAppHookToName(p, state, direction & (STREAM_TOSERVER | STREAM_TOCLIENT)); + if (name == NULL) { return -1; + } char generic_hook_name[256]; snprintf(generic_hook_name, sizeof(generic_hook_name), "%s:%s:generic", app_proto, name); @@ -5203,12 +5248,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-engine.h b/src/detect-engine.h index b6dbddaf7990..44f5ed89f9a8 100644 --- a/src/detect-engine.h +++ b/src/detect-engine.h @@ -210,6 +210,8 @@ void DetectLowerSetupCallback( void DeStateRegisterTests(void); +const char *DetectEngineAppHookToName( + const AppProto p, const uint8_t state, const uint8_t direction); int DetectEngineAppHookToSmlist(const AppProto p, const uint8_t state, const int direction); #endif /* SURICATA_DETECT_ENGINE_H */ diff --git a/src/detect-flowbits.c b/src/detect-flowbits.c index 80bb2d79079a..c0060f35e1dc 100644 --- a/src/detect-flowbits.c +++ b/src/detect-flowbits.c @@ -759,11 +759,11 @@ int DetectFlowbitsAnalyze(DetectEngineCtx *de_ctx) uint32_t new_fb_array_size = s->init_data->rule_state_flowbits_ids_size + 1; void *tmp_fb_ptr = SCRealloc(s->init_data->rule_state_flowbits_ids_array, new_fb_array_size * sizeof(uint32_t)); - s->init_data->rule_state_flowbits_ids_array = tmp_fb_ptr; - if (s->init_data->rule_state_flowbits_ids_array == NULL) { + if (tmp_fb_ptr == NULL) { SCLogError("Failed to reallocate memory for rule_state_variable_idx"); goto error; } + s->init_data->rule_state_flowbits_ids_array = tmp_fb_ptr; SCLogDebug( "realloc'ed array for flowbits ids, new size is %u", new_fb_array_size); s->init_data->rule_state_dependant_sids_size = new_array_size; diff --git a/src/detect-parse.c b/src/detect-parse.c index 7300bd2d1502..6c2b8af20090 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -2700,38 +2700,48 @@ static int SigValidateCheckBuffers( const DetectEngineAppInspectionEngine *app = de_ctx->app_inspect_engines; for (; app != NULL; app = app->next) { - if (app->sm_list == b->id && - (AppProtoEquals(s->alproto, app->alproto) || s->alproto == 0)) { - SCLogDebug("engine %s dir %d alproto %d", - DetectEngineBufferTypeGetNameById(de_ctx, app->sm_list), app->dir, - app->alproto); - SCLogDebug("b->id %d nlists %d", b->id, nlists); - - if (b->only_tc) { - if (app->dir == 1) - (*tc_excl)++; - } else if (b->only_ts) { - if (app->dir == 0) - (*ts_excl)++; - } else { - bufdir[b->id].ts += (app->dir == 0); - bufdir[b->id].tc += (app->dir == 1); - } + if (app->sm_list != b->id) + continue; + if (s->init_data->hook.type == SIGNATURE_HOOK_TYPE_APP) { /* only allow rules to use the hook for engines at that * exact progress for now. */ - if (s->init_data->hook.type == SIGNATURE_HOOK_TYPE_APP) { - if ((s->flags & SIG_FLAG_TOSERVER) && (app->dir == 0) && - app->progress != s->init_data->hook.t.app.app_progress) { - SCLogError("engine progress value %d doesn't match hook %u", app->progress, - s->init_data->hook.t.app.app_progress); - SCReturnInt(0); - } - if ((s->flags & SIG_FLAG_TOCLIENT) && (app->dir == 1) && - app->progress != s->init_data->hook.t.app.app_progress) { - SCLogError("engine progress value doesn't match hook"); - SCReturnInt(0); - } + if (app->alproto != s->alproto) { + continue; + } + } else { + if (!(AppProtoEquals(s->alproto, app->alproto) || s->alproto == 0)) { + continue; + } + } + + SCLogDebug("engine %s dir %d alproto %d", + DetectEngineBufferTypeGetNameById(de_ctx, app->sm_list), app->dir, + app->alproto); + SCLogDebug("b->id %d nlists %d", b->id, nlists); + + if (b->only_tc) { + if (app->dir == 1) + (*tc_excl)++; + } else if (b->only_ts) { + if (app->dir == 0) + (*ts_excl)++; + } else { + bufdir[b->id].ts += (app->dir == 0); + bufdir[b->id].tc += (app->dir == 1); + } + + if (s->init_data->hook.type == SIGNATURE_HOOK_TYPE_APP) { + if ((s->flags & SIG_FLAG_TOSERVER) && (app->dir == 0) && + app->progress != s->init_data->hook.t.app.app_progress) { + SCLogError("engine progress value %d doesn't match hook %u", app->progress, + s->init_data->hook.t.app.app_progress); + SCReturnInt(0); + } + if ((s->flags & SIG_FLAG_TOCLIENT) && (app->dir == 1) && + app->progress != s->init_data->hook.t.app.app_progress) { + SCLogError("engine progress value doesn't match hook"); + SCReturnInt(0); } } } @@ -3390,7 +3400,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 +3437,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 +3468,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 +3484,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; diff --git a/src/detect-reference.c b/src/detect-reference.c index 2981f3e5b68c..700d149c8c21 100644 --- a/src/detect-reference.c +++ b/src/detect-reference.c @@ -153,6 +153,9 @@ static DetectReference *DetectReferenceParse(const char *rawstr, DetectEngineCtx if (strlen(scheme)) { SCLogConfig("scheme value %s overrides key %s", scheme, key); ref->key = SCStrdup(scheme); + if (ref->key == NULL) { + goto error; + } /* already bound checked to be REFERENCE_SYSTEM_NAME_MAX or less */ ref->key_len = (uint16_t)strlen(scheme); } else { @@ -160,6 +163,9 @@ static DetectReference *DetectReferenceParse(const char *rawstr, DetectEngineCtx SCRConfReference *lookup_ref_conf = SCRConfGetReference(key, de_ctx); if (lookup_ref_conf != NULL) { ref->key = SCStrdup(lookup_ref_conf->url); + if (ref->key == NULL) { + goto error; + } /* already bound checked to be REFERENCE_SYSTEM_NAME_MAX or less */ ref->key_len = (uint16_t)strlen(ref->key); } else { 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 */ 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 */ 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 */ 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 */ 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 */ 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 { diff --git a/src/output-json-email-common.c b/src/output-json-email-common.c index b5e2dd29a80e..e4a1b21d04df 100644 --- a/src/output-json-email-common.c +++ b/src/output-json-email-common.c @@ -148,8 +148,7 @@ static bool EveEmailLogJsonData( smtp_state = (SMTPState *)state; if (smtp_state == NULL) { SCLogDebug("no smtp state, so no request logging"); - SCJbFree(sjs); - SCReturnPtr(NULL, "SCJsonBuilder"); + SCReturnBool(false); } SMTPTransaction *tx = vtx; mime_state = tx->mime_state; diff --git a/src/output-json-smtp.c b/src/output-json-smtp.c index e4a706ce25c1..de9e2623e90e 100644 --- a/src/output-json-smtp.c +++ b/src/output-json-smtp.c @@ -53,6 +53,10 @@ static void EveSmtpDataLogger(void *state, void *vtx, SCJsonBuilder *js) { + if (state == NULL || vtx == NULL) { + return; + } + SMTPTransaction *tx = vtx; SMTPString *rcptto_str; if (((SMTPState *)state)->helo) { 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)); diff --git a/src/packet.c b/src/packet.c index 37caada75b90..f2847c3bc49d 100644 --- a/src/packet.c +++ b/src/packet.c @@ -70,12 +70,16 @@ uint8_t PacketGetAction(const Packet *p) /** * \brief Initialize a packet structure for use. */ -void PacketInit(Packet *p) +bool PacketInit(Packet *p) { SCSpinInit(&p->persistent.tunnel_lock, 0); p->alerts.alerts = PacketAlertCreate(); + if (unlikely(p->alerts.alerts == NULL)) { + return false; + } p->livedev_id = 0; p->livedev_dst_id = 0; + return true; } void PacketReleaseRefs(Packet *p) diff --git a/src/packet.h b/src/packet.h index 5ad43c3ddfcc..044068ad17fc 100644 --- a/src/packet.h +++ b/src/packet.h @@ -32,7 +32,7 @@ static inline uint8_t PacketTestAction(const Packet *p, const uint8_t a) } #endif -void PacketInit(Packet *p); +bool PacketInit(Packet *p); void PacketReleaseRefs(Packet *p); void PacketReinit(Packet *p); void PacketRecycle(Packet *p); 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(); diff --git a/src/source-pcap-file-directory-helper.c b/src/source-pcap-file-directory-helper.c index bd244fbc5640..b2da74441e32 100644 --- a/src/source-pcap-file-directory-helper.c +++ b/src/source-pcap-file-directory-helper.c @@ -304,7 +304,7 @@ TmEcode PcapDirectoryPopulateBuffer(PcapFileDirectoryVars *pv, PendingFile *file_to_add = NULL; while ((dir = readdir(pv->directory)) != NULL) { -#ifndef OS_WIN32 +#if defined(DT_REG) if (dir->d_type != DT_REG) { continue; } diff --git a/src/suricata.c b/src/suricata.c index 78b3b34ed076..e6165b5f5fbf 100644 --- a/src/suricata.c +++ b/src/suricata.c @@ -3089,6 +3089,8 @@ int InitGlobal(void) void SuricataPreInit(const char *progname) { + UtilCpuEnableSparcMisalignEmulation(); + SCInstanceInit(&suricata, progname); if (InitGlobal() != 0) { @@ -3223,7 +3225,7 @@ void SuricataPostInit(void) #endif if (limit_nproc) { -#if defined(HAVE_SYS_RESOURCE_H) +#if defined(HAVE_SYS_RESOURCE_H) && defined(RLIMIT_NPROC) #ifdef linux if (geteuid() == 0) { SCLogWarning("setrlimit has no effect when running as root."); diff --git a/src/tests/fuzz/fuzz_decodebase64.c b/src/tests/fuzz/fuzz_decodebase64.c index 294b708f45d8..ea721e6c9768 100644 --- a/src/tests/fuzz/fuzz_decodebase64.c +++ b/src/tests/fuzz/fuzz_decodebase64.c @@ -19,6 +19,8 @@ static void Base64FuzzTest(const uint8_t *src, size_t len) { uint32_t decoded_len = SCBase64DecodeBufferSize((uint32_t)len); uint8_t *decoded = SCCalloc(decoded_len, sizeof(uint8_t)); + if (decoded == NULL) + return; for (uint8_t mode = SCBase64ModeRFC2045; mode <= SCBase64ModeStrict; mode++) { (void)SCBase64Decode(src, len, mode, decoded); diff --git a/src/threads.h b/src/threads.h index 117c4c4a6ab9..15d13b9f324e 100644 --- a/src/threads.h +++ b/src/threads.h @@ -244,7 +244,7 @@ enum { unsigned long _scgetthread_tid = (unsigned long)tpid; \ _scgetthread_tid; \ }) -#elif defined(sun) +#elif defined(__sun) #include #define SCGetThreadIdLong(...) ({ \ thread_t tmpthid = thr_self(); \ @@ -302,9 +302,7 @@ extern thread_local char t_thread_name[THREAD_NAME_LEN + 1]; SCLogDebug("Error setting thread name \"%s\": %s", tname, strerror(errno)); \ }) #else -#define SCSetThreadName(n) ({ \ - strlcpy(t_thread_name, n, sizeof(t_thread_name)); \ -} +#define SCSetThreadName(n) ({ strlcpy(t_thread_name, n, sizeof(t_thread_name)); }) #endif diff --git a/src/tm-threads.c b/src/tm-threads.c index c3ab08d14f53..be2a230eeab4 100644 --- a/src/tm-threads.c +++ b/src/tm-threads.c @@ -701,7 +701,7 @@ void TmSlotSetFuncAppend(ThreadVars *tv, TmModule *tm, const void *data) } } -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun static int SetCPUAffinitySet(cpu_set_t *cs) { #if defined OS_FREEBSD @@ -735,7 +735,7 @@ static int SetCPUAffinitySet(cpu_set_t *cs) */ static int SetCPUAffinity(uint16_t cpuid) { -#if defined __OpenBSD__ || defined sun +#if defined __OpenBSD__ || defined __sun return 0; #else int cpu = (int)cpuid; @@ -871,7 +871,7 @@ TmEcode TmThreadSetupOptions(ThreadVars *tv) SetCPUAffinity(tv->cpu_affinity); } -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun if (tv->thread_setup_flags & THREAD_SET_PRIORITY) TmThreadSetPrio(tv); if (tv->thread_setup_flags & THREAD_SET_AFFTYPE) { diff --git a/src/util-affinity.c b/src/util-affinity.c index 806310e3d941..cba5aec1d60a 100644 --- a/src/util-affinity.c +++ b/src/util-affinity.c @@ -65,7 +65,7 @@ ThreadsAffinityType thread_affinity[MAX_CPU_SET] = { int thread_affinity_init_done = 0; -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun #ifdef HAVE_HWLOC static hwloc_topology_t topology = NULL; #endif /* HAVE_HWLOC */ @@ -206,7 +206,7 @@ ThreadsAffinityType *GetOrAllocAffinityTypeForIfaceOfName( return parent_affinity; } -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun static void AffinitySetupInit(void) { int i, j; @@ -587,7 +587,7 @@ static bool AffinityConfigIsLegacy(void) */ void AffinitySetupLoadFromConfig(void) { -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun if (thread_affinity_init_done == 0) { AffinitySetupInit(); AffinityConfigIsLegacy(); @@ -643,7 +643,7 @@ void AffinitySetupLoadFromConfig(void) #endif /* OS_WIN32 and __OpenBSD__ */ } -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun #ifdef HAVE_HWLOC static int HwLocDeviceNumaGet(hwloc_topology_t topo, hwloc_obj_t obj) { @@ -1006,7 +1006,7 @@ static bool AutopinEnabled(void) uint16_t AffinityGetNextCPU(ThreadVars *tv, ThreadsAffinityType *taf) { uint16_t ncpu = 0; -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun int iface_numa = -1; if (AutopinEnabled()) { #ifdef HAVE_HWLOC @@ -1043,7 +1043,7 @@ uint16_t AffinityGetNextCPU(ThreadVars *tv, ThreadsAffinityType *taf) uint16_t UtilAffinityGetAffinedCPUNum(ThreadsAffinityType *taf) { uint16_t ncpu = 0; -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun SCMutexLock(&taf->taf_mutex); for (int i = UtilCpuGetNumProcessorsOnline(); i >= 0; i--) if (CPU_ISSET(i, &taf->cpu_set)) { diff --git a/src/util-affinity.h b/src/util-affinity.h index a44f65e8a176..bcd4c6276337 100644 --- a/src/util-affinity.h +++ b/src/util-affinity.h @@ -75,7 +75,7 @@ typedef struct ThreadsAffinityType_ { struct ThreadsAffinityType_ *parent; // e.g. worker-cpu-set for interfaces SCMutex taf_mutex; -#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined sun +#if !defined __CYGWIN__ && !defined OS_WIN32 && !defined __OpenBSD__ && !defined __sun cpu_set_t cpu_set; cpu_set_t lowprio_cpu; cpu_set_t medprio_cpu; diff --git a/src/util-byte.h b/src/util-byte.h index 43452094f748..9cbcb7b6bff7 100644 --- a/src/util-byte.h +++ b/src/util-byte.h @@ -45,7 +45,12 @@ #define SCByteSwap16(x) OSSwapInt16(x) #define SCByteSwap32(x) OSSwapInt32(x) #define SCByteSwap64(x) OSSwapInt64(x) -#elif defined(__WIN32) || defined(_WIN32) || defined(sun) +#elif defined(__sun) +#include +#define SCByteSwap16(x) BSWAP_16(x) +#define SCByteSwap32(x) BSWAP_32(x) +#define SCByteSwap64(x) BSWAP_64(x) +#elif defined(__WIN32) || defined(_WIN32) /* Quick & dirty solution, nothing seems to exist for this in Win32 API */ #define SCByteSwap16(x) \ ((((x) & 0xff00) >> 8) \ diff --git a/src/util-cpu.c b/src/util-cpu.c index 1ae49e7abec0..1cb64d4ddca3 100644 --- a/src/util-cpu.c +++ b/src/util-cpu.c @@ -204,3 +204,16 @@ uint64_t UtilCpuGetTicks(void) #endif return val; } + +/** + * \brief Handle memory access miss align on SPARC processors + */ +void UtilCpuEnableSparcMisalignEmulation(void) +{ +/* 'ta 6' tells the kernel to synthesize any unaligned accesses this process + * makes, instead of just signalling an error and terminating the process. + */ +#ifdef __sparc + __asm("ta 6"); +#endif /* __sparc */ +} diff --git a/src/util-cpu.h b/src/util-cpu.h index 8e31c338f2f4..cd2830aaf670 100644 --- a/src/util-cpu.h +++ b/src/util-cpu.h @@ -33,4 +33,6 @@ void UtilCpuPrintSummary(void); uint64_t UtilCpuGetTicks(void); +void UtilCpuEnableSparcMisalignEmulation(void); + #endif /* SURICATA_UTIL_CPU_H */ diff --git a/src/util-log-redis.c b/src/util-log-redis.c index f0755e7473eb..adc6b23f7e59 100644 --- a/src/util-log-redis.c +++ b/src/util-log-redis.c @@ -673,6 +673,9 @@ int SCConfLogOpenRedis(SCConfNode *redis_node, void *lf_ctx) format string, whose length is limited by the length of the maxlen integer formatted as a string */ log_ctx->redis_setup.stream_format = SCCalloc(100, sizeof(char)); + if (unlikely(log_ctx->redis_setup.stream_format == NULL)) { + FatalError("Unable to allocate redis stream format"); + } snprintf(log_ctx->redis_setup.stream_format, 100, redis_stream_format_maxlen_tmpl, "%s", "%s", exact ? '=' : '~', maxlen, "%s"); log_ctx->redis_setup.format = log_ctx->redis_setup.stream_format; diff --git a/src/util-mpm-hs.c b/src/util-mpm-hs.c index 2d4daba19992..3b77a4de44c5 100644 --- a/src/util-mpm-hs.c +++ b/src/util-mpm-hs.c @@ -641,7 +641,7 @@ static int CompileDataExtensionsInit(hs_expr_ext_t **ext, const SCHSPattern *p) { if (p->flags & (MPM_PATTERN_FLAG_OFFSET | MPM_PATTERN_FLAG_DEPTH)) { *ext = SCCalloc(1, sizeof(hs_expr_ext_t)); - if ((*ext) == NULL) { + if (*ext == NULL) { return -1; } if (p->flags & MPM_PATTERN_FLAG_OFFSET) { @@ -1178,8 +1178,7 @@ void SCHSPrintInfo(MpmCtx *mpm_ctx) static MpmConfig *SCHSConfigInit(void) { - MpmConfig *c = SCCalloc(1, sizeof(MpmConfig)); - return c; + return SCCalloc(1, sizeof(MpmConfig)); } static void SCHSConfigDeinit(MpmConfig **c) diff --git a/src/util-path.c b/src/util-path.c index d7618856e471..072133c04baf 100644 --- a/src/util-path.c +++ b/src/util-path.c @@ -196,7 +196,7 @@ bool SCPathExists(const char *path) */ bool SCIsRegularDirectory(const struct dirent *const dir_entry) { -#ifndef OS_WIN32 +#if !defined(OS_WIN32) && !defined(__sun) if ((dir_entry->d_type == DT_DIR) && (strcmp(dir_entry->d_name, ".") != 0) && (strcmp(dir_entry->d_name, "..") != 0)) { @@ -214,7 +214,7 @@ bool SCIsRegularDirectory(const struct dirent *const dir_entry) */ bool SCIsRegularFile(const struct dirent *const dir_entry) { -#ifndef OS_WIN32 +#if defined(DT_REG) return dir_entry->d_type == DT_REG; #endif return false; diff --git a/src/util-syslog.c b/src/util-syslog.c index 482f206789aa..b69365e1d68a 100644 --- a/src/util-syslog.c +++ b/src/util-syslog.c @@ -28,12 +28,15 @@ #include "util-syslog.h" /* holds the string-enum mapping for the syslog facility in SCLogOPIfaceCtx */ +// clang-format off SCEnumCharMap sc_syslog_facility_map[] = { { "auth", LOG_AUTH }, { "authpriv", LOG_AUTHPRIV }, { "cron", LOG_CRON }, { "daemon", LOG_DAEMON }, +#if defined(LOG_FTP) { "ftp", LOG_FTP }, +#endif { "kern", LOG_KERN }, { "lpr", LOG_LPR }, { "mail", LOG_MAIL }, @@ -52,6 +55,7 @@ SCEnumCharMap sc_syslog_facility_map[] = { { "local7", LOG_LOCAL7 }, { NULL, -1 } }; +// clang-format on /** \brief returns the syslog facility enum map */ SCEnumCharMap *SCSyslogGetFacilityMap(void) diff --git a/src/util-time.c b/src/util-time.c index e05fba5dcf7d..2e68ec8c9225 100644 --- a/src/util-time.c +++ b/src/util-time.c @@ -463,7 +463,7 @@ time_t SCMkTimeUtc (struct tm *tp) result += tp->tm_min; result *= 60; result += tp->tm_sec; -#ifndef OS_WIN32 +#if !defined(OS_WIN32) && !defined(__sun) if (tp->tm_gmtoff) result -= tp->tm_gmtoff; #endif @@ -498,7 +498,7 @@ int SCStringPatternToTime (char *string, const char **patterns, int num_patterns tp->tm_hour = tp->tm_min = tp->tm_sec = 0; tp->tm_year = tp->tm_mon = tp->tm_mday = tp->tm_wday = INT_MIN; tp->tm_isdst = -1; -#ifndef OS_WIN32 +#if !defined(OS_WIN32) && !defined(__sun) tp->tm_gmtoff = 0; tp->tm_zone = NULL; #endif