diff --git a/etc/schema.json b/etc/schema.json index 5fbf3245ba..261201d55a 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -681,6 +681,11 @@ "type": "string" } } + }, + "suricata": { + "keywords": [ + "dnp3.iin" + ] } }, "request": { @@ -908,6 +913,11 @@ "type": "string" } } + }, + "suricata": { + "keywords": [ + "dnp3.iin" + ] } }, "src": { diff --git a/rust/src/dnp3/detect.rs b/rust/src/dnp3/detect.rs new file mode 100644 index 0000000000..b3e77f6d5b --- /dev/null +++ b/rust/src/dnp3/detect.rs @@ -0,0 +1,84 @@ +/* Copyright (C) 2025 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. + */ + +use crate::detect::uint::{detect_parse_uint_bitflags, DetectUintData}; + +use std::ffi::CStr; + +#[repr(u16)] +#[derive(EnumStringU16)] +#[allow(non_camel_case_types)] +pub enum Dnp3IndFlag { + Device_restart = 0x8000, + Device_trouble = 0x4000, + Local_control = 0x2000, + Need_time = 0x1000, + Class_3_events = 0x0800, + Class_2_events = 0x0400, + Class_1_events = 0x0200, + All_stations = 0x0100, + Reserved_1 = 0x0080, + Reserved_2 = 0x0040, + Config_corrupt = 0x0020, + Already_executing = 0x0010, + Event_buffer_overflow = 0x0008, + Parameter_error = 0x0004, + Object_unknown = 0x0002, + No_func_code_support = 0x0001, +} + +#[no_mangle] +pub unsafe extern "C" fn SCDnp3DetectIndParse( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = detect_parse_uint_bitflags::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn dnp3_ind_parse() { + let ctx = detect_parse_uint_bitflags::("0").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_bitflags::("1").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_bitflags::("0x0").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_bitflags::("0x0000").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_bitflags::("0x0001").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_bitflags::("0x8421").unwrap(); + assert_eq!(ctx.arg1, 0x8421); + assert!(detect_parse_uint_bitflags::("a").is_none()); + let ctx = detect_parse_uint_bitflags::("all_stations").unwrap(); + assert_eq!(ctx.arg1, 0x0100); + let ctx = detect_parse_uint_bitflags::("class_1_events , class_2_events") + .unwrap(); + assert_eq!(ctx.arg1, 0x600); + assert!(detect_parse_uint_bitflags::("something").is_none()); + } +} diff --git a/rust/src/dnp3/log.rs b/rust/src/dnp3/log.rs new file mode 100644 index 0000000000..11c2ff2da8 --- /dev/null +++ b/rust/src/dnp3/log.rs @@ -0,0 +1,42 @@ +/* Copyright (C) 2025 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. + */ + +use crate::detect::EnumString; +use crate::jsonbuilder::{JsonBuilder, JsonError}; + +use super::detect::Dnp3IndFlag; + +fn log_iin(js: &mut JsonBuilder, iin: u16) -> Result<(), JsonError> { + js.open_array("indicators")?; + + for i in 0..16 { + if (iin & (1 << i)) != 0 { + let ind = Dnp3IndFlag::from_u(1 << i).unwrap(); + js.append_string(ind.to_str())?; + } + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn SCJsonDNP3LogIin(js: &mut JsonBuilder, iin: u16) -> bool { + if iin == 0 { + return false; + } + log_iin(js, iin).is_ok() +} diff --git a/rust/src/dnp3/mod.rs b/rust/src/dnp3/mod.rs new file mode 100644 index 0000000000..2f7759a946 --- /dev/null +++ b/rust/src/dnp3/mod.rs @@ -0,0 +1,21 @@ +/* Copyright (C) 2025 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. + */ + +//! Application layer dnp3 detection. + +pub mod detect; +pub mod log; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1022212871..b53f78bfb5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -96,6 +96,7 @@ pub mod handshake; pub mod lua; +pub mod dnp3; pub mod dns; pub mod mdns; pub mod nfs; diff --git a/src/detect-dnp3.c b/src/detect-dnp3.c index 53fe9bd542..48be7e9d75 100644 --- a/src/detect-dnp3.c +++ b/src/detect-dnp3.c @@ -27,12 +27,14 @@ #include "detect-engine-mpm.h" #include "detect-engine-prefilter.h" #include "detect-engine-content-inspection.h" +#include "detect-engine-uint.h" #include "app-layer-dnp3.h" #include "util-byte.h" static int g_dnp3_match_buffer_id = 0; static int g_dnp3_data_buffer_id = 0; +static int g_dnp3_ind_buffer_id = 0; /** * The detection struct. @@ -43,10 +45,6 @@ typedef struct DetectDNP3_ { /* Function code for function code detection. */ uint8_t function_code; }; - struct { - /* Internal indicator flags for IIN detection. */ - uint16_t ind_flags; - }; struct { /* Object info for object detection. */ uint8_t obj_group; @@ -55,31 +53,6 @@ typedef struct DetectDNP3_ { }; } DetectDNP3; -/** - * Indicator names to value mappings (Snort compatible). - */ -DNP3Mapping DNP3IndicatorsMap[] = { - {"device_restart", 0x8000}, - {"device_trouble", 0x4000}, - {"local_control", 0x2000}, - {"need_time", 0x1000}, - {"class_3_events", 0x0800}, - {"class_2_events", 0x0400}, - {"class_1_events", 0x0200}, - {"all_stations", 0x0100}, - - {"reserved_1", 0x0080}, - {"reserved_2", 0x0040}, - {"config_corrupt", 0x0020}, - {"already_executing", 0x0010}, - {"event_buffer_overflow", 0x0008}, - {"parameter_error", 0x0004}, - {"object_unknown", 0x0002}, - {"no_func_code_support", 0x0001}, - - {NULL, 0}, -}; - /** * Application function code name to code mappings (Snort compatible). */ @@ -125,27 +98,9 @@ DNP3Mapping DNP3FunctionNameMap[] = { #ifdef UNITTESTS static void DetectDNP3FuncRegisterTests(void); -static void DetectDNP3IndRegisterTests(void); static void DetectDNP3ObjRegisterTests(void); #endif -/** - * \brief Utility function to trim leading and trailing whitespace - * from a string. - */ -static char *TrimString(char *str) -{ - char *end = str + strlen(str) - 1; - while (isspace(*str)) { - str++; - } - while (end > str && isspace(*end)) { - end--; - } - *(end + 1) = '\0'; - return str; -} - static InspectionBuffer *GetDNP3Data(DetectEngineThreadCtx *det_ctx, const DetectEngineTransforms *transforms, Flow *_f, const uint8_t flow_flags, @@ -235,80 +190,33 @@ error: SCReturnInt(-1); } -static int DetectDNP3IndParseByName(const char *str, uint16_t *flags) +static void DetectDNP3IndFree(DetectEngineCtx *de_ctx, void *ptr) { - char tmp[strlen(str) + 1]; - char *p, *last = NULL; - - strlcpy(tmp, str, sizeof(tmp)); - - for ((p = strtok_r(tmp, ",", &last)); p; (p = strtok_r(NULL, ",", &last))) { - p = TrimString(p); - int found = 0; - int i = 0; - while (DNP3IndicatorsMap[i].name != NULL) { - if (strcasecmp(p, DNP3IndicatorsMap[i].name) == 0) { - *flags |= DNP3IndicatorsMap[i].value; - found = 1; - break; - } - i++; - } - - if (!found) { - SCLogError("Bad argument \"%s\" supplied to dnp3.ind keyword.", p); - return 0; - } - } - - return 1; -} - -static int DetectDNP3IndParse(const char *str, uint16_t *flags) -{ - *flags = 0; - - if (StringParseUint16(flags, 0, (uint16_t)strlen(str), str) > 0) { - return 1; - } - - /* Parse by name - will log a more specific error message on error. */ - if (DetectDNP3IndParseByName(str, flags)) { - return 1; - } - - return 0; + SCDetectU16Free(ptr); } static int DetectDNP3IndSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) { SCEnter(); - DetectDNP3 *detect = NULL; - uint16_t flags; if (SCDetectSignatureSetAppProto(s, ALPROTO_DNP3) != 0) return -1; - if (!DetectDNP3IndParse(str, &flags)) { + DetectU16Data *detect = SCDnp3DetectIndParse(str); + if (detect == NULL) { SCLogError("Invalid argument \"%s\" supplied to dnp3.ind keyword.", str); return -1; } - detect = SCCalloc(1, sizeof(DetectDNP3)); - if (unlikely(detect == NULL)) { - goto error; - } - detect->ind_flags = flags; - if (SCSigMatchAppendSMToList( - de_ctx, s, DETECT_DNP3IND, (SigMatchCtx *)detect, g_dnp3_match_buffer_id) == NULL) { + de_ctx, s, DETECT_DNP3IND, (SigMatchCtx *)detect, g_dnp3_ind_buffer_id) == NULL) { goto error; } SCReturnInt(0); error: if (detect != NULL) { - SCFree(detect); + DetectDNP3IndFree(NULL, detect); } SCReturnInt(-1); } @@ -438,16 +346,9 @@ static int DetectDNP3IndMatch(DetectEngineThreadCtx *det_ctx, const SigMatchCtx *ctx) { DNP3Transaction *tx = (DNP3Transaction *)txv; - DetectDNP3 *detect = (DetectDNP3 *)ctx; + DetectU16Data *detect = (DetectU16Data *)ctx; - if (flags & STREAM_TOCLIENT) { - if ((tx->iin.iin1 & (detect->ind_flags >> 8)) || - (tx->iin.iin2 & (detect->ind_flags & 0xf))) { - return 1; - } - } - - return 0; + return DetectU16Match((uint16_t)((tx->iin.iin1 << 8) | tx->iin.iin2), detect); } static void DetectDNP3FuncRegister(void) @@ -481,10 +382,8 @@ static void DetectDNP3IndRegister(void) sigmatch_table[DETECT_DNP3IND].Match = NULL; sigmatch_table[DETECT_DNP3IND].AppLayerTxMatch = DetectDNP3IndMatch; sigmatch_table[DETECT_DNP3IND].Setup = DetectDNP3IndSetup; - sigmatch_table[DETECT_DNP3IND].Free = DetectDNP3Free; -#ifdef UNITTESTS - sigmatch_table[DETECT_DNP3IND].RegisterTests = DetectDNP3IndRegisterTests; -#endif + sigmatch_table[DETECT_DNP3IND].Free = DetectDNP3IndFree; + sigmatch_table[DETECT_DNP3IND].flags = SIGMATCH_INFO_UINT16 | SIGMATCH_INFO_BITFLAGS_UINT; SCReturn; } @@ -560,6 +459,9 @@ void DetectDNP3Register(void) g_dnp3_match_buffer_id = DetectBufferTypeRegister("dnp3"); + DetectAppLayerInspectEngineRegister( + "dnp3_ind", ALPROTO_DNP3, SIG_FLAG_TOCLIENT, 0, DetectEngineInspectGenericList, NULL); + g_dnp3_ind_buffer_id = DetectBufferTypeRegister("dnp3_ind"); } #ifdef UNITTESTS @@ -624,46 +526,6 @@ static int DetectDNP3FuncTest01(void) PASS; } -static int DetectDNP3IndTestParseAsInteger(void) -{ - uint16_t flags = 0; - - FAIL_IF(!DetectDNP3IndParse("0", &flags)); - FAIL_IF(flags != 0); - FAIL_IF(!DetectDNP3IndParse("1", &flags)); - FAIL_IF(flags != 0x0001); - - FAIL_IF(!DetectDNP3IndParse("0x0", &flags)); - FAIL_IF(flags != 0); - FAIL_IF(!DetectDNP3IndParse("0x0000", &flags)); - FAIL_IF(flags != 0); - FAIL_IF(!DetectDNP3IndParse("0x0001", &flags)); - FAIL_IF(flags != 0x0001); - - FAIL_IF(!DetectDNP3IndParse("0x8421", &flags)); - FAIL_IF(flags != 0x8421); - - FAIL_IF(DetectDNP3IndParse("a", &flags)); - - PASS; -} - -static int DetectDNP3IndTestParseByName(void) -{ - uint16_t flags = 0; - - FAIL_IF(!DetectDNP3IndParse("all_stations", &flags)); - FAIL_IF(!(flags & 0x0100)); - FAIL_IF(!DetectDNP3IndParse("class_1_events , class_2_events", &flags)); - FAIL_IF(!(flags & 0x0200)); - FAIL_IF(!(flags & 0x0400)); - FAIL_IF((flags & 0xf9ff)); - - FAIL_IF(DetectDNP3IndParse("something", &flags)); - - PASS; -} - static int DetectDNP3ObjSetupTest(void) { DetectEngineCtx *de_ctx = DetectEngineCtxInit(); @@ -711,14 +573,6 @@ static void DetectDNP3FuncRegisterTests(void) UtRegisterTest("DetectDNP3FuncTest01", DetectDNP3FuncTest01); } -static void DetectDNP3IndRegisterTests(void) -{ - UtRegisterTest("DetectDNP3IndTestParseAsInteger", - DetectDNP3IndTestParseAsInteger); - UtRegisterTest("DetectDNP3IndTestParseByName", - DetectDNP3IndTestParseByName); -} - static void DetectDNP3ObjRegisterTests(void) { UtRegisterTest("DetectDNP3ObjParseTest", DetectDNP3ObjParseTest); diff --git a/src/detect-dnp3.h b/src/detect-dnp3.h index 050158c182..f3d7d1ce95 100644 --- a/src/detect-dnp3.h +++ b/src/detect-dnp3.h @@ -26,9 +26,6 @@ typedef struct DNP3Mapping_ { uint16_t value; } DNP3Mapping; -/* Map of internal indicators to value for external use. */ -extern DNP3Mapping DNP3IndicatorsMap[]; - void DetectDNP3Register(void); #endif /* SURICATA_DETECT_DNP3_H */ diff --git a/src/output-json-dnp3.c b/src/output-json-dnp3.c index 59835dbb1a..2a9ce97e66 100644 --- a/src/output-json-dnp3.c +++ b/src/output-json-dnp3.c @@ -61,22 +61,6 @@ static void JsonDNP3LogLinkControl(SCJsonBuilder *js, uint8_t lc) SCJbSetUint(js, "function_code", DNP3_LINK_FC(lc)); } -static void JsonDNP3LogIin(SCJsonBuilder *js, uint16_t iin) -{ - if (iin) { - SCJbOpenArray(js, "indicators"); - - int mapping = 0; - do { - if (iin & DNP3IndicatorsMap[mapping].value) { - SCJbAppendString(js, DNP3IndicatorsMap[mapping].name); - } - mapping++; - } while (DNP3IndicatorsMap[mapping].name != NULL); - SCJbClose(js); - } -} - static void JsonDNP3LogApplicationControl(SCJsonBuilder *js, uint8_t ac) { SCJbSetBool(js, "fir", DNP3_APP_FIR(ac)); @@ -206,7 +190,7 @@ static void JsonDNP3LogResponse(SCJsonBuilder *js, DNP3Transaction *dnp3tx) SCJbClose(js); SCJbOpenObject(js, "iin"); - JsonDNP3LogIin(js, (uint16_t)(dnp3tx->iin.iin1 << 8 | dnp3tx->iin.iin2)); + SCJsonDNP3LogIin(js, (uint16_t)(dnp3tx->iin.iin1 << 8 | dnp3tx->iin.iin2)); SCJbClose(js); }