/src/suricata8/rust/src/detect/entropy.rs
Line | Count | Source |
1 | | /* Copyright (C) 2025 Open Information Security Foundation |
2 | | * |
3 | | * You can copy, redistribute or modify this Program under the terms of |
4 | | * the GNU General Public License version 2 as published by the Free |
5 | | * Software Foundation. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * version 2 along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
15 | | * 02110-1301, USA. |
16 | | */ |
17 | | |
18 | | // Author: Jeff Lucovsky <jlucovsky@oisf.net> |
19 | | // |
20 | | use crate::detect::error::RuleParseError; |
21 | | use crate::detect::float::{detect_match_float, detect_parse_float, DetectFloatData}; |
22 | | use crate::detect::parser::take_until_whitespace; |
23 | | |
24 | | use nom7::bytes::complete::tag; |
25 | | use nom7::character::complete::multispace0; |
26 | | use nom7::sequence::preceded; |
27 | | use nom7::{Err, IResult}; |
28 | | |
29 | | use std::ffi::CStr; |
30 | | use std::os::raw::{c_double, c_char, c_void}; |
31 | | use std::slice; |
32 | | |
33 | | #[repr(C)] |
34 | | #[derive(Debug)] |
35 | | pub struct DetectEntropyData { |
36 | | offset: i32, |
37 | | nbytes: i32, |
38 | | fv_idx: i32, |
39 | | value: DetectFloatData<f64>, |
40 | | } |
41 | | |
42 | | impl Default for DetectEntropyData { |
43 | 2.84k | fn default() -> Self { |
44 | 2.84k | DetectEntropyData { |
45 | 2.84k | offset: 0, |
46 | 2.84k | nbytes: 0, |
47 | 2.84k | fv_idx: 0, |
48 | 2.84k | value: DetectFloatData::<f64>::default(), |
49 | 2.84k | } |
50 | 2.84k | } |
51 | | } |
52 | | impl DetectEntropyData { |
53 | 2.84k | pub fn new() -> Self { |
54 | 2.84k | Self { |
55 | 2.84k | ..Default::default() |
56 | 2.84k | } |
57 | 2.84k | } |
58 | | } |
59 | | |
60 | | // All options have default values except for the entropy value |
61 | | const DETECT_ENTROPY_FIXED_PARAM_COUNT: usize = 1; |
62 | | const DETECT_ENTROPY_MAX_PARAM_COUNT: usize = 4; |
63 | | pub const DETECT_ENTROPY_FLAG_BYTES: u8 = 0x01; |
64 | | pub const DETECT_ENTROPY_FLAG_OFFSET: u8 = 0x02; |
65 | | pub const DETECT_ENTROPY_FLAG_VALUE: u8 = 0x04; |
66 | | |
67 | 2.84k | fn parse_entropy<'a>( |
68 | 2.84k | input: &'a str, flags: &'a mut u8, |
69 | 2.84k | ) -> IResult<&'a str, DetectEntropyData, RuleParseError<&'a str>> { |
70 | | // Inner utility function for easy error creation. |
71 | 381 | fn make_error(reason: String) -> nom7::Err<RuleParseError<&'static str>> { |
72 | 381 | Err::Error(RuleParseError::InvalidEntropy(reason)) |
73 | 381 | } |
74 | 2.84k | let (_, values) = nom7::multi::separated_list1( |
75 | 2.84k | tag(","), |
76 | 2.84k | preceded(multispace0, nom7::bytes::complete::is_not(",")), |
77 | 2.84k | )(input)?; |
78 | | |
79 | 2.84k | if values.len() < DETECT_ENTROPY_FIXED_PARAM_COUNT |
80 | 2.84k | || values.len() > DETECT_ENTROPY_MAX_PARAM_COUNT |
81 | | { |
82 | 7 | return Err(make_error(format!("Incorrect argument string; at least {} values must be specified but no more than {}: {:?}", |
83 | 7 | DETECT_ENTROPY_FIXED_PARAM_COUNT, DETECT_ENTROPY_MAX_PARAM_COUNT, input))); |
84 | 2.84k | } |
85 | | |
86 | 2.84k | let mut entropy = DetectEntropyData::new(); |
87 | 5.31k | for value in values { |
88 | 2.85k | let (mut val, mut name) = take_until_whitespace(value)?; |
89 | 2.85k | val = val.trim(); |
90 | 2.85k | name = name.trim(); |
91 | 2.85k | match name { |
92 | 2.85k | "bytes" => { |
93 | 3 | if 0 != (*flags & DETECT_ENTROPY_FLAG_BYTES) { |
94 | 0 | return Err(make_error("bytes already set".to_string())); |
95 | 3 | } |
96 | 3 | entropy.nbytes = val |
97 | 3 | .parse::<i32>() |
98 | 3 | .map_err(|_| make_error(format!("invalid bytes value: {}", val)))?; |
99 | 1 | *flags |= DETECT_ENTROPY_FLAG_BYTES; |
100 | | } |
101 | 2.84k | "offset" => { |
102 | 5 | if 0 != (*flags & DETECT_ENTROPY_FLAG_OFFSET) { |
103 | 0 | return Err(make_error("offset already set".to_string())); |
104 | 5 | } |
105 | 5 | entropy.offset = val |
106 | 5 | .parse::<i32>() |
107 | 5 | .map_err(|_| make_error(format!("invalid offset value: {}", val)))?; |
108 | 4 | if entropy.offset > 65535 || entropy.offset < -65535 { |
109 | 1 | return Err(make_error(format!( |
110 | 1 | "invalid offset value: must be between -65535 and 65535: {}", |
111 | 1 | val |
112 | 1 | ))); |
113 | 3 | } |
114 | 3 | *flags |= DETECT_ENTROPY_FLAG_OFFSET; |
115 | | } |
116 | 2.84k | "value" => { |
117 | 2.78k | if 0 != (*flags & DETECT_ENTROPY_FLAG_VALUE) { |
118 | 1 | return Err(make_error("value already set".to_string())); |
119 | 2.78k | } |
120 | 2.78k | if let Ok((_, ctx)) = detect_parse_float::<f64>(val) { |
121 | 2.47k | entropy.value = ctx; |
122 | 2.47k | *flags |= DETECT_ENTROPY_FLAG_VALUE; |
123 | 2.47k | } else { |
124 | 312 | return Err(make_error(format!("invalid entropy value: {}", val))); |
125 | | } |
126 | | } |
127 | | _ => { |
128 | 56 | return Err(make_error(format!("unknown entropy option: {}", name))); |
129 | | } |
130 | | }; |
131 | | } |
132 | | |
133 | | // an entropy value is required |
134 | 2.46k | if (*flags & DETECT_ENTROPY_FLAG_VALUE) != DETECT_ENTROPY_FLAG_VALUE { |
135 | 1 | return Err(make_error(format!( |
136 | 1 | "required entropy parameter missing: \"{:?}\"", |
137 | 1 | input |
138 | 1 | ))); |
139 | 2.46k | } |
140 | | |
141 | 2.46k | Ok((input, entropy)) |
142 | 2.84k | } |
143 | | |
144 | 77 | fn calculate_entropy(data: &[u8]) -> f64 { |
145 | 77 | if data.is_empty() { |
146 | 0 | return 0.0; |
147 | 77 | } |
148 | | |
149 | | // Use a 256-element array to store byte frequencies |
150 | 77 | let mut frequency = [0u32; 256]; |
151 | | |
152 | | // Calculate the frequency of each byte |
153 | 89.8k | for &byte in data.iter() { |
154 | 89.8k | frequency[byte as usize] += 1; |
155 | 89.8k | } |
156 | | |
157 | | // Calculate entropy using byte frequencies |
158 | 77 | let length_f64 = data.len() as f64; |
159 | 19.7k | frequency.iter().fold(0.0, |entropy, &count| { |
160 | 19.7k | if count > 0 { |
161 | 4.87k | let probability = count as f64 / length_f64; |
162 | 4.87k | entropy - probability * probability.log2() |
163 | | } else { |
164 | 14.8k | entropy |
165 | | } |
166 | 19.7k | }) |
167 | 77 | } |
168 | | |
169 | | #[no_mangle] |
170 | 77 | pub unsafe extern "C" fn SCDetectEntropyMatch( |
171 | 77 | c_data: *const c_void, length: i32, ctx: &DetectEntropyData, |
172 | 77 | calculated_entropy: *mut c_double, |
173 | 77 | ) -> bool { |
174 | 77 | if c_data.is_null() { |
175 | 0 | return false; |
176 | 77 | } |
177 | | |
178 | 77 | let buffer = std::slice::from_raw_parts(c_data as *const u8, length as usize); |
179 | 77 | let mut start = buffer; |
180 | 77 | let mut count = length; |
181 | | |
182 | | // Adjust start and count based on offset and nbytes from context |
183 | 77 | if ctx.offset > 0 { |
184 | 0 | let offset = ctx.offset; |
185 | 0 | if offset > count { |
186 | | SCLogDebug!("offset {} exceeds buffer length {}", offset, count); |
187 | 0 | return false; |
188 | 0 | } |
189 | 0 | start = &start[offset as usize..]; |
190 | 0 | count -= offset; |
191 | 77 | } |
192 | | |
193 | 77 | if ctx.nbytes > 0 { |
194 | 0 | let nbytes = ctx.nbytes; |
195 | 0 | if nbytes > count { |
196 | | SCLogDebug!("byte count {} exceeds buffer length {}", nbytes, count); |
197 | 0 | return false; |
198 | 0 | } |
199 | 0 | count = nbytes; |
200 | 77 | } |
201 | | |
202 | | // Calculate entropy based on the adjusted buffer slice |
203 | 77 | let data_slice = slice::from_raw_parts(start.as_ptr(), count as usize); |
204 | 77 | let entropy = calculate_entropy(data_slice); |
205 | | SCLogDebug!("entropy is {}", entropy); |
206 | | |
207 | | // Return entropy on request |
208 | 77 | if !calculated_entropy.is_null() { |
209 | 77 | *calculated_entropy = entropy; |
210 | 77 | } |
211 | | |
212 | 77 | detect_match_float::<f64>(&ctx.value, entropy) |
213 | 77 | } |
214 | | |
215 | | #[no_mangle] |
216 | 2.84k | pub unsafe extern "C" fn SCDetectEntropyParse(c_arg: *const c_char) -> *mut DetectEntropyData { |
217 | 2.84k | if c_arg.is_null() { |
218 | 0 | return std::ptr::null_mut(); |
219 | 2.84k | } |
220 | | |
221 | 2.84k | if let Ok(arg) = CStr::from_ptr(c_arg).to_str() { |
222 | 2.84k | let mut flags = 0; |
223 | 2.84k | match parse_entropy(arg, &mut flags) { |
224 | 2.46k | Ok((_, detect)) => return Box::into_raw(Box::new(detect)), |
225 | 382 | Err(_) => return std::ptr::null_mut(), |
226 | | } |
227 | 0 | } |
228 | 0 | return std::ptr::null_mut(); |
229 | 2.84k | } |
230 | | |
231 | | #[no_mangle] |
232 | 2.46k | pub unsafe extern "C" fn SCDetectEntropyFree(ptr: *mut c_void) { |
233 | 2.46k | if !ptr.is_null() { |
234 | 2.46k | let _ = Box::from_raw(ptr as *mut DetectEntropyData); |
235 | 2.46k | } |
236 | 2.46k | } |
237 | | |
238 | | #[cfg(test)] |
239 | | mod tests { |
240 | | use super::*; |
241 | | use crate::detect::float::DetectFloatMode; |
242 | | use num::traits::float::FloatCore; |
243 | | // structure equality only used by test cases |
244 | | impl PartialEq for DetectEntropyData { |
245 | | fn eq(&self, other: &Self) -> bool { |
246 | | self.value == other.value && self.offset == other.offset && self.nbytes == other.nbytes |
247 | | } |
248 | | } |
249 | | |
250 | | fn valid_test( |
251 | | args: &str, nbytes: i32, offset: i32, value: f64, mode: DetectFloatMode, flags: u8, |
252 | | ) { |
253 | | let ctx = DetectFloatData { |
254 | | arg1: value, |
255 | | arg2: FloatCore::min_value(), |
256 | | mode, |
257 | | }; |
258 | | let ded = DetectEntropyData { |
259 | | offset, |
260 | | nbytes, |
261 | | fv_idx: 0, |
262 | | value: ctx, |
263 | | }; |
264 | | |
265 | | let mut parsed_flags = 0; |
266 | | let (_, val) = parse_entropy(args, &mut parsed_flags).unwrap(); |
267 | | assert_eq!(flags, parsed_flags); |
268 | | assert_eq!(val, ded); |
269 | | } |
270 | | |
271 | | #[test] |
272 | | fn test_parse_entropy_valid() { |
273 | | valid_test( |
274 | | "value 7", |
275 | | 0, |
276 | | 0, |
277 | | 7.0, |
278 | | DetectFloatMode::DetectFloatModeEqual, |
279 | | DETECT_ENTROPY_FLAG_VALUE, |
280 | | ); |
281 | | valid_test( |
282 | | "bytes 4, value >= 7", |
283 | | 4, |
284 | | 0, |
285 | | 7.0, |
286 | | DetectFloatMode::DetectFloatModeGte, |
287 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, |
288 | | ); |
289 | | valid_test( |
290 | | "bytes 4, value != 7", |
291 | | 4, |
292 | | 0, |
293 | | 7.0, |
294 | | DetectFloatMode::DetectFloatModeNe, |
295 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, |
296 | | ); |
297 | | valid_test( |
298 | | "bytes 4, value <7", |
299 | | 4, |
300 | | 0, |
301 | | 7.0, |
302 | | DetectFloatMode::DetectFloatModeLt, |
303 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, |
304 | | ); |
305 | | valid_test( |
306 | | "bytes 4, value <= 7", |
307 | | 4, |
308 | | 0, |
309 | | 7.0, |
310 | | DetectFloatMode::DetectFloatModeLte, |
311 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, |
312 | | ); |
313 | | valid_test( |
314 | | "bytes 4, value = 7", |
315 | | 4, |
316 | | 0, |
317 | | 7.0, |
318 | | DetectFloatMode::DetectFloatModeEqual, |
319 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, |
320 | | ); |
321 | | valid_test( |
322 | | "bytes 4, value > 7", |
323 | | 4, |
324 | | 0, |
325 | | 7.0, |
326 | | DetectFloatMode::DetectFloatModeGt, |
327 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES, |
328 | | ); |
329 | | valid_test( |
330 | | "bytes 4, offset 30, value > 7", |
331 | | 4, |
332 | | 30, |
333 | | 7.0, |
334 | | DetectFloatMode::DetectFloatModeGt, |
335 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET, |
336 | | ); |
337 | | valid_test( |
338 | | "bytes 4, offset 30, value 7", |
339 | | 4, |
340 | | 30, |
341 | | 7.0, |
342 | | DetectFloatMode::DetectFloatModeEqual, |
343 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET, |
344 | | ); |
345 | | valid_test( |
346 | | "bytes 4, offset 30, value < 7", |
347 | | 4, |
348 | | 30, |
349 | | 7.0, |
350 | | DetectFloatMode::DetectFloatModeLt, |
351 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET, |
352 | | ); |
353 | | valid_test( |
354 | | "bytes 4, offset 30,value <= 7", |
355 | | 4, |
356 | | 30, |
357 | | 7.0, |
358 | | DetectFloatMode::DetectFloatModeLte, |
359 | | DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET, |
360 | | ); |
361 | | } |
362 | | |
363 | | #[test] |
364 | | fn test_parse_entropy_invalid() { |
365 | | let mut parsed_flags = 0; |
366 | | assert!(parse_entropy("", &mut parsed_flags,).is_err()); |
367 | | assert!(parse_entropy("value ? 7.0", &mut parsed_flags,).is_err()); |
368 | | assert!(parse_entropy("bytes 100", &mut parsed_flags,).is_err()); |
369 | | assert!(parse_entropy("offset 100", &mut parsed_flags,).is_err()); |
370 | | assert!(parse_entropy("bytes 100, offset 100", &mut parsed_flags,).is_err()); |
371 | | assert!(parse_entropy("bytes 1, offset 10, value 7.0, extra", &mut parsed_flags,).is_err()); |
372 | | } |
373 | | |
374 | | #[test] |
375 | | fn test_entropy_calculation() { |
376 | | // Test data |
377 | | let data = b"aaaaaaa"; // All the same byte |
378 | | |
379 | | // Calculate entropy |
380 | | let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) }; |
381 | | let entropy = calculate_entropy(data_slice); |
382 | | |
383 | | // Expected entropy is 0 (no randomness) |
384 | | assert!( |
385 | | (entropy - 0.0).abs() < 1e-6, |
386 | | "Entropy should be 0.0 for identical bytes" |
387 | | ); |
388 | | |
389 | | // Test data with more randomness |
390 | | let data = b"abcdabcd"; // Equal distribution |
391 | | |
392 | | // Calculate entropy |
393 | | let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) }; |
394 | | let entropy = calculate_entropy(data_slice); |
395 | | |
396 | | // Expected entropy is 2 (each byte has 1/4 probability) |
397 | | assert!( |
398 | | (entropy - 2.0).abs() < 1e-6, |
399 | | "Entropy should be 2.0 for uniform distribution of 4 values" |
400 | | ); |
401 | | |
402 | | // Test empty data |
403 | | let data: [u8; 0] = []; |
404 | | |
405 | | // Calculate entropy |
406 | | let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) }; |
407 | | let entropy = calculate_entropy(data_slice); |
408 | | |
409 | | // Expected entropy is 0 (no data) |
410 | | assert!( |
411 | | (entropy - 0.0).abs() < 1e-6, |
412 | | "Entropy should be 0.0 for empty data" |
413 | | ); |
414 | | |
415 | | // Test mixed data |
416 | | let data = b"aaabbcc"; |
417 | | |
418 | | // Calculate entropy |
419 | | let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) }; |
420 | | let entropy = calculate_entropy(data_slice); |
421 | | |
422 | | // Verify entropy is non-zero and less than maximum |
423 | | assert!( |
424 | | entropy > 0.0 && entropy <= 8.0, |
425 | | "Entropy should be between 0.0 and 8.0" |
426 | | ); |
427 | | } |
428 | | } |