diff --git a/doc/userguide/configuration/suricata-yaml.rst b/doc/userguide/configuration/suricata-yaml.rst index 0b39705d896b..4bf870f7366f 100644 --- a/doc/userguide/configuration/suricata-yaml.rst +++ b/doc/userguide/configuration/suricata-yaml.rst @@ -1761,7 +1761,8 @@ incompatible with ``decode-mime``. If both are enabled, Maximum transactions ~~~~~~~~~~~~~~~~~~~~ -MQTT, FTP, PostgreSQL, SMB, DCERPC and NFS have each a `max-tx` parameter that can be customized. +MQTT, FTP, PostgreSQL, SMB, DCERPC, ENIP and NFS have each a `max-tx` +parameter that can be customized. `max-tx` refers to the maximum number of live transactions for each flow. An app-layer event `protocol.too_many_transactions` is triggered when this value is reached. The point of this parameter is to find a balance between the completeness of analysis diff --git a/doc/userguide/rules/enip-keyword.rst b/doc/userguide/rules/enip-keyword.rst index 5899ca47883a..0b7baa765326 100644 --- a/doc/userguide/rules/enip-keyword.rst +++ b/doc/userguide/rules/enip-keyword.rst @@ -1,40 +1,43 @@ ENIP/CIP Keywords ================= -The enip_command and cip_service keywords can be used for matching on various properties of -ENIP requests. +enip_command +------------ -There are three ways of using this keyword: +For the ENIP command, we are matching against the command field found in the ENIP encapsulation. + +Examples:: -* matching on ENIP command with the setting "enip_command"; -* matching on CIP Service with the setting "cip_service". -* matching both the ENIP command and the CIP Service with "enip_command" and "cip_service" together + enip_command:99; + enip_command:ListIdentity; -For the ENIP command, we are matching against the command field found in the ENIP encapsulation. +cip_service +----------- For the CIP Service, we use a maximum of 3 comma separated values representing the Service, Class and Attribute. These values are described in the CIP specification. CIP Classes are associated with their Service, and CIP Attributes are associated with their Service. If you only need to match up until the Service, then only provide the Service value. If you want to match to the CIP Attribute, then you must provide all 3 values. - -Syntax:: - - enip_command: - cip_service: - enip_command:, cip_service: - - Examples:: - enip_command:99 cip_service:75 cip_service:16,246,6 - enip_command:111, cip_service:5 (cf. http://read.pudn.com/downloads166/ebook/763211/EIP-CIP-V1-1.0.pdf) Information on the protocol can be found here: ``_ + +enip.status +----------- + +For the ENIP status, we are matching against the status field found in the ENIP encapsulation. +It uses a 32-bit unsigned integer as value. + +Examples:: + + enip.status:100; + enip.status:>106; diff --git a/etc/schema.json b/etc/schema.json index c194017ddf6f..22142957fd67 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -1399,6 +1399,75 @@ }, "additionalProperties": false }, + "enip": { + "type": "object", + "properties": { + "request": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "status": { + "type": "string" + }, + "length": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "response": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "status": { + "type": "string" + }, + "length": { + "type": "integer" + }, + "identity": { + "type": "object", + "properties": { + "protocol_version": { + "type": "integer" + }, + "revision": { + "type": "string" + }, + "vendor_id": { + "type": "integer" + }, + "device_type": { + "type": "integer" + }, + "product_code": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "serial": { + "type": "integer" + }, + "product_name": { + "type": "string" + }, + "state": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "ether": { "type": "object", "properties": { diff --git a/rules/Makefile.am b/rules/Makefile.am index d0ea6eda622f..710f0b06af4b 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -6,6 +6,7 @@ decoder-events.rules \ dhcp-events.rules \ dnp3-events.rules \ dns-events.rules \ +enip-events.rules \ files.rules \ ftp-events.rules \ http-events.rules \ diff --git a/rules/enip-events.rules b/rules/enip-events.rules new file mode 100644 index 000000000000..6c7efd8e3c67 --- /dev/null +++ b/rules/enip-events.rules @@ -0,0 +1,8 @@ +# ENIP app layer event rules +# +# SID's fall in the 2223000+ range. See https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer +# +# These sigs fire at most once per connection. +# +alert enip any any -> any any (msg:"SURICATA ENIP too many transactions"; app-layer-event:enip.too_many_transactions; classtype:protocol-command-decode; sid:2234000; rev:1;) +alert enip any any -> any any (msg:"SURICATA ENIP invalid PDU"; app-layer-event:enip.invalid_pdu; classtype:protocol-command-decode; sid:2234001; rev:1;) diff --git a/rust/src/applayer.rs b/rust/src/applayer.rs index 97db321e2249..86cf190a5dd1 100644 --- a/rust/src/applayer.rs +++ b/rust/src/applayer.rs @@ -487,6 +487,7 @@ extern { pub fn AppLayerParserStateIssetFlag(state: *mut c_void, flag: u16) -> u16; pub fn AppLayerParserSetStreamDepth(ipproto: u8, alproto: AppProto, stream_depth: u32); pub fn AppLayerParserConfParserEnabled(ipproto: *const c_char, proto: *const c_char) -> c_int; + pub fn AppLayerParserRegisterParserAcceptableDataDirection(ipproto: u8, alproto: AppProto, dir: u8); } #[repr(C)] diff --git a/rust/src/enip/detect.rs b/rust/src/enip/detect.rs new file mode 100644 index 000000000000..ef7ae5aa4b1a --- /dev/null +++ b/rust/src/enip/detect.rs @@ -0,0 +1,391 @@ +/* Copyright (C) 2023 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 nom7::branch::alt; +use nom7::character::complete::{char, digit1, space0}; +use nom7::combinator::{map_opt, opt, verify}; +use nom7::error::{make_error, ErrorKind}; +use nom7::IResult; + +use std::ffi::c_void; + +use crate::enip::enip::EnipTransaction; +use crate::enip::parser::{ + CipData, CipDir, EnipCipRequestPayload, EnipCipResponsePayload, EnipItemPayload, EnipPayload, + CIP_MULTIPLE_SERVICE, ENIP_CIP_PATH_ATTR_16BIT, ENIP_CIP_PATH_ATTR_8BIT, + ENIP_CIP_PATH_CLASS_16BIT, ENIP_CIP_PATH_CLASS_8BIT, ENIP_CMD_CANCEL, ENIP_CMD_INDICATE_STATUS, + ENIP_CMD_LIST_IDENTITY, ENIP_CMD_LIST_INTERFACES, ENIP_CMD_LIST_SERVICES, ENIP_CMD_NOP, + ENIP_CMD_REGISTER_SESSION, ENIP_CMD_SEND_RRDATA, ENIP_CMD_SEND_UNIT_DATA, + ENIP_CMD_UNREGISTER_SESSION, +}; + +use crate::core::Direction; + +use std::ffi::CStr; + +fn enip_detect_parse_u16(i: &str) -> IResult<&str, u16> { + let (i, r) = map_opt(digit1, |s: &str| s.parse::().ok())(i)?; + return Ok((i, r)); +} + +fn enip_parse_command_string(i: &str) -> IResult<&str, u16> { + let su = i.to_uppercase(); + let su_slice: &str = &su; + match su_slice { + "NOP" => Ok((i, ENIP_CMD_NOP)), + "LISTSERVICES" => Ok((i, ENIP_CMD_LIST_SERVICES)), + "LISTIDENTITY" => Ok((i, ENIP_CMD_LIST_IDENTITY)), + "LISTINTERFACES" => Ok((i, ENIP_CMD_LIST_INTERFACES)), + "REGISTERSESSION" => Ok((i, ENIP_CMD_REGISTER_SESSION)), + "UNREGISTERSESSION" => Ok((i, ENIP_CMD_UNREGISTER_SESSION)), + "SENDRRDATA" => Ok((i, ENIP_CMD_SEND_RRDATA)), + "SENDUNITDATA" => Ok((i, ENIP_CMD_SEND_UNIT_DATA)), + "INDICATESTATUS" => Ok((i, ENIP_CMD_INDICATE_STATUS)), + "CANCEL" => Ok((i, ENIP_CMD_CANCEL)), + _ => Err(nom7::Err::Error(nom7::error::make_error( + i, + nom7::error::ErrorKind::MapOpt, + ))), + } +} + +fn enip_parse_command(i: &str) -> IResult<&str, u16> { + let (i, v) = alt((enip_detect_parse_u16, enip_parse_command_string))(i)?; + return Ok((i, v)); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_parse_command( + raw: *const std::os::raw::c_char, value: *mut u16, +) -> bool { + let raw2: &CStr = CStr::from_ptr(raw); //unsafe + if let Ok(s) = raw2.to_str() { + if let Ok((_, v)) = enip_parse_command(s) { + *value = v; + return true; + } + } + return false; +} + +fn enip_tx_is_cmd( + tx: &mut EnipTransaction, direction: Direction, value: u16, +) -> std::os::raw::c_int { + if direction == Direction::ToServer { + if let Some(req) = &tx.request { + if req.header.cmd == value { + return 1; + } + } + } else if let Some(resp) = &tx.response { + if resp.header.cmd == value { + return 1; + } + } + return 0; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_tx_is_cmd( + tx: *mut std::os::raw::c_void, direction: u8, value: u16, +) -> std::os::raw::c_int { + let tx = cast_pointer!(tx, EnipTransaction); + return enip_tx_is_cmd(tx, direction.into(), value); +} + +#[derive(Clone, Debug, Default)] +pub struct DetectCipServiceData { + pub service: u8, + pub class: Option, + pub attribute: Option, +} + +fn enip_parse_cip_service(i: &str) -> IResult<&str, DetectCipServiceData> { + let (i, _) = space0(i)?; + let (i, service) = verify(map_opt(digit1, |s: &str| s.parse::().ok()), |&v| { + v < 0x80 + })(i)?; + let mut class = None; + let mut attribute = None; + let (i, _) = space0(i)?; + let (i, comma) = opt(char(','))(i)?; + let mut input = i; + if comma.is_some() { + let (i, _) = space0(i)?; + let (i, class1) = map_opt(digit1, |s: &str| s.parse::().ok())(i)?; + class = Some(class1); + let (i, _) = space0(i)?; + let (i, comma) = opt(char(','))(i)?; + input = i; + if comma.is_some() { + let (i, _) = space0(i)?; + let (i, negation) = opt(char('!'))(i)?; + let (i, attr1) = map_opt(digit1, |s: &str| s.parse::().ok())(i)?; + if negation.is_none() { + attribute = Some(attr1); + } + input = i; + } + } + let (i, _) = space0(input)?; + if !i.is_empty() { + return Err(nom7::Err::Error(make_error(i, ErrorKind::NonEmpty))); + } + return Ok(( + i, + DetectCipServiceData { + service, + class, + attribute, + }, + )); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_parse_cip_service( + raw: *const std::os::raw::c_char, +) -> *mut c_void { + let raw2: &CStr = CStr::from_ptr(raw); //unsafe + if let Ok(s) = raw2.to_str() { + if let Ok((_, ctx)) = enip_parse_cip_service(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_cip_service_free(ctx: *mut c_void) { + std::mem::drop(Box::from_raw(ctx as *mut DetectCipServiceData)); +} + +fn enip_cip_has_attribute(cipdir: &CipDir, attr: u16) -> std::os::raw::c_int { + if let CipDir::Request(req) = cipdir { + for seg in req.path.iter() { + match seg.segment_type { + ENIP_CIP_PATH_ATTR_8BIT | ENIP_CIP_PATH_ATTR_16BIT => { + if seg.value == attr { + return 1; + } + } + _ => {} + } + } + if let EnipCipRequestPayload::GetAttributeList(ga) = &req.payload { + for attrg in ga.attr_list.iter() { + if attr == *attrg { + return 1; + } + } + } + } + return 0; +} + +fn enip_cip_has_class(cipdir: &CipDir, class: u16) -> bool { + if let CipDir::Request(req) = cipdir { + for seg in req.path.iter() { + match seg.segment_type { + ENIP_CIP_PATH_CLASS_8BIT | ENIP_CIP_PATH_CLASS_16BIT if seg.value == class => { + return true; + } + _ => {} + } + } + } + return false; +} + +fn enip_cip_match_service(d: &CipData, ctx: &DetectCipServiceData) -> std::os::raw::c_int { + if d.service == ctx.service { + if let Some(class) = ctx.class { + if enip_cip_has_class(&d.cipdir, class) { + if let Some(attr) = ctx.attribute { + return enip_cip_has_attribute(&d.cipdir, attr); + } //else + return 1; + } //else + return 0; + } //else + return 1; + } else if d.service == CIP_MULTIPLE_SERVICE { + match &d.cipdir { + CipDir::Request(req) => { + if let EnipCipRequestPayload::Multiple(m) = &req.payload { + for p in m.packet_list.iter() { + if enip_cip_match_service(p, ctx) == 1 { + return 1; + } + } + } + } + CipDir::Response(resp) => { + if let EnipCipResponsePayload::Multiple(m) = &resp.payload { + for p in m.packet_list.iter() { + if enip_cip_match_service(p, ctx) == 1 { + return 1; + } + } + } + } + _ => {} + } + } + return 0; +} + +fn enip_tx_has_cip_service( + tx: &mut EnipTransaction, direction: Direction, ctx: &DetectCipServiceData, +) -> std::os::raw::c_int { + let pduo = if direction == Direction::ToServer { + &tx.request + } else { + &tx.response + }; + if let Some(pdu) = pduo { + if let EnipPayload::Cip(c) = &pdu.payload { + for item in c.items.iter() { + if let EnipItemPayload::Data(d) = &item.payload { + return enip_cip_match_service(&d.cip, ctx); + } + } + } + } + return 0; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_tx_has_cip_service( + tx: *mut std::os::raw::c_void, direction: u8, ctx: *const c_void, +) -> std::os::raw::c_int { + let tx = cast_pointer!(tx, EnipTransaction); + let ctx = cast_pointer!(ctx, DetectCipServiceData); + return enip_tx_has_cip_service(tx, direction.into(), ctx); +} + +fn enip_get_status(tx: &mut EnipTransaction, direction: Direction) -> Option { + if direction == Direction::ToServer { + if let Some(req) = &tx.request { + return Some(req.header.status); + } + } else if let Some(resp) = &tx.response { + return Some(resp.header.status); + } + return None; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_get_status( + tx: *mut std::os::raw::c_void, direction: u8, value: *mut u32, +) -> bool { + let tx = cast_pointer!(tx, EnipTransaction); + match enip_get_status(tx, direction.into()) { + Some(x) => { + *value = x; + return true; + } + _ => { + return false; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Simple test of some valid data. + #[test] + fn test_enip_parse_cip_service() { + let buf1 = "12"; + let r1 = enip_parse_cip_service(buf1); + match r1 { + Ok((remainder, csd)) => { + // Check the first message. + assert_eq!(csd.service, 12); + assert_eq!(csd.class, None); + assert_eq!(remainder.len(), 0); + } + Err(_) => { + panic!("Result should not be an error."); + } + } + + // with spaces and all values + let buf2 = "12 , 123 , 45678"; + let r2 = enip_parse_cip_service(buf2); + match r2 { + Ok((remainder, csd)) => { + // Check the first message. + assert_eq!(csd.service, 12); + assert_eq!(csd.class, Some(123)); + assert_eq!(csd.attribute, Some(45678)); + assert_eq!(remainder.len(), 0); + } + Err(_) => { + panic!("Result should not be an error."); + } + } + + // too big for service + let buf3 = "202"; + let r3 = enip_parse_cip_service(buf3); + match r3 { + Ok((_, _)) => { + panic!("Result should be an error."); + } + Err(_) => {} + } + + // non numerical after comma + let buf4 = "123,toto"; + let r4 = enip_parse_cip_service(buf4); + match r4 { + Ok((_, _)) => { + panic!("Result should be an error."); + } + Err(_) => {} + } + + // too many commas/values + let buf5 = "1,2,3,4"; + let r5 = enip_parse_cip_service(buf5); + match r5 { + Ok((_, _)) => { + panic!("Result should be an error."); + } + Err(_) => {} + } + + // too many commas/values + let buf6 = "1,2,!3"; + let r6 = enip_parse_cip_service(buf6); + match r6 { + Ok((remainder, csd)) => { + // Check the first message. + assert_eq!(csd.service, 1); + assert_eq!(csd.class, Some(2)); + assert_eq!(csd.attribute, None); + assert_eq!(remainder.len(), 0); + } + Err(_) => { + panic!("Result should not be an error."); + } + } + } +} diff --git a/rust/src/enip/enip.rs b/rust/src/enip/enip.rs new file mode 100644 index 000000000000..03232f8e61f8 --- /dev/null +++ b/rust/src/enip/enip.rs @@ -0,0 +1,617 @@ +/* Copyright (C) 2023 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 super::parser; +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::{ + AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP, IPPROTO_UDP, + STREAM_TOCLIENT, STREAM_TOSERVER, +}; +use crate::frames::Frame; +use nom7 as nom; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; + +static mut ALPROTO_ENIP: AppProto = ALPROTO_UNKNOWN; + +static mut ENIP_MAX_TX: usize = 1024; + +#[derive(AppLayerEvent)] +enum EnipEvent { + TooManyTransactions, + InvalidPdu, +} + +#[derive(Default)] +pub struct EnipTransaction { + tx_id: u64, + pub request: Option, + pub response: Option, + pub done: bool, + + tx_data: AppLayerTxData, +} + +impl Transaction for EnipTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +pub struct EnipState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + request_gap: bool, + response_gap: bool, +} + +impl State for EnipState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&EnipTransaction> { + self.transactions.get(index) + } +} + +impl EnipState { + pub fn new() -> Self { + Default::default() + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&EnipTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self) -> EnipTransaction { + let mut tx = EnipTransaction::default(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn purge_tx_flood(&mut self) { + let mut event_set = false; + for tx in self.transactions.iter_mut() { + tx.done = true; + if !event_set { + tx.tx_data.set_event(EnipEvent::TooManyTransactions as u8); + event_set = true; + } + } + } + + fn find_request(&mut self, pdu: &parser::EnipPdu) -> Option<&mut EnipTransaction> { + for tx in self.transactions.iter_mut() { + if let Some(req) = &tx.request { + if tx.response.is_none() { + tx.done = true; + if response_matches_request(req, pdu) { + return Some(tx); + } + } + } + } + None + } + + fn parse_udp( + &mut self, stream_slice: StreamSlice, request: bool, flow: *const Flow, + ) -> AppLayerResult { + let input = stream_slice.as_slice(); + match parser::parse_enip_pdu(input) { + Ok((_, pdu)) => { + let _pdu = Frame::new( + flow, + &stream_slice, + input, + ENIP_HEADER_LEN as i64, + EnipFrameType::EnipHeader as u8, + ); + let _pdu = Frame::new( + flow, + &stream_slice, + &input[ENIP_HEADER_LEN as usize..], + pdu.header.pdulen as i64, + EnipFrameType::EnipPayload as u8, + ); + if let parser::EnipPayload::Cip(c) = &pdu.payload { + for item in c.items.iter() { + if let parser::EnipItemPayload::Data(_d) = &item.payload { + let _pdu = Frame::new( + flow, + &stream_slice, + &input[item.start_offset..], + item.item_length as i64, + EnipFrameType::Cip as u8, + ); + } + } + } + if request { + if self.transactions.len() < unsafe { ENIP_MAX_TX } { + let mut tx = self.new_tx(); + if pdu.invalid { + tx.tx_data.set_event(EnipEvent::InvalidPdu as u8); + } + tx.request = Some(pdu); + self.transactions.push_back(tx); + } else { + self.purge_tx_flood(); + } + } else if let Some(tx) = self.find_request(&pdu) { + if pdu.invalid { + tx.tx_data.set_event(EnipEvent::InvalidPdu as u8); + } + tx.response = Some(pdu); + } else { + let mut tx = self.new_tx(); + if pdu.invalid { + tx.tx_data.set_event(EnipEvent::InvalidPdu as u8); + } + tx.response = Some(pdu); + tx.done = true; + self.transactions.push_back(tx); + } + return AppLayerResult::ok(); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + fn parse_tcp(&mut self, input: &[u8], request: bool) -> AppLayerResult { + if request { + if self.request_gap { + if !probe(input) { + return AppLayerResult::ok(); + } + self.request_gap = false; + } + } else if self.response_gap { + if !probe(input) { + return AppLayerResult::ok(); + } + self.response_gap = false; + } + let mut start = input; + while !start.is_empty() { + match parser::parse_enip_pdu(start) { + Ok((rem, pdu)) => { + start = rem; + if request { + if self.transactions.len() < unsafe { ENIP_MAX_TX } { + let mut tx = self.new_tx(); + if pdu.invalid { + tx.tx_data.set_event(EnipEvent::InvalidPdu as u8); + } + tx.request = Some(pdu); + self.transactions.push_back(tx); + } else { + self.purge_tx_flood(); + } + } else if let Some(tx) = self.find_request(&pdu) { + if pdu.invalid { + tx.tx_data.set_event(EnipEvent::InvalidPdu as u8); + } + tx.response = Some(pdu); + } else { + let mut tx = self.new_tx(); + if pdu.invalid { + tx.tx_data.set_event(EnipEvent::InvalidPdu as u8); + } + tx.response = Some(pdu); + tx.done = true; + self.transactions.push_back(tx); + } + } + Err(nom::Err::Incomplete(_)) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + // All input was fully consumed. + return AppLayerResult::ok(); + } + + fn on_request_gap(&mut self, _size: u32) { + self.request_gap = true; + } + + fn on_response_gap(&mut self, _size: u32) { + self.response_gap = true; + } +} + +fn response_matches_request(req: &parser::EnipPdu, resp: &parser::EnipPdu) -> bool { + if req.header.cmd != resp.header.cmd { + return false; + } + if req.header.session != resp.header.session + && req.header.cmd != parser::ENIP_CMD_REGISTER_SESSION + { + // register session response has session hanbdle when request has 0 + return false; + } + if let parser::EnipPayload::Cip(c1) = &req.payload { + if let parser::EnipPayload::Cip(c2) = &resp.payload { + // connection ids are different in each direction + // and need to see beginning of stream to catch it + if c1.items.len() >= 2 + && c2.items.len() >= 2 + && c1.items[1].item_type == parser::ENIP_ITEM_TYPE_CONNECTED_DATA + && c2.items[1].item_type == parser::ENIP_ITEM_TYPE_CONNECTED_DATA + { + if let parser::EnipItemPayload::Data(d1) = &c1.items[1].payload { + if let parser::EnipItemPayload::Data(d2) = &c2.items[1].payload { + if d1.seq_num.is_some() && d1.seq_num == d2.seq_num { + return true; + } + } + } + // sequences number did not match even if they were present + return false; + } + // we do not have CIP sequence numbers + return true; + } // else default to false + } else { + if let parser::EnipPayload::Cip(_c2) = &resp.payload { + // request has no cip but response has it + return false; + } + // no cip in either + return true; + } + return false; +} + +/// Probe for a valid header. +/// +/// As this enip protocol uses messages prefixed with the size +/// as a string followed by a ':', we look at up to the first 10 +/// characters for that pattern. +fn probe(input: &[u8]) -> bool { + match parser::parse_enip_header(input) { + Ok((rem, header)) => { + match header.status { + parser::ENIP_STATUS_SUCCESS + | parser::ENIP_STATUS_INVALID_CMD + | parser::ENIP_STATUS_NO_RESOURCES + | parser::ENIP_STATUS_INCORRECT_DATA + | parser::ENIP_STATUS_INVALID_SESSION + | parser::ENIP_STATUS_INVALID_LENGTH + | parser::ENIP_STATUS_UNSUPPORTED_PROT_REV + | parser::ENIP_STATUS_ENCAP_HEADER_ERROR => {} // Ok so far, continue + _ => { + return false; + } + } + + match header.cmd { + parser::ENIP_CMD_NOP => { + if header.options != 0 { + return false; + } + } + parser::ENIP_CMD_REGISTER_SESSION => { + if header.pdulen != 4 { + return false; + } + } + parser::ENIP_CMD_UNREGISTER_SESSION => { + if header.pdulen != 4 && header.pdulen != 0 { + return false; + } + } + parser::ENIP_CMD_LIST_INTERFACES => { + if parser::parse_enip_list_interfaces(rem).is_err() { + return false; + } + } + parser::ENIP_CMD_LIST_SERVICES + | parser::ENIP_CMD_LIST_IDENTITY + | parser::ENIP_CMD_SEND_RRDATA + | parser::ENIP_CMD_SEND_UNIT_DATA + | parser::ENIP_CMD_INDICATE_STATUS + | parser::ENIP_CMD_CANCEL => {} // Ok so far, continue + _ => { + return false; + } + } + return true; + } + _ => { + return false; + } + } +} + +// C exports. + +unsafe extern "C" fn enip_probing_parser_udp( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + // Need at least 24 bytes. + if input_len >= ENIP_HEADER_LEN && !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if probe(slice) { + return ALPROTO_ENIP; + } + } + return ALPROTO_FAILED; +} + +const ENIP_HEADER_LEN: u32 = 24; + +unsafe extern "C" fn enip_probing_parser_tcp( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + // Need at least 24 bytes. + if input.is_null() { + return ALPROTO_FAILED; + } + if input_len < ENIP_HEADER_LEN { + return ALPROTO_UNKNOWN; + } + let slice = build_slice!(input, input_len as usize); + if probe(slice) { + return ALPROTO_ENIP; + } + return ALPROTO_FAILED; +} + +extern "C" fn rs_enip_state_new(_orig_state: *mut c_void, _orig_proto: AppProto) -> *mut c_void { + let state = EnipState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn enip_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut EnipState)); +} + +unsafe extern "C" fn enip_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, EnipState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn enip_parse_request_udp( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, EnipState); + state.parse_udp(stream_slice, true, flow) +} + +unsafe extern "C" fn enip_parse_response_udp( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, EnipState); + state.parse_udp(stream_slice, false, flow) +} + +unsafe extern "C" fn enip_parse_request_tcp( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0; + if eof { + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, EnipState); + if stream_slice.is_gap() { + state.on_request_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + debug_validate_bug_on!(buf.is_empty()); + state.parse_tcp(buf, true) + } +} + +unsafe extern "C" fn enip_parse_response_tcp( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0; + if eof { + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, EnipState); + if stream_slice.is_gap() { + state.on_response_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + debug_validate_bug_on!(buf.is_empty()); + state.parse_tcp(buf, false) + } +} + +unsafe extern "C" fn rs_enip_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, EnipState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn rs_enip_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, EnipState); + return state.tx_id; +} + +unsafe extern "C" fn rs_enip_tx_get_alstate_progress(tx: *mut c_void, direction: u8) -> c_int { + let tx = cast_pointer!(tx, EnipTransaction); + + // Transaction is done if we have a response. + if tx.done { + return 1; + } + let dir: Direction = direction.into(); + if dir == Direction::ToServer { + if tx.request.is_some() { + return 1; + } + } else if tx.response.is_some() { + return 1; + } + return 0; +} + +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum EnipFrameType { + EnipHeader, + EnipPayload, + Cip, +} + +export_tx_data_get!(rs_enip_get_tx_data, EnipTransaction); +export_state_data_get!(rs_enip_get_state_data, EnipState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"enip\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_register_parsers() { + let default_port = CString::new("[44818]").unwrap(); + let mut parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_UDP, + probe_ts: Some(enip_probing_parser_udp), + probe_tc: Some(enip_probing_parser_udp), + min_depth: 0, + max_depth: 16, + state_new: rs_enip_state_new, + state_free: enip_state_free, + tx_free: enip_state_tx_free, + parse_ts: enip_parse_request_udp, + parse_tc: enip_parse_response_udp, + get_tx_count: rs_enip_state_get_tx_count, + get_tx: rs_enip_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_enip_tx_get_alstate_progress, + get_eventinfo: Some(EnipEvent::get_event_info), + get_eventinfo_byid: Some(EnipEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(applayer::state_get_tx_iterator::), + get_tx_data: rs_enip_get_tx_data, + get_state_data: rs_enip_get_state_data, + apply_tx_config: None, + flags: 0, + truncate: None, + get_frame_id_by_name: Some(EnipFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(EnipFrameType::ffi_name_from_id), + }; + + let ip_proto_str = CString::new("udp").unwrap(); + + if let Some(val) = conf_get("app-layer.protocols.enip.max-tx") { + if let Ok(v) = val.parse::() { + ENIP_MAX_TX = v; + } else { + SCLogError!("Invalid value for enip.max-tx"); + } + } + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_ENIP = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogDebug!("Rust enip parser registered for UDP."); + unsafe { + AppLayerParserRegisterParserAcceptableDataDirection( + IPPROTO_UDP, + ALPROTO_ENIP, + STREAM_TOSERVER | STREAM_TOCLIENT, + ); + } + } else { + SCLogDebug!("Protocol detector and parser disabled for ENIP on UDP."); + } + + parser.ipproto = IPPROTO_TCP; + parser.probe_ts = Some(enip_probing_parser_tcp); + parser.probe_tc = Some(enip_probing_parser_tcp); + parser.parse_ts = enip_parse_request_tcp; + parser.parse_tc = enip_parse_response_tcp; + parser.flags = APP_LAYER_PARSER_OPT_ACCEPT_GAPS; + + let ip_proto_str = CString::new("tcp").unwrap(); + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_ENIP = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogDebug!("Rust enip parser registered for TCP."); + unsafe { + AppLayerParserRegisterParserAcceptableDataDirection( + IPPROTO_TCP, + ALPROTO_ENIP, + STREAM_TOSERVER | STREAM_TOCLIENT, + ); + } + } else { + SCLogDebug!("Protocol detector and parser disabled for ENIP on TCP."); + } +} diff --git a/rust/src/enip/logger.rs b/rust/src/enip/logger.rs new file mode 100644 index 000000000000..9a8a1531cdf9 --- /dev/null +++ b/rust/src/enip/logger.rs @@ -0,0 +1,95 @@ +/* Copyright (C) 2023 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 super::enip::EnipTransaction; +use super::parser::{ + enip_command_string, enip_status_string, EnipHeader, EnipItemPayload, EnipPayload, +}; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +fn log_enip_header(h: &EnipHeader, js: &mut JsonBuilder) -> Result<(), JsonError> { + match enip_command_string(h.cmd) { + Some(val) => { + js.set_string("command", val)?; + } + None => { + js.set_string("command", &format!("unknown-{}", h.cmd))?; + } + } + match enip_status_string(h.status) { + Some(val) => { + js.set_string("status", val)?; + } + None => { + js.set_string("status", &format!("unknown-{}", h.status))?; + } + } + js.set_uint("length", h.pdulen.into())?; + if h.options != 0 { + js.set_uint("options", h.options.into())?; + } + Ok(()) +} + +fn log_enip(tx: &EnipTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("enip")?; + if let Some(ref request) = tx.request { + js.open_object("request")?; + log_enip_header(&request.header, js)?; + js.close()?; + } + if let Some(ref response) = tx.response { + js.open_object("response")?; + log_enip_header(&response.header, js)?; + match &response.payload { + EnipPayload::ListIdentity(lip) if !lip.is_empty() => { + if let EnipItemPayload::Identity(li) = &lip[0].payload { + js.open_object("identity")?; + js.set_uint("protocol_version", li.protocol_version.into())?; + js.set_string( + "revision", + &format!("{}.{}", li.revision_major, li.revision_minor), + )?; + js.set_uint("vendor_id", li.vendor_id.into())?; + js.set_uint("device_type", li.device_type.into())?; + js.set_uint("product_code", li.product_code.into())?; + js.set_uint("status", li.status.into())?; + js.set_uint("serial", li.serial.into())?; + js.set_string("product_name", &String::from_utf8_lossy(&li.product_name))?; + js.set_uint("state", li.state.into())?; + js.close()?; + } + } + _ => {} + } + js.close()?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_enip_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, EnipTransaction); + if tx.request.is_none() && tx.response.is_none() { + return false; + } + log_enip(tx, js).is_ok() +} diff --git a/rust/src/enip/mod.rs b/rust/src/enip/mod.rs new file mode 100644 index 000000000000..35470cb29976 --- /dev/null +++ b/rust/src/enip/mod.rs @@ -0,0 +1,23 @@ +/* Copyright (C) 2023 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 enip parser and logger module. + +pub mod detect; +pub mod enip; +pub mod logger; +mod parser; diff --git a/rust/src/enip/parser.rs b/rust/src/enip/parser.rs new file mode 100644 index 000000000000..e0dff30f865f --- /dev/null +++ b/rust/src/enip/parser.rs @@ -0,0 +1,542 @@ +/* Copyright (C) 2023 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 nom7::bytes::streaming::take; +use nom7::error::{make_error, ErrorKind}; +use nom7::multi::count; +use nom7::number::streaming::{le_u16, le_u32, le_u64, le_u8}; +use nom7::IResult; + +pub const ENIP_STATUS_SUCCESS: u32 = 0; +pub const ENIP_STATUS_INVALID_CMD: u32 = 1; +pub const ENIP_STATUS_NO_RESOURCES: u32 = 2; +pub const ENIP_STATUS_INCORRECT_DATA: u32 = 3; +pub const ENIP_STATUS_INVALID_SESSION: u32 = 0x64; +pub const ENIP_STATUS_INVALID_LENGTH: u32 = 0x65; +pub const ENIP_STATUS_UNSUPPORTED_PROT_REV: u32 = 0x69; +//Found in wireshark +pub const ENIP_STATUS_ENCAP_HEADER_ERROR: u32 = 0x6A; + +pub fn enip_status_string(v: u32) -> Option<&'static str> { + match v { + ENIP_STATUS_SUCCESS => Some("Success"), + ENIP_STATUS_INVALID_CMD => Some("InvalidCmd"), + ENIP_STATUS_NO_RESOURCES => Some("NoResources"), + ENIP_STATUS_INCORRECT_DATA => Some("IncorrectData"), + ENIP_STATUS_INVALID_SESSION => Some("InvalidSession"), + ENIP_STATUS_INVALID_LENGTH => Some("InvalidLength"), + ENIP_STATUS_UNSUPPORTED_PROT_REV => Some("UnsupportedProtRev"), + ENIP_STATUS_ENCAP_HEADER_ERROR => Some("EncapHeaderError"), + _ => None, + } +} + +pub const ENIP_CMD_NOP: u16 = 0; +pub const ENIP_CMD_LIST_SERVICES: u16 = 4; +pub const ENIP_CMD_LIST_IDENTITY: u16 = 0x63; +pub const ENIP_CMD_LIST_INTERFACES: u16 = 0x64; +pub const ENIP_CMD_REGISTER_SESSION: u16 = 0x65; +pub const ENIP_CMD_UNREGISTER_SESSION: u16 = 0x66; +pub const ENIP_CMD_SEND_RRDATA: u16 = 0x6F; +pub const ENIP_CMD_SEND_UNIT_DATA: u16 = 0x70; +pub const ENIP_CMD_INDICATE_STATUS: u16 = 0x72; +pub const ENIP_CMD_CANCEL: u16 = 0x73; + +pub fn enip_command_string(v: u16) -> Option<&'static str> { + match v { + ENIP_CMD_NOP => Some("Nop"), + ENIP_CMD_LIST_SERVICES => Some("ListServices"), + ENIP_CMD_LIST_IDENTITY => Some("ListIdentity"), + ENIP_CMD_LIST_INTERFACES => Some("ListInterfaces"), + ENIP_CMD_REGISTER_SESSION => Some("RegisterSession"), + ENIP_CMD_UNREGISTER_SESSION => Some("UnregisterSession"), + ENIP_CMD_SEND_RRDATA => Some("SendRRData"), + ENIP_CMD_SEND_UNIT_DATA => Some("SendUnitData"), + ENIP_CMD_INDICATE_STATUS => Some("IndicateStatus"), + ENIP_CMD_CANCEL => Some("Cancel"), + _ => None, + } +} + +#[derive(Clone, Debug, Default)] +pub struct EnipHeader { + pub cmd: u16, + pub pdulen: u16, + pub session: u32, + pub status: u32, + pub context: u64, + pub options: u32, +} + +pub fn parse_enip_header(i: &[u8]) -> IResult<&[u8], EnipHeader> { + let (i, cmd) = le_u16(i)?; + let (i, pdulen) = le_u16(i)?; + let (i, session) = le_u32(i)?; + let (i, status) = le_u32(i)?; + let (i, context) = le_u64(i)?; + let (i, options) = le_u32(i)?; + Ok(( + i, + EnipHeader { + cmd, + pdulen, + session, + status, + context, + options, + }, + )) +} + +pub fn parse_enip_list_interfaces(i: &[u8]) -> IResult<&[u8], Vec> { + let (i, nb) = le_u16(i)?; + let (i, r) = count(le_u16, nb.into())(i)?; + Ok((i, r)) +} + +#[derive(Clone, Debug, Default)] +pub enum EnipPayload { + #[default] + Unparsed, + Cip(EnipCIP), + ListIdentity(Vec), +} + +#[derive(Clone, Debug, Default)] +pub struct EnipItemConnBased { + pub conn_id: u32, +} + +#[derive(Clone, Debug, Default)] +pub struct EnipItemSequenceAddr { + pub conn_id: u32, + pub seq_num: u32, +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCipPathSegment { + pub segment_type: u8, + pub value: u16, +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCipRequestGetAttributeList { + pub attr_list: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCipReqRespMultipleService { + pub packet_list: Vec, +} + +#[derive(Clone, Debug, Default)] +pub enum EnipCipRequestPayload { + #[default] + Unhandled, + GetAttributeList(EnipCipRequestGetAttributeList), + Multiple(EnipCipReqRespMultipleService), +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCipRequest { + pub path: Vec, + pub payload: EnipCipRequestPayload, +} + +#[derive(Clone, Debug, Default)] +pub enum EnipCipResponsePayload { + #[default] + Unhandled, + Multiple(EnipCipReqRespMultipleService), +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCipResponse { + pub status: u16, + pub payload: EnipCipResponsePayload, +} + +#[derive(Clone, Debug, Default)] +pub enum CipDir { + #[default] + None, + Request(EnipCipRequest), + Response(EnipCipResponse), +} + +#[derive(Clone, Debug, Default)] +pub struct CipData { + pub service: u8, + pub cipdir: CipDir, +} + +pub const ENIP_CIP_PATH_CLASS_8BIT: u8 = 0x20; +pub const ENIP_CIP_PATH_INSTANCE_8BIT: u8 = 0x24; +pub const ENIP_CIP_PATH_ATTR_8BIT: u8 = 0x30; +pub const ENIP_CIP_PATH_CLASS_16BIT: u8 = 0x21; +pub const ENIP_CIP_PATH_INSTANCE_16BIT: u8 = 0x25; +pub const ENIP_CIP_PATH_ATTR_16BIT: u8 = 0x31; + +pub fn parse_cip_path_segment(i: &[u8]) -> IResult<&[u8], EnipCipPathSegment> { + let (i, segment_type) = le_u8(i)?; + let (i, value) = match segment_type { + ENIP_CIP_PATH_CLASS_8BIT | ENIP_CIP_PATH_INSTANCE_8BIT | ENIP_CIP_PATH_ATTR_8BIT => { + let (i, v) = le_u8(i)?; + Ok((i, v as u16)) + } + ENIP_CIP_PATH_CLASS_16BIT | ENIP_CIP_PATH_INSTANCE_16BIT | ENIP_CIP_PATH_ATTR_16BIT => { + let (i, _pad) = le_u8(i)?; + le_u16(i) + } + // There may be more cases to handle + _ => Err(nom7::Err::Error(make_error(i, ErrorKind::Switch))), + }?; + return Ok(( + i, + EnipCipPathSegment { + segment_type, + value, + }, + )); +} + +pub fn parse_cip_path(i: &[u8]) -> IResult<&[u8], Vec> { + let (i, nb) = le_u8(i)?; + let (i, data) = take(2 * (nb as usize))(i)?; + let mut rem = data; + let mut segments = Vec::new(); + while !rem.is_empty() { + let (rem2, seg) = parse_cip_path_segment(rem)?; + segments.push(seg); + rem = rem2; + } + return Ok((i, segments)); +} + +pub const CIP_GET_ATTR_LIST: u8 = 3; +pub const CIP_MULTIPLE_SERVICE: u8 = 0xa; + +pub fn parse_cip_request_get_attr_list(i: &[u8]) -> IResult<&[u8], EnipCipRequestGetAttributeList> { + let (i, nb) = le_u16(i)?; + let (i, attr_list) = count(le_u16, nb.into())(i)?; + Ok((i, EnipCipRequestGetAttributeList { attr_list })) +} + +pub fn parse_cip_reqresp_multiple(i: &[u8]) -> IResult<&[u8], EnipCipReqRespMultipleService> { + let start = i; + let (i, nb) = le_u16(i)?; + let (i, offset_list) = count(le_u16, nb.into())(i)?; + let mut packet_list = Vec::new(); + let mut rem = i; + for j in 0..nb as usize { + if (offset_list[j] as usize) < start.len() { + let (rem2, packet) = parse_cip_multi(&start[offset_list[j] as usize..])?; + packet_list.push(packet); + rem = rem2; + } + } + Ok((rem, EnipCipReqRespMultipleService { packet_list })) +} + +pub fn parse_cip_request(i: &[u8], service: u8, multi: bool) -> IResult<&[u8], EnipCipRequest> { + let (i, path) = parse_cip_path(i)?; + let (i, payload) = match service { + CIP_GET_ATTR_LIST => { + let (i, ga) = parse_cip_request_get_attr_list(i)?; + Ok((i, EnipCipRequestPayload::GetAttributeList(ga))) + } + // CIP_SET_ATTR_LIST : need to parse attribute value variant + CIP_MULTIPLE_SERVICE if multi => { + let (i, m) = parse_cip_reqresp_multiple(i)?; + Ok((i, EnipCipRequestPayload::Multiple(m))) + } + _ => Ok((i, EnipCipRequestPayload::Unhandled)), + }?; + return Ok((i, EnipCipRequest { path, payload })); +} + +pub fn parse_cip_response(i: &[u8], service: u8, multi: bool) -> IResult<&[u8], EnipCipResponse> { + let (i, _reserved) = le_u8(i)?; + let (i, status) = le_u16(i)?; + let (i, payload) = match service { + // CIP_GET_ATTR_LIST : need to parse attribute value variant, based on cip class + CIP_MULTIPLE_SERVICE if multi => { + let (i, m) = parse_cip_reqresp_multiple(i)?; + Ok((i, EnipCipResponsePayload::Multiple(m))) + } + _ => Ok((i, EnipCipResponsePayload::Unhandled)), + }?; + + return Ok((i, EnipCipResponse { status, payload })); +} + +pub fn parse_cip_base(i: &[u8]) -> IResult<&[u8], CipData> { + parse_cip(i, true) +} + +pub fn parse_cip_multi(i: &[u8]) -> IResult<&[u8], CipData> { + // have only one level of recursion + parse_cip(i, false) +} + +pub fn parse_cip(i: &[u8], multi: bool) -> IResult<&[u8], CipData> { + let (i, service) = le_u8(i)?; + let (i, cipdir) = if service & 0x80 == 0 { + let (i, req) = parse_cip_request(i, service, multi)?; + Ok((i, CipDir::Request(req))) + } else { + let (i, resp) = parse_cip_response(i, service & 0x7F, multi)?; + Ok((i, CipDir::Response(resp))) + }?; + return Ok(( + i, + CipData { + service: service & 0x7F, + cipdir, + }, + )); +} + +#[derive(Clone, Debug, Default)] +pub struct EnipItemData { + pub seq_num: Option, + pub cip: CipData, +} + +#[derive(Clone, Debug, Default)] +pub struct EnipItemIdentity { + pub protocol_version: u16, + pub vendor_id: u16, + pub device_type: u16, + pub product_code: u16, + pub revision_major: u8, + pub revision_minor: u8, + pub status: u16, + pub serial: u32, + pub product_name: Vec, + pub state: u8, +} + +#[derive(Clone, Debug, Default)] +pub enum EnipItemPayload { + #[default] + Unparsed, + ConnBased(EnipItemConnBased), + SequenceAddr(EnipItemSequenceAddr), + Data(EnipItemData), + Identity(EnipItemIdentity), +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCipItem { + pub item_type: u16, + pub item_length: u16, + pub start_offset: usize, + pub payload: EnipItemPayload, +} + +pub const ENIP_ITEM_TYPE_CONNECTION_BASED: u16 = 0xa1; +pub const ENIP_ITEM_TYPE_SEQUENCE_ADDR: u16 = 0x8002; +pub const ENIP_ITEM_TYPE_CONNECTED_DATA: u16 = 0xb1; +pub const ENIP_ITEM_TYPE_UNCONNECTED_DATA: u16 = 0xb2; +pub const ENIP_ITEM_TYPE_IDENTITY: u16 = 0xc; + +pub fn parse_cip_identity(i: &[u8]) -> IResult<&[u8], EnipItemIdentity> { + let (i, protocol_version) = le_u16(i)?; + let (i, _sock_addr) = take(16_usize)(i)?; + let (i, vendor_id) = le_u16(i)?; + let (i, device_type) = le_u16(i)?; + let (i, product_code) = le_u16(i)?; + let (i, revision_major) = le_u8(i)?; + let (i, revision_minor) = le_u8(i)?; + let (i, status) = le_u16(i)?; + let (i, serial) = le_u32(i)?; + let (i, prod_name_len) = le_u8(i)?; + let (i, product_name) = take(prod_name_len as usize)(i)?; + let (i, state) = le_u8(i)?; + + return Ok(( + i, + EnipItemIdentity { + protocol_version, + vendor_id, + device_type, + product_code, + revision_major, + revision_minor, + status, + serial, + product_name: product_name.to_vec(), + state, + }, + )); +} + +pub fn parse_enip_cip_item(i: &[u8], start: usize) -> IResult<&[u8], EnipCipItem> { + let (i, item_type) = le_u16(i)?; + let (i, item_length) = le_u16(i)?; + let mut start_offset = start + 4; + let (i, data) = take(item_length as usize)(i)?; + let (_, payload) = match item_type { + ENIP_ITEM_TYPE_IDENTITY => { + let (_, li) = parse_cip_identity(data)?; + Ok((data, EnipItemPayload::Identity(li))) + } + ENIP_ITEM_TYPE_CONNECTION_BASED => { + let (data, conn_id) = le_u32(data)?; + Ok(( + data, + EnipItemPayload::ConnBased(EnipItemConnBased { conn_id }), + )) + } + ENIP_ITEM_TYPE_SEQUENCE_ADDR => { + let (data, conn_id) = le_u32(data)?; + let (data, seq_num) = le_u32(data)?; + Ok(( + data, + EnipItemPayload::SequenceAddr(EnipItemSequenceAddr { conn_id, seq_num }), + )) + } + ENIP_ITEM_TYPE_CONNECTED_DATA => { + let (data, seq_num) = le_u16(data)?; + start_offset += 2; + let (_, cip) = parse_cip_base(data)?; + Ok(( + data, + EnipItemPayload::Data(EnipItemData { + seq_num: Some(seq_num), + cip, + }), + )) + } + ENIP_ITEM_TYPE_UNCONNECTED_DATA => { + let (_, cip) = parse_cip_base(data)?; + Ok(( + data, + EnipItemPayload::Data(EnipItemData { seq_num: None, cip }), + )) + } + _ => Ok((data, EnipItemPayload::Unparsed)), + }?; + Ok(( + i, + EnipCipItem { + item_type, + item_length, + start_offset, + payload, + }, + )) +} + +#[derive(Clone, Debug, Default)] +pub struct EnipCIP { + pub handle: u32, + pub timeout: u16, + pub items: Vec, +} + +pub fn parse_enip_cip_items(i: &[u8]) -> IResult<&[u8], Vec> { + let (i, nb) = le_u16(i)?; + let mut start_offset = 26; // ENIP_HEADER_LEN + fields parsed + let mut items = Vec::new(); + let mut rem = i; + for _j in 0..nb { + let (rem2, item) = parse_enip_cip_item(rem, start_offset)?; + items.push(item); + start_offset += rem.len() - rem2.len(); + rem = rem2; + } + Ok((i, items)) +} + +pub fn parse_enip_cip(i: &[u8]) -> IResult<&[u8], EnipCIP> { + let (i, handle) = le_u32(i)?; + let (i, timeout) = le_u16(i)?; + let (i, nb) = le_u16(i)?; + let mut start_offset = 32; // ENIP_HEADER_LEN + fields parsed + let mut items = Vec::new(); + let mut rem = i; + for _j in 0..nb { + let (rem2, item) = parse_enip_cip_item(rem, start_offset)?; + items.push(item); + start_offset += rem.len() - rem2.len(); + rem = rem2; + } + Ok(( + i, + EnipCIP { + handle, + timeout, + items, + }, + )) +} + +#[derive(Clone, Debug, Default)] +pub struct EnipPdu { + pub header: EnipHeader, + pub payload: EnipPayload, + pub invalid: bool, +} + +pub fn parse_enip_pdu(i: &[u8]) -> IResult<&[u8], EnipPdu> { + let (i, header) = parse_enip_header(i)?; + let (i, data) = take(header.pdulen as usize)(i)?; + let mut invalid = false; + match header.cmd { + ENIP_CMD_LIST_IDENTITY if header.pdulen > 0 => { + // request is empty, response has data + if let Ok((_, li)) = parse_enip_cip_items(data) { + return Ok(( + i, + EnipPdu { + header, + payload: EnipPayload::ListIdentity(li), + invalid, + }, + )); + } else { + invalid = true; + } + } + ENIP_CMD_SEND_RRDATA | ENIP_CMD_SEND_UNIT_DATA => { + if let Ok((_, cip)) = parse_enip_cip(data) { + return Ok(( + i, + EnipPdu { + header, + payload: EnipPayload::Cip(cip), + invalid, + }, + )); + } else { + //used to set event + invalid = true; + } + } + _ => {} + } + Ok(( + i, + EnipPdu { + header, + payload: EnipPayload::Unparsed, + invalid, + }, + )) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index da2859637783..a1f860dd0a90 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -103,6 +103,7 @@ pub mod rfb; pub mod mqtt; pub mod pgsql; pub mod telnet; +pub mod enip; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/src/Makefile.am b/src/Makefile.am index 21e1dfe5fbeb..4ebd4d56fbab 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -17,8 +17,6 @@ noinst_HEADERS = \ app-layer-detect-proto.h \ app-layer-dnp3.h \ app-layer-dnp3-objects.h \ - app-layer-enip-common.h \ - app-layer-enip.h \ app-layer-events.h \ app-layer-expectation.h \ app-layer-frames.h \ @@ -132,7 +130,6 @@ noinst_HEADERS = \ detect-engine-build.h \ detect-engine-content-inspection.h \ detect-engine-dcepayload.h \ - detect-engine-enip.h \ detect-engine-event.h \ detect-engine-file.h \ detect-engine-frame.h \ @@ -153,6 +150,8 @@ noinst_HEADERS = \ detect-engine-tag.h \ detect-engine-threshold.h \ detect-engine-uint.h \ + detect-enip-command.h \ + detect-enip-status.h \ detect-fast-pattern.h \ detect-file-data.h \ detect-file-hash-common.h \ @@ -405,6 +404,7 @@ noinst_HEADERS = \ output-json-dns.h \ output-json-drop.h \ output-json-email-common.h \ + output-json-enip.h \ output-json-file.h \ output-json-flow.h \ output-json-frame.h \ @@ -635,8 +635,6 @@ libsuricata_c_a_SOURCES = \ app-layer-detect-proto.c \ app-layer-dnp3.c \ app-layer-dnp3-objects.c \ - app-layer-enip.c \ - app-layer-enip-common.c \ app-layer-events.c \ app-layer-expectation.c \ app-layer-ftp.c \ @@ -749,7 +747,6 @@ libsuricata_c_a_SOURCES = \ detect-engine.c \ detect-engine-content-inspection.c \ detect-engine-dcepayload.c \ - detect-engine-enip.c \ detect-engine-event.c \ detect-engine-file.c \ detect-engine-frame.c \ @@ -769,6 +766,8 @@ libsuricata_c_a_SOURCES = \ detect-engine-tag.c \ detect-engine-threshold.c \ detect-engine-uint.c \ + detect-enip-command.c \ + detect-enip-status.c \ detect-fast-pattern.c \ detect-file-data.c \ detect-file-hash-common.c \ @@ -1019,6 +1018,7 @@ libsuricata_c_a_SOURCES = \ output-json-dns.c \ output-json-drop.c \ output-json-email-common.c \ + output-json-enip.c \ output-json-file.c \ output-json-flow.c \ output-json-frame.c \ diff --git a/src/app-layer-enip-common.c b/src/app-layer-enip-common.c deleted file mode 100644 index 305eb8312b0a..000000000000 --- a/src/app-layer-enip-common.c +++ /dev/null @@ -1,960 +0,0 @@ -/* Copyright (C) 2015-2022 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 - * - * \author Kevin Wong - * - * App-layer parser for ENIP protocol common code - * - */ - -#include "suricata-common.h" -#include "util-unittest.h" -#include "util-unittest-helper.h" -#include "detect-parse.h" -#include "detect-engine.h" -#include "util-byte.h" -#include "pkt-var.h" -#include "util-profiling.h" - -#include "app-layer-enip-common.h" - -/** - * \brief Extract 8 bits and move up the offset - * @param res - * @param input - * @param offset - */ -static int ENIPExtractUint8(uint8_t *res, const uint8_t *input, uint16_t *offset, uint32_t input_len) -{ - - if (input_len < sizeof(uint8_t) || *offset > (input_len - sizeof(uint8_t))) - { - SCLogDebug("ENIPExtractUint8: Parsing beyond payload length"); - return 0; - } - - *res = *(input + *offset); - *offset += sizeof(uint8_t); - return 1; -} - -/** - * \brief Extract 16 bits and move up the offset - * @param res - * @param input - * @param offset - */ -static int ENIPExtractUint16(uint16_t *res, const uint8_t *input, uint16_t *offset, uint32_t input_len) -{ - - if (input_len < sizeof(uint16_t) || *offset > (input_len - sizeof(uint16_t))) { - SCLogDebug("ENIPExtractUint16: Parsing beyond payload length"); - return 0; - } - - if (ByteExtractUint16(res, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), - (const uint8_t *)(input + *offset)) == -1) { - return 0; - } - - *offset += sizeof(uint16_t); - return 1; -} - -/** - * \brief Extract 32 bits and move up the offset - * @param res - * @param input - * @param offset - */ -static int ENIPExtractUint32(uint32_t *res, const uint8_t *input, uint16_t *offset, uint32_t input_len) -{ - - if (input_len < sizeof(uint32_t) || *offset > (input_len - sizeof(uint32_t))) - { - SCLogDebug("ENIPExtractUint32: Parsing beyond payload length"); - return 0; - } - - if (ByteExtractUint32(res, BYTE_LITTLE_ENDIAN, sizeof(uint32_t), - (const uint8_t *)(input + *offset)) == -1) { - return 0; - } - - *offset += sizeof(uint32_t); - return 1; -} - -/** - * \brief Extract 64 bits and move up the offset - * @param res - * @param input - * @param offset - */ -static int ENIPExtractUint64(uint64_t *res, const uint8_t *input, uint16_t *offset, uint32_t input_len) -{ - - if (input_len < sizeof(uint64_t) || *offset > (input_len - sizeof(uint64_t))) - { - SCLogDebug("ENIPExtractUint64: Parsing beyond payload length"); - return 0; - } - - if (ByteExtractUint64(res, BYTE_LITTLE_ENDIAN, sizeof(uint64_t), - (const uint8_t *)(input + *offset)) == -1) { - return 0; - } - - *offset += sizeof(uint64_t); - return 1; -} - - -/** - * \brief Create service entry, add to transaction - * @param tx Transaction - * @return service entry - */ -static CIPServiceEntry *CIPServiceAlloc(ENIPTransaction *tx) -{ - - CIPServiceEntry *svc = (CIPServiceEntry *) SCCalloc(1, - sizeof(CIPServiceEntry)); - if (unlikely(svc == NULL)) - return NULL; - - memset(svc, 0x00, sizeof(CIPServiceEntry)); - - TAILQ_INIT(&svc->segment_list); - TAILQ_INIT(&svc->attrib_list); - - TAILQ_INSERT_TAIL(&tx->service_list, svc, next); - tx->service_count++; - return svc; - -} - -#if 0 -/** - * \brief Delete service entry - */ - -static void CIPServiceFree(void *s) -{ - SCEnter(); - if (s) - { - CIPServiceEntry *svc = (CIPServiceEntry *) s; - - SegmentEntry *seg = NULL; - while ((seg = TAILQ_FIRST(&svc->segment_list))) - { - TAILQ_REMOVE(&svc->segment_list, seg, next); - SCFree(seg); - } - - AttributeEntry *attr = NULL; - while ((attr = TAILQ_FIRST(&svc->attrib_list))) - { - TAILQ_REMOVE(&svc->attrib_list, attr, next); - SCFree(attr); - } - - SCFree(s); - } - SCReturn; -} -#endif - -/** - * \brief Decode ENIP Encapsulation Header - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ -int DecodeENIPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data) -{ - int ret = 1; - - uint16_t offset = 0; //byte offset - - //Decode Encapsulation Header - uint16_t cmd; - uint16_t len; - uint32_t session; - uint32_t status; - uint64_t context; - uint32_t option; - if (ENIPExtractUint16(&cmd, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint16(&len, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint32(&session, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint32(&status, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint64(&context, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint32(&option, input, &offset, input_len) != 1) - { - return 0; - } - - enip_data->header.command = cmd; - enip_data->header.length = len; - enip_data->header.session = session; - enip_data->header.status = status; - enip_data->header.context = context; - enip_data->header.option = option; - - switch (enip_data->header.command) - { - case NOP: - SCLogDebug("DecodeENIP - NOP"); - break; - case LIST_SERVICES: - SCLogDebug("DecodeENIP - LIST_SERVICES"); - break; - case LIST_IDENTITY: - SCLogDebug("DecodeENIP - LIST_IDENTITY"); - break; - case LIST_INTERFACES: - SCLogDebug("DecodeENIP - LIST_INTERFACES"); - break; - case REGISTER_SESSION: - SCLogDebug("DecodeENIP - REGISTER_SESSION"); - break; - case UNREGISTER_SESSION: - SCLogDebug("DecodeENIP - UNREGISTER_SESSION"); - break; - case SEND_RR_DATA: - SCLogDebug( - "DecodeENIP - SEND_RR_DATA - parse Common Packet Format"); - ret = DecodeCommonPacketFormatPDU(input, input_len, enip_data, - offset); - break; - case SEND_UNIT_DATA: - SCLogDebug( - "DecodeENIP - SEND UNIT DATA - parse Common Packet Format"); - ret = DecodeCommonPacketFormatPDU(input, input_len, enip_data, - offset); - break; - case INDICATE_STATUS: - SCLogDebug("DecodeENIP - INDICATE_STATUS"); - break; - case CANCEL: - SCLogDebug("DecodeENIP - CANCEL"); - break; - default: - SCLogDebug("DecodeENIP - UNSUPPORTED COMMAND 0x%x", - enip_data->header.command); - } - - return ret; -} - - -/** - * \brief Decode Common Packet Format - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ -int DecodeCommonPacketFormatPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset) -{ - - if (enip_data->header.length < sizeof(ENIPEncapDataHdr)) - { - SCLogDebug("DecodeCommonPacketFormat: Malformed ENIP packet"); - return 0; - } - - uint32_t handle; - uint16_t timeout; - uint16_t count; - if (ENIPExtractUint32(&handle, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint16(&timeout, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint16(&count, input, &offset, input_len) != 1) - { - return 0; - } - enip_data->encap_data_header.interface_handle = handle; - enip_data->encap_data_header.timeout = timeout; - enip_data->encap_data_header.item_count = count; - - uint16_t address_type; - uint16_t address_length; //length of connection id in bytes - uint32_t address_connectionid = 0; - uint32_t address_sequence = 0; - - if (ENIPExtractUint16(&address_type, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint16(&address_length, input, &offset, input_len) != 1) - { - return 0; - } - - //depending on addr type, get connection id, sequence if needed. Can also use addr length too? - if (address_type == CONNECTION_BASED) - { //get 4 byte connection id - if (ENIPExtractUint32(&address_connectionid, input, &offset, input_len) != 1) - { - return 0; - } - } else if (address_type == SEQUENCE_ADDR_ITEM) - { // get 4 byte connection id and 4 byte sequence - if (ENIPExtractUint32(&address_connectionid, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint32(&address_sequence, input, &offset, input_len) != 1) - { - return 0; - } - } - - enip_data->encap_addr_item.type = address_type; - enip_data->encap_addr_item.length = address_length; - enip_data->encap_addr_item.conn_id = address_connectionid; - enip_data->encap_addr_item.sequence_num = address_sequence; - - uint16_t data_type; - uint16_t data_length; //length of data in bytes - uint16_t data_sequence_count; - - if (ENIPExtractUint16(&data_type, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint16(&data_length, input, &offset, input_len) != 1) - { - return 0; - } - - enip_data->encap_data_item.type = data_type; - enip_data->encap_data_item.length = data_length; - - if (enip_data->encap_data_item.type == CONNECTED_DATA_ITEM) - { //connected data items have seq number - if (ENIPExtractUint16(&data_sequence_count, input, &offset, input_len) != 1) - { - return 0; - } - enip_data->encap_data_item.sequence_count = data_sequence_count; - } - - switch (enip_data->encap_data_item.type) { - case CONNECTED_DATA_ITEM: - SCLogDebug( - "DecodeCommonPacketFormat - CONNECTED DATA ITEM - parse CIP"); - DecodeCIPPDU(input, input_len, enip_data, offset); - break; - case UNCONNECTED_DATA_ITEM: - SCLogDebug("DecodeCommonPacketFormat - UNCONNECTED DATA ITEM"); - DecodeCIPPDU(input, input_len, enip_data, offset); - break; - default: - SCLogDebug("DecodeCommonPacketFormat - UNKNOWN TYPE 0x%x", - enip_data->encap_data_item.type); - return 0; - } - - return 1; -} - -/** - * \brief Decode CIP packet - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ - -int DecodeCIPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset) -{ - int ret = 1; - - if (enip_data->encap_data_item.length == 0) - { - SCLogDebug("DecodeCIP: No CIP Data"); - return 0; - } - - if (offset > (input_len - sizeof(uint8_t))) - { - SCLogDebug("DecodeCIP: Parsing beyond payload length"); - return 0; - } - - uint8_t service = 0; - service = *(input + offset); - - //SCLogDebug("CIP Service 0x%x", service); - - //use service code first bit to determine request/response, no need to save or push offset - if (service >> 7) - { - ret = DecodeCIPResponsePDU(input, input_len, enip_data, offset); - } else - { - ret = DecodeCIPRequestPDU(input, input_len, enip_data, offset); - } - - return ret; -} - - - -/** - * \brief Decode CIP Request - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ -int DecodeCIPRequestPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset) -{ - int ret = 1; - - if (enip_data->encap_data_item.length < sizeof(CIPReqHdr)) - { - SCLogDebug("DecodeCIPRequest - Malformed CIP Data"); - return 0; - } - - uint8_t service = 0; //<-----CIP SERVICE - uint8_t path_size = 0; - - if (ENIPExtractUint8(&service, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint8(&path_size, input, &offset, input_len) != 1) - { - return 0; - } - - if (service > MAX_CIP_SERVICE) - { // service codes of value 0x80 or greater are not permitted because in the CIP protocol the highest order bit is used to flag request(0)/response(1) - SCLogDebug("DecodeCIPRequest - INVALID CIP SERVICE 0x%x", service); - return 0; - } - - //reached maximum number of services - if (enip_data->service_count > 32) - { - SCLogDebug("DecodeCIPRequest: Maximum services reached"); - return 0; - } - - //save CIP data - CIPServiceEntry *node = CIPServiceAlloc(enip_data); - if (node == NULL) - { - SCLogDebug("DecodeCIPRequest: Unable to create CIP service"); - return 0; - } - node->direction = 0; - node->service = service; - node->request.path_size = path_size; - node->request.path_offset = offset; - // SCLogDebug("DecodeCIPRequestPDU: service 0x%x size %d", node->service, - // node->request.path_size); - - DecodeCIPRequestPathPDU(input, input_len, node, offset); - - offset += path_size * sizeof(uint16_t); //move offset past pathsize - - //list of CIP services is large and can be vendor specific, store CIP service anyways and let the rule decide the action - switch (service) - { - case CIP_RESERVED: - SCLogDebug("DecodeCIPRequest - CIP_RESERVED"); - break; - case CIP_GET_ATTR_ALL: - SCLogDebug("DecodeCIPRequest - CIP_GET_ATTR_ALL"); - break; - case CIP_GET_ATTR_LIST: - SCLogDebug("DecodeCIPRequest - CIP_GET_ATTR_LIST"); - break; - case CIP_SET_ATTR_LIST: - SCLogDebug("DecodeCIPRequest - CIP_SET_ATTR_LIST"); - break; - case CIP_RESET: - SCLogDebug("DecodeCIPRequest - CIP_RESET"); - break; - case CIP_START: - SCLogDebug("DecodeCIPRequest - CIP_START"); - break; - case CIP_STOP: - SCLogDebug("DecodeCIPRequest - CIP_STOP"); - break; - case CIP_CREATE: - SCLogDebug("DecodeCIPRequest - CIP_CREATE"); - break; - case CIP_DELETE: - SCLogDebug("DecodeCIPRequest - CIP_DELETE"); - break; - case CIP_MSP: - SCLogDebug("DecodeCIPRequest - CIP_MSP"); - DecodeCIPRequestMSPPDU(input, input_len, enip_data, offset); - break; - case CIP_APPLY_ATTR: - SCLogDebug("DecodeCIPRequest - CIP_APPLY_ATTR"); - break; - case CIP_KICK_TIMER: - SCLogDebug("DecodeCIPRequest - CIP_KICK_TIMER"); - break; - case CIP_OPEN_CONNECTION: - SCLogDebug("DecodeCIPRequest - CIP_OPEN_CONNECTION"); - break; - case CIP_CHANGE_START: - SCLogDebug("DecodeCIPRequest - CIP_CHANGE_START"); - break; - case CIP_GET_STATUS: - SCLogDebug("DecodeCIPRequest - CIP_GET_STATUS"); - break; - default: - SCLogDebug("DecodeCIPRequest - CIP SERVICE 0x%x", service); - } - - return ret; -} - -/** - * \brief Decode CIP Request Path - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @param cipserviced the cip service rule - * @return 1 Packet matches - * @return 0 Packet not match - */ -int DecodeCIPRequestPathPDU(const uint8_t *input, uint32_t input_len, - CIPServiceEntry *node, uint16_t offset) -{ - //SCLogDebug("DecodeCIPRequestPath: service 0x%x size %d length %d", - // node->service, node->request.path_size, input_len); - - if (node->request.path_size < 1) - { - //SCLogDebug("DecodeCIPRequestPath: empty path or CIP Response"); - return 0; - } - - int bytes_remain = node->request.path_size; - - uint8_t reserved; //unused byte reserved by ODVA - - //8 bit fields - uint8_t req_path_instance8; - uint8_t req_path_attr8; - - //16 bit fields - uint16_t req_path_class16; - uint16_t req_path_instance16; - - uint16_t class = 0; - - SegmentEntry *seg = NULL; - - while (bytes_remain > 0) - { - uint8_t segment = 0; - if (ENIPExtractUint8(&segment, input, &offset, input_len) != 1) - { - return 0; - } - switch (segment) - { //assume order is class then instance. Can have multiple - case PATH_CLASS_8BIT: { - uint8_t req_path_class8 = 0; - if (ENIPExtractUint8(&req_path_class8, input, &offset, input_len) != 1) { - return 0; - } - class = (uint16_t) req_path_class8; - SCLogDebug("DecodeCIPRequestPathPDU: 8bit class 0x%x", class); - - seg = SCMalloc(sizeof(SegmentEntry)); - if (unlikely(seg == NULL)) - return 0; - seg->segment = segment; - seg->value = class; - TAILQ_INSERT_TAIL(&node->segment_list, seg, next); - - bytes_remain--; - break; - } - case PATH_INSTANCE_8BIT: - if (ENIPExtractUint8(&req_path_instance8, input, &offset, input_len) != 1) - { - return 0; - } - //skip instance, don't need to store - bytes_remain--; - break; - case PATH_ATTR_8BIT: //single attribute - if (ENIPExtractUint8(&req_path_attr8, input, &offset, input_len) != 1) - { - return 0; - } - //uint16_t attrib = (uint16_t) req_path_attr8; - //SCLogDebug("DecodeCIPRequestPath: 8bit attr 0x%x", attrib); - - seg = SCMalloc(sizeof(SegmentEntry)); - if (unlikely(seg == NULL)) - return 0; - seg->segment = segment; - seg->value = class; - TAILQ_INSERT_TAIL(&node->segment_list, seg, next); - - bytes_remain--; - break; - case PATH_CLASS_16BIT: - if (ENIPExtractUint8(&reserved, input, &offset, input_len) != 1) //skip reserved - { - return 0; - } - if (ENIPExtractUint16(&req_path_class16, input, &offset, input_len) != 1) - { - return 0; - } - class = req_path_class16; - SCLogDebug("DecodeCIPRequestPath: 16bit class 0x%x", class); - - seg = SCMalloc(sizeof(SegmentEntry)); - if (unlikely(seg == NULL)) - return 0; - seg->segment = segment; - seg->value = class; - TAILQ_INSERT_TAIL(&node->segment_list, seg, next); - if (bytes_remain >= 2) - { - bytes_remain = bytes_remain - 2; - } else - { - bytes_remain = 0; - } - break; - case PATH_INSTANCE_16BIT: - if (ENIPExtractUint8(&reserved, input, &offset, input_len) != 1) // skip reserved - { - return 0; - } - if (ENIPExtractUint16(&req_path_instance16, input, &offset, input_len) != 1) - { - return 0; - } - //skip instance, don't need to store - if (bytes_remain >= 2) - { - bytes_remain = bytes_remain - 2; - } else - { - bytes_remain = 0; - } - break; - default: - SCLogDebug( - "DecodeCIPRequestPath: UNKNOWN SEGMENT 0x%x service 0x%x", - segment, node->service); - return 0; - } - } - - if ((node->service == CIP_SET_ATTR_LIST) || (node->service - == CIP_GET_ATTR_LIST)) - { - uint16_t attr_list_count; - uint16_t attribute; - //parse get/set attribute list - - if (ENIPExtractUint16(&attr_list_count, input, &offset, input_len) != 1) - { - return 0; - } - SCLogDebug("DecodeCIPRequestPathPDU: attribute list count %d", - attr_list_count); - for (int i = 0; i < attr_list_count; i++) - { - if (ENIPExtractUint16(&attribute, input, &offset, input_len) != 1) - { - return 0; - } - SCLogDebug("DecodeCIPRequestPathPDU: attribute %d", attribute); - //save attrs - AttributeEntry *attr = SCMalloc(sizeof(AttributeEntry)); - if (unlikely(attr == NULL)) - return 0; - attr->attribute = attribute; - TAILQ_INSERT_TAIL(&node->attrib_list, attr, next); - - } - } - - return 1; -} - -/** - * \brief Decode CIP Response - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ -int DecodeCIPResponsePDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset) -{ - int ret = 1; - - if (enip_data->encap_data_item.length < sizeof(CIPRespHdr)) - { - SCLogDebug("DecodeCIPResponse - Malformed CIP Data"); - return 0; - } - - uint8_t service = 0; //<----CIP SERVICE - uint8_t reserved; //unused byte reserved by ODVA - uint16_t status; - - if (ENIPExtractUint8(&service, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint8(&reserved, input, &offset, input_len) != 1) - { - return 0; - } - if (ENIPExtractUint16(&status, input, &offset, input_len) != 1) - { - return 0; - } - - //SCLogDebug("DecodeCIPResponse: service 0x%x",service); - service &= 0x7f; //strip off top bit to get service code. Responses have first bit as 1 - - SCLogDebug("CIP service 0x%x status 0x%x", service, status); - - //reached maximum number of services - if (enip_data->service_count > 32) - { - SCLogDebug("DecodeCIPRequest: Maximum services reached"); - return 0; - } - - //save CIP data - CIPServiceEntry *node = CIPServiceAlloc(enip_data); - if (node == NULL) - { - SCLogDebug("DecodeCIPRequest: Unable to create CIP service"); - return 0; - } - node->direction = 1; - node->service = service; - node->response.status = status; - - SCLogDebug("DecodeCIPResponsePDU: service 0x%x size %d", node->service, - node->request.path_size); - - //list of CIP services is large and can be vendor specific, store CIP service anyways and let the rule decide the action - switch (service) - { - case CIP_RESERVED: - SCLogDebug("DecodeCIPResponse - CIP_RESERVED"); - break; - case CIP_GET_ATTR_ALL: - SCLogDebug("DecodeCIPResponse - CIP_GET_ATTR_ALL"); - break; - case CIP_GET_ATTR_LIST: - SCLogDebug("DecodeCIPResponse - CIP_GET_ATTR_LIST"); - break; - case CIP_SET_ATTR_LIST: - SCLogDebug("DecodeCIPResponse - CIP_SET_ATTR_LIST"); - break; - case CIP_RESET: - SCLogDebug("DecodeCIPResponse - CIP_RESET"); - break; - case CIP_START: - SCLogDebug("DecodeCIPResponse - CIP_START"); - break; - case CIP_STOP: - SCLogDebug("DecodeCIPResponse - CIP_STOP"); - break; - case CIP_CREATE: - SCLogDebug("DecodeCIPResponse - CIP_CREATE"); - break; - case CIP_DELETE: - SCLogDebug("DecodeCIPResponse - CIP_DELETE"); - break; - case CIP_MSP: - SCLogDebug("DecodeCIPResponse - CIP_MSP"); - DecodeCIPResponseMSPPDU(input, input_len, enip_data, offset); - break; - case CIP_APPLY_ATTR: - SCLogDebug("DecodeCIPResponse - CIP_APPLY_ATTR"); - break; - case CIP_KICK_TIMER: - SCLogDebug("DecodeCIPResponse - CIP_KICK_TIMER"); - break; - case CIP_OPEN_CONNECTION: - SCLogDebug("DecodeCIPResponse - CIP_OPEN_CONNECTION"); - break; - case CIP_CHANGE_START: - SCLogDebug("DecodeCIPResponse - CIP_CHANGE_START"); - break; - case CIP_GET_STATUS: - SCLogDebug("DecodeCIPResponse - CIP_GET_STATUS"); - break; - default: - SCLogDebug("DecodeCIPResponse - CIP SERVICE 0x%x", service); - } - - return ret; -} - - -/** - * \brief Decode CIP Request Multi Service Packet - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ -int DecodeCIPRequestMSPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset) -{ - int ret = 1; - if (offset >= (input_len - sizeof(uint16_t))) - { - SCLogDebug("DecodeCIPRequestMSPPDU: Parsing beyond payload length"); - return 0; - } - //use temp_offset just to grab the service offset, don't want to use and push offset - uint16_t temp_offset = offset; - uint16_t num_services; - if (ByteExtractUint16(&num_services, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), - (const uint8_t *)(input + temp_offset)) == -1) { - return 0; - } - - temp_offset += sizeof(uint16_t); - //SCLogDebug("DecodeCIPRequestMSP number of services %d",num_services); - - for (int svc = 1; svc < num_services + 1; svc++) - { - if (temp_offset >= (input_len - sizeof(uint16_t))) - { - SCLogDebug("DecodeCIPRequestMSPPDU: Parsing beyond payload length"); - return 0; - } - - uint16_t svc_offset; //read set of service offsets - if (ByteExtractUint16(&svc_offset, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), - (const uint8_t *)(input + temp_offset)) == -1) { - return 0; - } - temp_offset += sizeof(uint16_t); - //SCLogDebug("parseCIPRequestMSP service %d offset %d",svc, svc_offset); - - DecodeCIPPDU(input, input_len, enip_data, offset + svc_offset); //parse CIP at found offset - } - - return ret; -} - - - -/** - * \brief Decode CIP Response MultiService Packet. - * @param input, input_len data stream - * @param enip_data stores data from Packet - * @param offset current point in the packet - * @return 1 Packet ok - * @return 0 Packet has errors - */ -int DecodeCIPResponseMSPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset) -{ - int ret = 1; - - if (offset >= (input_len - sizeof(uint16_t))) - { - SCLogDebug("DecodeCIPResponseMSPPDU: Parsing beyond payload length"); - return 0; - } - //use temp_offset just to grab the service offset, don't want to use and push offset - uint16_t temp_offset = offset; - uint16_t num_services; - if (ByteExtractUint16(&num_services, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), - (const uint8_t *)(input + temp_offset)) == -1) { - return 0; - } - temp_offset += sizeof(uint16_t); - //SCLogDebug("DecodeCIPResponseMSP number of services %d", num_services); - - for (int svc = 0; svc < num_services; svc++) { - if (temp_offset >= (input_len - sizeof(uint16_t))) - { - SCLogDebug("DecodeCIPResponseMSP: Parsing beyond payload length"); - return 0; - } - - uint16_t svc_offset; //read set of service offsets - if (ByteExtractUint16(&svc_offset, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), - (const uint8_t *)(input + temp_offset)) == -1) { - return 0; - } - temp_offset += sizeof(uint16_t); - //SCLogDebug("parseCIPResponseMSP service %d offset %d", svc, svc_offset); - - DecodeCIPPDU(input, input_len, enip_data, offset + svc_offset); //parse CIP at found offset - } - - return ret; -} diff --git a/src/app-layer-enip-common.h b/src/app-layer-enip-common.h deleted file mode 100644 index 1578343a69eb..000000000000 --- a/src/app-layer-enip-common.h +++ /dev/null @@ -1,245 +0,0 @@ -/* Copyright (C) 2015 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 - * - * \author Kevin Wong - */ - -#ifndef __APP_LAYER_ENIP_COMMON_H__ -#define __APP_LAYER_ENIP_COMMON_H__ - -#include "rust.h" - -// EtherNet/IP commands -#define NOP 0x0000 -#define LIST_SERVICES 0x0004 -#define LIST_IDENTITY 0x0063 -#define LIST_INTERFACES 0x0064 -#define REGISTER_SESSION 0x0065 -#define UNREGISTER_SESSION 0x0066 -#define SEND_RR_DATA 0x006F -#define SEND_UNIT_DATA 0x0070 -#define INDICATE_STATUS 0x0072 -#define CANCEL 0x0073 - -//Common Packet Format Types -#define NULL_ADDR 0x0000 -#define CONNECTION_BASED 0x00a1 -#define CONNECTED_DATA_ITEM 0x00b1 -#define UNCONNECTED_DATA_ITEM 0x00b2 -#define SEQUENCE_ADDR_ITEM 0xB002 - -//status codes -#define SUCCESS 0x0000 -#define INVALID_CMD 0x0001 -#define NO_RESOURCES 0x0002 -#define INCORRECT_DATA 0x0003 -#define INVALID_SESSION 0x0064 -#define INVALID_LENGTH 0x0065 -#define UNSUPPORTED_PROT_REV 0x0069 -//Found in wireshark -#define ENCAP_HEADER_ERROR 0x006A - -#define MAX_CIP_SERVICE 127 -#define MAX_CIP_CLASS 65535 -#define MAX_CIP_ATTRIBUTE 65535 - -// CIP service codes -#define CIP_RESERVED 0x00 -#define CIP_GET_ATTR_ALL 0x01 -#define CIP_GET_ATTR_LIST 0x03 -#define CIP_SET_ATTR_LIST 0x04 -#define CIP_RESET 0x05 -#define CIP_START 0x06 -#define CIP_STOP 0x07 -#define CIP_CREATE 0x08 -#define CIP_DELETE 0x09 -#define CIP_MSP 0x0a -#define CIP_APPLY_ATTR 0x0d -#define CIP_GET_ATTR_SINGLE 0x0e -#define CIP_SET_ATTR_SINGLE 0x10 -#define CIP_KICK_TIMER 0x4b -#define CIP_OPEN_CONNECTION 0x4c -#define CIP_CHANGE_START 0x4f -#define CIP_GET_STATUS 0x50 - -//PATH sizing codes -#define PATH_CLASS_8BIT 0x20 -#define PATH_CLASS_16BIT 0x21 -#define PATH_INSTANCE_8BIT 0x24 -#define PATH_INSTANCE_16BIT 0x25 -#define PATH_ATTR_8BIT 0x30 -#define PATH_ATTR_16BIT 0x31 //possible value - -/** - * ENIP encapsulation header - */ -typedef struct ENIPEncapHdr_ -{ - uint64_t context; - uint32_t session; - uint32_t status; - uint32_t option; - uint16_t command; - uint16_t length; -} ENIPEncapHdr; - -/** - * ENIP encapsulation data header - */ -typedef struct ENIPEncapDataHdr_ -{ - uint32_t interface_handle; - uint16_t timeout; - uint16_t item_count; -} ENIPEncapDataHdr; - -/** - * ENIP encapsulation address item - */ -typedef struct ENIPEncapAddressItem_ { - uint16_t type; - uint16_t length; - uint32_t conn_id; - uint32_t sequence_num; -} ENIPEncapAddressItem; - -/** - * ENIP encapsulation data item - */ -typedef struct ENIPEncapDataItem_ -{ - uint16_t type; - uint16_t length; - uint16_t sequence_count; -} ENIPEncapDataItem; - -/** - * CIP Request Header - */ -typedef struct CIPReqHdr_ -{ - uint8_t service; - uint8_t path_size; -} CIPReqHdr; - -/** - * CIP Response Header - */ -typedef struct CIPRespHdr_ -{ - uint8_t service; - uint8_t pad; - uint8_t status; - uint8_t status_size; -} CIPRespHdr; - -typedef struct SegmentEntry_ -{ - uint16_t segment; /**< segment type */ - uint16_t value; /**< segment value (class or attribute) */ - - TAILQ_ENTRY(SegmentEntry_) next; -} SegmentEntry; - -typedef struct AttributeEntry_ -{ - uint16_t attribute; /**< segment class */ - - TAILQ_ENTRY(AttributeEntry_) next; -} AttributeEntry; - -typedef struct CIPServiceEntry_ -{ - uint8_t service; /**< cip service */ - uint8_t direction; - union - { - struct - { - uint8_t path_size; /**< cip path size */ - uint16_t path_offset; /**< offset to cip path */ - } request; - struct - { - uint16_t status; - } response; - }; - - TAILQ_HEAD(, SegmentEntry_) segment_list; /**< list for CIP segment */ - TAILQ_HEAD(, AttributeEntry_) attrib_list; /**< list for CIP segment */ - - TAILQ_ENTRY(CIPServiceEntry_) next; -} CIPServiceEntry; - -typedef struct ENIPTransaction_ -{ - struct ENIPState_ *enip; - uint64_t tx_num; /**< internal: id */ - uint16_t tx_id; /**< transaction id */ - uint16_t service_count; - - ENIPEncapHdr header; /**< encapsulation header */ - ENIPEncapDataHdr encap_data_header; /**< encapsulation data header */ - ENIPEncapAddressItem encap_addr_item; /**< encapsulated address item */ - ENIPEncapDataItem encap_data_item; /**< encapsulated data item */ - - TAILQ_HEAD(, CIPServiceEntry_) service_list; /**< list for CIP */ - - TAILQ_ENTRY(ENIPTransaction_) next; - AppLayerTxData tx_data; -} ENIPTransaction; - -/** \brief Per flow ENIP state container */ -typedef struct ENIPState_ -{ - AppLayerStateData state_data; - TAILQ_HEAD(, ENIPTransaction_) tx_list; /**< transaction list */ - ENIPTransaction *curr; /**< ptr to current tx */ - ENIPTransaction *iter; - uint64_t transaction_max; - uint64_t tx_with_detect_state_cnt; - - uint16_t events; - uint16_t givenup; - - /* used by TCP only */ - uint16_t offset; - uint16_t record_len; - uint8_t *buffer; -} ENIPState; - -int DecodeENIPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data); -int DecodeCommonPacketFormatPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset); -int DecodeCIPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset); -int DecodeCIPRequestPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset); -int DecodeCIPResponsePDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset); -int DecodeCIPRequestPathPDU(const uint8_t *input, uint32_t input_len, - CIPServiceEntry *node, uint16_t offset); -int DecodeCIPRequestMSPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset); -int DecodeCIPResponseMSPPDU(const uint8_t *input, uint32_t input_len, - ENIPTransaction *enip_data, uint16_t offset); - -#endif /* __APP_LAYER_ENIP_COMMON_H__ */ diff --git a/src/app-layer-enip.c b/src/app-layer-enip.c deleted file mode 100644 index 94c707c144b8..000000000000 --- a/src/app-layer-enip.c +++ /dev/null @@ -1,712 +0,0 @@ -/* Copyright (C) 2015 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 - * - * \author Kevin Wong - * - * App-layer parser for ENIP protocol - * - */ - -#include "suricata-common.h" -#include "suricata.h" - -#include "util-debug.h" -#include "util-byte.h" -#include "util-enum.h" -#include "util-mem.h" -#include "util-misc.h" - -#include "stream.h" - -#include "app-layer.h" -#include "app-layer-protos.h" -#include "app-layer-parser.h" -#include "app-layer-enip.h" -#include "app-layer-enip-common.h" - -#include "app-layer-detect-proto.h" - -#include "conf.h" -#include "decode.h" - -#include "detect-parse.h" -#include "detect-engine.h" -#include "util-unittest.h" -#include "util-unittest-helper.h" -#include "pkt-var.h" -#include "util-profiling.h" - - -SCEnumCharMap enip_decoder_event_table[ ] = { - { NULL, -1 }, -}; - -/** \brief get value for 'complete' status in ENIP - * - * For ENIP we use a simple bool. - */ -static int ENIPGetAlstateProgress(void *tx, uint8_t direction) -{ - return 1; -} - -static AppLayerTxData *ENIPGetTxData(void *vtx) -{ - ENIPTransaction *tx = (ENIPTransaction *)vtx; - return &tx->tx_data; -} - -static AppLayerStateData *ENIPGetStateData(void *vstate) -{ - ENIPState *state = (ENIPState *)vstate; - return &state->state_data; -} - -static void *ENIPGetTx(void *alstate, uint64_t tx_id) -{ - ENIPState *enip = (ENIPState *) alstate; - ENIPTransaction *tx = NULL; - - if (enip->curr && enip->curr->tx_num == tx_id + 1) - return enip->curr; - - TAILQ_FOREACH(tx, &enip->tx_list, next) { - if (tx->tx_num != (tx_id+1)) - continue; - - SCLogDebug("returning tx %p", tx); - return tx; - } - - return NULL; -} - -static uint64_t ENIPGetTxCnt(void *alstate) -{ - return ((ENIPState *)alstate)->transaction_max; -} - -static int ENIPStateGetEventInfo(const char *event_name, int *event_id, AppLayerEventType *event_type) -{ - *event_id = SCMapEnumNameToValue(event_name, enip_decoder_event_table); - - if (*event_id == -1) { - SCLogError("event \"%s\" not present in " - "enip's enum map table.", - event_name); - /* yes this is fatal */ - return -1; - } - - *event_type = APP_LAYER_EVENT_TYPE_TRANSACTION; - - return 0; -} - -static int ENIPStateGetEventInfoById(int event_id, const char **event_name, - AppLayerEventType *event_type) -{ - *event_name = SCMapEnumValueToName(event_id, enip_decoder_event_table); - if (*event_name == NULL) { - SCLogError("event \"%d\" not present in " - "enip's enum map table.", - event_id); - /* yes this is fatal */ - return -1; - } - - *event_type = APP_LAYER_EVENT_TYPE_TRANSACTION; - - return 0; -} - -/** \brief Allocate enip state - * - * return state - */ -static void *ENIPStateAlloc(void *orig_state, AppProto proto_orig) -{ - SCLogDebug("ENIPStateAlloc"); - void *s = SCMalloc(sizeof(ENIPState)); - if (unlikely(s == NULL)) - return NULL; - - memset(s, 0, sizeof(ENIPState)); - - ENIPState *enip_state = (ENIPState *) s; - - TAILQ_INIT(&enip_state->tx_list); - return s; -} - -/** \internal - * \brief Free a ENIP TX - * \param tx ENIP TX to free */ -static void ENIPTransactionFree(ENIPTransaction *tx, ENIPState *state) -{ - SCEnter(); - SCLogDebug("ENIPTransactionFree"); - CIPServiceEntry *svc = NULL; - while ((svc = TAILQ_FIRST(&tx->service_list))) - { - TAILQ_REMOVE(&tx->service_list, svc, next); - - SegmentEntry *seg = NULL; - while ((seg = TAILQ_FIRST(&svc->segment_list))) - { - TAILQ_REMOVE(&svc->segment_list, seg, next); - SCFree(seg); - } - - AttributeEntry *attr = NULL; - while ((attr = TAILQ_FIRST(&svc->attrib_list))) - { - TAILQ_REMOVE(&svc->attrib_list, attr, next); - SCFree(attr); - } - - SCFree(svc); - } - - AppLayerDecoderEventsFreeEvents(&tx->tx_data.events); - - if (tx->tx_data.de_state != NULL) { - DetectEngineStateFree(tx->tx_data.de_state); - - state->tx_with_detect_state_cnt--; - } - - if (state->iter == tx) - state->iter = NULL; - - SCFree(tx); - SCReturn; -} - -/** \brief Free enip state - * - */ -static void ENIPStateFree(void *s) -{ - SCEnter(); - SCLogDebug("ENIPStateFree"); - if (s) - { - ENIPState *enip_state = (ENIPState *) s; - - ENIPTransaction *tx = NULL; - while ((tx = TAILQ_FIRST(&enip_state->tx_list))) - { - TAILQ_REMOVE(&enip_state->tx_list, tx, next); - ENIPTransactionFree(tx, enip_state); - } - - if (enip_state->buffer != NULL) - { - SCFree(enip_state->buffer); - } - - SCFree(s); - } - SCReturn; -} - -/** \internal - * \brief Allocate a ENIP TX - * \retval tx or NULL */ -static ENIPTransaction *ENIPTransactionAlloc(ENIPState *state) -{ - SCLogDebug("ENIPStateTransactionAlloc"); - ENIPTransaction *tx = (ENIPTransaction *) SCCalloc(1, - sizeof(ENIPTransaction)); - if (unlikely(tx == NULL)) - return NULL; - - state->curr = tx; - state->transaction_max++; - - memset(tx, 0x00, sizeof(ENIPTransaction)); - TAILQ_INIT(&tx->service_list); - - tx->enip = state; - tx->tx_num = state->transaction_max; - tx->service_count = 0; - - TAILQ_INSERT_TAIL(&state->tx_list, tx, next); - - return tx; -} - -/** - * \brief enip transaction cleanup callback - */ -static void ENIPStateTransactionFree(void *state, uint64_t tx_id) -{ - SCEnter(); - SCLogDebug("ENIPStateTransactionFree"); - ENIPState *enip_state = state; - ENIPTransaction *tx = NULL; - TAILQ_FOREACH(tx, &enip_state->tx_list, next) - { - - if ((tx_id+1) < tx->tx_num) - break; - else if ((tx_id+1) > tx->tx_num) - continue; - - if (tx == enip_state->curr) - enip_state->curr = NULL; - - if (tx->tx_data.events != NULL) { - if (tx->tx_data.events->cnt <= enip_state->events) - enip_state->events -= tx->tx_data.events->cnt; - else - enip_state->events = 0; - } - - TAILQ_REMOVE(&enip_state->tx_list, tx, next); - ENIPTransactionFree(tx, state); - break; - } - SCReturn; -} - -/** \internal - * - * \brief This function is called to retrieve a ENIP - * - * \param state ENIP state structure for the parser - * \param input Input line of the command - * \param input_len Length of the request - * - * \retval 1 when the command is parsed, 0 otherwise - */ -static AppLayerResult ENIPParse(Flow *f, void *state, AppLayerParserState *pstate, - StreamSlice stream_slice, void *local_data, uint8_t direction) -{ - SCEnter(); - ENIPState *enip = (ENIPState *) state; - ENIPTransaction *tx; - - const uint8_t *input = StreamSliceGetData(&stream_slice); - uint32_t input_len = StreamSliceGetDataLen(&stream_slice); - - if (input == NULL && AppLayerParserStateIssetFlag(pstate, - APP_LAYER_PARSER_EOF_TS|APP_LAYER_PARSER_EOF_TC)) - { - SCReturnStruct(APP_LAYER_OK); - } else if (input == NULL && input_len != 0) { - // GAP - SCReturnStruct(APP_LAYER_OK); - } else if (input == NULL || input_len == 0) - { - SCReturnStruct(APP_LAYER_ERROR); - } - - while (input_len > 0) - { - tx = ENIPTransactionAlloc(enip); - if (tx == NULL) - SCReturnStruct(APP_LAYER_OK); - - if (direction == STREAM_TOCLIENT) - tx->tx_data.detect_flags_ts |= APP_LAYER_TX_SKIP_INSPECT_FLAG; - else - tx->tx_data.detect_flags_tc |= APP_LAYER_TX_SKIP_INSPECT_FLAG; - - SCLogDebug("ENIPParse input len %d", input_len); - DecodeENIPPDU(input, input_len, tx); - uint32_t pkt_len = tx->header.length + sizeof(ENIPEncapHdr); - SCLogDebug("ENIPParse packet len %d", pkt_len); - if (pkt_len > input_len) - { - SCLogDebug("Invalid packet length"); - break; - } - - input += pkt_len; - input_len -= pkt_len; - //SCLogDebug("remaining %d", input_len); - - if (input_len < sizeof(ENIPEncapHdr)) - { - //SCLogDebug("Not enough data"); //not enough data for ENIP - break; - } - } - - SCReturnStruct(APP_LAYER_OK); -} - -static AppLayerResult ENIPParseRequest(Flow *f, void *state, AppLayerParserState *pstate, - StreamSlice stream_slice, void *local_data) -{ - return ENIPParse(f, state, pstate, stream_slice, local_data, STREAM_TOSERVER); -} - -static AppLayerResult ENIPParseResponse(Flow *f, void *state, AppLayerParserState *pstate, - StreamSlice stream_slice, void *local_data) -{ - return ENIPParse(f, state, pstate, stream_slice, local_data, STREAM_TOCLIENT); -} - -#define ENIP_LEN_REGISTER_SESSION 4 // protocol u16, options u16 - -static uint16_t ENIPProbingParser(Flow *f, uint8_t direction, - const uint8_t *input, uint32_t input_len, uint8_t *rdir) -{ - // SCLogDebug("ENIPProbingParser %d", input_len); - if (input_len < sizeof(ENIPEncapHdr)) - { - SCLogDebug("length too small to be a ENIP header"); - return ALPROTO_UNKNOWN; - } - uint16_t cmd; - uint16_t enip_len; - uint32_t status; - uint32_t option; - uint16_t nbitems; - - int ret = ByteExtractUint32( - &status, BYTE_LITTLE_ENDIAN, sizeof(uint32_t), (const uint8_t *)(input + 8)); - if (ret < 0) { - return ALPROTO_FAILED; - } - switch (status) { - case SUCCESS: - case INVALID_CMD: - case NO_RESOURCES: - case INCORRECT_DATA: - case INVALID_SESSION: - case INVALID_LENGTH: - case UNSUPPORTED_PROT_REV: - case ENCAP_HEADER_ERROR: - break; - default: - return ALPROTO_FAILED; - } - ret = ByteExtractUint16(&cmd, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), (const uint8_t *)(input)); - if(ret < 0) { - return ALPROTO_FAILED; - } - ret = ByteExtractUint32( - &option, BYTE_LITTLE_ENDIAN, sizeof(uint32_t), (const uint8_t *)(input + 20)); - if (ret < 0) { - return ALPROTO_FAILED; - } - ret = ByteExtractUint16( - &enip_len, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), (const uint8_t *)(input + 2)); - if (ret < 0) { - return ALPROTO_FAILED; - } - - //ok for all the known commands - switch(cmd) { - case NOP: - if (option != 0) { - return ALPROTO_FAILED; - } - break; - case REGISTER_SESSION: - if (enip_len != ENIP_LEN_REGISTER_SESSION) { - return ALPROTO_FAILED; - } - break; - case UNREGISTER_SESSION: - if (enip_len != ENIP_LEN_REGISTER_SESSION && enip_len != 0) { - // 0 for request and 4 for response - return ALPROTO_FAILED; - } - break; - case LIST_SERVICES: - case LIST_IDENTITY: - case SEND_RR_DATA: - case SEND_UNIT_DATA: - case INDICATE_STATUS: - case CANCEL: - break; - case LIST_INTERFACES: - if (input_len < sizeof(ENIPEncapHdr) + 2) { - SCLogDebug("length too small to be a ENIP LIST_INTERFACES"); - return ALPROTO_UNKNOWN; - } - ret = ByteExtractUint16( - &nbitems, BYTE_LITTLE_ENDIAN, sizeof(uint16_t), (const uint8_t *)(input)); - if(ret < 0) { - return ALPROTO_FAILED; - } - if (enip_len < sizeof(ENIPEncapHdr) + 2 * (size_t)nbitems) { - return ALPROTO_FAILED; - } - break; - default: - return ALPROTO_FAILED; - } - return ALPROTO_ENIP; -} - -static AppLayerGetTxIterTuple ENIPGetTxIterator(const uint8_t ipproto, const AppProto alproto, - void *alstate, uint64_t min_tx_id, uint64_t max_tx_id, AppLayerGetTxIterState *state) -{ - ENIPState *enip_state = (ENIPState *)alstate; - AppLayerGetTxIterTuple no_tuple = { NULL, 0, false }; - if (enip_state) { - ENIPTransaction *tx_ptr; - if (state->un.ptr == NULL) { - tx_ptr = TAILQ_FIRST(&enip_state->tx_list); - } else { - tx_ptr = (ENIPTransaction *)state->un.ptr; - } - if (tx_ptr) { - while (tx_ptr->tx_num < min_tx_id + 1) { - tx_ptr = TAILQ_NEXT(tx_ptr, next); - if (!tx_ptr) { - return no_tuple; - } - } - if (tx_ptr->tx_num >= max_tx_id + 1) { - return no_tuple; - } - state->un.ptr = TAILQ_NEXT(tx_ptr, next); - AppLayerGetTxIterTuple tuple = { - .tx_ptr = tx_ptr, - .tx_id = tx_ptr->tx_num - 1, - .has_next = (state->un.ptr != NULL), - }; - return tuple; - } - } - return no_tuple; -} - -/** - * \brief Function to register the ENIP protocol parsers and other functions - */ -void RegisterENIPUDPParsers(void) -{ - SCEnter(); - const char *proto_name = "enip"; - - if (AppLayerProtoDetectConfProtoDetectionEnabledDefault("udp", proto_name, false)) { - AppLayerProtoDetectRegisterProtocol(ALPROTO_ENIP, proto_name); - - if (RunmodeIsUnittests()) - { - AppLayerProtoDetectPPRegister(IPPROTO_UDP, "44818", ALPROTO_ENIP, - 0, sizeof(ENIPEncapHdr), STREAM_TOSERVER, ENIPProbingParser, NULL); - - AppLayerProtoDetectPPRegister(IPPROTO_UDP, "44818", ALPROTO_ENIP, - 0, sizeof(ENIPEncapHdr), STREAM_TOCLIENT, ENIPProbingParser, NULL); - - } else - { - if (!AppLayerProtoDetectPPParseConfPorts("udp", IPPROTO_UDP, - proto_name, ALPROTO_ENIP, 0, sizeof(ENIPEncapHdr), - ENIPProbingParser, ENIPProbingParser)) - { - SCLogDebug( - "no ENIP UDP config found enabling ENIP detection on port 44818."); - - AppLayerProtoDetectPPRegister(IPPROTO_UDP, "44818", - ALPROTO_ENIP, 0, sizeof(ENIPEncapHdr), STREAM_TOSERVER, - ENIPProbingParser, NULL); - - AppLayerProtoDetectPPRegister(IPPROTO_UDP, "44818", - ALPROTO_ENIP, 0, sizeof(ENIPEncapHdr), STREAM_TOCLIENT, - ENIPProbingParser, NULL); - } - } - - } else { - SCLogConfig("Protocol detection and parser disabled for %s protocol.", - proto_name); - return; - } - - if (AppLayerParserConfParserEnabled("udp", proto_name)) - { - AppLayerParserRegisterParser(IPPROTO_UDP, ALPROTO_ENIP, STREAM_TOSERVER, ENIPParseRequest); - AppLayerParserRegisterParser(IPPROTO_UDP, ALPROTO_ENIP, STREAM_TOCLIENT, ENIPParseResponse); - - AppLayerParserRegisterStateFuncs(IPPROTO_UDP, ALPROTO_ENIP, - ENIPStateAlloc, ENIPStateFree); - - AppLayerParserRegisterGetTx(IPPROTO_UDP, ALPROTO_ENIP, ENIPGetTx); - AppLayerParserRegisterGetTxIterator(IPPROTO_UDP, ALPROTO_ENIP, ENIPGetTxIterator); - AppLayerParserRegisterTxDataFunc(IPPROTO_UDP, ALPROTO_ENIP, ENIPGetTxData); - AppLayerParserRegisterStateDataFunc(IPPROTO_UDP, ALPROTO_ENIP, ENIPGetStateData); - AppLayerParserRegisterGetTxCnt(IPPROTO_UDP, ALPROTO_ENIP, ENIPGetTxCnt); - AppLayerParserRegisterTxFreeFunc(IPPROTO_UDP, ALPROTO_ENIP, ENIPStateTransactionFree); - - AppLayerParserRegisterGetStateProgressFunc(IPPROTO_UDP, ALPROTO_ENIP, ENIPGetAlstateProgress); - AppLayerParserRegisterStateProgressCompletionStatus(ALPROTO_ENIP, 1, 1); - - AppLayerParserRegisterGetEventInfo(IPPROTO_UDP, ALPROTO_ENIP, ENIPStateGetEventInfo); - AppLayerParserRegisterGetEventInfoById(IPPROTO_UDP, ALPROTO_ENIP, ENIPStateGetEventInfoById); - - AppLayerParserRegisterParserAcceptableDataDirection( - IPPROTO_UDP, ALPROTO_ENIP, STREAM_TOSERVER | STREAM_TOCLIENT); - } else - { - SCLogInfo( - "Parsed disabled for %s protocol. Protocol detection" "still on.", - proto_name); - } - -#ifdef UNITTESTS - AppLayerParserRegisterProtocolUnittests(IPPROTO_UDP, ALPROTO_ENIP, ENIPParserRegisterTests); -#endif - - SCReturn; -} - -/** - * \brief Function to register the ENIP protocol parsers and other functions - */ -void RegisterENIPTCPParsers(void) -{ - SCEnter(); - const char *proto_name = "enip"; - - if (AppLayerProtoDetectConfProtoDetectionEnabledDefault("tcp", proto_name, false)) { - AppLayerProtoDetectRegisterProtocol(ALPROTO_ENIP, proto_name); - - if (RunmodeIsUnittests()) - { - AppLayerProtoDetectPPRegister(IPPROTO_TCP, "44818", ALPROTO_ENIP, - 0, sizeof(ENIPEncapHdr), STREAM_TOSERVER, ENIPProbingParser, NULL); - - AppLayerProtoDetectPPRegister(IPPROTO_TCP, "44818", ALPROTO_ENIP, - 0, sizeof(ENIPEncapHdr), STREAM_TOCLIENT, ENIPProbingParser, NULL); - - } else - { - if (!AppLayerProtoDetectPPParseConfPorts("tcp", IPPROTO_TCP, - proto_name, ALPROTO_ENIP, 0, sizeof(ENIPEncapHdr), - ENIPProbingParser, ENIPProbingParser)) - { - return; - } - } - - } else { - SCLogDebug("Protocol detection and parser disabled for %s protocol.", - proto_name); - return; - } - - if (AppLayerParserConfParserEnabled("tcp", proto_name)) - { - AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_ENIP, STREAM_TOSERVER, ENIPParseRequest); - AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_ENIP, STREAM_TOCLIENT, ENIPParseResponse); - AppLayerParserRegisterStateFuncs(IPPROTO_TCP, ALPROTO_ENIP, - ENIPStateAlloc, ENIPStateFree); - - AppLayerParserRegisterGetTx(IPPROTO_TCP, ALPROTO_ENIP, ENIPGetTx); - AppLayerParserRegisterGetTxIterator(IPPROTO_TCP, ALPROTO_ENIP, ENIPGetTxIterator); - AppLayerParserRegisterTxDataFunc(IPPROTO_TCP, ALPROTO_ENIP, ENIPGetTxData); - AppLayerParserRegisterStateDataFunc(IPPROTO_TCP, ALPROTO_ENIP, ENIPGetStateData); - AppLayerParserRegisterGetTxCnt(IPPROTO_TCP, ALPROTO_ENIP, ENIPGetTxCnt); - AppLayerParserRegisterTxFreeFunc(IPPROTO_TCP, ALPROTO_ENIP, ENIPStateTransactionFree); - - AppLayerParserRegisterGetStateProgressFunc(IPPROTO_TCP, ALPROTO_ENIP, ENIPGetAlstateProgress); - AppLayerParserRegisterStateProgressCompletionStatus(ALPROTO_ENIP, 1, 1); - - AppLayerParserRegisterGetEventInfo(IPPROTO_TCP, ALPROTO_ENIP, ENIPStateGetEventInfo); - - AppLayerParserRegisterParserAcceptableDataDirection(IPPROTO_TCP, - ALPROTO_ENIP, STREAM_TOSERVER | STREAM_TOCLIENT); - - /* This parser accepts gaps. */ - AppLayerParserRegisterOptionFlags(IPPROTO_TCP, ALPROTO_ENIP, - APP_LAYER_PARSER_OPT_ACCEPT_GAPS); - - AppLayerParserRegisterOptionFlags(IPPROTO_TCP, ALPROTO_ENIP, 0); - } else - { - SCLogConfig("Parser disabled for %s protocol. Protocol detection still on.", - proto_name); - } - -#ifdef UNITTESTS - AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_ENIP, ENIPParserRegisterTests); -#endif - - SCReturn; -} - -/* UNITTESTS */ -#ifdef UNITTESTS -#include "flow-util.h" -#include "stream-tcp.h" - -static uint8_t listIdentity[] = {/* List ID */ 0x63, 0x00, - /* Length */ 0x00, 0x00, - /* Session */ 0x00, 0x00, 0x00, 0x00, - /* Status */ 0x00, 0x00, 0x00, 0x00, - /* Delay*/ 0x00, - /* Context */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* Quantity of coils */ 0x00, 0x00, 0x00, 0x00, 0x00}; - -/** - * \brief Test if ENIP Packet matches signature - */ -static int ALDecodeENIPTest(void) -{ - AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc(); - Flow f; - TcpSession ssn; - - memset(&f, 0, sizeof(f)); - memset(&ssn, 0, sizeof(ssn)); - - f.protoctx = (void *)&ssn; - f.proto = IPPROTO_TCP; - f.alproto = ALPROTO_ENIP; - - StreamTcpInitConfig(true); - - int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_ENIP, STREAM_TOSERVER, - listIdentity, sizeof(listIdentity)); - FAIL_IF(r != 0); - - ENIPState *enip_state = f.alstate; - FAIL_IF_NULL(enip_state); - - ENIPTransaction *tx = ENIPGetTx(enip_state, 0); - FAIL_IF_NULL(tx); - - FAIL_IF(tx->header.command != 99); - - AppLayerParserThreadCtxFree(alp_tctx); - StreamTcpFreeConfig(true); - FLOW_DESTROY(&f); - - PASS; -} - -#endif /* UNITTESTS */ - -void ENIPParserRegisterTests(void) -{ -#ifdef UNITTESTS - UtRegisterTest("ALDecodeENIPTest", ALDecodeENIPTest); -#endif /* UNITTESTS */ -} diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 572e15f628cc..e41a606fd2e9 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -47,7 +47,6 @@ #include "app-layer-ssl.h" #include "app-layer-ssh.h" #include "app-layer-modbus.h" -#include "app-layer-enip.h" #include "app-layer-dnp3.h" #include "app-layer-nfs-tcp.h" #include "app-layer-nfs-udp.h" @@ -1752,8 +1751,7 @@ void AppLayerParserRegisterProtocolParsers(void) rs_dns_tcp_register_parser(); rs_bittorrent_dht_udp_register_parser(); RegisterModbusParsers(); - RegisterENIPUDPParsers(); - RegisterENIPTCPParsers(); + rs_enip_register_parsers(); RegisterDNP3Parsers(); RegisterNFSTCPParsers(); RegisterNFSUDPParsers(); diff --git a/src/app-layer.c b/src/app-layer.c index b031afce8ac8..fb101a71c27f 100644 --- a/src/app-layer.c +++ b/src/app-layer.c @@ -1049,6 +1049,23 @@ void AppLayerRegisterGlobalCounters(void) StatsRegisterGlobalCounter("app_layer.expectations", ExpectationGetCounter); } +static bool AppLayerParserBothTcpUdp(AppProto alproto) +{ + switch (alproto) { + case ALPROTO_DCERPC: + // fallthrough + case ALPROTO_DNS: + // fallthrough + case ALPROTO_ENIP: + // fallthrough + case ALPROTO_KRB5: + // fallthrough + case ALPROTO_NFS: + return true; + } + return false; +} + #define IPPROTOS_MAX 2 void AppLayerSetupCounters(void) { @@ -1062,7 +1079,6 @@ void AppLayerSetupCounters(void) for (uint8_t p = 0; p < IPPROTOS_MAX; p++) { const uint8_t ipproto = ipprotos[p]; const uint8_t ipproto_map = FlowGetProtoMapping(ipproto); - const uint8_t other_ipproto = ipproto == IPPROTO_TCP ? IPPROTO_UDP : IPPROTO_TCP; const char *ipproto_suffix = (ipproto == IPPROTO_TCP) ? "_tcp" : "_udp"; for (AppProto alproto = 0; alproto < ALPROTO_MAX; alproto++) { @@ -1070,8 +1086,7 @@ void AppLayerSetupCounters(void) const char *tx_str = "app_layer.tx."; const char *alproto_str = AppLayerGetProtoName(alproto); - if (AppLayerParserProtoIsRegistered(ipproto, alproto) && - AppLayerParserProtoIsRegistered(other_ipproto, alproto)) { + if (AppLayerParserBothTcpUdp(alproto)) { snprintf(applayer_counter_names[ipproto_map][alproto].name, sizeof(applayer_counter_names[ipproto_map][alproto].name), "%s%s%s", str, alproto_str, ipproto_suffix); diff --git a/src/detect-cipservice.c b/src/detect-cipservice.c index 00b9a75ca099..aa6756ece892 100644 --- a/src/detect-cipservice.c +++ b/src/detect-cipservice.c @@ -24,172 +24,26 @@ */ #include "suricata-common.h" -#include "util-unittest.h" #include "detect-parse.h" #include "detect-engine.h" -#include "util-byte.h" +#include "rust.h" -#include "app-layer-enip-common.h" #include "detect-cipservice.h" -#include "detect-engine-enip.h" /* * CIP SERVICE CODE */ -/** - * \brief CIP Service Detect Prototypes - */ -static int DetectCipServiceSetup(DetectEngineCtx *, Signature *, const char *); -static void DetectCipServiceFree(DetectEngineCtx *, void *); -#ifdef UNITTESTS -static void DetectCipServiceRegisterTests(void); -#endif static int g_cip_buffer_id = 0; /** - * \brief Registration function for cip_service: keyword - */ -void DetectCipServiceRegister(void) -{ - SCEnter(); - sigmatch_table[DETECT_CIPSERVICE].name = "cip_service"; //rule keyword - sigmatch_table[DETECT_CIPSERVICE].desc = "match on CIP Service"; - sigmatch_table[DETECT_CIPSERVICE].url = "/rules/enip-keyword.html#enip-cip-keywords"; - sigmatch_table[DETECT_CIPSERVICE].Match = NULL; - sigmatch_table[DETECT_CIPSERVICE].Setup = DetectCipServiceSetup; - sigmatch_table[DETECT_CIPSERVICE].Free = DetectCipServiceFree; -#ifdef UNITTESTS - sigmatch_table[DETECT_CIPSERVICE].RegisterTests - = DetectCipServiceRegisterTests; -#endif - DetectAppLayerInspectEngineRegister2( - "cip", ALPROTO_ENIP, SIG_FLAG_TOSERVER, 0, DetectEngineInspectCIP, NULL); - DetectAppLayerInspectEngineRegister2( - "cip", ALPROTO_ENIP, SIG_FLAG_TOCLIENT, 0, DetectEngineInspectCIP, NULL); - - g_cip_buffer_id = DetectBufferTypeGetByName("cip"); - - SCReturn; -} - -/** - * \brief This function is used to parse cip_service options passed via cip_service: keyword - * - * \param rulestr Pointer to the user provided rulestr options - * Takes comma seperated string with numeric tokens. Only first 3 are used + * \brief this function will free memory associated with DetectCipServiceData * - * \retval cipserviced pointer to DetectCipServiceData on success - * \retval NULL on failure + * \param ptr pointer to DetectCipServiceData */ -static DetectCipServiceData *DetectCipServiceParse(const char *rulestrc) +static void DetectCipServiceFree(DetectEngineCtx *de_ctx, void *ptr) { - const char delims[] = ","; - DetectCipServiceData *cipserviced = NULL; - - //SCLogDebug("DetectCipServiceParse - rule string %s", rulestr); - - /* strtok_r modifies the string so work with a copy */ - char *rulestr = SCStrdup(rulestrc); - if (unlikely(rulestr == NULL)) - goto error; - - cipserviced = SCMalloc(sizeof(DetectCipServiceData)); - if (unlikely(cipserviced == NULL)) - goto error; - - cipserviced->cipservice = 0; - cipserviced->cipclass = 0; - cipserviced->matchattribute = 1; - cipserviced->cipattribute = 0; - - char* token; - char *save; - uint8_t var; - uint8_t input[3] = { 0, 0, 0 }; - uint8_t i = 0; - - token = strtok_r(rulestr, delims, &save); - while (token != NULL) - { - if (i > 2) //for now only need 3 parameters - { - SCLogError("too many parameters"); - goto error; - } - - if (i < 2) //if on service or class - { - if (!isdigit((int) *token)) - { - SCLogError("parameter error %s", token); - goto error; - } - } else //if on attribute - { - - if (token[0] == '!') - { - cipserviced->matchattribute = 0; - token++; - } - - if (!isdigit((int) *token)) - { - SCLogError("attribute error %s", token); - goto error; - } - - } - - unsigned long num = atol(token); - if ((num > MAX_CIP_SERVICE) && (i == 0))//if service greater than 7 bit - { - SCLogError("invalid CIP service %lu", num); - goto error; - } else if ((num > MAX_CIP_CLASS) && (i == 1))//if service greater than 16 bit - { - SCLogError("invalid CIP class %lu", num); - goto error; - } else if ((num > MAX_CIP_ATTRIBUTE) && (i == 2))//if service greater than 16 bit - { - SCLogError("invalid CIP attribute %lu", num); - goto error; - } - - sscanf(token, "%2" SCNu8, &var); - input[i++] = var; - - token = strtok_r(NULL, delims, &save); - } - - if (i == 0) { - SCLogError("no tokens found"); - goto error; - } - - cipserviced->cipservice = input[0]; - cipserviced->cipclass = input[1]; - cipserviced->cipattribute = input[2]; - cipserviced->tokens = i; - - SCLogDebug("DetectCipServiceParse - tokens %d", cipserviced->tokens); - SCLogDebug("DetectCipServiceParse - service %d", cipserviced->cipservice); - SCLogDebug("DetectCipServiceParse - class %d", cipserviced->cipclass); - SCLogDebug("DetectCipServiceParse - match attribute %d", - cipserviced->matchattribute); - SCLogDebug("DetectCipServiceParse - attribute %d", - cipserviced->cipattribute); - - SCFree(rulestr); - SCReturnPtr(cipserviced, "DetectENIPFunction"); - -error: - if (cipserviced) - SCFree(cipserviced); - if (rulestr) - SCFree(rulestr); - SCReturnPtr(NULL, "DetectENIP"); + rs_enip_cip_service_free(ptr); } /** @@ -207,256 +61,64 @@ static int DetectCipServiceSetup(DetectEngineCtx *de_ctx, Signature *s, { SCEnter(); - DetectCipServiceData *cipserviced = NULL; + void *cipserviced = NULL; SigMatch *sm = NULL; if (DetectSignatureSetAppProto(s, ALPROTO_ENIP) != 0) return -1; - cipserviced = DetectCipServiceParse(rulestr); + cipserviced = rs_enip_parse_cip_service(rulestr); if (cipserviced == NULL) - goto error; + return -1; sm = SigMatchAlloc(); - if (sm == NULL) - goto error; + if (sm == NULL) { + DetectCipServiceFree(de_ctx, cipserviced); + return -1; + } sm->type = DETECT_CIPSERVICE; sm->ctx = (void *) cipserviced; SigMatchAppendSMToList(s, sm, g_cip_buffer_id); SCReturnInt(0); - -error: - if (cipserviced != NULL) - DetectCipServiceFree(de_ctx, cipserviced); - if (sm != NULL) - SCFree(sm); - SCReturnInt(-1); } /** - * \brief this function will free memory associated with DetectCipServiceData + * \brief This function is used to match enip command type rule option on a transaction with those + * passed via enip_command: * - * \param ptr pointer to DetectCipServiceData - */ -static void DetectCipServiceFree(DetectEngineCtx *de_ctx, void *ptr) -{ - DetectCipServiceData *cipserviced = (DetectCipServiceData *) ptr; - SCFree(cipserviced); -} - -#ifdef UNITTESTS - -/** - * \test Test CIP Command parameter parsing + * \retval 0 no match + * \retval 1 match */ -static int DetectCipServiceParseTest01 (void) -{ - DetectCipServiceData *cipserviced = NULL; - cipserviced = DetectCipServiceParse("7"); - FAIL_IF_NULL(cipserviced); - FAIL_IF(cipserviced->cipservice != 7); - DetectCipServiceFree(NULL, cipserviced); - PASS; -} +static int DetectCipServiceMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) -/** - * \test Test CIP Service signature - */ -static int DetectCipServiceSignatureTest01 (void) { - DetectEngineCtx *de_ctx = DetectEngineCtxInit(); - FAIL_IF_NULL(de_ctx); - Signature *sig = DetectEngineAppendSig(de_ctx, "alert tcp any any -> any any (cip_service:1; sid:1; rev:1;)"); - FAIL_IF_NULL(sig); - DetectEngineCtxFree(de_ctx); - PASS; + return rs_enip_tx_has_cip_service(txv, flags, ctx); } /** - * \brief this function registers unit tests for DetectCipService + * \brief Registration function for cip_service: keyword */ -static void DetectCipServiceRegisterTests(void) +void DetectCipServiceRegister(void) { - UtRegisterTest("DetectCipServiceParseTest01", - DetectCipServiceParseTest01); - UtRegisterTest("DetectCipServiceSignatureTest01", - DetectCipServiceSignatureTest01); -} -#endif /* UNITTESTS */ - -/* - * ENIP COMMAND CODE - */ - -/** - * \brief ENIP Command Detect Prototypes - */ -static int DetectEnipCommandSetup(DetectEngineCtx *, Signature *, const char *); -static void DetectEnipCommandFree(DetectEngineCtx *, void *); -#ifdef UNITTESTS -static void DetectEnipCommandRegisterTests(void); -#endif -static int g_enip_buffer_id = 0; + SCEnter(); + sigmatch_table[DETECT_CIPSERVICE].name = "cip_service"; // rule keyword + sigmatch_table[DETECT_CIPSERVICE].desc = + "match on CIP Service, and optionnally class and attribute"; + sigmatch_table[DETECT_CIPSERVICE].url = "/rules/enip-keyword.html#cip_service"; + sigmatch_table[DETECT_CIPSERVICE].Match = NULL; + sigmatch_table[DETECT_CIPSERVICE].AppLayerTxMatch = DetectCipServiceMatch; + sigmatch_table[DETECT_CIPSERVICE].Setup = DetectCipServiceSetup; + sigmatch_table[DETECT_CIPSERVICE].Free = DetectCipServiceFree; -/** - * \brief Registration function for enip_command: keyword - */ -void DetectEnipCommandRegister(void) -{ - sigmatch_table[DETECT_ENIPCOMMAND].name = "enip_command"; //rule keyword - sigmatch_table[DETECT_ENIPCOMMAND].desc - = "rules for detecting EtherNet/IP command"; - sigmatch_table[DETECT_ENIPCOMMAND].url = "/rules/enip-keyword.html#enip-cip-keywords"; - sigmatch_table[DETECT_ENIPCOMMAND].Match = NULL; - sigmatch_table[DETECT_ENIPCOMMAND].Setup = DetectEnipCommandSetup; - sigmatch_table[DETECT_ENIPCOMMAND].Free = DetectEnipCommandFree; -#ifdef UNITTESTS - sigmatch_table[DETECT_ENIPCOMMAND].RegisterTests - = DetectEnipCommandRegisterTests; -#endif DetectAppLayerInspectEngineRegister2( - "enip", ALPROTO_ENIP, SIG_FLAG_TOSERVER, 0, DetectEngineInspectENIP, NULL); + "cip", ALPROTO_ENIP, SIG_FLAG_TOSERVER, 0, DetectEngineInspectGenericList, NULL); DetectAppLayerInspectEngineRegister2( - "enip", ALPROTO_ENIP, SIG_FLAG_TOCLIENT, 0, DetectEngineInspectENIP, NULL); - - g_enip_buffer_id = DetectBufferTypeGetByName("enip"); -} - -/** - * \brief This function is used to parse cip_service options passed via enip_command: keyword - * - * \param rulestr Pointer to the user provided rulestr options - * Takes single numeric value - * - * \retval enipcmdd pointer to DetectCipServiceData on success - * \retval NULL on failure - */ -static DetectEnipCommandData *DetectEnipCommandParse(const char *rulestr) -{ - DetectEnipCommandData *enipcmdd = NULL; - - enipcmdd = SCMalloc(sizeof(DetectEnipCommandData)); - if (unlikely(enipcmdd == NULL)) - goto error; - - if (!(isdigit((int) *rulestr))) { - SCLogError("invalid ENIP command %s", rulestr); - goto error; - } - - uint16_t cmd; - if (StringParseUint16(&cmd, 10, 0, rulestr) < 0) { - SCLogError("invalid ENIP command" - ": \"%s\"", - rulestr); - goto error; - } - - enipcmdd->enipcommand = cmd; - - return enipcmdd; - -error: - if (enipcmdd) - SCFree(enipcmdd); - return NULL; -} - -/** - * \brief this function is used by enipcmdd to parse enip_command data into the current signature - * - * \param de_ctx pointer to the Detection Engine Context - * \param s pointer to the Current Signature - * \param rulestr pointer to the user provided enip command options - * - * \retval 0 on Success - * \retval -1 on Failure - */ -static int DetectEnipCommandSetup(DetectEngineCtx *de_ctx, Signature *s, - const char *rulestr) -{ - DetectEnipCommandData *enipcmdd = NULL; - SigMatch *sm = NULL; - - if (DetectSignatureSetAppProto(s, ALPROTO_ENIP) != 0) - return -1; - - enipcmdd = DetectEnipCommandParse(rulestr); - if (enipcmdd == NULL) - goto error; - - sm = SigMatchAlloc(); - if (sm == NULL) - goto error; - - sm->type = DETECT_ENIPCOMMAND; - sm->ctx = (void *) enipcmdd; - - SigMatchAppendSMToList(s, sm, g_enip_buffer_id); - SCReturnInt(0); - -error: - if (enipcmdd != NULL) - DetectEnipCommandFree(de_ctx, enipcmdd); - if (sm != NULL) - SCFree(sm); - SCReturnInt(-1); -} - -/** - * \brief this function will free memory associated with DetectEnipCommandData - * - * \param ptr pointer to DetectEnipCommandData - */ -static void DetectEnipCommandFree(DetectEngineCtx *de_ctx, void *ptr) -{ - DetectEnipCommandData *enipcmdd = (DetectEnipCommandData *) ptr; - SCFree(enipcmdd); -} - -#ifdef UNITTESTS - -/** - * \test ENIP parameter test - */ - -static int DetectEnipCommandParseTest01 (void) -{ - DetectEnipCommandData *enipcmdd = NULL; - - enipcmdd = DetectEnipCommandParse("1"); - FAIL_IF_NULL(enipcmdd); - FAIL_IF_NOT(enipcmdd->enipcommand == 1); - - DetectEnipCommandFree(NULL, enipcmdd); - PASS; -} - -/** - * \test ENIP Command signature test - */ -static int DetectEnipCommandSignatureTest01 (void) -{ - DetectEngineCtx *de_ctx = DetectEngineCtxInit(); - FAIL_IF_NULL(de_ctx); - - Signature *sig = DetectEngineAppendSig(de_ctx, "alert tcp any any -> any any (enip_command:1; sid:1; rev:1;)"); - FAIL_IF_NULL(sig); + "cip", ALPROTO_ENIP, SIG_FLAG_TOCLIENT, 0, DetectEngineInspectGenericList, NULL); - DetectEngineCtxFree(de_ctx); - PASS; -} + g_cip_buffer_id = DetectBufferTypeGetByName("cip"); -/** - * \brief this function registers unit tests for DetectEnipCommand - */ -static void DetectEnipCommandRegisterTests(void) -{ - UtRegisterTest("DetectEnipCommandParseTest01", - DetectEnipCommandParseTest01); - UtRegisterTest("DetectEnipCommandSignatureTest01", - DetectEnipCommandSignatureTest01); + SCReturn; } -#endif /* UNITTESTS */ diff --git a/src/detect-cipservice.h b/src/detect-cipservice.h index 6a9c500cc9fa..9cc5fcb03f61 100644 --- a/src/detect-cipservice.h +++ b/src/detect-cipservice.h @@ -24,48 +24,6 @@ #ifndef _DETECT_CIPSERVICE_H #define _DETECT_CIPSERVICE_H -/** - * CIP Service rule data structure - */ -typedef struct DetectCipServiceData_ -{ - uint8_t cipservice; /* cip service type */ - uint16_t cipclass; - uint16_t cipattribute; - uint8_t matchattribute; /* whether to match on attribute*/ - uint8_t tokens; /* number of parameters*/ -} DetectCipServiceData; - -/** - * ENIP Command rule data structure - */ -typedef struct DetectEnipCommandData_ -{ - uint16_t enipcommand; /* enip command */ -} DetectEnipCommandData; - void DetectCipServiceRegister(void); -void DetectEnipCommandRegister(void); - -/** - * link list node for storing CIP service data - */ -typedef struct CIPServiceData_ -{ - uint8_t service; //cip service - union - { - struct - { - uint8_t path_size; //cip path size - uint16_t path_offset; //offset to cip path - } request; - struct - { - uint8_t status; - } response; - }; - struct CIPServiceData* next; -} CIPServiceData; #endif /* _DETECT_CIPSERVICE_H */ diff --git a/src/detect-engine-enip.c b/src/detect-engine-enip.c deleted file mode 100644 index 0c5fb0a81cbe..000000000000 --- a/src/detect-engine-enip.c +++ /dev/null @@ -1,288 +0,0 @@ -/* Copyright (C) 2015-2022 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 - * - * \author Kevin Wong - * - * Based on detect-engine-modbus.c - */ - -#include "suricata-common.h" - -#include "app-layer.h" -#include "app-layer-enip-common.h" - -#include "detect.h" -#include "detect-cipservice.h" -#include "detect-engine-enip.h" - -#include "flow.h" - -#include "util-debug.h" - -#if 0 -/** - * \brief Print fields from ENIP Packet - * @param enip_data - */ -void PrintENIPAL(ENIPTransaction *enip_data) -{ - SCLogDebug("============================================"); - SCLogDebug("ENCAP HEADER cmd 0x%x, length %d, session 0x%x, status 0x%x", - enip_data->header.command, enip_data->header.length, - enip_data->header.session, enip_data->header.status); - //SCLogDebug("context 0x%x option 0x%x", enip_data->header.context, enip_data->header.option); - SCLogDebug("ENCAP DATA HEADER handle 0x%x, timeout %d, count %d", - enip_data->encap_data_header.interface_handle, - enip_data->encap_data_header.timeout, - enip_data->encap_data_header.item_count); - SCLogDebug("ENCAP ADDR ITEM type 0x%x, length %d", - enip_data->encap_addr_item.type, enip_data->encap_addr_item.length); - SCLogDebug("ENCAP DATA ITEM type 0x%x, length %d sequence 0x%x", - enip_data->encap_data_item.type, enip_data->encap_data_item.length, - enip_data->encap_data_item.sequence_count); - - CIPServiceEntry *svc = NULL; - - int count = 0; - TAILQ_FOREACH(svc, &enip_data->service_list, next) - { - //SCLogDebug("CIP Service #%d : 0x%x", count, svc->service); - count++; - } -} -#endif - -/** - * \brief Matches the rule to the CIP segment in ENIP Packet - * @param svc - the CIP service entry - * * @param cipserviced - the CIP service rule - */ -static int CIPPathMatch(CIPServiceEntry *svc, DetectCipServiceData *cipserviced) -{ - uint16_t class = 0; - uint16_t attrib = 0; - int found_class = 0; - - SegmentEntry *seg = NULL; - TAILQ_FOREACH(seg, &svc->segment_list, next) - { - switch(seg->segment) - { - case PATH_CLASS_8BIT: - class = seg->value; - if (cipserviced->cipclass == class) - { - if (cipserviced->tokens == 2) - {// if rule only has class - return 1; - } else - { - found_class = 1; - } - } - break; - case PATH_INSTANCE_8BIT: - break; - case PATH_ATTR_8BIT: //single attribute - attrib = seg->value; - if ((cipserviced->tokens == 3) && - (cipserviced->cipclass == class) && - (cipserviced->cipattribute == attrib) && - (cipserviced->matchattribute == 1)) - { // if rule has class & attribute, matched all here - return 1; - } - if ((cipserviced->tokens == 3) && - (cipserviced->cipclass == class) && - (cipserviced->matchattribute == 0)) - { // for negation rule on attribute - return 1; - } - break; - case PATH_CLASS_16BIT: - class = seg->value; - if (cipserviced->cipclass == class) - { - if (cipserviced->tokens == 2) - {// if rule only has class - return 1; - } else - { - found_class = 1; - } - } - break; - case PATH_INSTANCE_16BIT: - break; - default: - return 0; - } - } - - if (found_class == 0) - { // if haven't matched class yet, no need to check attribute - return 0; - } - - if ((svc->service == CIP_SET_ATTR_LIST) || - (svc->service == CIP_GET_ATTR_LIST)) - { - AttributeEntry *attr = NULL; - TAILQ_FOREACH (attr, &svc->attrib_list, next) - { - if (cipserviced->cipattribute == attr->attribute) - { - return 1; - } - } - } - - return 0; -} - -/** - * \brief Matches the rule to the ENIP Transaction - * @param enip_data - the ENIP transaction - * * @param cipserviced - the CIP service rule - */ - -static int CIPServiceMatch(ENIPTransaction *enip_data, - DetectCipServiceData *cipserviced) -{ -#ifdef DEBUG - int count = 1; -#endif - CIPServiceEntry *svc = NULL; - //SCLogDebug("CIPServiceMatchAL"); - TAILQ_FOREACH(svc, &enip_data->service_list, next) - { - SCLogDebug("CIPServiceMatchAL service #%d : 0x%x dir %d", - count, svc->service, svc->direction); - - if (cipserviced->cipservice == svc->service) - { // compare service - //SCLogDebug("Rule Match for cip service %d",cipserviced->cipservice ); - - if (cipserviced->tokens > 1) - { //if rule params have class and attribute - - - if ((svc->service == CIP_SET_ATTR_LIST) || (svc->service - == CIP_SET_ATTR_SINGLE) || (svc->service - == CIP_GET_ATTR_LIST) || (svc->service - == CIP_GET_ATTR_SINGLE)) - { //decode path - if (CIPPathMatch(svc, cipserviced) == 1) - { - if (svc->direction == 1) return 0; //don't match responses - - return 1; - } - } - } else - { - if (svc->direction == 1) return 0; //don't match responses - - // SCLogDebug("CIPServiceMatchAL found"); - return 1; - } - } -#ifdef DEBUG - count++; -#endif - } - return 0; -} - -/** \brief Do the content inspection & validation for a signature - * - * \param de_ctx Detection engine context - * \param det_ctx Detection engine thread context - * \param s Signature to inspect ( and sm: SigMatch to inspect) - * \param f Flow - * \param flags App layer flags - * \param alstate App layer state - * \param txv Pointer to ENIP Transaction structure - * - * \retval 0 no match or 1 match - */ -uint8_t DetectEngineInspectCIP(DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, - const struct DetectEngineAppInspectionEngine_ *engine, const Signature *s, Flow *f, - uint8_t flags, void *alstate, void *txv, uint64_t tx_id) -{ - SCEnter(); - - - ENIPTransaction *tx = (ENIPTransaction *) txv; - DetectCipServiceData *cipserviced = (DetectCipServiceData *)engine->smd->ctx; - - if (cipserviced == NULL) - { - SCLogDebug("no cipservice state, no match"); - SCReturnInt(0); - } - //SCLogDebug("DetectEngineInspectCIP %d", cipserviced->cipservice); - - if (CIPServiceMatch(tx, cipserviced) == 1) - { - //SCLogDebug("DetectCIPServiceMatchAL found"); - SCReturnInt(1); - } - - SCReturnInt(0); -} - -/** \brief Do the content inspection & validation for a signature - * - * \param de_ctx Detection engine context - * \param det_ctx Detection engine thread context - * \param s Signature to inspect ( and sm: SigMatch to inspect) - * \param f Flow - * \param flags App layer flags - * \param alstate App layer state - * \param txv Pointer to ENIP Transaction structure - * - * \retval 0 no match or 1 match - */ - -uint8_t DetectEngineInspectENIP(DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, - const struct DetectEngineAppInspectionEngine_ *engine, const Signature *s, Flow *f, - uint8_t flags, void *alstate, void *txv, uint64_t tx_id) -{ - SCEnter(); - - ENIPTransaction *tx = (ENIPTransaction *) txv; - DetectEnipCommandData *enipcmdd = (DetectEnipCommandData *)engine->smd->ctx; - - if (enipcmdd == NULL) - { - SCLogDebug("no enipcommand state, no match"); - SCReturnInt(0); - } - - //SCLogDebug("DetectEngineInspectENIP %d, %d", enipcmdd->enipcommand, tx->header.command); - - if (enipcmdd->enipcommand == tx->header.command) - { - // SCLogDebug("DetectENIPCommandMatchAL found!"); - SCReturnInt(1); - } - - SCReturnInt(0); -} diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 0f459eccb67b..e964370f7bc9 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -272,6 +272,8 @@ #include "detect-ssl-state.h" #include "detect-modbus.h" #include "detect-cipservice.h" +#include "detect-enip-command.h" +#include "detect-enip-status.h" #include "detect-dnp3.h" #include "detect-ike-exch-type.h" #include "detect-ike-spi.h" @@ -518,6 +520,7 @@ void SigTableSetup(void) DetectModbusRegister(); DetectCipServiceRegister(); DetectEnipCommandRegister(); + DetectEnipStatusRegister(); DetectDNP3Register(); DetectIkeExchTypeRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 273aa10d7c9b..36b96f772c8b 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -245,6 +245,7 @@ enum DetectKeywordId { DETECT_AL_MODBUS, DETECT_CIPSERVICE, DETECT_ENIPCOMMAND, + DETECT_ENIPSTATUS, DETECT_AL_DNP3DATA, DETECT_AL_DNP3FUNC, diff --git a/src/detect-enip-command.c b/src/detect-enip-command.c new file mode 100644 index 000000000000..ad499101a524 --- /dev/null +++ b/src/detect-enip-command.c @@ -0,0 +1,120 @@ +/* Copyright (C) 2023 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 + * + * \author Philippe Antoine + * + * Set up ENIP Command rule parsing and entry point for matching + */ + +#include "suricata-common.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "rust.h" + +#include "detect-enip-command.h" + +static int g_enip_buffer_id = 0; + +/** + * \brief this function will free memory associated + * + * \param ptr pointer to u16 + */ +static void DetectEnipCommandFree(DetectEngineCtx *de_ctx, void *ptr) +{ + SCFree(ptr); +} + +/** + * \brief this function is used by enipcmdd to parse enip_command data into the current signature + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the Current Signature + * \param rulestr pointer to the user provided enip command options + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectEnipCommandSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr) +{ + SigMatch *sm = NULL; + uint16_t cmdparsed; + + if (DetectSignatureSetAppProto(s, ALPROTO_ENIP) != 0) + return -1; + + if (!rs_enip_parse_command(rulestr, &cmdparsed)) { + SCLogWarning("rule %u has invalid value for enip_command %s", s->id, rulestr); + return -1; + } + + uint16_t *enipcmdd = SCCalloc(1, sizeof(uint16_t)); + if (enipcmdd == NULL) + return -1; + *enipcmdd = cmdparsed; + + sm = SigMatchAlloc(); + if (sm == NULL) { + DetectEnipCommandFree(de_ctx, enipcmdd); + SCReturnInt(-1); + } + + sm->type = DETECT_ENIPCOMMAND; + sm->ctx = (void *)enipcmdd; + + SigMatchAppendSMToList(s, sm, g_enip_buffer_id); + SCReturnInt(0); +} + +/** + * \brief This function is used to match enip command type rule option on a transaction with those + * passed via enip_command: + * + * \retval 0 no match + * \retval 1 match + */ +static int DetectEnipCommandMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) + +{ + uint16_t *cmd = (uint16_t *)ctx; + return rs_enip_tx_is_cmd(txv, flags, *cmd); +} + +/** + * \brief Registration function for enip_command: keyword + */ +void DetectEnipCommandRegister(void) +{ + sigmatch_table[DETECT_ENIPCOMMAND].name = "enip_command"; // rule keyword + sigmatch_table[DETECT_ENIPCOMMAND].desc = "rules for detecting EtherNet/IP command"; + sigmatch_table[DETECT_ENIPCOMMAND].url = "/rules/enip-keyword.html#enip_command"; + sigmatch_table[DETECT_ENIPCOMMAND].Match = NULL; + sigmatch_table[DETECT_ENIPCOMMAND].AppLayerTxMatch = DetectEnipCommandMatch; + sigmatch_table[DETECT_ENIPCOMMAND].Setup = DetectEnipCommandSetup; + sigmatch_table[DETECT_ENIPCOMMAND].Free = DetectEnipCommandFree; + + DetectAppLayerInspectEngineRegister2( + "enip", ALPROTO_ENIP, SIG_FLAG_TOSERVER, 0, DetectEngineInspectGenericList, NULL); + DetectAppLayerInspectEngineRegister2( + "enip", ALPROTO_ENIP, SIG_FLAG_TOCLIENT, 0, DetectEngineInspectGenericList, NULL); + + g_enip_buffer_id = DetectBufferTypeGetByName("enip"); +} diff --git a/src/app-layer-enip.h b/src/detect-enip-command.h similarity index 68% rename from src/app-layer-enip.h rename to src/detect-enip-command.h index 25cb1d5745da..43f450f35ba3 100644 --- a/src/app-layer-enip.h +++ b/src/detect-enip-command.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2015 Open Information Security Foundation +/* Copyright (C) 2023 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 @@ -18,15 +18,12 @@ /** * \file * - * \author Kevin Wong + * \author Philippe Antoine */ -#ifndef __APP_LAYER_ENIP_H__ -#define __APP_LAYER_ENIP_H__ +#ifndef _DETECT_ENIP_COMMAND_H +#define _DETECT_ENIP_COMMAND_H +void DetectEnipCommandRegister(void); -void RegisterENIPUDPParsers(void); -void RegisterENIPTCPParsers(void); -void ENIPParserRegisterTests(void); - -#endif /* __APP_LAYER_ENIP_H__ */ +#endif /* _DETECT_ENIP_COMMAND_H */ diff --git a/src/detect-enip-status.c b/src/detect-enip-status.c new file mode 100644 index 000000000000..f14de3939bac --- /dev/null +++ b/src/detect-enip-status.c @@ -0,0 +1,118 @@ +/* Copyright (C) 2023 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 + * + * \author Philippe Antoine + * + * Set up ENIP Status rule parsing and entry point for matching + */ + +#include "suricata-common.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-uint.h" +#include "rust.h" + +#include "detect-enip-status.h" + +static int g_enip_status_id = 0; + +/** + * \brief this function will free memory associated + * + * \param ptr pointer to u16 + */ +static void DetectEnipStatusFree(DetectEngineCtx *de_ctx, void *ptr) +{ + rs_detect_u32_free(ptr); +} + +/** + * \brief this function is used by enipcmdd to parse enip_status data into the current signature + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the Current Signature + * \param rulestr pointer to the user provided enip status options + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectEnipStatusSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr) +{ + SigMatch *sm = NULL; + + if (DetectSignatureSetAppProto(s, ALPROTO_ENIP) != 0) + return -1; + + DetectU32Data *du32 = DetectU32Parse(rulestr); + if (du32 == NULL) { + return -1; + } + + sm = SigMatchAlloc(); + if (sm == NULL) { + DetectEnipStatusFree(de_ctx, du32); + SCReturnInt(-1); + } + + sm->type = DETECT_ENIPSTATUS; + sm->ctx = (void *)du32; + + SigMatchAppendSMToList(s, sm, g_enip_status_id); + SCReturnInt(0); +} + +/** + * \brief This function is used to match enip status type rule option on a transaction with those + * passed via enip_status: + * + * \retval 0 no match + * \retval 1 match + */ +static int DetectEnipStatusMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) + +{ + uint32_t status; + if (!rs_enip_get_status(txv, flags, &status)) + SCReturnInt(0); + const DetectU32Data *du32 = (const DetectU32Data *)ctx; + return DetectU32Match(status, du32); +} + +/** + * \brief Registration function for enip_status: keyword + */ +void DetectEnipStatusRegister(void) +{ + sigmatch_table[DETECT_ENIPSTATUS].name = "enip.status"; // rule keyword + sigmatch_table[DETECT_ENIPSTATUS].desc = "rules for detecting EtherNet/IP status"; + sigmatch_table[DETECT_ENIPSTATUS].url = "/rules/enip-keyword.html#enip-status"; + sigmatch_table[DETECT_ENIPSTATUS].Match = NULL; + sigmatch_table[DETECT_ENIPSTATUS].AppLayerTxMatch = DetectEnipStatusMatch; + sigmatch_table[DETECT_ENIPSTATUS].Setup = DetectEnipStatusSetup; + sigmatch_table[DETECT_ENIPSTATUS].Free = DetectEnipStatusFree; + + DetectAppLayerInspectEngineRegister2("enip.status", ALPROTO_ENIP, SIG_FLAG_TOSERVER, 0, + DetectEngineInspectGenericList, NULL); + DetectAppLayerInspectEngineRegister2("enip.status", ALPROTO_ENIP, SIG_FLAG_TOCLIENT, 0, + DetectEngineInspectGenericList, NULL); + + g_enip_status_id = DetectBufferTypeGetByName("enip.status"); +} diff --git a/src/detect-engine-enip.h b/src/detect-enip-status.h similarity index 50% rename from src/detect-engine-enip.h rename to src/detect-enip-status.h index 3c263f999770..bfa2ef2d8f27 100644 --- a/src/detect-engine-enip.h +++ b/src/detect-enip-status.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2015 Open Information Security Foundation +/* Copyright (C) 2023 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 @@ -15,20 +15,15 @@ * 02110-1301, USA. */ -/** \file +/** + * \file * - * \author Kevin Wong + * \author Philippe Antoine */ -#ifndef __DETECT_ENGINE_ENIP_H__ -#define __DETECT_ENGINE_ENIP_H__ +#ifndef _DETECT_ENIP_STATUS_H +#define _DETECT_ENIP_STATUS_H -uint8_t DetectEngineInspectCIP(DetectEngineCtx *de_ctx, DetectEngineThreadCtx *, - const struct DetectEngineAppInspectionEngine_ *, const Signature *, Flow *, uint8_t, void *, - void *, uint64_t); +void DetectEnipStatusRegister(void); -uint8_t DetectEngineInspectENIP(DetectEngineCtx *de_ctx, DetectEngineThreadCtx *, - const struct DetectEngineAppInspectionEngine_ *, const Signature *, Flow *, uint8_t, void *, - void *, uint64_t); - -#endif /* __DETECT_ENGINE_ENIP_H__ */ +#endif /* _DETECT_ENIP_STATUS_H */ diff --git a/src/output-json-alert.c b/src/output-json-alert.c index c3886231c001..fb52d8c3abd5 100644 --- a/src/output-json-alert.c +++ b/src/output-json-alert.c @@ -592,6 +592,7 @@ static void AlertAddAppLayer(const Packet *p, JsonBuilder *jb, case ALPROTO_BITTORRENT_DHT: AlertJsonBitTorrentDHT(p->flow, tx_id, jb); break; + // TODOlol enip default: break; } diff --git a/src/output-json-enip.c b/src/output-json-enip.c new file mode 100644 index 000000000000..77106a079982 --- /dev/null +++ b/src/output-json-enip.c @@ -0,0 +1,161 @@ +/* Copyright (C) 2023 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 + * + * \author Philippe Antoine + * + * Implement JSON/eve logging app-layer Enip. + */ + +#include "suricata-common.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "output-json-enip.h" +#include "rust.h" + +typedef struct LogEnipFileCtx_ { + uint32_t flags; + OutputJsonCtx *eve_ctx; +} LogEnipFileCtx; + +typedef struct LogEnipLogThread_ { + LogEnipFileCtx *eniplog_ctx; + OutputJsonThreadCtx *ctx; +} LogEnipLogThread; + +static int JsonEnipLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, void *state, + void *tx, uint64_t tx_id) +{ + LogEnipLogThread *thread = thread_data; + + JsonBuilder *js = + CreateEveHeader(p, LOG_DIR_PACKET, "enip", NULL, thread->eniplog_ctx->eve_ctx); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + if (!rs_enip_logger_log(tx, js)) { + goto error; + } + + OutputJsonBuilderBuffer(js, thread->ctx); + jb_free(js); + + return TM_ECODE_OK; + +error: + jb_free(js); + return TM_ECODE_FAILED; +} + +static void OutputEnipLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogEnipFileCtx *eniplog_ctx = (LogEnipFileCtx *)output_ctx->data; + SCFree(eniplog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputEnipLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogEnipFileCtx *eniplog_ctx = SCCalloc(1, sizeof(*eniplog_ctx)); + if (unlikely(eniplog_ctx == NULL)) { + return result; + } + eniplog_ctx->eve_ctx = ajt; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(eniplog_ctx); + return result; + } + output_ctx->data = eniplog_ctx; + output_ctx->DeInit = OutputEnipLogDeInitCtxSub; + + AppLayerParserRegisterLogger(IPPROTO_UDP, ALPROTO_ENIP); + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_ENIP); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +static TmEcode JsonEnipLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogEnipLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogEnip. \"initdata\" is NULL."); + goto error_exit; + } + + thread->eniplog_ctx = ((OutputCtx *)initdata)->data; + thread->ctx = CreateEveThreadCtx(t, thread->eniplog_ctx->eve_ctx); + if (!thread->ctx) { + goto error_exit; + } + *data = (void *)thread; + + return TM_ECODE_OK; + +error_exit: + SCFree(thread); + return TM_ECODE_FAILED; +} + +static TmEcode JsonEnipLogThreadDeinit(ThreadVars *t, void *data) +{ + LogEnipLogThread *thread = (LogEnipLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + FreeEveThreadCtx(thread->ctx); + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonEnipLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonEnipLog", "eve-log.enip", + OutputEnipLogInitSub, ALPROTO_ENIP, JsonEnipLogger, JsonEnipLogThreadInit, + JsonEnipLogThreadDeinit, NULL); +} diff --git a/src/output-json-enip.h b/src/output-json-enip.h new file mode 100644 index 000000000000..27832bb85022 --- /dev/null +++ b/src/output-json-enip.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 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 + * + * \author Philippe Antoine + */ + +#ifndef __OUTPUT_JSON_ENIP_H__ +#define __OUTPUT_JSON_ENIP_H__ + +void JsonEnipLogRegister(void); + +#endif /* __OUTPUT_JSON_ENIP_H__ */ diff --git a/src/output.c b/src/output.c index c13ab4862eda..189a934d6f68 100644 --- a/src/output.c +++ b/src/output.c @@ -78,6 +78,7 @@ #include "output-json-rfb.h" #include "output-json-mqtt.h" #include "output-json-pgsql.h" +#include "output-json-enip.h" #include "output-json-template.h" #include "output-json-rdp.h" #include "output-json-http2.h" @@ -1115,6 +1116,8 @@ void OutputRegisterLoggers(void) JsonMQTTLogRegister(); /* Pgsql JSON logger. */ JsonPgsqlLogRegister(); + /* Enip JSON logger. */ + JsonEnipLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); /* RDP JSON logger. */ diff --git a/src/runmode-unittests.c b/src/runmode-unittests.c index 1150bad89580..7518bbe0349d 100644 --- a/src/runmode-unittests.c +++ b/src/runmode-unittests.c @@ -38,7 +38,6 @@ #include "detect-engine-dcepayload.h" #include "detect-engine-state.h" #include "detect-engine-tag.h" -#include "detect-engine-enip.h" #include "detect-fast-pattern.h" #include "flow.h" #include "flow-timeout.h" diff --git a/suricata.yaml.in b/suricata.yaml.in index 630399126dbe..7a0732d4fcff 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -279,6 +279,7 @@ outputs: #md5: [body, subject] #- dnp3 + #- enip - ftp - rdp - nfs