diff --git a/doc/userguide/configuration/suricata-yaml.rst b/doc/userguide/configuration/suricata-yaml.rst index bf13de9ffe..745c3690ff 100644 --- a/doc/userguide/configuration/suricata-yaml.rst +++ b/doc/userguide/configuration/suricata-yaml.rst @@ -2050,3 +2050,21 @@ in which two threads have to wait for each other . When using two threads, the time threads might have to wait for each other will be taken in account when/during profiling packets. For more information see :doc:`../performance/packet-profiling`. + +Application layers +------------------ + +Modbus +~~~~~~ + +According to MODBUS Messaging on TCP/IP Implementation Guide V1.0b, it +is recommended to keep the TCP connection opened with a remote device +and not to open and close it for each MODBUS/TCP transaction. +In that case, it is important to set the stream-depth of the modbus as +unlimited. + +:: + + modbus: + # Stream reassembly size for modbus, default is 0 + stream-depth: 0 diff --git a/src/app-layer-modbus.c b/src/app-layer-modbus.c index cffb61cef6..de09843aa1 100644 --- a/src/app-layer-modbus.c +++ b/src/app-layer-modbus.c @@ -43,6 +43,7 @@ #include "util-misc.h" #include "stream.h" +#include "stream-tcp.h" #include "app-layer-protos.h" #include "app-layer-parser.h" @@ -51,6 +52,7 @@ #include "app-layer-detect-proto.h" #include "conf.h" +#include "conf-yaml-loader.h" #include "decode.h" SCEnumCharMap modbus_decoder_event_table[ ] = { @@ -163,7 +165,11 @@ typedef struct ModbusHeader_ ModbusHeader; /* Modbus Default unreplied Modbus requests are considered a flood */ #define MODBUS_CONFIG_DEFAULT_REQUEST_FLOOD 500 +/* Modbus default stream reassembly depth */ +#define MODBUS_CONFIG_DEFAULT_STREAM_DEPTH 0 + static uint32_t request_flood = MODBUS_CONFIG_DEFAULT_REQUEST_FLOOD; +static uint32_t stream_depth = MODBUS_CONFIG_DEFAULT_STREAM_DEPTH; int ModbusStateGetEventInfo(const char *event_name, int *event_id, AppLayerEventType *event_type) { *event_id = SCMapEnumNameToValue(event_name, modbus_decoder_event_table); @@ -1463,7 +1469,8 @@ void RegisterModbusParsers(void) } } - ConfNode *p = ConfGetNode("app-layer.protocols.modbus.request-flood"); + ConfNode *p = NULL; + p = ConfGetNode("app-layer.protocols.modbus.request-flood"); if (p != NULL) { uint32_t value; if (ParseSizeStringU32(p->val, &value) < 0) { @@ -1473,6 +1480,17 @@ void RegisterModbusParsers(void) } } SCLogConfig("Modbus request flood protection level: %u", request_flood); + + p = ConfGetNode("app-layer.protocols.modbus.stream-depth"); + if (p != NULL) { + uint32_t value; + if (ParseSizeStringU32(p->val, &value) < 0) { + SCLogError(SC_ERR_MODBUS_CONFIG, "invalid value for stream-depth %s", p->val); + } else { + stream_depth = value; + } + } + SCLogInfo("Modbus stream depth: %u", stream_depth); } else { #ifndef AFLFUZZ_APPLAYER SCLogConfig("Protocol detection and parser disabled for %s protocol.", proto_name); @@ -1506,6 +1524,8 @@ void RegisterModbusParsers(void) AppLayerParserRegisterGetEventInfo(IPPROTO_TCP, ALPROTO_MODBUS, ModbusStateGetEventInfo); AppLayerParserRegisterParserAcceptableDataDirection(IPPROTO_TCP, ALPROTO_MODBUS, STREAM_TOSERVER); + + AppLayerParserSetStreamDepth(IPPROTO_TCP, ALPROTO_MODBUS, stream_depth); } else { SCLogConfig("Parsed disabled for %s protocol. Protocol detection" "still on.", proto_name); } @@ -3107,6 +3127,111 @@ end: UTHFreePackets(&p, 1); return result; } + +/** \test Checks if stream_depth is correct */ +static int ModbusParserTest17(void) { + AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc(); + Flow f; + TcpSession ssn; + + FAIL_IF(alp_tctx == NULL); + + memset(&f, 0, sizeof(f)); + memset(&ssn, 0, sizeof(ssn)); + + FLOW_INITIALIZE(&f); + f.protoctx = (void *)&ssn; + f.proto = IPPROTO_TCP; + + StreamTcpInitConfig(TRUE); + + FLOWLOCK_WRLOCK(&f); + int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_MODBUS, STREAM_TOSERVER, + readCoilsReq, sizeof(readCoilsReq)); + FAIL_IF(r != 0); + FLOWLOCK_UNLOCK(&f); + + FAIL_IF(f.alstate == NULL); + + FAIL_IF(((TcpSession *)(f.protoctx))->reassembly_depth != MODBUS_CONFIG_DEFAULT_STREAM_DEPTH); + + FLOWLOCK_WRLOCK(&f); + r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_MODBUS, STREAM_TOCLIENT, + readCoilsRsp, sizeof(readCoilsRsp)); + FAIL_IF(r != 0); + FLOWLOCK_UNLOCK(&f); + + FAIL_IF(((TcpSession *)(f.protoctx))->reassembly_depth != MODBUS_CONFIG_DEFAULT_STREAM_DEPTH); + + AppLayerParserThreadCtxFree(alp_tctx); + StreamTcpFreeConfig(TRUE); + FLOW_DESTROY(&f); + PASS; +} + +/*/ \test Checks if stream depth is correct over 2 TCP packets */ +static int ModbusParserTest18(void) { + AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc(); + Flow f; + TcpSession ssn; + + uint32_t input_len = sizeof(readCoilsReq), part2_len = 3; + uint8_t *input = readCoilsReq; + + FAIL_IF(alp_tctx == NULL); + + memset(&f, 0, sizeof(f)); + memset(&ssn, 0, sizeof(ssn)); + + FLOW_INITIALIZE(&f); + f.protoctx = (void *)&ssn; + f.proto = IPPROTO_TCP; + + StreamTcpInitConfig(TRUE); + + FLOWLOCK_WRLOCK(&f); + int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_MODBUS, STREAM_TOSERVER, + input, input_len - part2_len); + FAIL_IF(r != 0); + FLOWLOCK_UNLOCK(&f); + + FAIL_IF(((TcpSession *)(f.protoctx))->reassembly_depth != MODBUS_CONFIG_DEFAULT_STREAM_DEPTH); + + FLOWLOCK_WRLOCK(&f); + r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_MODBUS, STREAM_TOSERVER, + input, input_len); + FAIL_IF(r != 0); + FLOWLOCK_UNLOCK(&f); + + FAIL_IF(((TcpSession *)(f.protoctx))->reassembly_depth != MODBUS_CONFIG_DEFAULT_STREAM_DEPTH); + + FAIL_IF(f.alstate == NULL); + + input_len = sizeof(readCoilsRsp); + part2_len = 10; + input = readCoilsRsp; + + FLOWLOCK_WRLOCK(&f); + r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_MODBUS, STREAM_TOCLIENT, + input, input_len - part2_len); + FAIL_IF(r != 0); + FLOWLOCK_UNLOCK(&f); + + FAIL_IF(((TcpSession *)(f.protoctx))->reassembly_depth != MODBUS_CONFIG_DEFAULT_STREAM_DEPTH); + + FLOWLOCK_WRLOCK(&f); + r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_MODBUS, STREAM_TOCLIENT, + input, input_len); + FAIL_IF(r != 0); + FLOWLOCK_UNLOCK(&f); + + FAIL_IF(((TcpSession *)(f.protoctx))->reassembly_depth != MODBUS_CONFIG_DEFAULT_STREAM_DEPTH); + + AppLayerParserThreadCtxFree(alp_tctx); + StreamTcpFreeConfig(TRUE); + FLOW_DESTROY(&f); + PASS; +} #endif /* UNITTESTS */ void ModbusParserRegisterTests(void) { @@ -3143,5 +3268,9 @@ void ModbusParserRegisterTests(void) { ModbusParserTest15); UtRegisterTest("ModbusParserTest16 - Modbus invalid Write single register request", ModbusParserTest16); + UtRegisterTest("ModbusParserTest17 - Modbus stream depth", + ModbusParserTest17); + UtRegisterTest("ModbusParserTest18 - Modbus stream depth in 2 TCP packets", + ModbusParserTest18); #endif /* UNITTESTS */ } diff --git a/suricata.yaml.in b/suricata.yaml.in index 7fb4ac5559..0920b2da98 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -700,6 +700,9 @@ app-layer: # If the limit is reached, app-layer-event:modbus.flooded; will match. #request-flood: 500 + # Stream reassembly size for modbus, default is 0 + stream-depth: 0 + enabled: no detection-ports: dp: 502