diff --git a/src/analyzer/protocol/modbus/events.bif b/src/analyzer/protocol/modbus/events.bif index 537820f37d..b52a5dbe05 100644 --- a/src/analyzer/protocol/modbus/events.bif +++ b/src/analyzer/protocol/modbus/events.bif @@ -294,3 +294,24 @@ event modbus_read_fifo_queue_request%(c: connection, headers: ModbusHeaders, sta ## fifos: The register values read from the FIFO queue on the device. event modbus_read_fifo_queue_response%(c: connection, headers: ModbusHeaders, fifos: ModbusRegisters%); +## Generated for a Modbus Diagnostics request. +## +## c: The connection. +## +## headers: The headers for the modbus function. +## +## subfunction: The subfunction for the diagnostics request. +## +## data: The data passed in the diagnostics request. +event modbus_diagnostics_request%(c: connection, headers: ModbusHeaders, subfunction: count, data: string%); + +## Generated for a Modbus Diagnostics response. +## +## c: The connection. +## +## headers: The headers for the modbus function. +## +## subfunction: The subfunction for the diagnostics response. +## +## data: The data passed in the diagnostics response. +event modbus_diagnostics_response%(c: connection, headers: ModbusHeaders, subfunction: count, data: string%); diff --git a/src/analyzer/protocol/modbus/modbus-analyzer.pac b/src/analyzer/protocol/modbus/modbus-analyzer.pac index 4bb47b1fd6..38a2e54ecf 100644 --- a/src/analyzer/protocol/modbus/modbus-analyzer.pac +++ b/src/analyzer/protocol/modbus/modbus-analyzer.pac @@ -355,6 +355,93 @@ refine flow ModbusTCP_Flow += { %} + # REQUEST FC=8 + function deliver_DiagnosticsRequest(header: ModbusTCP_TransportHeader, message: DiagnosticsRequest): bool + %{ + if ( ::modbus_diagnostics_request ) + { + auto data = to_stringval(${message.data}); + + // Data should always be a multiple of two bytes. For everything except + // "Return Query Data (0x00)" it should be two bytes long. + if ( data->Len() < 2 || data->Len() % 2 != 0 || + (${message.subfunction} != DIAGNOSTICS_RETURN_QUERY_DATA && data->Len() != 2) ) + { + zeek::reporter->Weird("modbus_diag_invalid_request_data", + zeek::util::fmt("%s", data->CheckString())); + } + + switch (${message.subfunction}) + { + case DIAGNOSTICS_RESTART_COMMUNICATIONS_OPTION: + // For "Restart Communications Option" it's either 0x0000 or 0xFF00. + if ( ( data->Bytes()[0] != 0x00 && data->Bytes()[0] != 0xFF ) || + data->Bytes()[1] != 0x00 ) + { + zeek::reporter->Weird("modbus_diag_invalid_request_data", + zeek::util::fmt("%s", data->CheckString())); + } + break; + case DIAGNOSTICS_RETURN_DIAGNOSTIC_REGISTER: + case DIAGNOSTICS_FORCE_LISTEN_ONLY_MODE: + case DIAGNOSTICS_CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER: + case DIAGNOSTICS_RETURN_BUS_MESSAGE_COUNT: + case DIAGNOSTICS_RETURN_BUS_COMMUNICATION_ERROR_COUNT: + case DIAGNOSTICS_RETURN_BUS_EXCEPTION_ERROR_COUNT: + case DIAGNOSTICS_RETURN_SERVER_MESSAGE_COUNT: + case DIAGNOSTICS_RETURN_SERVER_NO_RESPONSE_COUNT: + case DIAGNOSTICS_RETURN_SERVER_NAK_COUNT: + case DIAGNOSTICS_RETURN_SERVER_BUSY_COUNT: + case DIAGNOSTICS_RETURN_BUS_CHARACTER_OVERRUN_COUNT: + case DIAGNOSTICS_CLEAR_OVERRUN_COUNTER_AND_FLAG: + // For all of these subfunctions, the data should be 0x0000. + if ( data->Bytes()[0] != 0x00 || data->Bytes()[1] != 0x00 ) + { + zeek::reporter->Weird("modbus_diag_invalid_request_data", + zeek::util::fmt("%s", data->CheckString())); + } + break; + + case DIAGNOSTICS_CHANGE_ASCII_INPUT_DELIMITER: + // For "Change ASCII Input Delimiter", it should be an ascii character + // followed by a zero. + if ( ! isascii(data->Bytes()[0]) || data->Bytes()[1] != 0x00 ) + { + zeek::reporter->Weird("modbus_diag_invalid_request_data", + zeek::util::fmt("%s", data->CheckString())); + } + break; + + default: + zeek::reporter->Weird("modbus_diag_unknown_request_subfunction", + zeek::util::fmt("%d", ${message.subfunction})); + break; + } + + zeek::BifEvent::enqueue_modbus_diagnostics_request(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + HeaderToVal(header), + ${message.subfunction}, to_stringval(${message.data})); + } + + return true; + %} + + # RESPONSE FC=8 + function deliver_DiagnosticsResponse(header: ModbusTCP_TransportHeader, message: DiagnosticsResponse): bool + %{ + if ( ::modbus_diagnostics_response ) + { + zeek::BifEvent::enqueue_modbus_diagnostics_response(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + HeaderToVal(header), + ${message.subfunction}, to_stringval(${message.data})); + } + + return true; + %} + + # REQUEST FC=15 function deliver_WriteMultipleCoilsRequest(header: ModbusTCP_TransportHeader, message: WriteMultipleCoilsRequest): bool %{ diff --git a/src/analyzer/protocol/modbus/modbus-protocol.pac b/src/analyzer/protocol/modbus/modbus-protocol.pac index fba0a9cd3a..9fe0de9531 100644 --- a/src/analyzer/protocol/modbus/modbus-protocol.pac +++ b/src/analyzer/protocol/modbus/modbus-protocol.pac @@ -15,7 +15,7 @@ enum function_codes { WRITE_SINGLE_COIL = 0x05, WRITE_SINGLE_REGISTER = 0x06, # READ_EXCEPTION_STATUS = 0x07, - # DIAGNOSTICS = 0x08, + DIAGNOSTICS = 0x08, # GET_COMM_EVENT_COUNTER = 0x0B, # GET_COMM_EVENT_LOG = 0x0C, WRITE_MULTIPLE_COILS = 0x0F, @@ -48,6 +48,7 @@ enum function_codes { WRITE_SINGLE_COIL_EXCEPTION = 0x85, WRITE_SINGLE_REGISTER_EXCEPTION = 0x86, READ_EXCEPTION_STATUS_EXCEPTION = 0x87, + DIAGNOSTICS_EXCEPTION = 0x88, WRITE_MULTIPLE_COILS_EXCEPTION = 0x8F, WRITE_MULTIPLE_REGISTERS_EXCEPTION = 0x90, READ_FILE_RECORD_EXCEPTION = 0x94, @@ -57,6 +58,24 @@ enum function_codes { READ_FIFO_QUEUE_EXCEPTION = 0x98, }; +enum diagnostic_subfunctions { + DIAGNOSTICS_RETURN_QUERY_DATA = 0x00, + DIAGNOSTICS_RESTART_COMMUNICATIONS_OPTION = 0x01, + DIAGNOSTICS_RETURN_DIAGNOSTIC_REGISTER = 0x02, + DIAGNOSTICS_CHANGE_ASCII_INPUT_DELIMITER = 0x03, + DIAGNOSTICS_FORCE_LISTEN_ONLY_MODE = 0x04, + DIAGNOSTICS_CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER = 0x0A, + DIAGNOSTICS_RETURN_BUS_MESSAGE_COUNT = 0x0B, + DIAGNOSTICS_RETURN_BUS_COMMUNICATION_ERROR_COUNT = 0x0C, + DIAGNOSTICS_RETURN_BUS_EXCEPTION_ERROR_COUNT = 0x0D, + DIAGNOSTICS_RETURN_SERVER_MESSAGE_COUNT = 0x0E, + DIAGNOSTICS_RETURN_SERVER_NO_RESPONSE_COUNT = 0x0F, + DIAGNOSTICS_RETURN_SERVER_NAK_COUNT = 0x10, + DIAGNOSTICS_RETURN_SERVER_BUSY_COUNT = 0x11, + DIAGNOSTICS_RETURN_BUS_CHARACTER_OVERRUN_COUNT = 0x12, + DIAGNOSTICS_CLEAR_OVERRUN_COUNTER_AND_FLAG = 0x14, +}; + # Main Modbus/TCP PDU type ModbusTCP_PDU(is_orig: bool) = record { header: ModbusTCP_TransportHeader; @@ -86,7 +105,7 @@ type ModbusTCP_Request(header: ModbusTCP_TransportHeader) = case header.fc of { WRITE_SINGLE_COIL -> writeSingleCoil: WriteSingleCoilRequest(header); WRITE_SINGLE_REGISTER -> writeSingleRegister: WriteSingleRegisterRequest(header); #READ_EXCEPTION_STATUS -> readExceptionStatus: ReadExceptionStatusRequest(header); - #DIAGNOSTICS -> diagnostics: DiagnosticsRequest(header); + DIAGNOSTICS -> diagnostics: DiagnosticsRequest(header); #GET_COMM_EVENT_COUNTER -> getCommEventCounter: GetCommEventCounterRequest(header); #GET_COMM_EVENT_LOG -> getCommEventLog: GetCommEventLogRequest(header); WRITE_MULTIPLE_COILS -> writeMultipleCoils: WriteMultipleCoilsRequest(header); @@ -113,7 +132,7 @@ type ModbusTCP_Response(header: ModbusTCP_TransportHeader) = case header.fc of { WRITE_SINGLE_COIL -> writeSingleCoil: WriteSingleCoilResponse(header); WRITE_SINGLE_REGISTER -> writeSingleRegister: WriteSingleRegisterResponse(header); #READ_EXCEPTION_STATUS -> readExceptionStatus: ReadExceptionStatusResponse(header); - #DIAGNOSTICS -> diagnostics: DiagnosticsResponse(header); + DIAGNOSTICS -> diagnostics: DiagnosticsResponse(header); #GET_COMM_EVENT_COUNTER -> getCommEventCounter: GetCommEventCounterResponse(header); #GET_COMM_EVENT_LOG -> getCommEventLog: GetCommEventLogResponse(header); WRITE_MULTIPLE_COILS -> writeMultipleCoils: WriteMultipleCoilsResponse(header); @@ -134,6 +153,7 @@ type ModbusTCP_Response(header: ModbusTCP_TransportHeader) = case header.fc of { WRITE_SINGLE_COIL_EXCEPTION -> writeCoilException: Exception(header); WRITE_SINGLE_REGISTER_EXCEPTION -> writeSingleRegisterException: Exception(header); READ_EXCEPTION_STATUS_EXCEPTION -> readExceptionStatusException: Exception(header); + DIAGNOSTICS_EXCEPTION -> diagnosticsException: Exception(header); WRITE_MULTIPLE_COILS_EXCEPTION -> forceMultipleCoilsException: Exception(header); READ_FILE_RECORD_EXCEPTION -> readGeneralReferenceException: Exception(header); WRITE_FILE_RECORD_EXCEPTION -> writeGeneralReferenceException: Exception(header); @@ -247,6 +267,22 @@ type WriteSingleRegisterResponse(header: ModbusTCP_TransportHeader) = record { deliver: bool = $context.flow.deliver_WriteSingleRegisterResponse(header, this); } &byteorder=bigendian; +# REQUEST FC=8 +type DiagnosticsRequest(header: ModbusTCP_TransportHeader) = record { + subfunction: uint16; + data: bytestring &restofdata; +} &let { + deliver: bool = $context.flow.deliver_DiagnosticsRequest(header, this); +} &byteorder=bigendian; + +# RESPONSE FC=8 +type DiagnosticsResponse(header: ModbusTCP_TransportHeader) = record { + subfunction: uint16; + data: bytestring &restofdata; +} &let { + deliver: bool = $context.flow.deliver_DiagnosticsResponse(header, this); +} &byteorder=bigendian; + # REQUEST FC=15 type WriteMultipleCoilsRequest(header: ModbusTCP_TransportHeader) = record { start_address: uint16; diff --git a/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_big/coverage b/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_big/coverage index 85dce7a72c..8adf0256d1 100644 --- a/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_big/coverage +++ b/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_big/coverage @@ -1,2 +1,2 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -5 of 28 events triggered by trace +5 of 30 events triggered by trace diff --git a/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_small/coverage b/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_small/coverage index 85dce7a72c..8adf0256d1 100644 --- a/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_small/coverage +++ b/testing/btest/Baseline/scripts.base.protocols.modbus.coil_parsing_small/coverage @@ -1,2 +1,2 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -5 of 28 events triggered by trace +5 of 30 events triggered by trace diff --git a/testing/btest/Baseline/scripts.base.protocols.modbus.events/coverage b/testing/btest/Baseline/scripts.base.protocols.modbus.events/coverage index e45a4bd00d..be63650b49 100644 --- a/testing/btest/Baseline/scripts.base.protocols.modbus.events/coverage +++ b/testing/btest/Baseline/scripts.base.protocols.modbus.events/coverage @@ -1,2 +1,2 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -18 of 28 events triggered by trace +20 of 30 events triggered by trace diff --git a/testing/btest/Baseline/scripts.base.protocols.modbus.events/output b/testing/btest/Baseline/scripts.base.protocols.modbus.events/output index d010961970..f9ae4a3439 100644 --- a/testing/btest/Baseline/scripts.base.protocols.modbus.events/output +++ b/testing/btest/Baseline/scripts.base.protocols.modbus.events/output @@ -1,4 +1,12 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +3 modbus_diagnostics_request, [orig_h=10.0.0.57, orig_p=2578/tcp, resp_h=10.0.0.3, resp_p=502/tcp], [tid=0, pid=0, uid=10, function_code=8], 1, \x00\x00 +2 modbus_diagnostics_request, [orig_h=10.0.0.57, orig_p=2578/tcp, resp_h=10.0.0.3, resp_p=502/tcp], [tid=0, pid=0, uid=10, function_code=8], 10, \x00\x00 +3 modbus_diagnostics_request, [orig_h=10.0.0.57, orig_p=2578/tcp, resp_h=10.0.0.3, resp_p=502/tcp], [tid=0, pid=0, uid=10, function_code=8], 4, \x00\x00 +1 modbus_diagnostics_request, [orig_h=192.168.66.235, orig_p=2582/tcp, resp_h=166.161.16.230, resp_p=502/tcp], [tid=0, pid=0, uid=1, function_code=8], 0, \x00\x00 +2 modbus_diagnostics_response, [orig_h=10.0.0.57, orig_p=2578/tcp, resp_h=10.0.0.3, resp_p=502/tcp], [tid=0, pid=0, uid=10, function_code=8], 1, \x00\x00 +2 modbus_diagnostics_response, [orig_h=10.0.0.57, orig_p=2578/tcp, resp_h=10.0.0.3, resp_p=502/tcp], [tid=0, pid=0, uid=10, function_code=8], 10, \x00\x00 +1 modbus_diagnostics_response, [orig_h=192.168.66.235, orig_p=2582/tcp, resp_h=166.161.16.230, resp_p=502/tcp], [tid=0, pid=0, uid=1, function_code=8], 0, \x00\x00 +4 modbus_exception, [orig_h=10.0.0.57, orig_p=2578/tcp, resp_h=10.0.0.3, resp_p=502/tcp], [tid=0, pid=0, uid=10, function_code=136], 11 8 modbus_exception, [orig_h=192.168.66.235, orig_p=2582/tcp, resp_h=166.161.16.230, resp_p=502/tcp], [tid=0, pid=0, uid=1, function_code=129], 2 1 modbus_exception, [orig_h=192.168.66.235, orig_p=2582/tcp, resp_h=166.161.16.230, resp_p=502/tcp], [tid=0, pid=0, uid=1, function_code=129], 3 1 modbus_exception, [orig_h=192.168.66.235, orig_p=2582/tcp, resp_h=166.161.16.230, resp_p=502/tcp], [tid=0, pid=0, uid=1, function_code=130], 3 diff --git a/testing/btest/Baseline/scripts.base.protocols.modbus.events/weird.log b/testing/btest/Baseline/scripts.base.protocols.modbus.events/weird.log new file mode 100644 index 0000000000..dbc509e427 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.modbus.events/weird.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path weird +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p name addl notice peer source +#types time string addr port addr port string string bool string string +XXXXXXXXXX.XXXXXX - - - - - modbus_diag_unknown_request_subfunction 0 F zeek - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.modbus.policy/modbus.log b/testing/btest/Baseline/scripts.base.protocols.modbus.policy/modbus.log index 2d9675f5a5..e334b77b4f 100644 --- a/testing/btest/Baseline/scripts.base.protocols.modbus.policy/modbus.log +++ b/testing/btest/Baseline/scripts.base.protocols.modbus.policy/modbus.log @@ -8,9 +8,13 @@ #fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p tid unit func pdu_type exception #types time string addr port addr port count count string string string XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS REQ - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 unknown-136 RESP GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS REQ - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 unknown-136 RESP GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS REQ - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 unknown-136 RESP GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS REQ - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 unknown-136 RESP GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS REQ - XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS RESP - XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 10.0.0.57 2578 10.0.0.3 502 0 10 DIAGNOSTICS REQ - diff --git a/testing/btest/scripts/base/protocols/modbus/events.zeek b/testing/btest/scripts/base/protocols/modbus/events.zeek index 4c83299f68..e3074383f0 100644 --- a/testing/btest/scripts/base/protocols/modbus/events.zeek +++ b/testing/btest/scripts/base/protocols/modbus/events.zeek @@ -6,9 +6,11 @@ # @TEST-EXEC: echo `cat covered` of `cat total` events triggered by trace >coverage # @TEST-EXEC: btest-diff coverage # @TEST-EXEC: btest-diff conn.log +# @TEST-EXEC: btest-diff weird.log @load base/protocols/modbus @load base/protocols/conn +@load base/frameworks/notice/weird redef DPD::ignore_violations_after = 1; @@ -152,3 +154,12 @@ event modbus_read_fifo_queue_response(c: connection, headers: ModbusHeaders, fif print "modbus_read_fifo_queue_response", c$id, headers, fifos; } +event modbus_diagnostics_request(c: connection, headers: ModbusHeaders, subfunction: count, data: string) +{ + print "modbus_diagnostics_request", c$id, headers, subfunction, data; +} + +event modbus_diagnostics_response(c: connection, headers: ModbusHeaders, subfunction: count, data: string) +{ + print "modbus_diagnostics_response", c$id, headers, subfunction, data; +}