Modbus: Add support for Diagnostics (FC=8) requests and responses

This commit is contained in:
Tim Wojtulewicz 2023-07-19 17:20:41 -07:00
parent bacc0dcae3
commit f14be0de29
10 changed files with 184 additions and 6 deletions

View file

@ -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%);

View file

@ -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
%{

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 -

View file

@ -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;
}