From: Shravan Rangarajuvenkata (shrarang) Date: Fri, 8 Nov 2019 14:11:53 +0000 (-0500) Subject: Merge pull request #1789 in SNORT/snort3 from ~JIAWU2/snort3:service_inspector_cip_po... X-Git-Tag: 3.0.0-265~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=feeee5b888ae1760c7c5491d78e824499524f8e2;p=thirdparty%2Fsnort3.git Merge pull request #1789 in SNORT/snort3 from ~JIAWU2/snort3:service_inspector_cip_porting to master Squashed commit of the following: commit 4777c5b25a30d46c1f79488488c9a4c731f48971 Author: Jian Wu Date: Tue Oct 8 18:19:43 2019 -0400 cip: ips rule support for Common Industrial Protocol (CIP) --- diff --git a/src/pub_sub/CMakeLists.txt b/src/pub_sub/CMakeLists.txt index 4f0cb9c53..290e4d7aa 100644 --- a/src/pub_sub/CMakeLists.txt +++ b/src/pub_sub/CMakeLists.txt @@ -1,5 +1,6 @@ set (PUB_SUB_INCLUDES appid_events.h + cip_events.h daq_message_event.h expect_events.h finalize_packet_event.h @@ -9,6 +10,7 @@ set (PUB_SUB_INCLUDES add_library( pub_sub OBJECT ${PUB_SUB_INCLUDES} + cip_events.cc http_events.cc sip_events.cc ) diff --git a/src/pub_sub/cip_events.cc b/src/pub_sub/cip_events.cc new file mode 100644 index 000000000..1d40dd3ae --- /dev/null +++ b/src/pub_sub/cip_events.cc @@ -0,0 +1,36 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- +// sip_events.cc author Jian Wu + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "cip_events.h" + +#include "service_inspectors/cip/cip.h" + +using namespace snort; +using namespace std; + +CipEvent::CipEvent(const Packet* p, const CipEventData* EventData) +{ + this->p = p; + this->EventData = EventData; +} + diff --git a/src/pub_sub/cip_events.h b/src/pub_sub/cip_events.h new file mode 100644 index 000000000..804020f81 --- /dev/null +++ b/src/pub_sub/cip_events.h @@ -0,0 +1,58 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- +// cip_events.h author Jian Wu + +#ifndef CIP_EVENTS_H +#define CIP_EVENTS_H + +// This event conveys data published by the CIP service inspector to be consumed +// by data bus subscribers + +#include + +#include "framework/data_bus.h" + +#define CIP_EVENT_TYPE_CIP_DATA_KEY "cip_event_type_cip_data" + +enum CipEventType +{ + CIP_EVENT_TYPE_CIP_DATA +}; + +namespace snort +{ +struct Packet; +struct SfIp; +} + +struct CipEventData; + +class CipEvent : public snort::DataEvent +{ +public: + CipEvent(const snort::Packet*, const CipEventData*); + + const snort::Packet* get_packet() override + { return p; } + +private: + const snort::Packet* p; + const CipEventData* EventData; +}; + +#endif diff --git a/src/service_inspectors/CMakeLists.txt b/src/service_inspectors/CMakeLists.txt index 962f60433..f1a831eca 100644 --- a/src/service_inspectors/CMakeLists.txt +++ b/src/service_inspectors/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(back_orifice) +add_subdirectory(cip) add_subdirectory(dce_rpc) add_subdirectory(dnp3) add_subdirectory(dns) @@ -21,6 +22,7 @@ add_subdirectory(s7commplus) if (STATIC_INSPECTORS) set (STATIC_INSPECTOR_OBJS $ + $ $ $ $ diff --git a/src/service_inspectors/cip/CMakeLists.txt b/src/service_inspectors/cip/CMakeLists.txt new file mode 100644 index 000000000..4ca03de26 --- /dev/null +++ b/src/service_inspectors/cip/CMakeLists.txt @@ -0,0 +1,34 @@ +set( FILE_LIST + cip.cc + cip.h + cip_definitions.h + cip_module.cc + cip_module.h + cip_paf.cc + cip_paf.h + cip_parsing.cc + cip_parsing.h + cip_session.cc + cip_session.h + cip_util.h + ips_cip_attribute.cc + ips_cip_class.cc + ips_cip_connpathclass.cc + ips_cip_enipcommand.cc + ips_cip_enipreq.cc + ips_cip_eniprsp.cc + ips_cip_instance.cc + ips_cip_req.cc + ips_cip_rsp.cc + ips_cip_service.cc + ips_cip_status.cc +) + +if (STATIC_INSPECTORS) + add_library(cip OBJECT ${FILE_LIST}) + +else (STATIC_INSPECTORS) + add_dynamic_module(cip inspectors ${FILE_LIST}) + +endif (STATIC_INSPECTORS) + diff --git a/src/service_inspectors/cip/cip.cc b/src/service_inspectors/cip/cip.cc new file mode 100644 index 000000000..235242366 --- /dev/null +++ b/src/service_inspectors/cip/cip.cc @@ -0,0 +1,406 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip.cc author Jian Wu + +/* Description: service inspector for the CIP protocol. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "cip.h" + +#include "detection/detection_engine.h" +#include "events/event_queue.h" +#include "log/messages.h" +#include "managers/inspector_manager.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" +#include "pub_sub/cip_events.h" +#include "stream/stream_splitter.h" +#include "utils/util.h" // for snort_calloc + +#include "cip_module.h" +#include "cip_paf.h" +#include "cip_parsing.h" + +using namespace snort; + +THREAD_LOCAL ProfileStats cip_perf_stats; + +unsigned CipFlowData::inspector_id = 0; + +static void free_cip_data(void* data); + +CipFlowData::CipFlowData() : FlowData(inspector_id) +{ + memset(&session, 0, sizeof(session)); + cip_stats.sessions++; + cip_stats.concurrent_sessions++; + if (cip_stats.max_concurrent_sessions < cip_stats.concurrent_sessions) + cip_stats.max_concurrent_sessions = cip_stats.concurrent_sessions; +} + +CipFlowData::~CipFlowData() +{ + free_cip_data(&session); + assert(cip_stats.concurrent_sessions > 0); + cip_stats.concurrent_sessions--; +} + +CipSessionData* get_cip_session_data(const Flow* flow) +{ + CipFlowData* fd = static_cast(flow->get_flow_data(CipFlowData::inspector_id)); + return fd ? &fd->session : nullptr; +} + +static CipSessionData* set_new_cip_session_data(CipProtoConf* config, Packet* p) +{ + CipFlowData* fd = new CipFlowData; + CipSessionData* css = &fd->session; + + p->flow->set_flow_data(fd); + + /* Only allocate global state data for TCP connections. */ + if (p->has_tcp_data()) + { + css->global_data.connection_list.list_size = config->max_cip_connections; + css->global_data.connection_list.list + = static_cast(snort_calloc(config->max_cip_connections, + sizeof(CipConnection))); + + css->global_data.unconnected_list.list_size = config->max_unconnected_messages; + css->global_data.unconnected_list.list + = static_cast(snort_calloc(config->max_unconnected_messages, + sizeof(CipUnconnectedMessage))); + } + + return &fd->session; +} + +static void free_cip_data(void* data) +{ + CipSessionData* css = static_cast(data); + + if ( css->global_data.connection_list.list ) + { + snort_free(css->global_data.connection_list.list); + css->global_data.connection_list.list = nullptr; + } + + if ( css->global_data.unconnected_list.list ) + { + snort_free(css->global_data.unconnected_list.list); + css->global_data.unconnected_list.list = nullptr; + } +} + +static void print_cip_conf(CipProtoConf* config) +{ + if (config == nullptr) + return; + LogMessage("CIP config: \n"); + LogMessage(" Embedded Enabled: %s\n", + config->embedded_cip_enabled ? "ENABLED" : "DISABLED"); + if (config->embedded_cip_enabled) + { + LogMessage(" Embedded Class: 0x%x\n", config->embedded_cip_class_id); + LogMessage(" Embedded Service: 0x%x\n", config->embedded_cip_service_id); + } + + LogMessage(" Unconnected Timeout: %d (seconds)\n", config->unconnected_timeout); + LogMessage(" Max CIP connections per TCP connection: %d\n", + static_cast(config->max_cip_connections)); + LogMessage(" Max unconnected messages per TCP connection: %d\n", + static_cast(config->max_unconnected_messages)); + + LogMessage("\n"); +} + +static CipPacketDirection get_packet_direction(Packet* p) +{ + if (!p->has_tcp_data()) + { + return CIP_FROM_UNKNOWN; + } + if (p->packet_flags & PKT_FROM_CLIENT) + { + return CIP_FROM_CLIENT; + } + return CIP_FROM_SERVER; +} + +static void publish_data_to_appId(Packet* packet, CipCurrentData& current_data) +{ + CipEventData cip_event_data; + CipEvent cip_event(packet, &cip_event_data); + + bool publish_appid = true; + + // Set one specific matching type for this PDU, in order of priority. + if (current_data.invalid_fatal) + { + cip_event_data.type = CIP_DATA_TYPE_MALFORMED; + } + else if (current_data.cip_message_type == CipMessageTypeExplicit) + { + if (current_data.cip_msg.is_cip_request) + { + /* Just Cip implement this function in parsing.cc */ + pack_cip_request_event(¤t_data.cip_msg.request, &cip_event_data); + } + else + { + // Do not attempt to set applications for CIP responses. + publish_appid = false; + } + } + else if (current_data.cip_message_type == CipMessageTypeImplicit) + { + cip_event_data.type = CIP_DATA_TYPE_IMPLICIT; + cip_event_data.class_id = current_data.enip_data.connection_class_id; + } + else if (current_data.enip_data.enip_decoded) + { + cip_event_data.type = CIP_DATA_TYPE_ENIP_COMMAND; + cip_event_data.enip_command_id = current_data.enip_data.enip_header.command; + } + else + { + cip_event_data.type = CIP_DATA_TYPE_OTHER; + } + + if (publish_appid) + { + DataBus::publish(CIP_EVENT_TYPE_CIP_DATA_KEY, cip_event, packet->flow); + } +} + +static void log_cip_validity_errors(const CipCurrentData& current_data, + CipGlobalSessionData& global_data) +{ + if (current_data.invalid_fatal) + { + /* what is engine */ + DetectionEngine::queue_event(GID_CIP, CIP_MALFORMED); + } + else if (current_data.enip_data.enip_invalid_nonfatal != 0 + || current_data.cip_msg.request.cip_req_invalid_nonfatal != 0) + { + DetectionEngine::queue_event(GID_CIP, CIP_NON_CONFORMING); + } + + if (global_data.connection_list.connection_pruned) + { + DetectionEngine::queue_event(GID_CIP, CIP_CONNECTION_LIMIT); + global_data.connection_list.connection_pruned = false; + } + + if (global_data.unconnected_list.request_pruned) + { + DetectionEngine::queue_event(GID_CIP, CIP_REQUEST_LIMIT); + global_data.unconnected_list.request_pruned = false; + } +} + +static void cip_current_data_process(CipSessionData* css, CipCurrentData& current_data, + CipProtoConf* config, Packet* p) +{ + /* Current Data should be implemented as the same with c files */ + memset(¤t_data, 0, sizeof(CipCurrentData)); + current_data.direction = get_packet_direction(p); + + css->global_data.config = config; + css->global_data.snort_packet = p; + + /* parse_enip_layer should be implemented specifically */ + current_data.invalid_fatal = !parse_enip_layer(p->data, + p->dsize, + p->has_tcp_data(), + ¤t_data, + &css->global_data); + + if (!current_data.invalid_fatal + && (p->dsize != current_data.enip_data.enip_header.length + ENIP_HEADER_SIZE)) + { + current_data.enip_data.enip_invalid_nonfatal |= ENIP_INVALID_PAYLOAD_SIZE; + } +} + +static void snort_cip(CipProtoConf* config, Packet* p) +{ + Profile profile(cip_perf_stats); + + if (p->has_tcp_data() && !p->is_full_pdu()) + return; + + p->packet_flags |= PKT_ALLOW_MULTIPLE_DETECT; + CipSessionData* css = get_cip_session_data(p->flow); + + if (css == nullptr) + { + css = set_new_cip_session_data(config, p); + } + + CipCurrentData& current_data = css->current_data; + cip_current_data_process(css, current_data, config, p); + publish_data_to_appId(p, current_data); + log_cip_validity_errors(current_data, css->global_data); +} + +//------------------------------------------------------------------------- +// class stuff +//------------------------------------------------------------------------- + +class Cip : public Inspector +{ +public: + Cip(CipProtoConf*); + ~Cip() override; + + void show(SnortConfig*) override; + void eval(Packet*) override; + + class StreamSplitter* get_splitter(bool c2s) override + { + return new CipSplitter(c2s); + } + +private: + CipProtoConf* config; +}; + +Cip::Cip(CipProtoConf* pc) +{ + config = pc; +} + +Cip::~Cip() +{ + if (config) + { + delete config; + } +} + +void Cip::show(SnortConfig*) +{ + /* defined in module.cc */ + print_cip_conf(config); +} + +void Cip::eval(Packet* p) +{ + assert(p->has_tcp_data() || p->has_udp_data()); + assert(p->flow); + cip_stats.packets++; + snort_cip(config, p); +} + +//------------------------------------------------------------------------- +// api stuff +//------------------------------------------------------------------------- + +static Module* mod_ctor() +{ + return new CipModule; +} + +static void mod_dtor(Module* m) +{ + delete m; +} + +static void cip_init() +{ + CipFlowData::init(); +} + +static Inspector* cip_ctor(Module* m) +{ + CipModule* mod = static_cast(m); + return new Cip(mod->get_data()); +} + +static void cip_dtor(Inspector* p) +{ + delete p; +} + +const InspectApi cip_api = +{ + { + PT_INSPECTOR, + sizeof(InspectApi), + INSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + CIP_NAME, + CIP_HELP, + mod_ctor, + mod_dtor + }, + IT_SERVICE, + PROTO_BIT__PDU, + nullptr, // buffers + "cip", + cip_init, + nullptr, // pterm + nullptr, // tinit + nullptr, // tterm + cip_ctor, + cip_dtor, + nullptr, // ssn + nullptr // reset +}; + +extern const BaseApi* ips_cip_attribute; +extern const BaseApi* ips_cip_class; +extern const BaseApi* ips_cip_connpathclass; +extern const BaseApi* ips_cip_enipcommand; +extern const BaseApi* ips_cip_enipreq; +extern const BaseApi* ips_cip_eniprsp; +extern const BaseApi* ips_cip_instance; +extern const BaseApi* ips_cip_req; +extern const BaseApi* ips_cip_rsp; +extern const BaseApi* ips_cip_service; +extern const BaseApi* ips_cip_status; + +#ifdef BUILDING_SO +SO_PUBLIC const BaseApi* snort_plugins[] = +#else +const BaseApi* sin_cip[] = +#endif +{ + &cip_api.base, + ips_cip_attribute, + ips_cip_class, + ips_cip_connpathclass, + ips_cip_enipcommand, + ips_cip_enipreq, + ips_cip_eniprsp, + ips_cip_instance, + ips_cip_req, + ips_cip_rsp, + ips_cip_service, + ips_cip_status, + nullptr +}; + diff --git a/src/service_inspectors/cip/cip.h b/src/service_inspectors/cip/cip.h new file mode 100644 index 000000000..5a4b6cc15 --- /dev/null +++ b/src/service_inspectors/cip/cip.h @@ -0,0 +1,114 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip.h author RA/Cisco Jian Wu + +#ifndef CIP_H +#define CIP_H +// Implementation header with definitions, datatypes and flowdata class for CIP service inspector. + +#include "flow/flow.h" +#include "framework/counts.h" +#include "main/thread.h" +#include "protocols/packet.h" + +#include "cip_definitions.h" + +namespace snort +{ +struct Packet; +} + +enum CipDataType +{ + CIP_DATA_TYPE_PATH_CLASS = 0, + CIP_DATA_TYPE_PATH_EXT_SYMBOL, + CIP_DATA_TYPE_SET_ATTRIBUTE, + CIP_DATA_TYPE_CONNECTION, + CIP_DATA_TYPE_IMPLICIT, + CIP_DATA_TYPE_OTHER, + CIP_DATA_TYPE_ENIP_COMMAND, + CIP_DATA_TYPE_MALFORMED +}; + +struct CipEventData +{ + // Specify the type of CIP data. + CipDataType type; + + // Used for: + // CIP_DATA_TYPE_ENIP_COMMAND + uint16_t enip_command_id; + + // Used for: + // CIP_DATA_TYPE_PATH_CLASS + // CIP_DATA_TYPE_PATH_EXT_SYMBOL + // CIP_DATA_TYPE_SET_ATTRIBUTE + uint8_t service_id; + + // Used for: + // CIP_DATA_TYPE_PATH_CLASS: This represents the Request Path Class. + // CIP_DATA_TYPE_SET_ATTRIBUTE: This represents the Request Path Class. + // CIP_DATA_TYPE_CONNECTION: This represents the Connection Path Class. + // CIP_DATA_TYPE_IMPLICIT: This represents the Connection Path Class from + // the original connection request, for this connection. + uint32_t class_id; + + // Used for: + // CIP_DATA_TYPE_SET_ATTRIBUTE: This represents the Request Path Instance. + uint32_t instance_id; + + // Used for: + // CIP_DATA_TYPE_SET_ATTRIBUTE: This represents the Request Path Attribute. + uint32_t attribute_id; + + // Pointer to snort::Packet + const snort::Packet* snort_packet; +}; + +class CipFlowData : public snort::FlowData +{ +public: + CipFlowData(); + ~CipFlowData() override; + + static void init() + { inspector_id = snort::FlowData::create_flow_data_id(); } + + size_t size_of() override + { return sizeof(*this); } + +public: + static unsigned inspector_id; + CipSessionData session; +}; + +CipSessionData* get_cip_session_data(const snort::Flow*); + +struct CipStats +{ + PegCount packets; + PegCount sessions; + PegCount concurrent_sessions; + PegCount max_concurrent_sessions; +}; + +extern THREAD_LOCAL CipStats cip_stats; + +#endif + diff --git a/src/service_inspectors/cip/cip_definitions.h b/src/service_inspectors/cip/cip_definitions.h new file mode 100644 index 000000000..b9bba15f9 --- /dev/null +++ b/src/service_inspectors/cip/cip_definitions.h @@ -0,0 +1,524 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_definitions.h author RA/Cisco + +/* Description: Common types for the CIP preprocessor. */ + +#ifndef CIP_DEFINITIONS_H +#define CIP_DEFINITIONS_H + +namespace snort +{ +struct Packet; +} + +#define MSEC_PER_SEC (1000) +#define USEC_PER_SEC (1000000) + +// CIP preprocessor configuration +struct CipProtoConf +{ + // Unconnected timeout, seconds. + uint32_t unconnected_timeout; + + // Maximum number of unconnected requests per TCP connection. + size_t max_unconnected_messages; + + // Maximum number of CIP connections per TCP connection. + size_t max_cip_connections; + + // Custom embedded packet parameters. + bool embedded_cip_enabled; + uint32_t embedded_cip_class_id; + uint8_t embedded_cip_service_id; +}; + +/// CIP Request/Response Management +enum CipRequestType +{ + CipRequestTypeOther = 0, + CipRequestTypeForwardOpen, + CipRequestTypeForwardClose, + CipRequestTypeUnconnectedSend, + CipRequestTypeMultipleServiceRequest, + + // Special case to represent when no request is found for a given response. + CipRequestTypeNoMatchFound +}; + +struct CipStatus +{ + uint8_t general_status; + size_t extended_status_size; +}; + +enum CipPacketDirection +{ + CIP_FROM_CLIENT, + CIP_FROM_SERVER, + CIP_FROM_UNKNOWN +}; + +/// EtherNet/IP encapsulation layer definitions. + +// EtherNet/IP encapsulation header +struct EnipHeader +{ + uint16_t command; + uint16_t length; + uint32_t session_handle; + uint32_t status; + uint64_t sender_context; + uint32_t options; +}; + +// This is an EtherNet/IP encapsulation layer common packet format item. +struct EnipCpfItem +{ + uint16_t type; + uint16_t length; + + // Used if length > 0. Data starts after the Length field. + const uint8_t* data; +}; + +// Largest number of allowed CPF items for standard EtherNet/IP commands. +#define MAX_NUM_CPF_ITEMS 4 + +// This is an EtherNet/IP encapsulation layer common packet format. +struct EnipCpf +{ + uint16_t item_count; + + // All CPF items in the list are valid up to and including array index item_count. + EnipCpfItem item_list[MAX_NUM_CPF_ITEMS]; +}; + +/// CIP layer definitions. +enum CipMessageType +{ + // Unknown CIP data type + CipMessageTypeUnknown, + + // CIP Explicit Data + CipMessageTypeExplicit, + + // CIP Implicit Data + CipMessageTypeImplicit +}; + +enum CipSegmentType +{ + CipSegment_Type_PORT_LINK_ADDRESS, + CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED, + + CipSegment_Type_LOGICAL_CLASS, + CipSegment_Type_LOGICAL_INSTANCE, + CipSegment_Type_LOGICAL_MEMBER, + CipSegment_Type_LOGICAL_CONN_POINT, + CipSegment_Type_LOGICAL_ATTRIBUTE, + CipSegment_Type_LOGICAL_ELECTRONIC_KEY, + CipSegment_Type_LOGICAL_EXTENDED, + CipSegment_Type_LOGICAL_SERVICE_ID, + + CipSegment_Type_NETWORK, + + CipSegment_Type_SYMBOLIC, + + CipSegment_Type_DATA_SIMPLE, + CipSegment_Type_DATA_EXT_SYMBOL, + + CipSegment_Type_UNKNOWN +}; + +#define CIP_STATUS_SUCCESS 0 +#define ENIP_STATUS_SUCCESS 0 + +// CIP Classes +#define MESSAGE_ROUTER_CLASS_ID 0x02 +#define CONNECTION_MANAGER_CLASS_ID 0x06 + +// CIP Services +#define SERVICE_SET_ATTRIBUTE_SINGLE 0x10 +#define SERVICE_MULTIPLE_SERVICE_PACKET 0x0A + +// CIP Connection Manager Services +#define CONNECTION_MANAGER_UNCONNECTED_SEND 0x52 +#define CONNECTION_MANAGER_FORWARD_OPEN 0x54 +#define CONNECTION_MANAGER_LARGE_FORWARD_OPEN 0x5B +#define CONNECTION_MANAGER_FORWARD_CLOSE 0x4E + +#define CIP_WORD_TO_BYTES 2 + +struct CipSegment +{ + CipSegmentType type; + + // Total size of this segment. + size_t size; + + // When type = CipSegment_Type_PORT_LINK_ADDRESS + // When type = CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED + uint16_t port_id; + + // When type = CipSegment_Type_PORT_LINK_ADDRESS + uint8_t link_address; + + // When type = CipSegment_Type_LOGICAL_CLASS + // When type = CipSegment_Type_LOGICAL_INSTANCE + // When type = CipSegment_Type_LOGICAL_MEMBER + // When type = CipSegment_Type_LOGICAL_CONN_POINT + // When type = CipSegment_Type_LOGICAL_ATTRIBUTE + uint32_t logical_value; + + // When type = CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED, this is the link address. + // When type = CipSegment_Type_DATA_EXT_SYMBOL, this is the symbol string. + // When type = CipSegment_Type_DATA_SIMPLE, this is the start of the data words. + // When type = CipSegment_Type_SYMBOLIC, this is the symbol string. + const uint8_t* data; + size_t data_size; +}; + +struct CipPath +{ + // Size of the entire path. + size_t full_path_size; + + // True if path has been decoded successfully. + bool decoded; + + // Main segment type for this path, which drives message target. + CipSegmentType primary_segment_type; + + bool has_class_id; + uint32_t class_id; + + bool has_instance_id; + uint32_t instance_id; + + bool has_attribute_id; + uint32_t attribute_id; + + bool has_unknown_segment; +}; + +// Matching pair of CIP Connection IDs. +struct ConnectionIdPair +{ + uint32_t ot_connection_id; + uint32_t to_connection_id; +}; + +// RPI and Network Connection Parameters from a Forward Open Request. +struct CipConnectionParameters +{ + uint32_t rpi; + uint32_t network_connection_parameters; + bool is_null_connection; +}; + +// Unique Connection Signature. This is unique to each CIP connection. This +// tuple is unique on a given EtherNet/IP session. +struct CipConnectionSignature +{ + uint16_t connection_serial_number; + uint16_t vendor_id; + uint32_t originator_serial_number; +}; + +#define TRANSPORT_CLASS_MASK 0x0F +struct CipForwardOpenRequest +{ + // Unconnected request timeout, milliseconds. + uint32_t timeout_ms; + + // Connection timeouts, microseconds. + uint64_t ot_connection_timeout_us; + uint64_t to_connection_timeout_us; + + CipConnectionSignature connection_signature; + + CipConnectionParameters ot_parameters; + CipConnectionParameters to_parameters; + uint8_t transport_class; + + CipPath connection_path; + + bool is_null_forward_open; + + // Timestamp for message request. + struct timeval timestamp; +}; + +struct CipForwardOpenResponse +{ + // True if this was a successful Forward Open Response. + bool success; + + // Properties for Success or Fail. + CipConnectionSignature connection_signature; + + // Properties for a Forward Open Response Success. + ConnectionIdPair connection_pair; + + size_t application_reply_size; + + // Timestamp for message response. + struct timeval timestamp; +}; + +struct CipForwardCloseRequest +{ + // Unconnected request timeout, milliseconds. + uint32_t timeout_ms; + + CipConnectionSignature connection_signature; + + CipPath connection_path; +}; + +// Used to set error flags in enip_invalid_nonfatal. +#define ENIP_INVALID_COMMAND (1 << 0) +#define ENIP_INVALID_DUPLICATE_SESSION (1 << 1) +#define ENIP_INVALID_SESSION_HANDLE (1 << 2) +#define ENIP_INVALID_INTERFACE_HANDLE (1 << 3) +#define ENIP_INVALID_CONNECTION_ID (1 << 4) +#define ENIP_INVALID_PAYLOAD_SIZE (1 << 5) +#define ENIP_INVALID_ENIP_COMMAND_CPF_MISMATCH (1 << 6) +#define ENIP_INVALID_RESERVED_FUTURE_CPF_TYPE (1 << 7) +#define ENIP_INVALID_STATUS (1 << 8) +#define ENIP_INVALID_ENIP_TCP_ONLY (1 << 9) + +struct EnipSessionData +{ + // True if the ENIP Header was parsed and is valid. + bool enip_decoded; + + // Full ENIP header. + EnipHeader enip_header; + + // Error states for non-fatal ENIP errors. Error conditions that could trigger this: + // - Command code was not valid according to CIP Volume 2, Section 2-3.2. + // - RegisterSession attempted when a session was already active. + // - Session Handle did not match an active session. + // - Interface Handle != 0 + // - Connection ID does not match an active connection. + // - Larger amount of ENIP data than specific in ENIP length. + // - Invalid CPF data item for a particular ENIP command. + // - CPF Item Type ID was found in the Reserved for future expansion range. + // - ENIP Status != 0, for a Request. + // - Attempting to send an ENIP command that is TCP only on a UDP connection. + uint32_t enip_invalid_nonfatal; + + // True if the Common Packet Format was parsed and is valid. + bool cpf_decoded; + + // True if the required CPF items are present for this EtherNet/IP command. + bool required_cpf_items_present; + + // Common Packet Format data. + EnipCpf enip_cpf; + + // Connection Class from original connection request, for connected messages. + uint32_t connection_class_id; +}; + +// Used to set error flags in cip_req_invalid_nonfatal. +#define CIP_REQ_INVALID_CONNECTION_ADD_FAILED (1 << 0) +#define CIP_REQ_INVALID_UNKNOWN_SEGMENT (1 << 1) +#define CIP_REQ_INVALID_TIMEOUT_MULTIPLIER (1 << 2) +struct CipRequest +{ + // CIP Service code. + uint8_t service; + + CipPath request_path; + + CipRequestType request_type; + + // This is only valid for Unconnected Send messages. + CipPath route_path; + + // CIP application payload data. This starts after the Request Path. + const uint8_t* cip_data; + size_t cip_data_size; + + // Unconnected request timeout, milliseconds. + bool has_timeout; + uint32_t timeout_ms; + + // True if this request was a Forward Open Request. + bool is_forward_open_request; + + // Class ID in the Forward Open Request Connection Path. + // Used only when is_forward_open_request is true. + uint32_t connection_path_class_id; + + // Error states for non-fatal CIP errors. Error conditions that could trigger this: + // - Forward Open Request received but couldn't add the connection to the list because a + // connection already existed with that signature. + // - Unknown segment type in request path. + // - Forward Open Request contained invalid Connection Timeout Multiplier. + uint32_t cip_req_invalid_nonfatal; +}; + +struct CipResponse +{ + // CIP Service code. This does not include the first bit set (0x80). + uint8_t service; + + CipStatus status; +}; + +struct CipMessage +{ + // True if this is a CIP request (vs response). + bool is_cip_request; + + // Used if is_cip_request is true. + CipRequest request; + + // Used if is_cip_request is false. + CipResponse response; +}; + +struct CipCurrentData +{ + CipPacketDirection direction; + + // ENIP layer data. + EnipSessionData enip_data; + + // CIP layer data. + CipMessageType cip_message_type; + + // Used if cip_message_type is CipMessageTypeExplicit + CipMessage cip_msg; + + // True if the packet was not able to be fully parsed. + bool invalid_fatal; +}; + +struct EnipSession +{ + // ENIP session handle. + uint32_t session_handle; + + // True if this session is active. + bool active; +}; + +// This represents an Unconnected message request. +// Sender Context -> Request Type +struct CipUnconnectedMessage +{ + uint64_t sender_context; + + CipRequestType request_type; + + // Unconnected request timeout, milliseconds. + uint32_t timeout_ms; + + // Timestamp for message request. + struct timeval timestamp; + + // True if this entry is in use. + bool slot_active; +}; + +struct CipUnconnectedMessageList +{ + CipUnconnectedMessage* list; + size_t list_size; + + // True if an active request was forced to be pruned. + bool request_pruned; + + size_t count; +}; + +// This represents a CIP connection. +// This is used to: +// a) Get the connection IDs, during a Forward Close. +// b) Get Class ID from the Connection Path. +struct CipConnection +{ + CipConnectionSignature signature; + + ConnectionIdPair connection_id_pair; + + // Class ID from the Connection Path + uint32_t class_id; + + // True if the connection is fully established. + bool established; + + // Connection timeouts, seconds. + uint32_t ot_connection_timeout_sec; + uint32_t to_connection_timeout_sec; + + // Timestamp for last time connection was active. + struct timeval ot_timestamp; + struct timeval to_timestamp; + + // True if this entry is in use. + bool slot_active; +}; + +struct CipConnectionList +{ + CipConnection* list; + size_t list_size; + + // True if an active connection was forced to be pruned. + bool connection_pruned; + + size_t count; +}; + +struct CipGlobalSessionData +{ + // ENIP Session for this TCP connection. + EnipSession enip_session; + + // List of CIP connections. + CipConnectionList connection_list; + + // List of outstanding unconnected messages (SendRRData). + CipUnconnectedMessageList unconnected_list; + + // Current configuration for use in lower-level parsing functions. + const CipProtoConf* config; + + snort::Packet* snort_packet; +}; + +// This is the overall structure used by Snort to store current and global data +// for a particular stream. +struct CipSessionData +{ + // Current data for this packet. + CipCurrentData current_data; + + // Overall data for this session. + CipGlobalSessionData global_data; +}; + +#endif // CIP_DEFINITIONS_H + diff --git a/src/service_inspectors/cip/cip_module.cc b/src/service_inspectors/cip/cip_module.cc new file mode 100644 index 000000000..e6fa31300 --- /dev/null +++ b/src/service_inspectors/cip/cip_module.cc @@ -0,0 +1,157 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_module.cc author Jian Wu + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "cip_module.h" + +#include + +#include "cip.h" + +using namespace snort; +using namespace std; + +#define CIP_MALFORMED_STR "CIP data is malformed." +#define CIP_NON_CONFORMING_STR "CIP data is non-conforming to ODVA standard." +#define CIP_CONNECTION_LIMIT_STR \ + "CIP connection limit exceeded. Least recently used connection removed." +#define CIP_REQUEST_LIMIT_STR "CIP unconnected request limit exceeded. Oldest request removed." + +static const Parameter c_params[] = +{ + { "embedded_cip_path", Parameter::PT_STRING, nullptr, "false", + "check embedded CIP path" }, + { "unconnected_timeout", Parameter::PT_INT, "0:360", "300", + "unconnected timeout in seconds" }, + { "max_cip_connections", Parameter::PT_INT, "1:10000", "100", + "max cip connections" }, + { "max_unconnected_messages", Parameter::PT_INT, "1:10000", "100", + "max unconnected cip messages" }, + { nullptr, Parameter::PT_STRING, nullptr, nullptr, nullptr } +}; + +static const RuleMap cip_rules[] = +{ + { CIP_MALFORMED, CIP_MALFORMED_STR }, + { CIP_NON_CONFORMING, CIP_NON_CONFORMING_STR }, + { CIP_CONNECTION_LIMIT, CIP_CONNECTION_LIMIT_STR }, + { CIP_REQUEST_LIMIT, CIP_REQUEST_LIMIT_STR }, + { 0, nullptr } +}; + +THREAD_LOCAL CipStats cip_stats; + +static const PegInfo cip_pegs[] = +{ + { CountType::SUM, "packets", "total packets" }, + { CountType::SUM, "session", "total sessions" }, + { CountType::NOW, "concurrent_sessions", "total concurrent SIP sessions" }, + { CountType::MAX, "max_concurrent_sessions", "maximum concurrent SIP sessions" }, + { CountType::END, nullptr, nullptr }, +}; + +//------------------------------------------------------------------------- +// cip module +//------------------------------------------------------------------------- + +CipModule::CipModule() : Module(CIP_NAME, CIP_HELP, c_params) +{ + conf = nullptr; +} + +CipModule::~CipModule() +{ + if ( conf ) + delete conf; +} + +const RuleMap* CipModule::get_rules() const +{ return cip_rules; } + +const PegInfo* CipModule::get_pegs() const +{ return cip_pegs; } + +PegCount* CipModule::get_counts() const +{ return (PegCount*)&cip_stats; } + +ProfileStats* CipModule::get_profile() const +{ return &cip_perf_stats; } + +bool CipModule::set(const char*, Value& v, SnortConfig*) +{ + if ( v.is("embedded_cip_path") ) + { + conf->embedded_cip_enabled = true; + embedded_path = v.get_string(); + } + else if ( v.is("unconnected_timeout") ) + conf->unconnected_timeout = v.get_uint32(); + + else if ( v.is("max_cip_connections") ) + conf->max_cip_connections = v.get_uint32(); + + else if ( v.is("max_unconnected_messages") ) + conf->max_unconnected_messages = v.get_uint32(); + + else + return false; + + return true; +} + +CipProtoConf* CipModule::get_data() +{ + CipProtoConf* tmp = conf; + conf = nullptr; + return tmp; +} + +bool CipModule::begin(const char*, int, SnortConfig*) +{ + assert(!conf); + conf = new CipProtoConf; + + conf->embedded_cip_enabled = false; + + return true; +} + +bool CipModule::end(const char*, int, SnortConfig*) +{ + Value v(embedded_path.c_str()); + std::string tok; + v.set_first_token(); + + if ( v.get_next_token(tok) ) + { + conf->embedded_cip_class_id = static_cast(::strtol(tok.c_str(), nullptr, 0)); + } + + if (v.get_next_token(tok) ) + { + conf->embedded_cip_service_id = static_cast(::strtol(tok.c_str(), nullptr, 0)); + } + + return true; +} + diff --git a/src/service_inspectors/cip/cip_module.h b/src/service_inspectors/cip/cip_module.h new file mode 100644 index 000000000..52eb5a935 --- /dev/null +++ b/src/service_inspectors/cip/cip_module.h @@ -0,0 +1,71 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_module.h author Jian Wu + +#ifndef CIP_MODULE_H +#define CIP_MODULE_H + +// Interface to the CIP service inspector + +#include "framework/module.h" + +#include "cip_definitions.h" + +#define GID_CIP 148 + +#define CIP_MALFORMED 1 +#define CIP_NON_CONFORMING 2 +#define CIP_CONNECTION_LIMIT 3 +#define CIP_REQUEST_LIMIT 4 + +#define CIP_NAME "cip" +#define CIP_HELP "cip inspection" + +extern THREAD_LOCAL snort::ProfileStats cip_perf_stats; + +class CipModule : public snort::Module +{ +public: + CipModule(); + ~CipModule() override; + + bool set(const char*, snort::Value&, snort::SnortConfig*) override; + bool begin(const char*, int, snort::SnortConfig*) override; + bool end(const char*, int, snort::SnortConfig*) override; + + unsigned get_gid() const override + { return GID_CIP; } + + const snort::RuleMap* get_rules() const override; + const PegInfo* get_pegs() const override; + PegCount* get_counts() const override; + snort::ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return INSPECT; } + + CipProtoConf* get_data(); + +private: + CipProtoConf* conf; + std::string embedded_path; +}; + +#endif + diff --git a/src/service_inspectors/cip/cip_paf.cc b/src/service_inspectors/cip/cip_paf.cc new file mode 100644 index 000000000..712f1b9d3 --- /dev/null +++ b/src/service_inspectors/cip/cip_paf.cc @@ -0,0 +1,104 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_paf.cc author RA/Cisco + +/* Description: Protocol-Aware Flushing (PAF) code for the CIP preprocessor.*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "cip_paf.h" +#include "cip_parsing.h" // For ENIP constants + +// PAF will skip over the ENIP Command and Length fields. +static const uint16_t ENIP_PAF_FIELD_SIZE = 4; + +using namespace snort; + +/* Function: CIPPaf() + + Purpose: CIP PAF callback. + Statefully inspects CIP traffic from the start of a session. + Reads up until the length octet is found, then sets a flush point. + The flushed PDU is a ENIP frame. +*/ + +static StreamSplitter::Status cip_paf(cip_paf_data* pafdata, const uint8_t* data, + uint32_t len, uint32_t* fp) +{ + uint32_t bytes_processed = 0; + + /* Process this packet 1 byte at a time */ + while (bytes_processed < len) + { + switch (pafdata->paf_state) + { + case CIP_PAF_STATE__COMMAND_1: + // Skip ENIP command. + pafdata->paf_state = CIP_PAF_STATE__COMMAND_2; + break; + + case CIP_PAF_STATE__COMMAND_2: + // Skip ENIP command. + pafdata->paf_state = CIP_PAF_STATE__LENGTH_1; + break; + + case CIP_PAF_STATE__LENGTH_1: + pafdata->enip_length = *(data + bytes_processed); + pafdata->paf_state = CIP_PAF_STATE__LENGTH_2; + break; + + case CIP_PAF_STATE__LENGTH_2: + pafdata->enip_length |= (*(data + bytes_processed) << 8); + pafdata->paf_state = CIP_PAF_STATE__SET_FLUSH; + break; + + case CIP_PAF_STATE__SET_FLUSH: + *fp = bytes_processed + + pafdata->enip_length + (ENIP_HEADER_SIZE - ENIP_PAF_FIELD_SIZE); + + pafdata->paf_state = CIP_PAF_STATE__COMMAND_1; + return StreamSplitter::FLUSH; + + default: + // Will not happen. + break; + } + + bytes_processed++; + } + + return StreamSplitter::SEARCH; +} + +CipSplitter::CipSplitter(bool c2s) : StreamSplitter(c2s) +{ + state.paf_state = CIP_PAF_STATE__COMMAND_1; + state.enip_length = 0; +} + +StreamSplitter::Status CipSplitter::scan( + Packet*, const uint8_t* data, uint32_t len, + uint32_t, uint32_t* fp) +{ + cip_paf_data* pfdata = &state; + return cip_paf(pfdata, data, len, fp); +} + diff --git a/src/service_inspectors/cip/cip_paf.h b/src/service_inspectors/cip/cip_paf.h new file mode 100644 index 000000000..12d522dda --- /dev/null +++ b/src/service_inspectors/cip/cip_paf.h @@ -0,0 +1,64 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_paf.h author RA/Cisco + +/* Description: Protocol-Aware Flushing (PAF) code for the CIP preprocessor. */ + +#ifndef CIP_PAF_H +#define CIP_PAF_H + +#include "stream/stream_splitter.h" + +#include "cip.h" + +/* State-tracking structs */ +enum cip_paf_state +{ + CIP_PAF_STATE__COMMAND_1 = 0, + CIP_PAF_STATE__COMMAND_2, + CIP_PAF_STATE__LENGTH_1, + CIP_PAF_STATE__LENGTH_2, + CIP_PAF_STATE__SET_FLUSH +}; + +struct cip_paf_data +{ + cip_paf_state paf_state; + uint16_t enip_length; +}; + +class CipSplitter : public snort::StreamSplitter +{ +public: + CipSplitter(bool c2s); + + Status scan(snort::Packet*, const uint8_t* data, uint32_t len, uint32_t flags, + uint32_t* fp) override; + + bool is_paf() override + { + return true; + } + +public: + cip_paf_data state; +}; + +#endif /* CIP_PAF_H */ + diff --git a/src/service_inspectors/cip/cip_parsing.cc b/src/service_inspectors/cip/cip_parsing.cc new file mode 100644 index 000000000..67034740a --- /dev/null +++ b/src/service_inspectors/cip/cip_parsing.cc @@ -0,0 +1,2107 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_parsing.cc author RA/Cisco + +/* Description: Data parsing for EtherNet/IP and CIP formats. + Note: No pointer parameters to these functions can be passed as NULL. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "cip_parsing.h" + +#include "framework/data_bus.h" + +#include "cip.h" +#include "cip_session.h" // For CIP connection tracking +#include "cip_util.h" // For GetLEUint16/32 + +using namespace snort; + +/// EtherNet/IP Parsing Constants + +// Common Packet Format Item IDs. +enum CpfItemId +{ + CPF_NULL_ADDRESS_ITEM_ID = 0x0000, + CPF_LIST_IDENTITY_ITEM_ID = 0x000C, + CPF_CONNECTED_ADDRESS_ITEM_ID = 0x00A1, + CPF_CONNECTED_DATA_ITEM_ID = 0x00B1, + CPF_UNCONNECTED_DATA_ITEM_ID = 0x00B2, + CPF_LIST_SERVICES_ITEM_ID = 0x0100, + CPF_SOCKADDR_INFO_OT_ITEM_ID = 0x8000, + CPF_SOCKADDR_INFO_TO_ITEM_ID = 0x8001, + CPF_SEQUENCED_ADDRESS_ITEM_ID = 0x8002 +}; + +#define CPF_ADDRESS_ITEM_SLOT 0 +#define CPF_DATA_ITEM_SLOT 1 +#define CPF_LIST_REPLY_SLOT 0 + +// Some ENIP command ranges are reserved for future range. +#define ENIP_COMMAND_RESERVED1_START 0x0006 +#define ENIP_COMMAND_RESERVED1_END 0x0062 +#define ENIP_COMMAND_RESERVED2_START 0x00C8 + +// Some CPF Item IDs are reserved for future range. +#define ENIP_CPF_ITEM_RESERVED1_START 0x0086 +#define ENIP_CPF_ITEM_RESERVED1_END 0x0090 +#define ENIP_CPF_ITEM_RESERVED2_START 0x0092 +#define ENIP_CPF_ITEM_RESERVED2_END 0x00A0 +#define ENIP_CPF_ITEM_RESERVED3_START 0x00A5 +#define ENIP_CPF_ITEM_RESERVED3_END 0x00B0 +#define ENIP_CPF_ITEM_RESERVED4_START 0x00B3 +#define ENIP_CPF_ITEM_RESERVED4_END 0x00FF +#define ENIP_CPF_ITEM_RESERVED5_START 0x0110 +#define ENIP_CPF_ITEM_RESERVED5_END 0x7FFF +#define ENIP_CPF_ITEM_RESERVED6_START 0x8004 + +#define REGISTER_SESSION_DATA_SIZE 4 + +/// CIP Layer Parsing Constants +const size_t CIP_PATH_SEGMENT_MIN_SIZE_BYTES = sizeof(uint16_t); + +// Typical payload size offset in CIP segment +#define CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET 1 + +// Offset to Segment Type/Format byte +#define CIP_PATH_TYPE_OFFSET 0 + +// Logical segment format mask and types. +#define CIP_PATH_LOGICAL_FORMAT_MASK 0x03 + +enum LogicalValueType +{ + CIP_PATH_LOGICAL_8_BIT = 0x00, + CIP_PATH_LOGICAL_16_BIT = 0x01, + CIP_PATH_LOGICAL_32_BIT = 0x02 +}; + +// Logical segment types +enum LogicalSegmentType +{ + CIP_PATH_LOGICAL_CLASS = 0x00, + CIP_PATH_LOGICAL_INSTANCE = 0x04, + CIP_PATH_LOGICAL_MEMBER = 0x08, + CIP_PATH_LOGICAL_CONN_POINT = 0x0c, + CIP_PATH_LOGICAL_ATTRIBUTE = 0x10, + CIP_PATH_LOGICAL_SPECIAL = 0x14, + CIP_PATH_LOGICAL_SERVICE_ID = 0x18, + CIP_PATH_LOGICAL_EXTENDED = 0x1C +}; + +enum SegmentType +{ + CIP_PATH_SEGMENT_PORT = 0x00, + CIP_PATH_SEGMENT_LOGICAL = 0x20, + CIP_PATH_SEGMENT_NETWORK = 0x40, + CIP_PATH_SEGMENT_SYMBOLIC = 0x60, + CIP_PATH_SEGMENT_DATA = 0x80 +}; + +enum ExtendedStringType +{ + EXTENDED_STRING_DOUBLE = 0x20, + EXTENDED_STRING_TRIPLE = 0x40, + EXTENDED_STRING_NUMERIC = 0xC0 +}; + +#define MESSAGE_ROUTER_RESPONSE_MASK 0x80 + +#define CIP_STATUS_MIN_SIZE 2 + +/// Prototypes +static bool parse_logical_address_format(const uint8_t* data, + size_t data_length, + bool logical_extended, + CipSegment* segment); + +static bool parse_message_router_request(const uint8_t* data, + size_t data_length, + CipRequest* cip_request, + CipGlobalSessionData* global_data); + +/// Functions +static bool enip_command_valid(uint16_t command) +{ + if ((ENIP_COMMAND_RESERVED1_START <= command && command <= ENIP_COMMAND_RESERVED1_END) + || (ENIP_COMMAND_RESERVED2_START <= command)) + { + return false; + } + + return true; +} + +static bool enip_command_tcp_only(uint16_t command) +{ + // Allocated command codes. + if (command == ENIP_COMMAND_NOP + || command == ENIP_COMMAND_REGISTER_SESSION + || command == ENIP_COMMAND_UNREGISTER_SESSION + || command == ENIP_COMMAND_SEND_RR_DATA + || command == ENIP_COMMAND_SEND_UNIT_DATA) + { + return true; + } + + return false; +} + +static bool parse_enip_header(const uint8_t* data, + size_t data_length, + EnipSessionData* enip_session) +{ + EnipHeader* enip_header = &enip_session->enip_header; + + if (data_length < ENIP_HEADER_SIZE) + { + return false; + } + + #define ENIP_HEADER_OFFSET_COMMAND 0 + #define ENIP_HEADER_OFFSET_LENGTH 2 + #define ENIP_HEADER_OFFSET_HANDLE 4 + #define ENIP_HEADER_OFFSET_STATUS 8 + #define ENIP_HEADER_OFFSET_CONTEXT 12 + #define ENIP_HEADER_OFFSET_OPTIONS 20 + + enip_header->command = GetLEUint16(&data[ENIP_HEADER_OFFSET_COMMAND]); + enip_header->length = GetLEUint16(&data[ENIP_HEADER_OFFSET_LENGTH]); + enip_header->session_handle = GetLEUint32(&data[ENIP_HEADER_OFFSET_HANDLE]); + enip_header->status = GetLEUint32(&data[ENIP_HEADER_OFFSET_STATUS]); + memcpy(&enip_header->sender_context, + &data[ENIP_HEADER_OFFSET_CONTEXT], + sizeof(enip_header->sender_context)); + enip_header->options = GetLEUint32(&data[ENIP_HEADER_OFFSET_OPTIONS]); + + if (!enip_command_valid(enip_header->command)) + { + enip_session->enip_invalid_nonfatal |= ENIP_INVALID_COMMAND; + } + + return true; +} + +static bool cpf_item_id_valid(uint16_t item_id) +{ + if ((ENIP_CPF_ITEM_RESERVED1_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED1_END) + || (ENIP_CPF_ITEM_RESERVED2_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED2_END) + || (ENIP_CPF_ITEM_RESERVED3_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED3_END) + || (ENIP_CPF_ITEM_RESERVED4_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED4_END) + || (ENIP_CPF_ITEM_RESERVED5_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED5_END) + || (ENIP_CPF_ITEM_RESERVED6_START <= item_id)) + { + return false; + } + + return true; +} + +static bool cpf_item_length_valid(uint16_t item_id, size_t item_length) +{ + #define NULL_ADDRESS_ITEM_DATA_SIZE 0 + #define CONNECTED_ADDRESS_ITEM_DATA_SIZE 4 + #define SOCKADDR_INFO_ITEM_DATA_SIZE 16 + #define SEQUENCED_ADDRESS_ITEM_DATA_SIZE 8 + + // Minimum data size for Connected and Unconnected Data Items when used with + // CIP Class 3 / Explicit data. + #define MIN_CPF_CIP_DATA_SIZE 2 + + bool valid = true; + + switch (item_id) + { + case CPF_NULL_ADDRESS_ITEM_ID: + if (item_length != NULL_ADDRESS_ITEM_DATA_SIZE) + { + valid = false; + } + break; + case CPF_CONNECTED_ADDRESS_ITEM_ID: + if (item_length != CONNECTED_ADDRESS_ITEM_DATA_SIZE) + { + valid = false; + } + break; + case CPF_CONNECTED_DATA_ITEM_ID: + if (item_length < MIN_CPF_CIP_DATA_SIZE) + { + valid = false; + } + break; + case CPF_UNCONNECTED_DATA_ITEM_ID: + if (item_length < MIN_CPF_CIP_DATA_SIZE) + { + valid = false; + } + break; + case CPF_SOCKADDR_INFO_OT_ITEM_ID: + case CPF_SOCKADDR_INFO_TO_ITEM_ID: + if (item_length != SOCKADDR_INFO_ITEM_DATA_SIZE) + { + valid = false; + } + break; + case CPF_SEQUENCED_ADDRESS_ITEM_ID: + if (item_length != SEQUENCED_ADDRESS_ITEM_DATA_SIZE) + { + valid = false; + } + break; + case CPF_LIST_IDENTITY_ITEM_ID: + case CPF_LIST_SERVICES_ITEM_ID: + default: + // No length checks for anything else. + break; + } + + return valid; +} + +static bool enip_command_cpf_valid(uint16_t command, const EnipCpf* enip_cpf) +{ + #define MIN_CPF_ITEMS_CIP_MESSAGE 2 + #define MIN_CPF_ITEMS_LIST_REPLY 1 + + bool valid = true; + + switch (command) + { + case ENIP_COMMAND_SEND_RR_DATA: + if (enip_cpf->item_count < MIN_CPF_ITEMS_CIP_MESSAGE + || enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].type != CPF_NULL_ADDRESS_ITEM_ID + || enip_cpf->item_list[CPF_DATA_ITEM_SLOT].type != CPF_UNCONNECTED_DATA_ITEM_ID) + { + valid = false; + } + break; + case ENIP_COMMAND_SEND_UNIT_DATA: + if (enip_cpf->item_count != MIN_CPF_ITEMS_CIP_MESSAGE + || enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].type != CPF_CONNECTED_ADDRESS_ITEM_ID + || enip_cpf->item_list[CPF_DATA_ITEM_SLOT].type != CPF_CONNECTED_DATA_ITEM_ID) + { + valid = false; + } + break; + case ENIP_COMMAND_LIST_SERVICES: + // Used in Reply only. + if (enip_cpf->item_count < MIN_CPF_ITEMS_LIST_REPLY + || enip_cpf->item_list[CPF_LIST_REPLY_SLOT].type != CPF_LIST_SERVICES_ITEM_ID) + { + valid = false; + } + break; + case ENIP_COMMAND_LIST_IDENTITY: + // Used in Reply only. + if (enip_cpf->item_count < MIN_CPF_ITEMS_LIST_REPLY + || enip_cpf->item_list[CPF_LIST_REPLY_SLOT].type != CPF_LIST_IDENTITY_ITEM_ID) + { + valid = false; + } + break; + default: + // Ignore commands without defined CPF items. + break; + } + + return valid; +} + +// Returns the CIP message type based on packet and session data. The data must already: +// 1. Be ENIP_COMMAND_SEND_UNIT_DATA or ENIP_COMMAND_SEND_RR_DATA +// 2. Have the required CPF items for that ENIP command. +// This also saves connection related data for the given packet and updates connection timestamps. +static CipMessageType get_cip_message_type(CipCurrentData* current_data, + CipGlobalSessionData* global_data) +{ + CipMessageType cip_message_type = CipMessageTypeUnknown; + + if (current_data->enip_data.enip_header.command == ENIP_COMMAND_SEND_RR_DATA) + { + cip_message_type = CipMessageTypeExplicit; + } + else // ENIP_COMMAND_SEND_UNIT_DATA + { + const EnipCpf* enip_cpf = ¤t_data->enip_data.enip_cpf; + + if (enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].length > 0) + { + uint32_t connection_id = GetLEUint32(enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].data); + + // Validate connected messages against CIP Connection List. + CipConnection* connection = cip_find_connection_by_id( + &global_data->connection_list, + current_data->direction, + connection_id, + true); + if (connection) + { + if (current_data->direction == CIP_FROM_CLIENT) + { + connection->ot_timestamp = global_data->snort_packet->pkth->ts; + } + else + { + connection->to_timestamp = global_data->snort_packet->pkth->ts; + } + + current_data->enip_data.connection_class_id = connection->class_id; + + if (connection->class_id == MESSAGE_ROUTER_CLASS_ID) + { + cip_message_type = CipMessageTypeExplicit; + } + else + { + cip_message_type = CipMessageTypeImplicit; + } + } + else + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_CONNECTION_ID; + cip_message_type = CipMessageTypeUnknown; + } + } + } + + return cip_message_type; +} + +static bool parse_common_packet_format(const uint8_t* data, + size_t data_length, + EnipCpf* enip_cpf, + CipCurrentData* current_data) +{ + // The total item count is always first. + #define CPF_ITEM_COUNT_SIZE 2 + if (data_length < CPF_ITEM_COUNT_SIZE) + { + return false; + } + + #define CPF_OFFSET_ITEM_COUNT 0 + #define CPF_ITEM_OFFSET_TYPE 0 + #define CPF_ITEM_OFFSET_LENGTH 2 + #define CPF_ITEM_OFFSET_DATA 4 + + enip_cpf->item_count = GetLEUint16(&data[CPF_OFFSET_ITEM_COUNT]); + data_length -= CPF_ITEM_COUNT_SIZE; + + bool valid = true; + + size_t current_item_offset = CPF_ITEM_COUNT_SIZE; + + int i; + for (i = 0; i < enip_cpf->item_count; ++i) + { + uint16_t item_type; + uint16_t item_length; + const uint8_t* item_data; + /* This contains Type ID and Length. */ + #define CPF_ITEM_HEADER_SIZE 4 + if (data_length < CPF_ITEM_HEADER_SIZE) + { + valid = false; + break; + } + + item_type = GetLEUint16(&data[current_item_offset + CPF_ITEM_OFFSET_TYPE]); + item_length = GetLEUint16(&data[current_item_offset + CPF_ITEM_OFFSET_LENGTH]); + item_data = nullptr; + if (item_length > 0) + { + item_data = &data[current_item_offset + CPF_ITEM_OFFSET_DATA]; + } + + data_length -= CPF_ITEM_HEADER_SIZE; + + if (!cpf_item_id_valid(item_type)) + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_RESERVED_FUTURE_CPF_TYPE; + } + + if (!cpf_item_length_valid(item_type, item_length)) + { + valid = false; + break; + } + + // Check that there is enough data left for the Item Length. + if (data_length < item_length) + { + valid = false; + break; + } + + // Validate every CPF item, but only store data for a set amount. + if (i < MAX_NUM_CPF_ITEMS) + { + enip_cpf->item_list[i].type = item_type; + enip_cpf->item_list[i].length = item_length; + enip_cpf->item_list[i].data = item_data; + } + + // Get data for the next item. + current_item_offset = current_item_offset + CPF_ITEM_HEADER_SIZE + item_length; + data_length -= item_length; + } + + if (valid) + { + current_data->enip_data.required_cpf_items_present + = enip_command_cpf_valid(current_data->enip_data.enip_header.command, enip_cpf); + if (!current_data->enip_data.required_cpf_items_present) + { + current_data->enip_data.enip_invalid_nonfatal |= + ENIP_INVALID_ENIP_COMMAND_CPF_MISMATCH; + } + } + + return valid; +} + +// If there in an unknown segment type, then just set this segment to include +// all of the data left. +static void set_unknown_segment_type(size_t data_length, + CipSegment* segment) +{ + segment->type = CipSegment_Type_UNKNOWN; + segment->size = data_length; +} + +// Return the timeout, in milliseconds, based on the priority/time_tick and time-out_ticks fields +// that are common to: Unconnected Send, Forward Open, Forward Close. +// This requires that enough data is available to read 2 bytes. +static uint32_t get_unconnected_timeout(const uint8_t* data) +{ + #define UNCONNECTED_OFFSET_PRIORITY_TIME_TICK 0 + #define UNCONNECTED_OFFSET_TIMEOUT_TICKS 1 + #define TICK_TIME_MASK 0xF + + uint8_t tick_time = data[UNCONNECTED_OFFSET_PRIORITY_TIME_TICK] & TICK_TIME_MASK; + uint8_t timeout_ticks = data[UNCONNECTED_OFFSET_TIMEOUT_TICKS]; + + return (1 << tick_time) * timeout_ticks; +} + +// Parses RPI and Network Connection Parameters from a Forward Open Request. +// Note: Assumes there is enough data to parse the RPI and Network Connection Parameters. +static void parse_connection_parameters(const uint8_t* data, + bool large_forward_open, + CipConnectionParameters* connection_parameters) +{ + #define NULL_CONNECTION_TYPE_MASK 0x6000 + static const uint32_t LARGE_NULL_CONNECTION_TYPE_MASK = 0x60000000; + + // Offsets to the RPI and Network Connection Parameters data, from the RPI data. + #define OFFSET_RPI 0 + #define OFFSET_NETWORK_PARAMETERS 4 + + connection_parameters->rpi = GetLEUint32(&data[OFFSET_RPI]); + + if (!large_forward_open) + { + uint16_t network_connection_parameters = GetLEUint16(&data[OFFSET_NETWORK_PARAMETERS]); + connection_parameters->network_connection_parameters = network_connection_parameters; + + if ((network_connection_parameters & NULL_CONNECTION_TYPE_MASK) == 0) + { + connection_parameters->is_null_connection = true; + } + } + else // SERVICE_LARGE_FORWARD_OPEN + { + uint32_t network_connection_parameters = GetLEUint32(&data[OFFSET_NETWORK_PARAMETERS]); + connection_parameters->network_connection_parameters = network_connection_parameters; + + if ((network_connection_parameters & LARGE_NULL_CONNECTION_TYPE_MASK) == 0) + { + connection_parameters->is_null_connection = true; + } + } +} + +// Note: Assumes there is enough data for a full Connection Signature. +static void parse_connection_signature(const uint8_t* data, + CipConnectionSignature* connection_signature) +{ + #define OFFSET_CONNECTION_SERIAL 0 + #define OFFSET_VENDOR 2 + #define OFFSET_ORIGINATOR_SERIAL 4 + + connection_signature->connection_serial_number = GetLEUint16(&data[OFFSET_CONNECTION_SERIAL]); + connection_signature->vendor_id = GetLEUint16(&data[OFFSET_VENDOR]); + connection_signature->originator_serial_number = GetLEUint32(&data[OFFSET_ORIGINATOR_SERIAL]); +} + +static bool parse_segment_electronic_key(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + #define ELECTRONIC_KEY_FORMAT_TABLE 0x04 + #define ELECTRONIC_KEY_FORMAT_TABLE_SIZE 10 + + // Check that there is enough size for the Key Format Table. + if (ELECTRONIC_KEY_FORMAT_TABLE_SIZE > data_length) + { + return false; + } + + // Currently, the only supported Key Format is the Key Format Table. + #define ELECTRONIC_KEY_OFFSET_FORMAT_TABLE 1 + if (data[ELECTRONIC_KEY_OFFSET_FORMAT_TABLE] != ELECTRONIC_KEY_FORMAT_TABLE) + { + return false; + } + + segment->type = CipSegment_Type_LOGICAL_ELECTRONIC_KEY; + segment->size = ELECTRONIC_KEY_FORMAT_TABLE_SIZE; + + return true; +} + +static bool parse_segment_extended_symbol(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + size_t symbol_size_bytes; + size_t segment_size_bytes; + symbol_size_bytes = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET]; + + // calculate expected size + segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + symbol_size_bytes; + + // add padding + segment_size_bytes += segment_size_bytes % 2; + + // Exit early, if we know we won't fit. + if (segment_size_bytes > data_length) + { + return false; + } + + segment->type = CipSegment_Type_DATA_EXT_SYMBOL; + segment->data = &data[CIP_PATH_SEGMENT_MIN_SIZE_BYTES]; + segment->data_size = symbol_size_bytes; + segment->size = segment_size_bytes; + + return true; +} + +static bool parse_segment_logical(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET]; + + // parse particular logical type + bool valid = true; + #define CIP_PATH_LOGICAL_TYPE_MASK 0x1C + switch (segment_type & CIP_PATH_LOGICAL_TYPE_MASK) + { + case CIP_PATH_LOGICAL_CLASS: + segment->type = CipSegment_Type_LOGICAL_CLASS; + valid = parse_logical_address_format(data, data_length, false, segment); + break; + case CIP_PATH_LOGICAL_INSTANCE: + segment->type = CipSegment_Type_LOGICAL_INSTANCE; + valid = parse_logical_address_format(data, data_length, false, segment); + break; + case CIP_PATH_LOGICAL_MEMBER: + segment->type = CipSegment_Type_LOGICAL_MEMBER; + valid = parse_logical_address_format(data, data_length, false, segment); + break; + case CIP_PATH_LOGICAL_CONN_POINT: + segment->type = CipSegment_Type_LOGICAL_CONN_POINT; + valid = parse_logical_address_format(data, data_length, false, segment); + break; + case CIP_PATH_LOGICAL_ATTRIBUTE: + segment->type = CipSegment_Type_LOGICAL_ATTRIBUTE; + valid = parse_logical_address_format(data, data_length, false, segment); + break; + case CIP_PATH_LOGICAL_EXTENDED: + segment->type = CipSegment_Type_LOGICAL_EXTENDED; + valid = parse_logical_address_format(data, data_length, true, segment); + break; + case CIP_PATH_LOGICAL_SPECIAL: + { + // Logical Segment Electronic Key Logical Format. + #define CIP_PATH_SEGMENT_ELECTRONIC_KEY 0x34 + + if (segment_type == CIP_PATH_SEGMENT_ELECTRONIC_KEY) + { + valid = parse_segment_electronic_key(data, + data_length, + segment); + } + else + { + set_unknown_segment_type(data_length, segment); + } + + break; + } + case CIP_PATH_LOGICAL_SERVICE_ID: + { + #define CIP_PATH_SEGMENT_SERVICE_ID 0x38 + if (segment_type == CIP_PATH_SEGMENT_SERVICE_ID) + { + segment->type = CipSegment_Type_LOGICAL_SERVICE_ID; + valid = parse_logical_address_format(data, data_length, false, segment); + } + else + { + set_unknown_segment_type(data_length, segment); + } + + break; + } + default: + // Can't happen. + set_unknown_segment_type(data_length, segment); + break; + } + + return valid; +} + +static bool parse_segment_network(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + #define CIP_PATH_NETWORK_FORMAT_MASK 0xF0 + #define CIP_PATH_NETWORK_ONE_BYTE 0x40 + + size_t segment_size_bytes = 0; + + uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET]; + uint8_t network_segment_type = segment_type & CIP_PATH_NETWORK_FORMAT_MASK; + if (network_segment_type == CIP_PATH_NETWORK_ONE_BYTE) + { + segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES; + } + else // Variable length network segment (0x50) + { + size_t data_size_bytes = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET] * CIP_WORD_TO_BYTES; + segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + data_size_bytes; + if (segment_size_bytes > data_length) + { + return false; + } + } + + segment->type = CipSegment_Type_NETWORK; + segment->size = segment_size_bytes; + + return true; +} + +static bool parse_segment_port(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET]; + + // set minimal expected segment size + size_t segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES; + + // port segment extended port threshold + #define CIP_PATH_PORT_EXTENDED 0x0F + + // calculate simple port (extended port is also a mask) + uint16_t port_number = segment_type & CIP_PATH_PORT_EXTENDED; + + bool is_port_extended = port_number == CIP_PATH_PORT_EXTENDED; + + #define CIP_PATH_PORT_EXTENDED_LINK_ADDRESS_MASK 0x10 + bool is_long_address = (segment_type & CIP_PATH_PORT_EXTENDED_LINK_ADDRESS_MASK) != 0; + + if (is_long_address) + { + // add length of address + segment_size_bytes += data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET]; + + // add padding + segment_size_bytes += segment_size_bytes % 2; + } + + if (is_port_extended) + { + // add length of extended port + segment_size_bytes += sizeof(uint16_t); + } + + // Exit early, if we know we won't fit. + if (segment_size_bytes > data_length) + { + return false; + } + + if (is_port_extended) + { + size_t extended_port_offset = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET; + + if (is_long_address) + { + extended_port_offset += sizeof(uint8_t); + } + + port_number = GetLEUint16(&data[extended_port_offset]); + } + + segment->port_id = port_number; + + if (!is_long_address) + { + size_t link_address_offset = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET; + + if (is_port_extended) + { + link_address_offset += sizeof(uint16_t); + } + + segment->type = CipSegment_Type_PORT_LINK_ADDRESS; + segment->link_address = data[link_address_offset]; + } + else + { + size_t link_address_offset = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET + sizeof(uint8_t); + + if (is_port_extended) + { + link_address_offset += sizeof(uint16_t); + } + + segment->type = CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED; + segment->data = &data[link_address_offset]; + segment->data_size = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET]; + } + + segment->size = segment_size_bytes; + + return true; +} + +static bool parse_segment_simple_data(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + size_t data_size_bytes + = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET] * CIP_WORD_TO_BYTES; + + // calculate expected size + size_t segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + data_size_bytes; + + // Exit early, if we know we won't fit. + if (segment_size_bytes > data_length) + { + return false; + } + + segment->type = CipSegment_Type_DATA_SIMPLE; + segment->data = &data[CIP_PATH_SEGMENT_MIN_SIZE_BYTES]; + segment->data_size = data_size_bytes; + segment->size = segment_size_bytes; + + return true; +} + +static bool parse_segment_symbolic_extended_string(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + #define EXTENDED_STRING_SIZE_MASK 0x1F + #define EXTENDED_STRING_FORMAT_MASK 0xE0 + + #define NUMERIC_SYMBOL_USINT 6 + #define NUMERIC_SYMBOL_UINT 7 + #define NUMERIC_SYMBOL_UDINT 8 + + #define DOUBLE_BYTE 2 + #define TRIPLE_BYTE 3 + + uint8_t extended_format_byte = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET]; + uint8_t extended_format_size = extended_format_byte & EXTENDED_STRING_SIZE_MASK; + + bool valid = true; + size_t data_size = 0; + switch (extended_format_byte & EXTENDED_STRING_FORMAT_MASK) + { + case EXTENDED_STRING_DOUBLE: + data_size = extended_format_size * DOUBLE_BYTE; + break; + case EXTENDED_STRING_TRIPLE: + data_size = extended_format_size * TRIPLE_BYTE; + break; + case EXTENDED_STRING_NUMERIC: + if (extended_format_size == NUMERIC_SYMBOL_USINT) + { + data_size = sizeof(uint8_t); + } + else if (extended_format_size == NUMERIC_SYMBOL_UINT) + { + data_size = sizeof(uint16_t); + } + else if (extended_format_size == NUMERIC_SYMBOL_UDINT) + { + data_size = sizeof(uint32_t); + } + else + { + valid = false; + } + break; + default: + valid = false; + break; + } + + size_t segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + data_size; + + // Add padding. + segment_size_bytes += segment_size_bytes % 2; + + if (data_length < segment_size_bytes) + { + return false; + } + + segment->type = CipSegment_Type_SYMBOLIC; + segment->data = &data[CIP_PATH_SEGMENT_MIN_SIZE_BYTES]; + segment->data_size = data_size; + segment->size = segment_size_bytes; + + return valid; +} + +static bool parse_segment_symbolic(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + #define CIP_PATH_SYMBOLIC_SIZE_MASK 0x1F + + bool valid = true; + + uint8_t symbol_size_bytes = data[CIP_PATH_TYPE_OFFSET] & CIP_PATH_SYMBOLIC_SIZE_MASK; + if (symbol_size_bytes == 0) + { + valid = parse_segment_symbolic_extended_string(data, data_length, segment); + } + else // Size 1 - 31. + { + size_t expected_segment_size = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET + symbol_size_bytes; + + // Add padding + expected_segment_size += expected_segment_size % 2; + + if (expected_segment_size > data_length) + { + valid = false; + } + else + { + segment->type = CipSegment_Type_SYMBOLIC; + segment->data = &data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET]; + segment->data_size = symbol_size_bytes; + segment->size = expected_segment_size; + } + } + + return valid; +} + +static bool parse_cip_segment(const uint8_t* data, + size_t data_length, + CipSegment* segment) +{ + bool valid = true; + + #define CIP_PATH_SEGMENT_TYPE_MASK 0xE0 + + uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET]; + switch (segment_type & CIP_PATH_SEGMENT_TYPE_MASK) + { + case CIP_PATH_SEGMENT_PORT: + valid = parse_segment_port( + data, + data_length, + segment); + break; + case CIP_PATH_SEGMENT_LOGICAL: + valid = parse_segment_logical( + data, + data_length, + segment); + break; + case CIP_PATH_SEGMENT_DATA: + { + #define CIP_PATH_SEGMENT_SIMPLE_DATA 0x80 + #define CIP_PATH_SEGMENT_EXT_SYMBOL 0x91 + + if (segment_type == CIP_PATH_SEGMENT_EXT_SYMBOL) + { + valid = parse_segment_extended_symbol( + data, + data_length, + segment); + } + else if (segment_type == CIP_PATH_SEGMENT_SIMPLE_DATA) + { + valid = parse_segment_simple_data( + data, + data_length, + segment); + } + else + { + set_unknown_segment_type(data_length, segment); + } + break; + } + case CIP_PATH_SEGMENT_NETWORK: + valid = parse_segment_network(data, data_length, segment); + break; + case CIP_PATH_SEGMENT_SYMBOLIC: + valid = parse_segment_symbolic(data, data_length, segment); + break; + default: + set_unknown_segment_type(data_length, segment); + break; + } + + return valid; +} + +static bool parse_cip_segments(const uint8_t* data, + size_t data_length, + CipPath* path) +{ + bool valid = true; + + // Parse all CIP segments. + while (data_length > 0) + { + CipSegment segment; + /* Check that there is enough data to start. */ + if (data_length < CIP_PATH_SEGMENT_MIN_SIZE_BYTES) + { + valid = false; + break; + } + + memset(&segment, 0, sizeof(segment)); + if (!parse_cip_segment(data, data_length, &segment)) + { + valid = false; + break; + } + + // Save off key data in this segment for later use. + if (segment.type == CipSegment_Type_LOGICAL_CLASS) + { + path->has_class_id = true; + path->class_id = segment.logical_value; + + path->primary_segment_type = CipSegment_Type_LOGICAL_CLASS; + } + else if (segment.type == CipSegment_Type_DATA_EXT_SYMBOL) + { + path->primary_segment_type = CipSegment_Type_DATA_EXT_SYMBOL; + } + else if (segment.type == CipSegment_Type_LOGICAL_INSTANCE) + { + path->has_instance_id = true; + path->instance_id = segment.logical_value; + } + else if (segment.type == CipSegment_Type_LOGICAL_ATTRIBUTE) + { + path->has_attribute_id = true; + path->attribute_id = segment.logical_value; + } + else if (segment.type == CipSegment_Type_UNKNOWN) + { + path->has_unknown_segment = true; + } + + // Move to the next segment. + data_length -= segment.size; + data += segment.size; + } + + return valid; +} + +static bool parse_cip_epath(const uint8_t* data, + size_t data_length, + bool path_contains_reserved_byte, + CipPath* path) +{ + #define PATH_SIZE_FIELD_BYTES 1 + #define PATH_SIZE_OFFSET 0 + size_t path_size_bytes; + size_t path_header_size; + + // There is a size byte and optionally a padding byte before the actual path data. + path_header_size = PATH_SIZE_FIELD_BYTES; + if (path_contains_reserved_byte) + { + path_header_size++; + } + + // Validate/Get the Path Size. + if (data_length < path_header_size) + { + return false; + } + + path_size_bytes = data[PATH_SIZE_OFFSET] * CIP_WORD_TO_BYTES; + if (data_length - path_header_size < path_size_bytes) + { + return false; + } + + if (!parse_cip_segments(data + path_header_size, path_size_bytes, path)) + { + return false; + } + + path->full_path_size = path_header_size + path_size_bytes; + path->decoded = true; + + return true; +} + +// Parse the logical addressing format which is common to all logical segment +// types, except Special and Service ID. +static bool parse_logical_address_format(const uint8_t* data, + size_t data_length, + bool logical_extended, + CipSegment* segment) +{ + #define LOGICAL_8_BIT_SIZE 2 + #define LOGICAL_8_BIT_EXTENDED_SIZE 4 + #define LOGICAL_16_BIT_SIZE 4 + #define LOGICAL_32_BIT_SIZE 6 + #define LOGICAL_DEFAULT_DATA_OFFSET 2 + #define LOGICAL_8_BIT_DATA_OFFSET 1 + + uint32_t logical_value; + bool valid = true; + + uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET]; + + // Get the expected segment size and data offset. + size_t segment_size = 0; + size_t data_offset = LOGICAL_DEFAULT_DATA_OFFSET; + switch (segment_type & CIP_PATH_LOGICAL_FORMAT_MASK) + { + case CIP_PATH_LOGICAL_32_BIT: + segment_size = LOGICAL_32_BIT_SIZE; + break; + case CIP_PATH_LOGICAL_16_BIT: + segment_size = LOGICAL_16_BIT_SIZE; + break; + case CIP_PATH_LOGICAL_8_BIT: + if (logical_extended) + { + segment_size = LOGICAL_8_BIT_EXTENDED_SIZE; + } + else + { + segment_size = LOGICAL_8_BIT_SIZE; + data_offset = LOGICAL_8_BIT_DATA_OFFSET; + } + break; + default: + valid = false; + break; + } + + // Exit early, if we know we won't fit. + if (segment_size > data_length) + { + return false; + } + + // Get the logical value. + logical_value = 0; + switch (segment_type & CIP_PATH_LOGICAL_FORMAT_MASK) + { + case CIP_PATH_LOGICAL_32_BIT: + logical_value = GetLEUint32(&data[data_offset]); + break; + case CIP_PATH_LOGICAL_16_BIT: + logical_value = GetLEUint16(&data[data_offset]); + break; + case CIP_PATH_LOGICAL_8_BIT: + logical_value = data[data_offset]; + break; + default: + valid = false; + break; + } + + segment->logical_value = logical_value; + segment->size = segment_size; + + return valid; +} + +static bool parse_cip_status(const uint8_t* data, + size_t data_length, + CipStatus* status) +{ + if (data_length < CIP_STATUS_MIN_SIZE) + { + return false; + } + + #define CIP_STATUS_OFFSET_GEN_STATUS 0 + status->general_status = data[CIP_STATUS_OFFSET_GEN_STATUS]; + + #define CIP_STATUS_OFFSET_EXT_STATUS_SIZE 1 + status->extended_status_size = data[CIP_STATUS_OFFSET_EXT_STATUS_SIZE] * CIP_WORD_TO_BYTES; + + // extended status size does not fit the response + if (data_length < (CIP_STATUS_MIN_SIZE + status->extended_status_size)) + { + return false; + } + + return true; +} + +/// Forward Open/Close parsing. +static bool parse_forward_open_request(const uint8_t* data, + size_t data_length, + bool large_forward_open, + CipForwardOpenRequest* forward_open_request, + CipRequest* cip_request) +{ + // This includes all data up to, but not including, the Connection Path Size. + #define CIP_FORWARD_OPEN_PREFIX_SIZE 35 + #define CIP_LARGE_FORWARD_OPEN_PREFIX_SIZE 39 + #define FWD_OPEN_OFFSET_CONN_SIGNATURE 10 + #define DEFAULT_CONNECTION_TIMEOUT (10 * USEC_PER_SEC) + size_t forward_open_prefix_size; + + // Size of the common connection-related parameters fields. This includes + // the RPI and the Network Connection Parameters. + size_t connection_parameters_size; + size_t offset_to_parameters; + size_t offset_transport_type_trigger; + size_t offset_connection_path_size; + uint8_t connection_timeout_multiplier; + const bool NO_PATH_RESERVED_BYTE = false; + + if (large_forward_open) + { + connection_parameters_size = sizeof(uint32_t) + sizeof(uint32_t); + forward_open_prefix_size = CIP_LARGE_FORWARD_OPEN_PREFIX_SIZE; + } + else + { + connection_parameters_size = sizeof(uint32_t) + sizeof(uint16_t); + forward_open_prefix_size = CIP_FORWARD_OPEN_PREFIX_SIZE; + } + + // Ensure that there is enough data for the common part of a Forward Open. + if (data_length < forward_open_prefix_size) + { + return false; + } + data_length -= forward_open_prefix_size; + + #define FWD_OPEN_OFFSET_TIMEOUT_MULTIPLIER 18 + #define FWD_OPEN_OFFSET_OT_RPI 22 + offset_to_parameters = FWD_OPEN_OFFSET_OT_RPI + connection_parameters_size; + offset_transport_type_trigger = offset_to_parameters + connection_parameters_size; + offset_connection_path_size = offset_transport_type_trigger + 1; + + forward_open_request->timeout_ms = get_unconnected_timeout(data); + parse_connection_signature(&data[FWD_OPEN_OFFSET_CONN_SIGNATURE], + &forward_open_request->connection_signature); + parse_connection_parameters(&data[FWD_OPEN_OFFSET_OT_RPI], + large_forward_open, + &forward_open_request->ot_parameters); + parse_connection_parameters(&data[offset_to_parameters], + large_forward_open, + &forward_open_request->to_parameters); + + // Get the overall connection timeouts. + connection_timeout_multiplier = data[FWD_OPEN_OFFSET_TIMEOUT_MULTIPLIER]; + #define MULTIPLIER_DEFAULT 2 + #define MAX_TIMEOUT_MULTIPLIER 7 + if (connection_timeout_multiplier <= MAX_TIMEOUT_MULTIPLIER) + { + uint16_t actual_multiplier = 1 << (connection_timeout_multiplier + MULTIPLIER_DEFAULT); + forward_open_request->ot_connection_timeout_us + = forward_open_request->ot_parameters.rpi * actual_multiplier; + forward_open_request->to_connection_timeout_us + = forward_open_request->to_parameters.rpi * actual_multiplier; + } + else + { + cip_request->cip_req_invalid_nonfatal |= CIP_REQ_INVALID_TIMEOUT_MULTIPLIER; + forward_open_request->ot_connection_timeout_us = DEFAULT_CONNECTION_TIMEOUT; + forward_open_request->to_connection_timeout_us = DEFAULT_CONNECTION_TIMEOUT; + } + + if (forward_open_request->ot_parameters.is_null_connection + && forward_open_request->to_parameters.is_null_connection) + { + forward_open_request->is_null_forward_open = true; + } + + uint8_t transport_type_trigger = data[offset_transport_type_trigger]; + forward_open_request->transport_class = transport_type_trigger & TRANSPORT_CLASS_MASK; + + // Parse out the Connection Path. This is a variable length section. + bool valid = parse_cip_epath(&data[offset_connection_path_size], + data_length, + NO_PATH_RESERVED_BYTE, + &forward_open_request->connection_path); + + return valid; +} + +static bool parse_forward_open_response_success(const uint8_t* data, + size_t data_length, + CipForwardOpenResponse* forward_open_response) +{ + #define FWD_OPEN_OFFSET_CON_SIGNATURE 8 + #define CIP_FORWARD_OPEN_RESPONSE_PREFIX_SIZE 26 + if (data_length < CIP_FORWARD_OPEN_RESPONSE_PREFIX_SIZE) + { + return false; + } + + #define FWD_OPEN_OFFSET_OT_CONNECTION 0 + #define FWD_OPEN_OFFSET_TO_CONNECTION 4 + #define FWD_OPEN_OFFSET_REPLY_SIZE 24 + + forward_open_response->connection_pair.ot_connection_id + = GetLEUint32(&data[FWD_OPEN_OFFSET_OT_CONNECTION]); + forward_open_response->connection_pair.to_connection_id + = GetLEUint32(&data[FWD_OPEN_OFFSET_TO_CONNECTION]); + parse_connection_signature(&data[FWD_OPEN_OFFSET_CON_SIGNATURE], + &forward_open_response->connection_signature); + forward_open_response->application_reply_size + = data[FWD_OPEN_OFFSET_REPLY_SIZE] * CIP_WORD_TO_BYTES; + + data_length -= CIP_FORWARD_OPEN_RESPONSE_PREFIX_SIZE; + if (data_length < forward_open_response->application_reply_size) + { + return false; + } + + forward_open_response->success = true; + + return true; +} + +static bool parse_forward_open_response_fail(const uint8_t* data, + size_t data_length, + CipForwardOpenResponse* forward_open_response) +{ + #define CIP_FORWARD_OPEN_RESPONSE_FAIL_SIZE 10 + if (data_length < CIP_FORWARD_OPEN_RESPONSE_FAIL_SIZE) + { + return false; + } + + parse_connection_signature(data, &forward_open_response->connection_signature); + + forward_open_response->success = false; + + return true; +} + +static bool parse_forward_open_response(const uint8_t* data, + size_t data_length, + uint8_t response_status, + CipForwardOpenResponse* forward_open_response) +{ + bool valid = true; + + // Forward Open Success and Failure cases have different formats. + if (response_status == CIP_STATUS_SUCCESS) + { + valid = parse_forward_open_response_success(data, data_length, forward_open_response); + } + else + { + valid = parse_forward_open_response_fail(data, data_length, forward_open_response); + } + + return valid; +} + +static bool parse_forward_close_request(const uint8_t* data, + size_t data_length, + CipForwardCloseRequest* forward_close_request) +{ + bool valid; + const bool PATH_RESERVED_BYTE = true; + #define CIP_FORWARD_CLOSE_PREFIX_SIZE 10 + if (data_length < CIP_FORWARD_CLOSE_PREFIX_SIZE) + { + return false; + } + + #define FWD_CLOSE_OFFSET_CONNECTION_SIGNATURE 2 + + forward_close_request->timeout_ms = get_unconnected_timeout(data); + parse_connection_signature(&data[FWD_CLOSE_OFFSET_CONNECTION_SIGNATURE], + &forward_close_request->connection_signature); + + // Parse out the Connection Path. This is a variable length section. + valid = parse_cip_epath(data + CIP_FORWARD_CLOSE_PREFIX_SIZE, + data_length - CIP_FORWARD_CLOSE_PREFIX_SIZE, + PATH_RESERVED_BYTE, + &forward_close_request->connection_path); + + return valid; +} + +// Returns size of the CIP Request Header that was parsed. +static bool parse_cip_request_header(const uint8_t* data, + size_t data_length, + size_t* header_size, + CipRequest* cip_request) +{ + bool valid; + CipPath* path; + const bool NO_PATH_RESERVED_BYTE = false; + #define CIP_SERVICE_SIZE 1 + if (data_length < CIP_SERVICE_SIZE) + { + return false; + } + + #define CIP_SERVICE_OFFSET 0 + cip_request->service = data[CIP_SERVICE_OFFSET]; + + // Reset all path information. + memset(&cip_request->request_path, 0, sizeof(cip_request->request_path)); + path = &cip_request->request_path; + + valid = parse_cip_epath(data + CIP_SERVICE_SIZE, + data_length - CIP_SERVICE_SIZE, + NO_PATH_RESERVED_BYTE, + path); + if (!valid) + { + return false; + } + + if (path->has_unknown_segment) + { + cip_request->cip_req_invalid_nonfatal |= CIP_REQ_INVALID_UNKNOWN_SEGMENT; + } + + *header_size = CIP_SERVICE_SIZE + path->full_path_size; + + return true; +} + +static size_t cip_status_size(const CipStatus* status) +{ + return CIP_STATUS_MIN_SIZE + status->extended_status_size; +} + +static bool parse_multiple_service_packet(const uint8_t* data, + size_t data_length, + CipRequest* cip_request, + CipGlobalSessionData* global_data) +{ + // Save the original data length for use in handling the offsets of embedded services. + uint16_t number_services; + size_t total_offset_size; + size_t data_offset; + size_t first_offset; + bool valid; + uint16_t i; + size_t original_data_length = data_length; + + // Check that the number of services will fit. + #define CIP_MSP_NUMBER_SERVICES_FIELD_SIZE 2 + if (data_length < CIP_MSP_NUMBER_SERVICES_FIELD_SIZE) + { + return false; + } + + #define CIP_MSP_OFFSET_NUMBER_SERVICES 0 + number_services = data[CIP_MSP_OFFSET_NUMBER_SERVICES]; + data_length -= CIP_MSP_NUMBER_SERVICES_FIELD_SIZE; + + // Check that the offsets will fit. + #define CIP_MSP_OFFSET_FIELD_SIZE 2 + total_offset_size = number_services * CIP_MSP_OFFSET_FIELD_SIZE; + if (data_length < total_offset_size) + { + return false; + } + + // Length of actual data left after the offsets. + data_length -= total_offset_size; + + // Check that offset data starts after the last offset. + data_offset = CIP_MSP_NUMBER_SERVICES_FIELD_SIZE + total_offset_size; + first_offset = GetLEUint16(&data[CIP_MSP_NUMBER_SERVICES_FIELD_SIZE]); + if (first_offset < data_offset) + { + return false; + } + + valid = true; + + // Process each embedded service. + for (i = 1; i <= number_services; ++i) + { + size_t msp_length; + CipRequest embedded_request; + CipEventData cip_event_data; + CipEvent cip_event(global_data->snort_packet, &cip_event_data); + + /* This if the offset from the Number of Services field, to the Offset field. */ + uint16_t buffer_offset = i * CIP_MSP_OFFSET_FIELD_SIZE; + + /* This if the offset from the Number of Services field to the data. */ + size_t msp_offset = GetLEUint16(&data[buffer_offset]); + + /* There is no end offset specified, so the next offset needs checked + to find the length of the current service. For the last packet, + this needs to use the total length of the Multiple Service Packet. */ + size_t msp_offset_end = 0; + if (i == number_services) + { + msp_offset_end = original_data_length; + } + else + { + uint16_t next_buffer_offset = buffer_offset + CIP_MSP_OFFSET_FIELD_SIZE; + msp_offset_end = GetLEUint16(&data[next_buffer_offset]); + } + + // Check that offsets are increasing. + if (msp_offset >= msp_offset_end) + { + valid = false; + break; + } + + // Check embedded length against the data size left. + msp_length = msp_offset_end - msp_offset; + if (data_length < msp_length) + { + valid = false; + break; + } + + data_length -= msp_length; + + memset(&embedded_request, 0, sizeof(embedded_request)); + if (!parse_message_router_request(data + msp_offset, + msp_length, + &embedded_request, + global_data)) + { + valid = false; + break; + } + + // Store embedded packet errors in the parent request. + cip_request->cip_req_invalid_nonfatal |= embedded_request.cip_req_invalid_nonfatal; + + // Publish embedded CIP data to appid. + memset(&cip_event_data, 0, sizeof(cip_event_data)); + + pack_cip_request_event(&embedded_request, &cip_event_data); + + DataBus::publish(CIP_EVENT_TYPE_CIP_DATA_KEY, cip_event, global_data->snort_packet->flow); + } + + return valid; +} + +static bool parse_unconnected_send_request(const uint8_t* data, + size_t data_length, + CipRequest* cip_request, + CipGlobalSessionData* global_data) +{ + bool valid; + uint16_t message_request_size; + const bool PATH_RESERVED_BYTE = true; + // This includes: Timeout data, embedded message size. + #define UNCONNECTED_SEND_HEADER_SIZE 4 + if (data_length < UNCONNECTED_SEND_HEADER_SIZE) + { + return false; + } + + cip_request->timeout_ms = get_unconnected_timeout(data); + cip_request->has_timeout = true; + + #define UNCONNECTED_SEND_OFFSET_MESSAGE_SIZE 2 + message_request_size = GetLEUint16(&data[UNCONNECTED_SEND_OFFSET_MESSAGE_SIZE]); + + data += UNCONNECTED_SEND_HEADER_SIZE; + data_length -= UNCONNECTED_SEND_HEADER_SIZE; + + // Verify that expected length of embedded request will fit in actual data. + if (message_request_size > data_length) + { + return false; + } + + if (!parse_message_router_request(data, message_request_size, cip_request, global_data)) + { + return false; + } + + // Parse the Route Path. + valid = parse_cip_epath(data + message_request_size, + data_length - message_request_size, + PATH_RESERVED_BYTE, + &cip_request->route_path); + + return valid; +} + +static bool parse_cip_command_specific_data_request(const uint8_t* data, + size_t data_length, + CipRequest* cip_request, + CipGlobalSessionData* global_data) +{ + const CipProtoConf* config; + /* If the request path doesn't have a Class ID, then we don't know how to + parse the response.*/ + if (!cip_request->request_path.has_class_id) + { + cip_request->request_type = CipRequestTypeOther; + return true; + } + + bool valid = true; + + uint8_t service = cip_request->service; + uint32_t class_id = cip_request->request_path.class_id; + if (service == SERVICE_MULTIPLE_SERVICE_PACKET) + { + valid = parse_multiple_service_packet(data, data_length, cip_request, global_data); + cip_request->request_type = CipRequestTypeMultipleServiceRequest; + } + else if (class_id == CONNECTION_MANAGER_CLASS_ID + && service == CONNECTION_MANAGER_UNCONNECTED_SEND) + { + valid = parse_unconnected_send_request(data, + data_length, + cip_request, + global_data); + cip_request->request_type = CipRequestTypeUnconnectedSend; + } + else if (class_id == CONNECTION_MANAGER_CLASS_ID + && (service == CONNECTION_MANAGER_FORWARD_OPEN + || service == CONNECTION_MANAGER_LARGE_FORWARD_OPEN)) + { + CipForwardOpenRequest forward_open_request; + memset(&forward_open_request, 0, sizeof(forward_open_request)); + + bool large_forward_open = (service == CONNECTION_MANAGER_LARGE_FORWARD_OPEN); + valid = parse_forward_open_request(data, + data_length, + large_forward_open, + &forward_open_request, + cip_request); + forward_open_request.timestamp = global_data->snort_packet->pkth->ts; + + if (valid) + { + // Only store connection information for Class 3, Non-Null connections. + if (!forward_open_request.is_null_forward_open + && forward_open_request.transport_class == 3) + { + if (!cip_add_connection_to_pending(&global_data->connection_list, + &forward_open_request)) + { + // Error if the connection couldn't be added to the list. + cip_request->cip_req_invalid_nonfatal |= CIP_REQ_INVALID_CONNECTION_ADD_FAILED; + } + } + + cip_request->is_forward_open_request = true; + cip_request->connection_path_class_id = forward_open_request.connection_path.class_id; + cip_request->timeout_ms = forward_open_request.timeout_ms; + cip_request->has_timeout = true; + } + + cip_request->request_type = CipRequestTypeForwardOpen; + } + else if (class_id == CONNECTION_MANAGER_CLASS_ID + && service == CONNECTION_MANAGER_FORWARD_CLOSE) + { + CipForwardCloseRequest forward_close_request; + memset(&forward_close_request, 0, sizeof(forward_close_request)); + + valid = parse_forward_close_request(data, + data_length, + &forward_close_request); + + if (valid) + { + const bool connection_established = true; + cip_remove_connection(&global_data->connection_list, + &forward_close_request.connection_signature, + connection_established); + + cip_request->timeout_ms = forward_close_request.timeout_ms; + cip_request->has_timeout = true; + } + + cip_request->request_type = CipRequestTypeForwardClose; + } + else + { + // This is a regular CIP request. No need to parse data. + cip_request->request_type = CipRequestTypeOther; + } + + // Parse any embedded CIP packet that is configured. + config = global_data->config; + if (config->embedded_cip_enabled + && class_id == config->embedded_cip_class_id + && service == config->embedded_cip_service_id) + { + valid = parse_message_router_request(data, data_length, cip_request, global_data); + } + + return valid; +} + +static bool parse_cip_command_specific_data_response(const CipStatus* status, + const uint8_t* data, + size_t data_length, + CipRequestType request_type, + CipGlobalSessionData* global_data) +{ + bool valid = true; + + if (request_type == CipRequestTypeForwardOpen) + { + CipForwardOpenResponse forward_open_response; + memset(&forward_open_response, 0, sizeof(forward_open_response)); + + valid = parse_forward_open_response(data, + data_length, + status->general_status, + &forward_open_response); + forward_open_response.timestamp = global_data->snort_packet->pkth->ts; + + if (forward_open_response.success) + { + cip_add_connection_to_active(&global_data->connection_list, &forward_open_response); + } + else + { + const bool connection_established = false; + cip_remove_connection(&global_data->connection_list, + &forward_open_response.connection_signature, + connection_established); + } + } + + return valid; +} + + +static bool parse_message_router_request(const uint8_t* data, + size_t data_length, + CipRequest* cip_request, + CipGlobalSessionData* global_data) +{ + size_t header_size = 0; + if (!parse_cip_request_header(data, + data_length, + &header_size, + cip_request)) + { + return false; + } + + cip_request->cip_data = data + header_size; + cip_request->cip_data_size = data_length - header_size; + + bool valid = parse_cip_command_specific_data_request(data + header_size, + data_length - header_size, + cip_request, + global_data); + + return valid; +} + +static bool parse_message_router_response(const uint8_t* data, + size_t data_length, + CipRequestType request_type, + CipResponse* cip_response, + CipGlobalSessionData* global_data) +{ + bool valid; + size_t response_header_size; + size_t status_size; + #define MESSAGE_ROUTER_RESPONSE_MIN_SIZE 4 + if (data_length < MESSAGE_ROUTER_RESPONSE_MIN_SIZE) + { + return false; + } + + #define CIP_SERVICE_OFFSET 0 + #define CIP_STATUS_OFFSET 2 + + cip_response->service = data[CIP_SERVICE_OFFSET] & ~MESSAGE_ROUTER_RESPONSE_MASK; + + if (!parse_cip_status(data + CIP_STATUS_OFFSET, + data_length - CIP_STATUS_OFFSET, + &cip_response->status)) + { + return false; + } + + status_size = cip_status_size(&cip_response->status); + + // This includes: Service, reserved field, total status data. + response_header_size = CIP_STATUS_OFFSET + status_size; + + valid = true; + // Don't attempt to decode the command specific response if there wasn't a + // match to an existing request. + if (request_type != CipRequestTypeNoMatchFound) + { + valid = parse_cip_command_specific_data_response(&cip_response->status, + data + response_header_size, + data_length - response_header_size, + request_type, + global_data); + } + + return valid; +} + +// Returns true if the serviceId was a request service. +static bool is_service_request(uint8_t service_id) +{ + return (service_id & MESSAGE_ROUTER_RESPONSE_MASK) == 0; +} + +static bool parse_message_router(const uint8_t* data, + size_t data_length, + CipCurrentData* current_data, + CipGlobalSessionData* global_data) +{ + bool valid = true; + + CipMessage* cip_msg = ¤t_data->cip_msg; + + cip_msg->is_cip_request = is_service_request(*data); + if (cip_msg->is_cip_request) + { + cip_msg->request.request_type = CipRequestTypeOther; + valid = parse_message_router_request(data, + data_length, + &cip_msg->request, + global_data); + + cip_request_add(&global_data->unconnected_list, + ¤t_data->enip_data, + &cip_msg->request, + &global_data->snort_packet->pkth->ts); + } + else + { + CipRequestType request_type = CipRequestTypeNoMatchFound; + cip_request_remove(&global_data->unconnected_list, + ¤t_data->enip_data, + &request_type); + + valid = parse_message_router_response(data, + data_length, + request_type, + &cip_msg->response, + global_data); + } + + return valid; +} + +// Returns true if this data contains valid CIP Explicit data. The data must already: +// 1. Be ENIP_COMMAND_SEND_UNIT_DATA or ENIP_COMMAND_SEND_RR_DATA +// 2. Have the required CPF items for that ENIP command. +static bool parse_cip_explicit_data(CipCurrentData* current_data, CipGlobalSessionData* global_data) +{ + // Assume that all data values/length inside the EnipCpf are valid. + const EnipCpf* enip_cpf = ¤t_data->enip_data.enip_cpf; + const EnipCpfItem* cpf_item = &enip_cpf->item_list[CPF_DATA_ITEM_SLOT]; + + // Get the offset of the CIP Message Router data. + size_t cpf_data_offset = 0; + if (cpf_item->type == CPF_CONNECTED_DATA_ITEM_ID) + { + // For CIP Class 3 data, Connected Data contains: Sequence Count, then CIP Data. + const size_t CPF_CONNECTED_DATA_SEQUENCE_COUNT_SIZE = sizeof(uint16_t); + cpf_data_offset = CPF_CONNECTED_DATA_SEQUENCE_COUNT_SIZE; + } + else // CPF_UNCONNECTED_DATA_ITEM_ID + { + cpf_data_offset = 0; + } + + bool valid = false; + if (cpf_item->length > cpf_data_offset) + { + const uint8_t* message_router_data = cpf_item->data + cpf_data_offset; + size_t message_router_data_length = cpf_item->length - cpf_data_offset; + + valid = parse_message_router(message_router_data, + message_router_data_length, + current_data, + global_data); + } + + return valid; +} + +// Used for parsing SendRRData and SendUnitData Command Specific Data. +static bool parse_enip_command_data(const uint8_t* data, + size_t data_length, + CipCurrentData* current_data, + CipGlobalSessionData* global_data) +{ + uint32_t interface_handle; + + // This should always contain: Interface Handle, Timeout. +#define ENIP_COMMAND_HEADER_SIZE 6 + if (data_length < ENIP_COMMAND_HEADER_SIZE) + { + return false; + } + + // Interface Handle + #define ENIP_OFFSET_INTERFACE_HANDLE 0 + interface_handle = GetLEUint32(&data[ENIP_OFFSET_INTERFACE_HANDLE]); + +#define CIP_INTERFACE_HANDLE 0 + if (interface_handle != CIP_INTERFACE_HANDLE) + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_INTERFACE_HANDLE; + } + + // Parse the Encapsulated Data as Common Packet Format. + current_data->enip_data.cpf_decoded = parse_common_packet_format(data + ENIP_COMMAND_HEADER_SIZE, + data_length - ENIP_COMMAND_HEADER_SIZE, + ¤t_data->enip_data.enip_cpf, + current_data); + if (!current_data->enip_data.cpf_decoded) + { + return false; + } + + // Exit early if CIP Explicit Data cannot be processed. + if (!current_data->enip_data.required_cpf_items_present) + { + return true; + } + + current_data->cip_message_type = get_cip_message_type(current_data, global_data); + + bool valid = true; + if (current_data->cip_message_type == CipMessageTypeExplicit) + { + valid = parse_cip_explicit_data(current_data, global_data); + } + + return valid; +} + +bool parse_enip_layer(const uint8_t* data, + size_t data_length, + bool is_TCP, + CipCurrentData* current_data, + CipGlobalSessionData* global_data) +{ + const EnipHeader* enip_header; + current_data->enip_data.enip_decoded = parse_enip_header(data, + data_length, + ¤t_data->enip_data); + if (!current_data->enip_data.enip_decoded) + { + return false; + } + + // Command Specific Data + data += ENIP_HEADER_SIZE; + data_length -= ENIP_HEADER_SIZE; + + // Verify that actual data matches data length field. + enip_header = ¤t_data->enip_data.enip_header; + if (data_length < enip_header->length) + { + return false; + } + + if (enip_command_tcp_only(enip_header->command) && !is_TCP) + { + // Flag as an error and exit early because there would be no way to tie this data + // to a particular TCP session. + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_ENIP_TCP_ONLY; + + return true; + } + + if (enip_header->status != ENIP_STATUS_SUCCESS) + { + if (current_data->direction == CIP_FROM_CLIENT) + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_STATUS; + } + else if (current_data->direction == CIP_FROM_SERVER) + { + // Remove any outstanding request. + CipRequestType request_type = CipRequestTypeNoMatchFound; + cip_request_remove(&global_data->unconnected_list, + ¤t_data->enip_data, + &request_type); + } + + // No more processing after a non-success status. + return true; + } + + bool valid = true; + + switch (enip_header->command) + { + case ENIP_COMMAND_REGISTER_SESSION: + { + if (data_length < REGISTER_SESSION_DATA_SIZE) + { + valid = false; + break; + } + + // Check that there is no active ENIP session. + if (current_data->direction == CIP_FROM_CLIENT) + { + if (global_data->enip_session.active) + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_DUPLICATE_SESSION; + } + } + + // Add ENIP session to the current TCP session. + if (current_data->direction == CIP_FROM_SERVER) + { + if (!enip_session_add(&global_data->enip_session, + enip_header->session_handle)) + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_DUPLICATE_SESSION; + } + } + } + break; + + case ENIP_COMMAND_SEND_RR_DATA: + case ENIP_COMMAND_SEND_UNIT_DATA: + valid = parse_enip_command_data(data, data_length, current_data, global_data); + + // Check that the Session Handle matches the active ENIP session. + if (!enip_session_handle_valid(&global_data->enip_session, enip_header->session_handle)) + { + current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_SESSION_HANDLE; + } + break; + case ENIP_COMMAND_NOP: + break; + case ENIP_COMMAND_LIST_SERVICES: + case ENIP_COMMAND_LIST_IDENTITY: + case ENIP_COMMAND_LIST_INTERFACES: + if (current_data->direction == CIP_FROM_SERVER) + { + current_data->enip_data.cpf_decoded = parse_common_packet_format(data, + data_length, + ¤t_data->enip_data.enip_cpf, + current_data); + if (!current_data->enip_data.cpf_decoded) + { + valid = false; + } + } + break; + case ENIP_COMMAND_UNREGISTER_SESSION: + // Remove ENIP session from the current TCP session. + enip_session_remove(&global_data->enip_session, enip_header->session_handle); + break; + default: + // Ignore legacy cases. + break; + } + + return valid; +} + +void pack_cip_request_event(const CipRequest* request, CipEventData* cip_event_data) +{ + cip_event_data->service_id = request->service; + + if (request->is_forward_open_request) + { + cip_event_data->type = CIP_DATA_TYPE_CONNECTION; + cip_event_data->class_id = request->connection_path_class_id; + } + else if (request->request_path.primary_segment_type == CipSegment_Type_LOGICAL_CLASS) + { + // Publish Set Attribute Single services separately than other requests. + if (cip_event_data->service_id == SERVICE_SET_ATTRIBUTE_SINGLE + && request->request_path.has_instance_id + && request->request_path.has_attribute_id) + { + cip_event_data->instance_id = request->request_path.instance_id; + cip_event_data->attribute_id = request->request_path.attribute_id; + + cip_event_data->type = CIP_DATA_TYPE_SET_ATTRIBUTE; + } + else + { + cip_event_data->type = CIP_DATA_TYPE_PATH_CLASS; + } + + cip_event_data->class_id = request->request_path.class_id; + } + else if (request->request_path.primary_segment_type == CipSegment_Type_DATA_EXT_SYMBOL) + { + cip_event_data->type = CIP_DATA_TYPE_PATH_EXT_SYMBOL; + } + else + { + cip_event_data->type = CIP_DATA_TYPE_OTHER; + } +} + diff --git a/src/service_inspectors/cip/cip_parsing.h b/src/service_inspectors/cip/cip_parsing.h new file mode 100644 index 000000000..667fb26bb --- /dev/null +++ b/src/service_inspectors/cip/cip_parsing.h @@ -0,0 +1,65 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_parsing.h author RA/Cisco + +/* Description: Data parsing for EtherNet/IP and CIP formats. */ + +#ifndef CIP_PARSING_H +#define CIP_PARSING_H + +#include +#include +#include "pub_sub/cip_events.h" // For CipEventData + +#include "cip_definitions.h" // For CIP structs + +namespace snort +{ +struct Packet; +} + +//// EtherNet/IP Parsing + +// Constants - EtherNet/IP encapsulation layer +#define ENIP_HEADER_SIZE 24u + +// EtherNet/IP commands. +enum EnipCommand +{ + ENIP_COMMAND_NOP = 0x0000, + ENIP_COMMAND_LIST_SERVICES = 0x0004, + ENIP_COMMAND_LIST_IDENTITY = 0x0063, + ENIP_COMMAND_LIST_INTERFACES = 0x0064, + ENIP_COMMAND_REGISTER_SESSION = 0x0065, + ENIP_COMMAND_UNREGISTER_SESSION = 0x0066, + ENIP_COMMAND_SEND_RR_DATA = 0x006F, + ENIP_COMMAND_SEND_UNIT_DATA = 0x0070 +}; + +/// EtherNet/IP data parsing functions. +bool parse_enip_layer(const uint8_t* data, + size_t data_length, + bool is_TCP, + CipCurrentData* current_data, + CipGlobalSessionData* global_data); + +void pack_cip_request_event(const CipRequest* request, CipEventData* cip_event_data); + +#endif // CIP_PARSING_H + diff --git a/src/service_inspectors/cip/cip_session.cc b/src/service_inspectors/cip/cip_session.cc new file mode 100644 index 000000000..d6860f72d --- /dev/null +++ b/src/service_inspectors/cip/cip_session.cc @@ -0,0 +1,534 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_session.cc author RA/Cisco + +/* Description: Functions for managing CIP state data across multiple packets and TCP connections. + Note: Performance of all lookup functions is O(n). */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "cip_session.h" + +#include +#include +#include +#include +#include +#include "time/timersub.h" // For TIMERSUB + +#include "cip_parsing.h" // For CIP constants + +static uint32_t f_unconnected_timeout_ms = DEFAULT_UNCONNECTED_REQUEST_TIMEOUT; + +bool enip_session_add(EnipSession* enip_session, uint32_t session_handle) +{ + // Only 1 ENIP session per TCP connection is allowed. + if (enip_session->active) + { + return false; + } + + enip_session->session_handle = session_handle; + enip_session->active = true; + + return true; +} + +bool enip_session_remove(EnipSession* enip_session, uint32_t session_handle) +{ + if (!enip_session->active) + { + return false; + } + + if (enip_session->session_handle != session_handle) + { + return false; + } + + enip_session->active = false; + + return true; +} + +bool enip_session_handle_valid(const EnipSession* enip_session, uint32_t session_handle) +{ + return (enip_session->active && enip_session->session_handle == session_handle); +} + +/// CIP Request and Connection Management. +static void prune_cip_unconnected_list(CipUnconnectedMessageList* unconnected_list, + const struct timeval* timestamp) +{ + struct timeval timestamp_diff; + + bool pruned = false; + size_t oldest_slot = 0; + + // Prune any message that has exceeded the CIP timeout. + size_t i; + for (i = 0; i < unconnected_list->list_size; ++i) + { + if (unconnected_list->list[i].slot_active) + { + TIMERSUB(timestamp, &unconnected_list->list[i].timestamp, ×tamp_diff); + + // Round up to the nearest whole second. + uint32_t timeout_sec + = ceil(unconnected_list->list[i].timeout_ms / (double)MSEC_PER_SEC); + + // If the message timeout has been exceeded, remove the request from the list. + if (timestamp_diff.tv_sec > timeout_sec) + { + unconnected_list->list[i].slot_active = false; + unconnected_list->count--; + pruned = true; + } + + // Check if the current item's timestamp is older than the previous oldest. + if (timercmp(&unconnected_list->list[i].timestamp, + &unconnected_list->list[oldest_slot].timestamp, + <) != 0) + { + oldest_slot = i; + } + } + } + + // If no timeout was exceeded, prune the oldest one. + if (!pruned) + { + unconnected_list->list[oldest_slot].slot_active = false; + unconnected_list->count--; + unconnected_list->request_pruned = true; + } +} + +static void prune_cip_connection_list(CipConnectionList* connection_list, + const struct timeval* timestamp) +{ + struct timeval ot_timestamp_diff; + struct timeval to_timestamp_diff; + + bool pruned = false; + + size_t stale_slot = 0; + struct timeval stale_timestamp_diff; + memset(&stale_timestamp_diff, 0, sizeof(stale_timestamp_diff)); + + // Prune any connection that has exceeded the CIP timeout. + size_t i; + for (i = 0; i < connection_list->list_size; ++i) + { + if (connection_list->list[i].slot_active) + { + TIMERSUB(timestamp, &connection_list->list[i].ot_timestamp, &ot_timestamp_diff); + TIMERSUB(timestamp, &connection_list->list[i].to_timestamp, &to_timestamp_diff); + + // If either OT or TO connection timeouts have been exceeded, remove the connection + // from the list. + if (ot_timestamp_diff.tv_sec > connection_list->list[i].ot_connection_timeout_sec + || to_timestamp_diff.tv_sec > connection_list->list[i].to_connection_timeout_sec) + { + connection_list->list[i].slot_active = false; + connection_list->count--; + pruned = true; + } + + // Pick the most recent timestamp for this connection. + struct timeval connection_timestamp; + if (timercmp(&connection_list->list[i].ot_timestamp, + &connection_list->list[i].to_timestamp, + >) != 0) + { + connection_timestamp = connection_list->list[i].ot_timestamp; + } + else + { + connection_timestamp = connection_list->list[i].to_timestamp; + } + + struct timeval timestamp_diff; + TIMERSUB(timestamp, &connection_timestamp, ×tamp_diff); + + // Check if the current connection is more stale than the previously found one. + if (timercmp(×tamp_diff, &stale_timestamp_diff, >) != 0) + { + stale_slot = i; + stale_timestamp_diff = timestamp_diff; + } + } + } + + // If no timeout was exceeded, prune the least recently used one. + if (!pruned) + { + connection_list->list[stale_slot].slot_active = false; + connection_list->count--; + connection_list->connection_pruned = true; + } +} + +static bool cip_connection_signature_match(const CipConnectionSignature* left, + const CipConnectionSignature* right) +{ + if (left->connection_serial_number == right->connection_serial_number + && left->originator_serial_number == right->originator_serial_number + && left->vendor_id == right->vendor_id) + { + return true; + } + else + { + return false; + } +} + +static CipConnection* cip_find_connection_slot(CipConnectionList* connection_list, + const struct timeval* timestamp) +{ + CipConnection* connection = NULL; + + // Prune old connections if the list is at max capacity. + if (connection_list->count == connection_list->list_size) + { + prune_cip_connection_list(connection_list, timestamp); + } + + size_t i; + for (i = 0; i < connection_list->list_size; ++i) + { + if (!connection_list->list[i].slot_active) + { + connection = &connection_list->list[i]; + break; + } + } + + return connection; +} + +CipConnection* cip_find_connection_by_id( + CipConnectionList* connection_list, + CipPacketDirection direction, + uint32_t connection_id, + bool established) +{ + CipConnection* connection = NULL; + + size_t i; + for (i = 0; i < connection_list->list_size; ++i) + { + if (connection_list->list[i].slot_active + && (connection_list->list[i].established == established)) + { + if (direction == CIP_FROM_CLIENT + && connection_list->list[i].connection_id_pair.ot_connection_id == connection_id) + { + connection = &connection_list->list[i]; + break; + } + + if (direction == CIP_FROM_SERVER + && connection_list->list[i].connection_id_pair.to_connection_id == connection_id) + { + connection = &connection_list->list[i]; + break; + } + } + } + + return connection; +} + +static const CipConnection* cip_find_connection_by_id_any( + const CipConnectionList* connection_list, + uint32_t ot_connection_id, + uint32_t to_connection_id) +{ + const CipConnection* connection = NULL; + + size_t i; + for (i = 0; i < connection_list->list_size; ++i) + { + if (connection_list->list[i].slot_active && connection_list->list[i].established) + { + if (connection_list->list[i].connection_id_pair.ot_connection_id == ot_connection_id) + { + connection = &connection_list->list[i]; + break; + } + + if (connection_list->list[i].connection_id_pair.to_connection_id == to_connection_id) + { + connection = &connection_list->list[i]; + break; + } + } + } + + return connection; +} + +static const CipConnection* cip_find_connection_any(const CipConnectionList* connection_list, + const CipConnectionSignature* signature) +{ + const CipConnection* connection = NULL; + + size_t i; + for (i = 0; i < connection_list->list_size; ++i) + { + if (connection_list->list[i].slot_active + && cip_connection_signature_match(&connection_list->list[i].signature, signature)) + { + connection = &connection_list->list[i]; + break; + } + } + + return connection; +} + +static CipConnection* cip_find_connection(CipConnectionList* connection_list, + const CipConnectionSignature* signature, + bool established) +{ + CipConnection* connection = NULL; + + size_t i; + for (i = 0; i < connection_list->list_size; ++i) + { + if (connection_list->list[i].slot_active + && (connection_list->list[i].established == established) + && cip_connection_signature_match(&connection_list->list[i].signature, signature)) + { + connection = &connection_list->list[i]; + break; + } + } + + return connection; +} + +bool cip_add_connection_to_active(CipConnectionList* connection_list, + const CipForwardOpenResponse* forward_open_response) +{ + // Check that no existing connection has a matching connection ID for either direction. + const CipConnection* existing_connection = cip_find_connection_by_id_any(connection_list, + forward_open_response->connection_pair.ot_connection_id, + forward_open_response->connection_pair.to_connection_id); + if (existing_connection) + { + return false; + } + + // Find the existing pending connection. + CipConnection* connection = cip_find_connection(connection_list, + &forward_open_response->connection_signature, + false); + if (!connection) + { + return false; + } + + // Save the new Connection ID information, and mark the connection as + // fully established. + connection->connection_id_pair = forward_open_response->connection_pair; + connection->established = true; + connection->to_timestamp = forward_open_response->timestamp; + + return true; +} + +bool cip_remove_connection(CipConnectionList* connection_list, + const CipConnectionSignature* connection_signature, + bool established) +{ + CipConnection* connection = cip_find_connection(connection_list, + connection_signature, + established); + if (!connection) + { + return false; + } + + connection->slot_active = false; + connection_list->count--; + + return true; +} + +bool cip_add_connection_to_pending(CipConnectionList* connection_list, + const CipForwardOpenRequest* forward_open_request) +{ + // Check that there are no pending or existing connections with this signature. + const CipConnection* existing_connection = cip_find_connection_any(connection_list, + &forward_open_request->connection_signature); + if (existing_connection) + { + return false; + } + + CipConnection* connection = cip_find_connection_slot(connection_list, + &forward_open_request->timestamp); + if (!connection) + { + return false; + } + + connection->signature = forward_open_request->connection_signature; + connection->class_id = forward_open_request->connection_path.class_id; + connection->established = false; + + // Round up to the nearest whole second. + connection->ot_connection_timeout_sec + = ceil(forward_open_request->ot_connection_timeout_us / (double)USEC_PER_SEC); + connection->to_connection_timeout_sec + = ceil(forward_open_request->to_connection_timeout_us / (double)USEC_PER_SEC); + + connection->ot_timestamp = forward_open_request->timestamp; + connection->to_timestamp = forward_open_request->timestamp; + + connection->slot_active = true; + connection_list->count++; + + return true; +} + +/// CIP Request/Response Matching. +static CipUnconnectedMessage* find_unconnected_request_slot( + CipUnconnectedMessageList* unconnected_list, + const struct timeval* timestamp) +{ + CipUnconnectedMessage* unconnected_message = NULL; + + // Prune old messages if the list is at max capacity. + if (unconnected_list->count == unconnected_list->list_size) + { + prune_cip_unconnected_list(unconnected_list, timestamp); + } + + size_t i; + for (i = 0; i < unconnected_list->list_size; ++i) + { + if (!unconnected_list->list[i].slot_active) + { + unconnected_message = &unconnected_list->list[i]; + break; + } + } + + return unconnected_message; +} + +static CipUnconnectedMessage* find_unconnected_request( + CipUnconnectedMessageList* unconnected_list, + uint64_t sender_context) +{ + CipUnconnectedMessage* unconnected_message = NULL; + + size_t i; + for (i = 0; i < unconnected_list->list_size; ++i) + { + if (unconnected_list->list[i].slot_active + && unconnected_list->list[i].sender_context == sender_context) + { + unconnected_message = &unconnected_list->list[i]; + break; + } + } + + return unconnected_message; +} + +bool cip_request_add(CipUnconnectedMessageList* unconnected_list, + const EnipSessionData* enip_data, + const CipRequest* cip_request, + const struct timeval* timestamp) +{ + bool valid = true; + + if (enip_data->enip_header.command == ENIP_COMMAND_SEND_RR_DATA) + { + CipUnconnectedMessage* slot = find_unconnected_request_slot(unconnected_list, timestamp); + if (slot) + { + slot->sender_context = enip_data->enip_header.sender_context; + slot->request_type = cip_request->request_type; + + if (cip_request->has_timeout) + { + slot->timeout_ms = cip_request->timeout_ms; + } + else + { + slot->timeout_ms = f_unconnected_timeout_ms; + } + + slot->timestamp = *timestamp; + slot->slot_active = true; + unconnected_list->count++; + + valid = true; + } + else + { + valid = false; + } + } + + return valid; +} + +bool cip_request_remove(CipUnconnectedMessageList* unconnected_list, + const EnipSessionData* enip_data, + CipRequestType* request_type) +{ + bool valid = true; + + if (enip_data->enip_header.command == ENIP_COMMAND_SEND_RR_DATA) + { + CipUnconnectedMessage* request = find_unconnected_request(unconnected_list, + enip_data->enip_header.sender_context); + if (request) + { + *request_type = request->request_type; + + // Remove the request from the list. + request->slot_active = false; + unconnected_list->count--; + + valid = true; + } + else + { + valid = false; + } + } + + return valid; +} + +void set_unconnected_timeout(uint32_t unconnected_timeout) +{ + f_unconnected_timeout_ms = unconnected_timeout; +} + diff --git a/src/service_inspectors/cip/cip_session.h b/src/service_inspectors/cip/cip_session.h new file mode 100644 index 000000000..2d5a1547d --- /dev/null +++ b/src/service_inspectors/cip/cip_session.h @@ -0,0 +1,75 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_session.h author RA/Cisco + +/* Description: Functions for managing CIP state data across multiple packets and TCP connections. + */ + +#ifndef CIP_SESSION_H +#define CIP_SESSION_H + +#include +#include +#include "main/snort_config.h" +#include "main/snort_types.h" + +#include "cip_definitions.h" + +// Default unconnected request timeout, milliseconds. +#define DEFAULT_UNCONNECTED_REQUEST_TIMEOUT (30000) + +/// ENIP Session Management. +bool enip_session_add(EnipSession* enip_session, uint32_t session_handle); +bool enip_session_remove(EnipSession* enip_session, uint32_t session_handle); + +// Returns true if session_handle matches the active session. +bool enip_session_handle_valid(const EnipSession* enip_session, uint32_t session_handle); + +/// CIP Connection Management. +CipConnection* cip_find_connection_by_id( + CipConnectionList* connection_list, + CipPacketDirection direction, + uint32_t connection_id, + bool established); + +bool cip_add_connection_to_active(CipConnectionList* connection_list, + const CipForwardOpenResponse* forward_open_response); +bool cip_remove_connection(CipConnectionList* connection_list, + const CipConnectionSignature* connection_signature, + bool established); + +bool cip_add_connection_to_pending(CipConnectionList* connection_list, + const CipForwardOpenRequest* forward_open_request); + +/// CIP Request/Response Matching. +bool cip_request_add(CipUnconnectedMessageList* unconnected_list, + const EnipSessionData* enip_data, + const CipRequest* cip_request, + const struct timeval* timestamp); + +// Find a request in the list, and remove it. +bool cip_request_remove(CipUnconnectedMessageList* unconnected_list, + const EnipSessionData* enip_data, + CipRequestType* request_type); + +// Set timeout (milliseconds) to use for unconnected messages that don't have a built-in timeout. +void set_unconnected_timeout(uint32_t unconnected_timeout); + +#endif // CIP_SESSION_H + diff --git a/src/service_inspectors/cip/cip_util.h b/src/service_inspectors/cip/cip_util.h new file mode 100644 index 000000000..d0f8b40ae --- /dev/null +++ b/src/service_inspectors/cip/cip_util.h @@ -0,0 +1,51 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// cip_util.h author RA/Cisco + +/* Description: Common utility functions. */ + +#ifndef CIP_UTIL_H +#define CIP_UTIL_H + +#include // For endian checks + +#if __BYTE_ORDER == __LITTLE_ENDIAN + +// Get 16-bit value from little endian byte array. +static inline uint16_t GetLEUint16(const uint8_t* pData) +{ + return (static_cast(*(pData + 1) << 8) + | static_cast(*(pData + 0) << 0)); +} + +// Get 32-bit value from little endian byte array. +static inline uint32_t GetLEUint32(const uint8_t* pData) +{ + return (static_cast(*(pData + 3) << 24) + | static_cast(*(pData + 2) << 16) + | static_cast(*(pData + 1) << 8) + | static_cast(*(pData + 0) << 0)); +} + +#else // __BYTE_ORDER +#error "CIP Preprocessor is only supported on Little Endian." +#endif // __BYTE_ORDER + +#endif /* CIP_UTIL_H */ + diff --git a/src/service_inspectors/cip/dev_notes.txt b/src/service_inspectors/cip/dev_notes.txt new file mode 100644 index 000000000..c314b674c --- /dev/null +++ b/src/service_inspectors/cip/dev_notes.txt @@ -0,0 +1,12 @@ +CIP (Common Industrial Protocol) is a protocol used in SCADA networks. The +standard is managed by ODVA (Open DeviceNet Vendor Association). + +The CIP inspector decodes the CIP protocol and provides rule options to +access certain protocol fields. This allows a user to write rules for CIP +packets without decoding the protocol with a series of ”content” and ”byte +test” options. + +The preprocessor only evaluates PAF-flushed PDUs. If the rule options don't +check for this, they'll fire on stale session data when the original packet +goes through before flushing. + diff --git a/src/service_inspectors/cip/ips_cip_attribute.cc b/src/service_inspectors/cip/ips_cip_attribute.cc new file mode 100644 index 000000000..b2dcb5688 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_attribute.cc @@ -0,0 +1,215 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_attribute.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_attribute" +#define s_help \ + "detection option to match CIP attribute" + +//------------------------------------------------------------------------- +// CIP Attribute rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_attribute_perf_stats; + +class CipAttributeOption : public IpsOption +{ +public: + CipAttributeOption(RangeCheck& v) : IpsOption(s_name) + { cip_attr = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_attr; +}; + +uint32_t CipAttributeOption::hash() const +{ + uint32_t a, b, c; + + a = cip_attr.op; + b = cip_attr.min; + c = cip_attr.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipAttributeOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipAttributeOption& rhs = static_cast(ips); + return ( cip_attr == rhs.cip_attr ); +} + +IpsOption::EvalStatus CipAttributeOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_attribute_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type != CipMessageTypeExplicit + || !session_data->current_data.cip_msg.is_cip_request + || !session_data->current_data.cip_msg.request.request_path.has_attribute_id) + { + return NO_MATCH; + } + + if ( cip_attr.eval(session_data->current_data.cip_msg.request.request_path.attribute_id) ) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:65535" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP attribute" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipAttributeModule : public Module +{ +public: + CipAttributeModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_attr; +}; + +bool CipAttributeModule::begin(const char*, int, SnortConfig*) +{ + cip_attr.init(); + return true; +} + +bool CipAttributeModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_attr.validate(v.get_string(), RANGE); +} + +ProfileStats* CipAttributeModule::get_profile() const +{ + return &cip_attribute_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_attribute_mod_ctor() +{ + return new CipAttributeModule; +} + +static void cip_attribute_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_attribute_ctor(Module* p, OptTreeNode*) +{ + CipAttributeModule* m = static_cast(p); + return new CipAttributeOption(m->cip_attr); +} + +static void cip_attribute_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_attribute_mod_ctor, + cip_attribute_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_attribute_ctor, + cip_attribute_dtor, + nullptr +}; + +const BaseApi* ips_cip_attribute = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_class.cc b/src/service_inspectors/cip/ips_cip_class.cc new file mode 100644 index 000000000..045a8885d --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_class.cc @@ -0,0 +1,214 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_class.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_class" +#define s_help \ + "detection option to match CIP class" + +//------------------------------------------------------------------------- +// CIP Class rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_class_perf_stats; + +class CipClassOption : public IpsOption +{ +public: + CipClassOption(const RangeCheck& v) : IpsOption(s_name) + { cip_class = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_class; +}; + +uint32_t CipClassOption::hash() const +{ + uint32_t a,b,c; + + a = cip_class.op; + b = cip_class.min; + c = cip_class.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipClassOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipClassOption& rhs = static_cast(ips); + return (cip_class == rhs.cip_class); +} + +IpsOption::EvalStatus CipClassOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_class_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type != CipMessageTypeExplicit + || !session_data->current_data.cip_msg.is_cip_request + || !session_data->current_data.cip_msg.request.request_path.has_class_id) + { + return NO_MATCH; + } + + if ( cip_class.eval(session_data->current_data.cip_msg.request.request_path.class_id) ) + return MATCH; + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:65535" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP class" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipClassModule : public Module +{ +public: + CipClassModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_class; +}; + +bool CipClassModule::begin(const char*, int, SnortConfig*) +{ + cip_class.init(); + return true; +} + +bool CipClassModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_class.validate(v.get_string(), RANGE); +} + +ProfileStats* CipClassModule::get_profile() const +{ + return &cip_class_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_class_mod_ctor() +{ + return new CipClassModule; +} + +static void cip_class_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_class_ctor(Module* p, OptTreeNode*) +{ + CipClassModule* m = static_cast(p); + return new CipClassOption(m->cip_class); +} + +static void cip_class_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_class_mod_ctor, + cip_class_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_class_ctor, + cip_class_dtor, + nullptr +}; + +const BaseApi* ips_cip_class = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_connpathclass.cc b/src/service_inspectors/cip/ips_cip_connpathclass.cc new file mode 100644 index 000000000..18665bca5 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_connpathclass.cc @@ -0,0 +1,215 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_connpathclass.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_conn_path_class" +#define s_help \ + "detection option to match CIP Connection Path Class" + +//------------------------------------------------------------------------- +// CIP Connpathclass rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_connpathclass_perf_stats; + +class CipConnpathclassOption : public IpsOption +{ +public: + CipConnpathclassOption(RangeCheck& v) : IpsOption(s_name) + { cip_cpc = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_cpc; +}; + +uint32_t CipConnpathclassOption::hash() const +{ + uint32_t a,b,c; + + a = cip_cpc.op; + b = cip_cpc.min; + c = cip_cpc.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipConnpathclassOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipConnpathclassOption& rhs = static_cast(ips); + return ( cip_cpc == rhs.cip_cpc ); +} + +IpsOption::EvalStatus CipConnpathclassOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_connpathclass_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type != CipMessageTypeExplicit + || !session_data->current_data.cip_msg.is_cip_request + || !session_data->current_data.cip_msg.request.is_forward_open_request) + { + return NO_MATCH; + } + + if ( cip_cpc.eval(session_data->current_data.cip_msg.request.connection_path_class_id) ) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:65535" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP Connection Path Class" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipConnpathclassModule : public Module +{ +public: + CipConnpathclassModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_cpc; +}; + +bool CipConnpathclassModule::begin(const char*, int, SnortConfig*) +{ + cip_cpc.init(); + return true; +} + +bool CipConnpathclassModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_cpc.validate(v.get_string(), RANGE); +} + +ProfileStats* CipConnpathclassModule::get_profile() const +{ + return &cip_connpathclass_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_connpathclass_mod_ctor() +{ + return new CipConnpathclassModule; +} + +static void cip_connpathclass_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_connpathclass_ctor(Module* p, OptTreeNode*) +{ + CipConnpathclassModule* m = static_cast(p); + return new CipConnpathclassOption(m->cip_cpc); +} + +static void cip_connpathclass_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_connpathclass_mod_ctor, + cip_connpathclass_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_connpathclass_ctor, + cip_connpathclass_dtor, + nullptr +}; + +const BaseApi* ips_cip_connpathclass = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_enipcommand.cc b/src/service_inspectors/cip/ips_cip_enipcommand.cc new file mode 100644 index 000000000..043261bf9 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_enipcommand.cc @@ -0,0 +1,208 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_enipcommand.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "enip_command" +#define s_help \ + "detection option to match CIP Enip Command" + +//------------------------------------------------------------------------- +// CIP EnipCommand rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_enipcommand_perf_stats; + +class CipEnipCommandOption : public IpsOption +{ +public: + CipEnipCommandOption(RangeCheck& v) : IpsOption(s_name) + { cip_enip_cmd = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_enip_cmd; +}; + +uint32_t CipEnipCommandOption::hash() const +{ + uint32_t a,b,c; + + a = cip_enip_cmd.op; + b = cip_enip_cmd.min; + c = cip_enip_cmd.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipEnipCommandOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipEnipCommandOption& rhs = static_cast(ips); + return ( cip_enip_cmd == rhs.cip_enip_cmd ); +} + +IpsOption::EvalStatus CipEnipCommandOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_enipcommand_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if ( cip_enip_cmd.eval(session_data->current_data.enip_data.enip_header.command) ) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:65535" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP Enip Command" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipEnipCommandModule : public Module +{ +public: + CipEnipCommandModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_enip_cmd; +}; + +bool CipEnipCommandModule::begin(const char*, int, SnortConfig*) +{ + cip_enip_cmd.init(); + return true; +} + +bool CipEnipCommandModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_enip_cmd.validate(v.get_string(), RANGE); +} + +ProfileStats* CipEnipCommandModule::get_profile() const +{ + return &cip_enipcommand_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_enipcommand_mod_ctor() +{ + return new CipEnipCommandModule; +} + +static void cip_enipcommand_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_enipcommand_ctor(Module* p, OptTreeNode*) +{ + CipEnipCommandModule* m = static_cast(p); + return new CipEnipCommandOption(m->cip_enip_cmd); +} + +static void cip_enipcommand_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_enipcommand_mod_ctor, + cip_enipcommand_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_enipcommand_ctor, + cip_enipcommand_dtor, + nullptr +}; + +const BaseApi* ips_cip_enipcommand = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_enipreq.cc b/src/service_inspectors/cip/ips_cip_enipreq.cc new file mode 100644 index 000000000..68e5e5930 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_enipreq.cc @@ -0,0 +1,165 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_enipreq.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "enip_req" +#define s_help \ + "detection option to match ENIP Request" + +//------------------------------------------------------------------------- +// CIP Enipreq rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_enipreq_perf_stats; + +class CipEnipreqOption : public IpsOption +{ +public: + CipEnipreqOption() : IpsOption(s_name) { } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + + EvalStatus eval(Cursor&, Packet*) override; +}; + +uint32_t CipEnipreqOption::hash() const +{ + uint32_t a = 0, b = 0, c = 0; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipEnipreqOption::operator==(const IpsOption& ips) const +{ + return !strcmp(get_name(), ips.get_name()); +} + +IpsOption::EvalStatus CipEnipreqOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_enipreq_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.direction == CIP_FROM_CLIENT) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +class CipEnipreqModule : public Module +{ +public: + CipEnipreqModule() : Module(s_name, s_help) { } + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } +}; + +ProfileStats* CipEnipreqModule::get_profile() const +{ + return &cip_enipreq_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_enipreq_mod_ctor() +{ + return new CipEnipreqModule; +} + +static void cip_enipreq_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_enipreq_ctor(Module*, OptTreeNode*) +{ + return new CipEnipreqOption; +} + +static void cip_enipreq_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_enipreq_mod_ctor, + cip_enipreq_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_enipreq_ctor, + cip_enipreq_dtor, + nullptr +}; + +const BaseApi* ips_cip_enipreq = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_eniprsp.cc b/src/service_inspectors/cip/ips_cip_eniprsp.cc new file mode 100644 index 000000000..bc014e719 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_eniprsp.cc @@ -0,0 +1,165 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_eniprsp.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "enip_rsp" +#define s_help \ + "detection option to match ENIP response" + +//------------------------------------------------------------------------- +// CIP EnipRsp rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_eniprsp_perf_stats; + +class CipEnipRspOption : public IpsOption +{ +public: + CipEnipRspOption() : IpsOption(s_name) { } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + + EvalStatus eval(Cursor&, Packet*) override; +}; + +uint32_t CipEnipRspOption::hash() const +{ + uint32_t a = 0, b = 0, c = 0; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipEnipRspOption::operator==(const IpsOption& ips) const +{ + return !strcmp(get_name(), ips.get_name()); +} + +IpsOption::EvalStatus CipEnipRspOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_eniprsp_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.direction == CIP_FROM_SERVER) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +class CipEnipRspModule : public Module +{ +public: + CipEnipRspModule() : Module(s_name, s_help) { } + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } +}; + +ProfileStats* CipEnipRspModule::get_profile() const +{ + return &cip_eniprsp_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_eniprsp_mod_ctor() +{ + return new CipEnipRspModule; +} + +static void cip_eniprsp_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_eniprsp_ctor(Module*, OptTreeNode*) +{ + return new CipEnipRspOption; +} + +static void cip_eniprsp_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_eniprsp_mod_ctor, + cip_eniprsp_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_eniprsp_ctor, + cip_eniprsp_dtor, + nullptr +}; + +const BaseApi* ips_cip_eniprsp = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_instance.cc b/src/service_inspectors/cip/ips_cip_instance.cc new file mode 100644 index 000000000..32e3656a0 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_instance.cc @@ -0,0 +1,215 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_instance.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_instance" +#define s_help \ + "detection option to match CIP instance" + +//------------------------------------------------------------------------- +// CIP Instance rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_instance_perf_stats; + +class CipInstanceOption : public IpsOption +{ +public: + CipInstanceOption(RangeCheck& v) : IpsOption(s_name) + { cip_inst = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_inst; +}; + +uint32_t CipInstanceOption::hash() const +{ + uint32_t a, b, c; + + a = cip_inst.op; + b = cip_inst.min; + c = cip_inst.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipInstanceOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipInstanceOption& rhs = static_cast(ips); + return ( cip_inst == rhs.cip_inst ); +} + +IpsOption::EvalStatus CipInstanceOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_instance_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type != CipMessageTypeExplicit + || !session_data->current_data.cip_msg.is_cip_request + || !session_data->current_data.cip_msg.request.request_path.has_instance_id) + { + return NO_MATCH; + } + + if ( cip_inst.eval(session_data->current_data.cip_msg.request.request_path.instance_id) ) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:4294967295" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP instance" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipInstanceModule : public Module +{ +public: + CipInstanceModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_inst; +}; + +bool CipInstanceModule::begin(const char*, int, SnortConfig*) +{ + cip_inst.init(); + return true; +} + +bool CipInstanceModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_inst.validate(v.get_string(), RANGE); +} + +ProfileStats* CipInstanceModule::get_profile() const +{ + return &cip_instance_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_instance_mod_ctor() +{ + return new CipInstanceModule; +} + +static void cip_instance_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_instance_ctor(Module* p, OptTreeNode*) +{ + CipInstanceModule* m = static_cast(p); + return new CipInstanceOption(m->cip_inst); +} + +static void cip_instance_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_instance_mod_ctor, + cip_instance_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_instance_ctor, + cip_instance_dtor, + nullptr +}; + +const BaseApi* ips_cip_instance = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_req.cc b/src/service_inspectors/cip/ips_cip_req.cc new file mode 100644 index 000000000..2511b3396 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_req.cc @@ -0,0 +1,166 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_req.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_req" +#define s_help \ + "detection option to match CIP request" + +//------------------------------------------------------------------------- +// CIP Req rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_req_perf_stats; + +class CipReqOption : public IpsOption +{ +public: + CipReqOption() : IpsOption(s_name) { } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + + EvalStatus eval(Cursor&, Packet*) override; +}; + +uint32_t CipReqOption::hash() const +{ + uint32_t a = 0, b = 0, c = 0; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipReqOption::operator==(const IpsOption& ips) const +{ + return !strcmp(get_name(), ips.get_name()); +} + +IpsOption::EvalStatus CipReqOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_req_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type == CipMessageTypeExplicit + && session_data->current_data.cip_msg.is_cip_request) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +class CipReqModule : public Module +{ +public: + CipReqModule() : Module(s_name, s_help) { } + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } +}; + +ProfileStats* CipReqModule::get_profile() const +{ + return &cip_req_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_req_mod_ctor() +{ + return new CipReqModule; +} + +static void cip_req_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_req_ctor(Module*, OptTreeNode*) +{ + return new CipReqOption; +} + +static void cip_req_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_req_mod_ctor, + cip_req_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_req_ctor, + cip_req_dtor, + nullptr +}; + +const BaseApi* ips_cip_req = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_rsp.cc b/src/service_inspectors/cip/ips_cip_rsp.cc new file mode 100644 index 000000000..88097b4a8 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_rsp.cc @@ -0,0 +1,166 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_rsp.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_rsp" +#define s_help \ + "detection option to match CIP response" + +//------------------------------------------------------------------------- +// CIP Rsp rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_rsp_perf_stats; + +class CipRspOption : public IpsOption +{ +public: + CipRspOption() : IpsOption(s_name) { } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + + EvalStatus eval(Cursor&, Packet*) override; +}; + +uint32_t CipRspOption::hash() const +{ + uint32_t a = 0, b = 0, c = 0; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipRspOption::operator==(const IpsOption& ips) const +{ + return !strcmp(get_name(), ips.get_name()); +} + +IpsOption::EvalStatus CipRspOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_rsp_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type == CipMessageTypeExplicit + && !session_data->current_data.cip_msg.is_cip_request) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +class CipRspModule : public Module +{ +public: + CipRspModule() : Module(s_name, s_help) { } + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } +}; + +ProfileStats* CipRspModule::get_profile() const +{ + return &cip_rsp_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_rsp_mod_ctor() +{ + return new CipRspModule; +} + +static void cip_rsp_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_rsp_ctor(Module*, OptTreeNode*) +{ + return new CipRspOption; +} + +static void cip_rsp_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_rsp_mod_ctor, + cip_rsp_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_rsp_ctor, + cip_rsp_dtor, + nullptr +}; + +const BaseApi* ips_cip_rsp = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_service.cc b/src/service_inspectors/cip/ips_cip_service.cc new file mode 100644 index 000000000..5ca1a4369 --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_service.cc @@ -0,0 +1,219 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_service.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_service" +#define s_help \ + "detection option to match CIP service" + +//------------------------------------------------------------------------- +// CIP Service rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_service_perf_stats; + +class CipServiceOption : public IpsOption +{ +public: + CipServiceOption(RangeCheck& v) : IpsOption(s_name) + { cip_serv = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_serv; +}; + +uint32_t CipServiceOption::hash() const +{ + uint32_t a, b, c; + + a = cip_serv.op; + b = cip_serv.min; + c = cip_serv.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipServiceOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipServiceOption& rhs = static_cast(ips); + return ( cip_serv == rhs.cip_serv ); +} + +IpsOption::EvalStatus CipServiceOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_service_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = static_cast(p->flow->get_flow_data(CipFlowData::inspector_id)); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type != CipMessageTypeExplicit) + { + return NO_MATCH; + } + + if (session_data->current_data.cip_msg.is_cip_request) + { + if ( cip_serv.eval(session_data->current_data.cip_msg.request.service) ) + return MATCH; + } + else + { + if ( cip_serv.eval(session_data->current_data.cip_msg.response.service) ) + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:127" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP service" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipServiceModule : public Module +{ +public: + CipServiceModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_serv; +}; + +bool CipServiceModule::begin(const char*, int, SnortConfig*) +{ + cip_serv.init(); + return true; +} + +bool CipServiceModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_serv.validate(v.get_string(), RANGE); +} + +ProfileStats* CipServiceModule::get_profile() const +{ + return &cip_service_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_service_mod_ctor() +{ + return new CipServiceModule; +} + +static void cip_service_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_service_ctor(Module* p, OptTreeNode*) +{ + CipServiceModule* m = static_cast(p); + return new CipServiceOption(m->cip_serv); +} + +static void cip_service_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_service_mod_ctor, + cip_service_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_service_ctor, + cip_service_dtor, + nullptr +}; + +const BaseApi* ips_cip_service = &ips_api.base; + diff --git a/src/service_inspectors/cip/ips_cip_status.cc b/src/service_inspectors/cip/ips_cip_status.cc new file mode 100644 index 000000000..d67a9b97c --- /dev/null +++ b/src/service_inspectors/cip/ips_cip_status.cc @@ -0,0 +1,214 @@ +//-------------------------------------------------------------------------- +// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License Version 2 as published +// by the Free Software Foundation. You may not use, modify or distribute +// this program under any other version of the GNU General Public License. +// +// 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 along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +//-------------------------------------------------------------------------- + +// ips_cip_status.cc author Jian Wu + +/* Description: Rule options for CIP preprocessor */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "framework/cursor.h" +#include "framework/ips_option.h" +#include "framework/module.h" +#include "framework/range.h" +#include "hash/hashfcn.h" +#include "profiler/profiler.h" +#include "protocols/packet.h" + +#include "cip.h" + +using namespace snort; + +#define s_name "cip_status" +#define s_help \ + "detection option to match CIP response status" + +//------------------------------------------------------------------------- +// CIP Status rule option +//------------------------------------------------------------------------- + +static THREAD_LOCAL ProfileStats cip_status_perf_stats; + +class CipStatusOption : public IpsOption +{ +public: + CipStatusOption(RangeCheck& v) : IpsOption(s_name) + { cip_status = v; } + + uint32_t hash() const override; + bool operator==(const IpsOption&) const override; + EvalStatus eval(Cursor&, Packet*) override; + +private: + RangeCheck cip_status; +}; + +uint32_t CipStatusOption::hash() const +{ + uint32_t a, b, c; + + a = cip_status.op; + b = cip_status.min; + c = cip_status.max; + + mix_str(a, b, c, get_name()); + finalize(a,b,c); + + return c; +} + +bool CipStatusOption::operator==(const IpsOption& ips) const +{ + if ( strcmp(get_name(), ips.get_name()) ) + return false; + + const CipStatusOption& rhs = static_cast(ips); + return ( cip_status == rhs.cip_status ); +} + +IpsOption::EvalStatus CipStatusOption::eval(Cursor&, Packet* p) +{ + Profile profile(cip_status_perf_stats); + + if ( !p->flow || !p->is_full_pdu() ) + return NO_MATCH; + + CipFlowData* fd = (CipFlowData*)p->flow->get_flow_data(CipFlowData::inspector_id); + + if (!fd) + return NO_MATCH; + + CipSessionData* session_data = &fd->session; + + if (session_data->current_data.cip_message_type != CipMessageTypeExplicit + || session_data->current_data.cip_msg.is_cip_request) + { + return NO_MATCH; + } + + if ( cip_status.eval(session_data->current_data.cip_msg.response.status.general_status) ) + { + return MATCH; + } + + return NO_MATCH; +} + +//------------------------------------------------------------------------- +// module +//------------------------------------------------------------------------- + +#define RANGE "0:255" + +static const Parameter s_params[] = +{ + { "~range", Parameter::PT_INTERVAL, RANGE, nullptr, + "match CIP response status" }, + + { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr } +}; + +class CipStatusModule : public Module +{ +public: + CipStatusModule() : Module(s_name, s_help, s_params) { } + + bool begin(const char*, int, SnortConfig*) override; + bool set(const char*, Value&, SnortConfig*) override; + ProfileStats* get_profile() const override; + + Usage get_usage() const override + { return DETECT; } + +public: + RangeCheck cip_status; +}; + +bool CipStatusModule::begin(const char*, int, SnortConfig*) +{ + cip_status.init(); + return true; +} + +bool CipStatusModule::set(const char*, Value& v, SnortConfig*) +{ + if ( !v.is("~range") ) + return false; + + return cip_status.validate(v.get_string(), RANGE); +} + +ProfileStats* CipStatusModule::get_profile() const +{ + return &cip_status_perf_stats; +} + +//------------------------------------------------------------------------- +// api +//------------------------------------------------------------------------- + +static Module* cip_status_mod_ctor() +{ + return new CipStatusModule; +} + +static void cip_status_mod_dtor(Module* m) +{ + delete m; +} + +static IpsOption* cip_status_ctor(Module* p, OptTreeNode*) +{ + CipStatusModule* m = static_cast(p); + return new CipStatusOption(m->cip_status); +} + +static void cip_status_dtor(IpsOption* p) +{ + delete p; +} + +static const IpsApi ips_api = +{ + { + PT_IPS_OPTION, + sizeof(IpsApi), + IPSAPI_VERSION, + 0, + API_RESERVED, + API_OPTIONS, + s_name, + s_help, + cip_status_mod_ctor, + cip_status_mod_dtor + }, + OPT_TYPE_DETECTION, + 0, PROTO_BIT__TCP | PROTO_BIT__UDP, + nullptr, + nullptr, + nullptr, + nullptr, + cip_status_ctor, + cip_status_dtor, + nullptr +}; + +const BaseApi* ips_cip_status = &ips_api.base; + diff --git a/src/service_inspectors/service_inspectors.cc b/src/service_inspectors/service_inspectors.cc index eba443523..aee10b8a8 100644 --- a/src/service_inspectors/service_inspectors.cc +++ b/src/service_inspectors/service_inspectors.cc @@ -48,6 +48,7 @@ extern const BaseApi* sin_telnet; extern const BaseApi* sin_wizard; // these define multiple plugins +extern const BaseApi* sin_cip[]; extern const BaseApi* sin_dce[]; extern const BaseApi* sin_dnp3[]; extern const BaseApi* sin_gtp[]; @@ -86,6 +87,7 @@ void load_service_inspectors() PluginManager::load_plugins(sin_ssl); #ifdef STATIC_INSPECTORS + PluginManager::load_plugins(sin_cip); PluginManager::load_plugins(sin_dce); PluginManager::load_plugins(sin_dnp3); PluginManager::load_plugins(sin_gtp);