From eda8babd7a1007eba84ca4a1061a9b34afc23633 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 12 Jan 2025 10:15:45 -0500 Subject: [PATCH 1/5] rust/detect: Add entropy support This commit adds - Parser for the entropy keyword - Calculation of content the Shannon entropy value The entropy keyword syntax is the keyword entropy followed by options and the entropy value for comparison. The minimum entropy keyword specification is: entropy: value This results in the calculated entropy value being compared with with the equality operator. A match occurs when the values and operator agree. This example matches if the calculated and entropy value are the same. When entropy keyword options are specified, all options and "value" must be comma-separated. Options and value may be specified in any order. Options have default values: - bytes is equal to the current content length - offset is 0 - oper is equality "==" entropy: [bytes ] [offset ] [oper ] value Using default values: entropy: bytes 0, offset 0, oper ==, value The following operators are available: - == (default): Match when calculated value equals entropy value - < Match when calculated value is strictly less than entropy value - <= Match when calculated value is less than or equal to entropy value - > Match when calculated value is strictly greater than entropy value - >= Match when calculated value is greater than or equal to entropy value - != Match when calculated value is not equal to entropy value --- rust/src/detect/entropy.rs | 477 +++++++++++++++++++++++++++++++++++++ rust/src/detect/error.rs | 1 + rust/src/detect/mod.rs | 1 + 3 files changed, 479 insertions(+) create mode 100644 rust/src/detect/entropy.rs diff --git a/rust/src/detect/entropy.rs b/rust/src/detect/entropy.rs new file mode 100644 index 000000000000..8d39112f017d --- /dev/null +++ b/rust/src/detect/entropy.rs @@ -0,0 +1,477 @@ +/* Copyright (C) 2024 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: Jeff Lucovsky +// +use crate::detect::error::RuleParseError; +use crate::detect::parser::take_until_whitespace; + +use nom7::bytes::complete::tag; +use nom7::character::complete::multispace0; +use nom7::sequence::preceded; +use nom7::{Err, IResult}; + +use std::ffi::CStr; +use std::os::raw::{c_char, c_void}; +use std::slice; + +#[repr(u8)] +#[derive(PartialEq, Debug)] +// operators: ==, <, <=, >, >=, != +pub enum DetectEntropyOperator { + OperatorEQ = 1, + OperatorLT = 2, + OperatorLTE = 3, + OperatorGT = 4, + OperatorGTE = 5, + OperatorNEQ = 6, +} + +fn detect_entropy_match(entropy: f64, value: f64, operator: &DetectEntropyOperator) -> bool { + let res = match operator { + DetectEntropyOperator::OperatorEQ => entropy == value, + DetectEntropyOperator::OperatorLT => entropy < value, + DetectEntropyOperator::OperatorLTE => entropy <= value, + DetectEntropyOperator::OperatorGT => entropy > value, + DetectEntropyOperator::OperatorGTE => entropy >= value, + DetectEntropyOperator::OperatorNEQ => entropy != value, + }; + res +} + +fn get_oper_value(value: &str) -> Result { + let res = match value { + "==" => DetectEntropyOperator::OperatorEQ, + "<" => DetectEntropyOperator::OperatorLT, + "<=" => DetectEntropyOperator::OperatorLTE, + ">" => DetectEntropyOperator::OperatorGT, + ">=" => DetectEntropyOperator::OperatorGTE, + "!=" => DetectEntropyOperator::OperatorNEQ, + _ => return Err(()), + }; + + Ok(res) +} +#[repr(C)] +#[derive(Debug)] +pub struct DetectEntropyData { + offset: i32, + nbytes: i32, + value: f64, + oper: DetectEntropyOperator, + flags: u8, +} + +impl Default for DetectEntropyData { + fn default() -> Self { + DetectEntropyData { + offset: 0, + nbytes: 0, + value: 0.0, + oper: DetectEntropyOperator::OperatorEQ, + flags: 0, + } + } +} +impl DetectEntropyData { + pub fn new() -> Self { + Self { + ..Default::default() + } + } +} + +// All options have default values except for the entropy value +const DETECT_ENTROPY_FIXED_PARAM_COUNT: usize = 1; +const DETECT_ENTROPY_MAX_PARAM_COUNT: usize = 4; +pub const DETECT_ENTROPY_FLAG_BYTES: u8 = 0x01; +pub const DETECT_ENTROPY_FLAG_OFFSET: u8 = 0x02; +pub const DETECT_ENTROPY_FLAG_VALUE: u8 = 0x04; +pub const DETECT_ENTROPY_FLAG_OPER: u8 = 0x08; + +fn parse_entropy(input: &str) -> IResult<&str, DetectEntropyData, RuleParseError<&str>> { + // Inner utility function for easy error creation. + fn make_error(reason: String) -> nom7::Err> { + Err::Error(RuleParseError::InvalidEntropy(reason)) + } + let (_, values) = nom7::multi::separated_list1( + tag(","), + preceded(multispace0, nom7::bytes::complete::is_not(",")), + )(input)?; + + if values.len() < DETECT_ENTROPY_FIXED_PARAM_COUNT + || values.len() > DETECT_ENTROPY_MAX_PARAM_COUNT + { + return Err(make_error(format!("Incorrect argument string; at least {} values must be specified but no more than {}: {:?}", + DETECT_ENTROPY_FIXED_PARAM_COUNT, DETECT_ENTROPY_MAX_PARAM_COUNT, input))); + } + + let mut entropy = DetectEntropyData::new(); + //for value in &values[0..] { + for value in values { + let (mut val, mut name) = take_until_whitespace(value)?; + val = val.trim(); + name = name.trim(); + match name { + "bytes" => { + if 0 != (entropy.flags & DETECT_ENTROPY_FLAG_BYTES) { + return Err(make_error("bytes already set".to_string())); + } + entropy.nbytes = val + .parse::() + .map_err(|_| make_error(format!("invalid bytes value: {}", val)))?; + entropy.flags |= DETECT_ENTROPY_FLAG_BYTES; + } + "offset" => { + if 0 != (entropy.flags & DETECT_ENTROPY_FLAG_OFFSET) { + return Err(make_error("offset already set".to_string())); + } + entropy.offset = val + .parse::() + .map_err(|_| make_error(format!("invalid offset value: {}", val)))?; + if entropy.offset > 65535 || entropy.offset < -65535 { + return Err(make_error(format!( + "invalid offset value: must be between -65535 and 65535: {}", + val + ))); + } + entropy.flags |= DETECT_ENTROPY_FLAG_OFFSET; + } + "oper" => { + if 0 != (entropy.flags & DETECT_ENTROPY_FLAG_OPER) { + return Err(make_error("operator already set".to_string())); + } + entropy.oper = match get_oper_value(val) { + Ok(val) => val, + Err(_) => { + return Err(make_error(format!("unknown operator value {}", val))); + } + }; + entropy.flags |= DETECT_ENTROPY_FLAG_OPER; + } + "value" => { + if 0 != (entropy.flags & DETECT_ENTROPY_FLAG_VALUE) { + return Err(make_error("value already set".to_string())); + } + entropy.value = val + .parse::() + .map_err(|_| make_error(format!("invalid entropy value: {}", val)))?; + entropy.flags |= DETECT_ENTROPY_FLAG_VALUE; + } + _ => { + return Err(make_error(format!("unknown entropy keyword: {}", name))); + } + }; + } + + // an entropy value is required; the default operator is equality + if (entropy.flags & DETECT_ENTROPY_FLAG_VALUE) != DETECT_ENTROPY_FLAG_VALUE { + return Err(make_error(format!( + "required entropy parameter missing: \"{:?}\"", + input + ))); + } + + Ok((input, entropy)) +} + +fn calculate_entropy(data: *const u8, length: i32) -> f64 { + if data.is_null() || length <= 0 { + return 0.0; + } + + // Convert the raw pointer to a slice safely + let data_slice = unsafe { slice::from_raw_parts(data, length as usize) }; + + // Use a 256-element array to store byte frequencies + let mut frequency = [0u32; 256]; + + // Calculate the frequency of each byte + for &byte in data_slice.iter() { + frequency[byte as usize] += 1; + } + + // Calculate entropy using byte frequencies + let length_f64 = length as f64; + frequency.iter().fold(0.0, |entropy, &count| { + if count > 0 { + let probability = count as f64 / length_f64; + entropy - probability * probability.log2() + } else { + entropy + } + }) +} + +#[no_mangle] +pub unsafe extern "C" fn SCDetectEntropyMatch( + c_data: *const c_void, length: i32, ctx: &DetectEntropyData, +) -> bool { + if c_data.is_null() { + return false; + } + + let buffer = std::slice::from_raw_parts(c_data as *const u8, length as usize); + let mut start = buffer; + let mut count = length; + + // Adjust start and count based on offset and nbytes from context + if ctx.offset > 0{ + let offset = ctx.offset; + if offset> count { + SCLogDebug!("offset {} exceeds buffer length {}", offset, count); + return false; + } + start = &start[offset as usize..]; + count -= offset; + } + + if ctx.nbytes > 0 { + let nbytes = ctx.nbytes; + if nbytes > count { + SCLogDebug!("byte count {} exceeds buffer length {}", nbytes, count); + return false; + } + count = nbytes; + } + + // Calculate entropy based on the adjusted buffer slice + let entropy = calculate_entropy(start.as_ptr(), count); + SCLogNotice!("entropy is {}", entropy); + + // Use a hypothetical `detect_entropy_match` function to check entropy + detect_entropy_match(entropy, ctx.value, &ctx.oper) +} + +#[no_mangle] +pub unsafe extern "C" fn SCDetectEntropyParse(c_arg: *const c_char) -> *mut DetectEntropyData { + if c_arg.is_null() { + return std::ptr::null_mut(); + } + + if let Ok(arg) = CStr::from_ptr(c_arg).to_str() { + match parse_entropy(arg) { + Ok((_, detect)) => return Box::into_raw(Box::new(detect)), + Err(_) => return std::ptr::null_mut(), + } + } + return std::ptr::null_mut(); +} + +#[no_mangle] +pub unsafe extern "C" fn SCDetectEntropyFree(ptr: *mut c_void) { + if !ptr.is_null() { + let _ = Box::from_raw(ptr as *mut DetectEntropyData); + } +} + + +#[cfg(test)] +mod tests { + use super::*; + // structure equality only used by test cases + impl PartialEq for DetectEntropyData { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + && self.oper == other.oper + && self.flags == other.flags + && self.offset == other.offset + && self.nbytes == other.nbytes + } + } + + fn valid_test( + args: &str, nbytes: i32, offset: i32, oper: DetectEntropyOperator, value: f64, flags: u8, + ) { + let ded = DetectEntropyData { + offset, + nbytes, + value, + oper, + flags, + }; + + let (_, val) = parse_entropy(args).unwrap(); + assert_eq!(val, ded); + } + + #[test] + fn test_parse_entropy_valid() { + valid_test( + "value 7", + 0, + 0, + DetectEntropyOperator::OperatorEQ, + 7.0, + DETECT_ENTROPY_FLAG_VALUE, + ); + valid_test( + "bytes 4, value 7, oper >=", + 4, + 0, + DetectEntropyOperator::OperatorGTE, + 7.0, + DETECT_ENTROPY_FLAG_OPER | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, + ); + valid_test( + "bytes 4, value 7, oper !=", + 4, + 0, + DetectEntropyOperator::OperatorNEQ, + 7.0, + DETECT_ENTROPY_FLAG_OPER | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, + ); + valid_test( + "bytes 4, value 7, oper <", + 4, + 0, + DetectEntropyOperator::OperatorLT, + 7.0, + DETECT_ENTROPY_FLAG_OPER | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, + ); + valid_test( + "bytes 4, value 7, oper <=", + 4, + 0, + DetectEntropyOperator::OperatorLTE, + 7.0, + DETECT_ENTROPY_FLAG_OPER | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, + ); + valid_test( + "bytes 4, value 7, oper ==", + 4, + 0, + DetectEntropyOperator::OperatorEQ, + 7.0, + DETECT_ENTROPY_FLAG_OPER | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, + ); + valid_test( + "bytes 4, value 7, oper >", + 4, + 0, + DetectEntropyOperator::OperatorGT, + 7.0, + DETECT_ENTROPY_FLAG_OPER | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, + ); + valid_test( + "bytes 4, offset 30, value 7, oper >", + 4, + 30, + DetectEntropyOperator::OperatorGT, + 7.0, + DETECT_ENTROPY_FLAG_OPER + | DETECT_ENTROPY_FLAG_VALUE + | DETECT_ENTROPY_FLAG_BYTES + | DETECT_ENTROPY_FLAG_OFFSET, + ); + valid_test( + "bytes 4, offset 30, value 7", + 4, + 30, + DetectEntropyOperator::OperatorEQ, + 7.0, + DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET, + ); + valid_test( + "bytes 4, offset 30, oper <, value 7", + 4, + 30, + DetectEntropyOperator::OperatorLT, + 7.0, + DETECT_ENTROPY_FLAG_VALUE + | DETECT_ENTROPY_FLAG_OPER + | DETECT_ENTROPY_FLAG_BYTES + | DETECT_ENTROPY_FLAG_OFFSET, + ); + valid_test( + "bytes 4, offset 30, oper <=,value 7", + 4, + 30, + DetectEntropyOperator::OperatorLTE, + 7.0, + DETECT_ENTROPY_FLAG_OPER + | DETECT_ENTROPY_FLAG_VALUE + | DETECT_ENTROPY_FLAG_BYTES + | DETECT_ENTROPY_FLAG_OFFSET, + ); + } + + #[test] + fn test_parse_entropy_invalid() { + assert!(parse_entropy("").is_err()); + assert!(parse_entropy("value ? 7.0").is_err()); + assert!(parse_entropy("bytes 100").is_err()); + assert!(parse_entropy("offset 100").is_err()); + assert!(parse_entropy("bytes 100, offset 100").is_err()); + assert!(parse_entropy("bytes 1, offset 10, oper >, value 7.0, extra").is_err()); + } + + #[test] + fn test_entropy_calculation() { + // Test data + let data = b"aaaaaaa"; // All the same byte + let length = data.len() as i32; + + // Calculate entropy + let entropy = calculate_entropy(data.as_ptr(), length); + + // Expected entropy is 0 (no randomness) + assert!( + (entropy - 0.0).abs() < 1e-6, + "Entropy should be 0.0 for identical bytes" + ); + + // Test data with more randomness + let data = b"abcdabcd"; // Equal distribution + let length = data.len() as i32; + + // Calculate entropy + let entropy = calculate_entropy(data.as_ptr(), length); + + // Expected entropy is 2 (each byte has 1/4 probability) + assert!( + (entropy - 2.0).abs() < 1e-6, + "Entropy should be 2.0 for uniform distribution of 4 values" + ); + + // Test empty data + let data: [u8; 0] = []; + let length = data.len() as i32; + + // Calculate entropy + let entropy = calculate_entropy(data.as_ptr(), length); + + // Expected entropy is 0 (no data) + assert!( + (entropy - 0.0).abs() < 1e-6, + "Entropy should be 0.0 for empty data" + ); + + // Test mixed data + let data = b"aaabbcc"; + let length = data.len() as i32; + + // Calculate entropy + let entropy = calculate_entropy(data.as_ptr(), length); + + // Verify entropy is non-zero and less than maximum + assert!( + entropy > 0.0 && entropy <= 8.0, + "Entropy should be between 0.0 and 8.0" + ); + } +} diff --git a/rust/src/detect/error.rs b/rust/src/detect/error.rs index 78ffda692ce6..d16ca7dbc01c 100644 --- a/rust/src/detect/error.rs +++ b/rust/src/detect/error.rs @@ -28,6 +28,7 @@ pub enum RuleParseError { InvalidIPRep(String), InvalidTransformBase64(String), InvalidByteExtract(String), + InvalidEntropy(String), Nom(I, ErrorKind), } diff --git a/rust/src/detect/mod.rs b/rust/src/detect/mod.rs index 1857c22ee2b2..08dedcf95d41 100644 --- a/rust/src/detect/mod.rs +++ b/rust/src/detect/mod.rs @@ -19,6 +19,7 @@ pub mod byte_extract; pub mod byte_math; +pub mod entropy; pub mod error; pub mod iprep; pub mod parser; From 34d7c35d2c6a1232e936db4b89233cefc11270a6 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 12 Jan 2025 10:11:57 -0500 Subject: [PATCH 2/5] detect/entropy: Add entropy keyword This commit adds keyword/build support for the entropy keyword. The entropy keyword compares an entropy value with a value calculated according to the Shannon entropy on the available content. --- src/Makefile.am | 2 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 1 + src/detect-entropy.c | 75 ++++++++++++++++++++++++++++++++++++ src/detect-entropy.h | 25 ++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 src/detect-entropy.c create mode 100644 src/detect-entropy.h diff --git a/src/Makefile.am b/src/Makefile.am index c2cf2dd93ab5..98050540eebd 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -147,6 +147,7 @@ noinst_HEADERS = \ detect-engine-tag.h \ detect-engine-threshold.h \ detect-engine-uint.h \ + detect-entropy.h \ detect-fast-pattern.h \ detect-file-data.h \ detect-file-hash-common.h \ @@ -712,6 +713,7 @@ libsuricata_c_a_SOURCES = \ detect-engine-tag.c \ detect-engine-threshold.c \ detect-engine-uint.c \ + detect-entropy.c \ detect-fast-pattern.c \ detect-file-data.c \ detect-file-hash-common.c \ diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 82153807ad58..82842d311d7e 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -210,6 +210,7 @@ #include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-string.h" #include "detect-ja4-hash.h" +#include "detect-entropy.h" #include "detect-bypass.h" #include "detect-ftpdata.h" @@ -571,6 +572,7 @@ void SigTableSetup(void) DetectBytetestRegister(); DetectBytejumpRegister(); DetectBytemathRegister(); + DetectEntropyRegister(); DetectSameipRegister(); DetectGeoipRegister(); DetectL3ProtoRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index f46bf688f0f8..348711daa49f 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -96,6 +96,7 @@ enum DetectKeywordId { DETECT_ISDATAAT, DETECT_AL_URILEN, DETECT_ABSENT, + DETECT_ENTROPY, /* end of content inspection */ DETECT_METADATA, diff --git a/src/detect-entropy.c b/src/detect-entropy.c new file mode 100644 index 000000000000..31a1ac8481a3 --- /dev/null +++ b/src/detect-entropy.c @@ -0,0 +1,75 @@ +/* Copyright (C) 2024 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. + */ + +#include "suricata-common.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" + +#include "detect-entropy.h" + +#include "rust.h" + +static int DetectEntropySetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg) +{ + DetectEntropyData *ded = SCDetectEntropyParse(arg); + if (ded == NULL) { + goto error; + } + + int sm_list = DETECT_SM_LIST_PMATCH; + if (s->init_data->list != DETECT_SM_LIST_NOTSET) { + if (DetectBufferGetActiveList(de_ctx, s) == -1) + goto error; + + sm_list = s->init_data->list; + } + + if (SigMatchAppendSMToList(de_ctx, s, DETECT_ENTROPY, (SigMatchCtx *)ded, sm_list) != NULL) { + SCReturnInt(0); + } + + /* fall through */ + +error: + SCLogDebug("error during entropy setup"); + if (ded != NULL) { + SCDetectEntropyFree(ded); + } + SCReturnInt(-1); +} + +static void DetectEntropyFree(DetectEngineCtx *de_ctx, void *ptr) +{ + SCDetectEntropyFree(ptr); +} + +bool DetectEntropyDoMatch(DetectEngineThreadCtx *det_ctx, const Signature *s, + const SigMatchCtx *ctx, const uint8_t *buffer, const uint32_t buffer_len) +{ + return SCDetectEntropyMatch(buffer, buffer_len, (const DetectEntropyData *)ctx); +} + +void DetectEntropyRegister(void) +{ + sigmatch_table[DETECT_ENTROPY].name = "entropy"; + sigmatch_table[DETECT_ENTROPY].desc = "calculate entropy"; + sigmatch_table[DETECT_BYTE_EXTRACT].url = "/rules/payload-keywords.html#entropy"; + sigmatch_table[DETECT_ENTROPY].Free = DetectEntropyFree; + sigmatch_table[DETECT_ENTROPY].Setup = DetectEntropySetup; +} diff --git a/src/detect-entropy.h b/src/detect-entropy.h new file mode 100644 index 000000000000..ea6958ee9057 --- /dev/null +++ b/src/detect-entropy.h @@ -0,0 +1,25 @@ +/* Copyright (C) 2024 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. + */ + +#ifndef SURICATA_DETECT_ENTROPY_H +#define SURICATA_DETECT_ENTROPY_H + +void DetectEntropyRegister(void); +bool DetectEntropyDoMatch(DetectEngineThreadCtx *det_ctx, const Signature *s, + const SigMatchCtx *ctx, const uint8_t *buffer, const uint32_t buffer_len); + +#endif From 784b19546f19f827fe3faab03df13e7236335417 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 12 Jan 2025 10:25:37 -0500 Subject: [PATCH 3/5] detect/entropy: Use entropy matching when needed This commmit causes the content inspection engine to recognize and invoke the entropy "match" function when the entropy keyword is used. --- src/detect-engine-content-inspection.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/detect-engine-content-inspection.c b/src/detect-engine-content-inspection.c index e43e693b2151..35a46e420243 100644 --- a/src/detect-engine-content-inspection.c +++ b/src/detect-engine-content-inspection.c @@ -41,6 +41,7 @@ #include "detect-bytemath.h" #include "detect-bytejump.h" #include "detect-byte-extract.h" +#include "detect-entropy.h" #include "detect-replace.h" #include "detect-engine-content-inspection.h" #include "detect-uricontent.h" @@ -482,6 +483,11 @@ static int DetectEngineContentInspectionInternal(DetectEngineThreadCtx *det_ctx, det_ctx->pcre_match_start_offset = prev_offset; } while (1); + } else if (smd->type == DETECT_ENTROPY) { + if (!DetectEntropyDoMatch(det_ctx, s, smd->ctx, buffer, buffer_len)) { + goto no_match; + } + goto match; } else if (smd->type == DETECT_BYTETEST) { const DetectBytetestData *btd = (const DetectBytetestData *)smd->ctx; uint16_t btflags = btd->flags; From 03aac436f0faf22202b9f835a2c029889e227a2e Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Mon, 13 Jan 2025 08:33:14 -0500 Subject: [PATCH 4/5] doc/entropy: Add documentation for the entropy keyword This commit updates the - Upgrade notes for 7 to 8 - Payload keyword section Both are update to document the new entropy keyword. --- doc/userguide/rules/payload-keywords.rst | 51 ++++++++++++++++++++++++ doc/userguide/upgrade.rst | 2 + 2 files changed, 53 insertions(+) diff --git a/doc/userguide/rules/payload-keywords.rst b/doc/userguide/rules/payload-keywords.rst index 780ad111f7dc..16466cbc08e2 100644 --- a/doc/userguide/rules/payload-keywords.rst +++ b/doc/userguide/rules/payload-keywords.rst @@ -669,6 +669,57 @@ Example:: flow:established,to_server; content:"|00 FF|"; \ byte_extract:2,0,cmp_ver,relative; content:"FooBar"; distance:0; byte_test:2,=,cmp_ver,0; sid:3;) +.. _keyword_entropy: + +entropy +------- + +The ``entropy`` keyword calculates the Shannon entropy value for content and compares it with +an entropy value. When there is a match, rule processing will continue. + +The ``entropy`` keyword syntax is the keyword entropy followed by options +and the entropy value for comparison. + +The minimum entropy keyword specification is:: + + entropy: value + +This results in the calculated entropy value being compared with +`entropy-val` using the equality operator. + +A match occurs when the values and operator agree. This example matches +if the calculated and entropy value are the same. + +Options have default values: +- bytes is equal to the current content length +- offset is 0 +- oper is the equality operator: "==" + +When entropy keyword options are specified, all options and "value" must +be comma-separated. Options and value may be specified in any order. + +The complete format for the ``entropy`` keyword is:: + + entropy: [bytes ] [offset ] [oper ] value + +This example shows all possible options with default values:: + + entropy: bytes 0, offset 0, oper ==, value + +The following operators are available:: + + * == (default): Match when calculated value equals entropy value + * < Match when calculated value is strictly less than entropy value + * <= Match when calculated value is less than or equal to entropy value + * > Match when calculated value is strictly greater than entropy value + * >= Match when calculated value is greater than or equal to entropy value + * != Match when calculated value is not equal to entropy value + +This example matches if the `file.data` content for an HTTP transaction has +a Shannon entropy value of 4 or higher:: + + alert http any any -> any any (msg:"entropy simple test"; file.data; entropy: value 4, oper >=; sid:1;) + rpc --- diff --git a/doc/userguide/upgrade.rst b/doc/userguide/upgrade.rst index 4bf74b65284d..cb5d3dec5bb8 100644 --- a/doc/userguide/upgrade.rst +++ b/doc/userguide/upgrade.rst @@ -82,6 +82,8 @@ Major changes - Unknown requirements in the ``requires`` keyword will now be treated as unmet requirements, causing the rule to not be loaded. See :ref:`keyword_requires`. +- New rule keyword ``entropy`` for alerting based on entropy values. See + :ref:`keyword_entropy`. Removals ~~~~~~~~ From aa5e2abc2db894eb4f56c071391c13531b488d3b Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Mon, 13 Jan 2025 08:34:35 -0500 Subject: [PATCH 5/5] doc/typo: Correct misspelling. --- doc/userguide/rules/meta.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/userguide/rules/meta.rst b/doc/userguide/rules/meta.rst index 8312c91de091..5856e702b91c 100644 --- a/doc/userguide/rules/meta.rst +++ b/doc/userguide/rules/meta.rst @@ -225,7 +225,7 @@ errors. Requirements that follow the valid format of `` `` but are not known to Suricata are allowed for future -compatiblity, however unknown requirement expressions will lead to the +compatibility, however unknown requirement expressions will lead to the requirement not being met, skipping the rule. When parsing rules, the parser attempts to process the ``requires``