# # The development of Zeek's Modbus analyzer has been made possible thanks to # the support of the Ministry of Security and Justice of the Kingdom of the # Netherlands within the projects of Hermes, Castor and Midas. # # Useful references: http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf # http://www.simplymodbus.ca/faq.htm # %header{ zeek::VectorValPtr bytestring_to_coils(const bytestring& coils, uint quantity); zeek::RecordValPtr HeaderToVal(ModbusTCP_TransportHeader* header); zeek::VectorValPtr create_vector_of_count(); %} %code{ zeek::VectorValPtr bytestring_to_coils(const bytestring& coils, uint quantity) { auto modbus_coils = zeek::make_intrusive(zeek::BifType::Vector::ModbusCoils); for ( uint i = 0; i < quantity && (i/8) < static_cast(coils.length()); i++ ) { char currentCoil = (coils[i/8] >> (i % 8)) % 2; modbus_coils->Assign(i, zeek::val_mgr->Bool(currentCoil)); } return modbus_coils; } zeek::RecordValPtr HeaderToVal(ModbusTCP_TransportHeader* header) { auto modbus_header = zeek::make_intrusive(zeek::BifType::Record::ModbusHeaders); modbus_header->Assign(0, header->tid()); modbus_header->Assign(1, header->pid()); modbus_header->Assign(2, header->uid()); modbus_header->Assign(3, header->fc()); modbus_header->Assign(4, header->len()); return modbus_header; } zeek::VectorValPtr create_vector_of_count() { auto vt = zeek::make_intrusive(zeek::base_type(zeek::TYPE_COUNT)); auto vv = zeek::make_intrusive(std::move(vt)); return vv; } %} refine connection ModbusTCP_Conn += { %member{ // Fields used to determine if the protocol has been confirmed or not. bool confirmed; bool orig_pdu; bool resp_pdu; %} %init{ confirmed = false; orig_pdu = false; resp_pdu = false; %} function SetPDU(is_orig: bool): bool %{ if ( is_orig ) orig_pdu = true; else resp_pdu = true; return true; %} function SetConfirmed(): bool %{ confirmed = true; return true; %} function IsConfirmed(): bool %{ return confirmed && orig_pdu && resp_pdu; %} }; refine flow ModbusTCP_Flow += { function deliver_message(header: ModbusTCP_TransportHeader): bool %{ if ( ::modbus_message ) { zeek::BifEvent::enqueue_modbus_message(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), is_orig()); } return true; %} function deliver_ModbusTCP_PDU(message: ModbusTCP_PDU): bool %{ // We will assume that if an entire PDU from both sides // is successfully parsed then this is definitely modbus. connection()->SetPDU(${message.is_orig}); if ( ! connection()->IsConfirmed() ) { connection()->SetConfirmed(); connection()->zeek_analyzer()->AnalyzerConfirmation(); } return true; %} # EXCEPTION function deliver_Exception(header: ModbusTCP_TransportHeader, message: Exception): bool %{ if ( ::modbus_exception ) { zeek::BifEvent::enqueue_modbus_exception(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.code}); } return true; %} # REQUEST FC=1 function deliver_ReadCoilsRequest(header: ModbusTCP_TransportHeader, message: ReadCoilsRequest): bool %{ if ( ::modbus_read_coils_request ) { zeek::BifEvent::enqueue_modbus_read_coils_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, ${message.quantity}); } return true; %} # RESPONSE FC=1 function deliver_ReadCoilsResponse(header: ModbusTCP_TransportHeader, message: ReadCoilsResponse): bool %{ if ( ::modbus_read_coils_response ) { zeek::BifEvent::enqueue_modbus_read_coils_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), bytestring_to_coils(${message.bits}, ${message.bits}.length()*8)); } return true; %} # REQUEST FC=2 function deliver_ReadDiscreteInputsRequest(header: ModbusTCP_TransportHeader, message: ReadDiscreteInputsRequest): bool %{ if ( ::modbus_read_discrete_inputs_request ) { zeek::BifEvent::enqueue_modbus_read_discrete_inputs_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, ${message.quantity}); } return true; %} # RESPONSE FC=2 function deliver_ReadDiscreteInputsResponse(header: ModbusTCP_TransportHeader, message: ReadDiscreteInputsResponse): bool %{ if ( ::modbus_read_discrete_inputs_response ) { zeek::BifEvent::enqueue_modbus_read_discrete_inputs_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), bytestring_to_coils(${message.bits}, ${message.bits}.length()*8)); } return true; %} # REQUEST FC=3 function deliver_ReadHoldingRegistersRequest(header: ModbusTCP_TransportHeader, message: ReadHoldingRegistersRequest): bool %{ if ( ::modbus_read_holding_registers_request ) { zeek::BifEvent::enqueue_modbus_read_holding_registers_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, ${message.quantity}); } return true; %} # RESPONSE FC=3 function deliver_ReadHoldingRegistersResponse(header: ModbusTCP_TransportHeader, message: ReadHoldingRegistersResponse): bool %{ if ( ${message.byte_count} % 2 != 0 ) { connection()->zeek_analyzer()->AnalyzerViolation( zeek::util::fmt("invalid value for modbus read holding register response byte count %d", ${message.byte_count})); return false; } if ( ::modbus_read_holding_registers_response ) { auto t = zeek::make_intrusive(zeek::BifType::Vector::ModbusRegisters); for ( unsigned int i=0; i < ${message.registers}->size(); ++i ) { auto r = zeek::val_mgr->Count(${message.registers[i]}); t->Assign(i, r); } zeek::BifEvent::enqueue_modbus_read_holding_registers_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), std::move(t)); } return true; %} # REQUEST FC=4 function deliver_ReadInputRegistersRequest(header: ModbusTCP_TransportHeader, message: ReadInputRegistersRequest): bool %{ if ( ::modbus_read_input_registers_request ) { zeek::BifEvent::enqueue_modbus_read_input_registers_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, ${message.quantity}); } return true; %} # RESPONSE FC=4 function deliver_ReadInputRegistersResponse(header: ModbusTCP_TransportHeader, message: ReadInputRegistersResponse): bool %{ if ( ${message.byte_count} % 2 != 0 ) { connection()->zeek_analyzer()->AnalyzerViolation( zeek::util::fmt("invalid value for modbus read input register response byte count %d", ${message.byte_count})); return false; } if ( ::modbus_read_input_registers_response ) { auto t = zeek::make_intrusive(zeek::BifType::Vector::ModbusRegisters); for ( unsigned int i=0; i < (${message.registers})->size(); ++i ) { auto r = zeek::val_mgr->Count(${message.registers[i]}); t->Assign(i, r); } zeek::BifEvent::enqueue_modbus_read_input_registers_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), std::move(t)); } return true; %} # REQUEST FC=5 function deliver_WriteSingleCoilRequest(header: ModbusTCP_TransportHeader, message: WriteSingleCoilRequest): bool %{ if ( ::modbus_write_single_coil_request ) { int val; if ( ${message.value} == 0x0000 ) val = 0; else if ( ${message.value} == 0xFF00 ) val = 1; else { connection()->zeek_analyzer()->AnalyzerViolation(zeek::util::fmt("invalid value for modbus write single coil request %d", ${message.value})); return false; } zeek::BifEvent::enqueue_modbus_write_single_coil_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.address}, val); } return true; %} # RESPONSE FC=5 function deliver_WriteSingleCoilResponse(header: ModbusTCP_TransportHeader, message: WriteSingleCoilResponse): bool %{ if ( ::modbus_write_single_coil_response ) { int val; if ( ${message.value} == 0x0000 ) val = 0; else if ( ${message.value} == 0xFF00 ) val = 1; else { connection()->zeek_analyzer()->AnalyzerViolation(zeek::util::fmt("invalid value for modbus write single coil response %d", ${message.value})); return false; } zeek::BifEvent::enqueue_modbus_write_single_coil_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.address}, val); } return true; %} # REQUEST FC=6 function deliver_WriteSingleRegisterRequest(header: ModbusTCP_TransportHeader, message: WriteSingleRegisterRequest): bool %{ if ( ::modbus_write_single_register_request ) { zeek::BifEvent::enqueue_modbus_write_single_register_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.address}, ${message.value}); } return true; %} # RESPONSE FC=6 function deliver_WriteSingleRegisterResponse(header: ModbusTCP_TransportHeader, message: WriteSingleRegisterResponse): bool %{ if ( ::modbus_write_single_register_response ) { zeek::BifEvent::enqueue_modbus_write_single_register_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.address}, ${message.value}); } return true; %} # 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 %{ if ( ::modbus_write_multiple_coils_request ) { zeek::BifEvent::enqueue_modbus_write_multiple_coils_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, bytestring_to_coils(${message.coils}, ${message.quantity})); } return true; %} # RESPONSE FC=15 function deliver_WriteMultipleCoilsResponse(header: ModbusTCP_TransportHeader, message: WriteMultipleCoilsResponse): bool %{ if ( ::modbus_write_multiple_coils_response ) { zeek::BifEvent::enqueue_modbus_write_multiple_coils_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, ${message.quantity}); } return true; %} # REQUEST FC=16 function deliver_WriteMultipleRegistersRequest(header: ModbusTCP_TransportHeader, message: WriteMultipleRegistersRequest): bool %{ if ( ${message.byte_count} % 2 != 0 ) { connection()->zeek_analyzer()->AnalyzerViolation( zeek::util::fmt("invalid value for modbus write multiple registers request byte count %d", ${message.byte_count})); return false; } if ( ::modbus_write_multiple_registers_request ) { auto t = zeek::make_intrusive(zeek::BifType::Vector::ModbusRegisters); for ( unsigned int i = 0; i < (${message.registers}->size()); ++i ) { auto r = zeek::val_mgr->Count(${message.registers[i]}); t->Assign(i, r); } zeek::BifEvent::enqueue_modbus_write_multiple_registers_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, std::move(t)); } return true; %} # RESPONSE FC=16 function deliver_WriteMultipleRegistersResponse(header: ModbusTCP_TransportHeader, message: WriteMultipleRegistersResponse): bool %{ if ( ::modbus_write_multiple_registers_response ) { zeek::BifEvent::enqueue_modbus_write_multiple_registers_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}, ${message.quantity}); } return true; %} # REQUEST FC=20 function deliver_ReadFileRecordRequest(header: ModbusTCP_TransportHeader, message: ReadFileRecordRequest): bool %{ if ( ::modbus_read_file_record_request ) { //TODO: this need to be a vector of some Reference Request record type //auto t = create_vector_of_count(); //for ( unsigned int i = 0; i < (${message.references}->size()); ++i ) // { // auto r = zeek::val_mgr->Count((${message.references[i].ref_type})); // t->Assign(i, r); // // auto k = zeek::val_mgr->Count((${message.references[i].file_num})); // t->Assign(i, k); // // auto l = zeek::val_mgr->Count((${message.references[i].record_num})); // t->Assign(i, l); // } zeek::BifEvent::enqueue_modbus_read_file_record_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header)); } return true; %} # RESPONSE FC=20 function deliver_ReadFileRecordResponse(header: ModbusTCP_TransportHeader, message: ReadFileRecordResponse): bool %{ if ( ::modbus_read_file_record_response ) { //auto t = create_vector_of_count(); //for ( unsigned int i = 0; i < ${message.references}->size(); ++i ) // { // //TODO: work the reference type in here somewhere // auto r = zeek::val_mgr->Count(${message.references[i].record_data})); // t->Assign(i, r); // } zeek::BifEvent::enqueue_modbus_read_file_record_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header)); } return true; %} # REQUEST FC=21 function deliver_WriteFileRecordRequest(header: ModbusTCP_TransportHeader, message: WriteFileRecordRequest): bool %{ if ( ::modbus_write_file_record_request ) { //auto t = create_vector_of_count(); //for ( unsigned int i = 0; i < (${message.references}->size()); ++i ) // { // auto r = zeek::val_mgr->Count((${message.references[i].ref_type})); // t->Assign(i, r); // // auto k = zeek::val_mgr->Count((${message.references[i].file_num})); // t->Assign(i, k); // // auto n = zeek::val_mgr->Count((${message.references[i].record_num})); // t->Assign(i, n); // // for ( unsigned int j = 0; j < (${message.references[i].register_value}->size()); ++j ) // { // k = zeek::val_mgr->Count((${message.references[i].register_value[j]})); // t->Assign(i, k); // } // } zeek::BifEvent::enqueue_modbus_write_file_record_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header)); } return true; %} # RESPONSE FC=21 function deliver_WriteFileRecordResponse(header: ModbusTCP_TransportHeader, message: WriteFileRecordResponse): bool %{ if ( ::modbus_write_file_record_response ) { //auto t = create_vector_of_count(); //for ( unsigned int i = 0; i < (${messages.references}->size()); ++i ) // { // auto r = zeek::val_mgr->Count((${message.references[i].ref_type})); // t->Assign(i, r); // // auto f = zeek::val_mgr->Count((${message.references[i].file_num})); // t->Assign(i, f); // // auto rn = zeek::val_mgr->Count((${message.references[i].record_num})); // t->Assign(i, rn); // // for ( unsigned int j = 0; j<(${message.references[i].register_value}->size()); ++j ) // { // auto k = zeek::val_mgr->Count((${message.references[i].register_value[j]})); // t->Assign(i, k); // } zeek::BifEvent::enqueue_modbus_write_file_record_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header)); } return true; %} # REQUEST FC=22 function deliver_MaskWriteRegisterRequest(header: ModbusTCP_TransportHeader, message: MaskWriteRegisterRequest): bool %{ if ( ::modbus_mask_write_register_request ) { zeek::BifEvent::enqueue_modbus_mask_write_register_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.address}, ${message.and_mask}, ${message.or_mask}); } return true; %} # RESPONSE FC=22 function deliver_MaskWriteRegisterResponse(header: ModbusTCP_TransportHeader, message: MaskWriteRegisterResponse): bool %{ if ( ::modbus_mask_write_register_response ) { zeek::BifEvent::enqueue_modbus_mask_write_register_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.address}, ${message.and_mask}, ${message.or_mask}); } return true; %} # REQUEST FC=23 function deliver_ReadWriteMultipleRegistersRequest(header: ModbusTCP_TransportHeader, message: ReadWriteMultipleRegistersRequest): bool %{ if ( ${message.write_byte_count} % 2 != 0 ) { connection()->zeek_analyzer()->AnalyzerViolation( zeek::util::fmt("invalid value for modbus read write multiple registers request write byte count %d", ${message.write_byte_count})); return false; } if ( ::modbus_read_write_multiple_registers_request ) { auto t = zeek::make_intrusive(zeek::BifType::Vector::ModbusRegisters); for ( unsigned int i = 0; i < ${message.write_register_values}->size(); ++i ) { auto r = zeek::val_mgr->Count(${message.write_register_values[i]}); t->Assign(i, r); } zeek::BifEvent::enqueue_modbus_read_write_multiple_registers_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.read_start_address}, ${message.read_quantity}, ${message.write_start_address}, std::move(t)); } return true; %} # RESPONSE FC=23 function deliver_ReadWriteMultipleRegistersResponse(header: ModbusTCP_TransportHeader, message: ReadWriteMultipleRegistersResponse): bool %{ if ( ${message.byte_count} % 2 != 0 ) { connection()->zeek_analyzer()->AnalyzerViolation( zeek::util::fmt("invalid value for modbus read write multiple registers response byte count %d", ${message.byte_count})); return false; } if ( ::modbus_read_write_multiple_registers_response ) { auto t = zeek::make_intrusive(zeek::BifType::Vector::ModbusRegisters); for ( unsigned int i = 0; i < ${message.registers}->size(); ++i ) { auto r = zeek::val_mgr->Count(${message.registers[i]}); t->Assign(i, r); } zeek::BifEvent::enqueue_modbus_read_write_multiple_registers_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), std::move(t)); } return true; %} # REQUEST FC=24 function deliver_ReadFIFOQueueRequest(header: ModbusTCP_TransportHeader, message: ReadFIFOQueueRequest): bool %{ if ( ::modbus_read_fifo_queue_request ) { zeek::BifEvent::enqueue_modbus_read_fifo_queue_request(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.start_address}); } return true; %} # RESPONSE FC=24 function deliver_ReadFIFOQueueResponse(header: ModbusTCP_TransportHeader, message: ReadFIFOQueueResponse): bool %{ if ( ${message.byte_count} % 2 != 0 ) { connection()->zeek_analyzer()->AnalyzerViolation( zeek::util::fmt("invalid value for modbus read FIFO queue response byte count %d", ${message.byte_count})); return false; } if ( ::modbus_read_fifo_queue_response ) { auto t = create_vector_of_count(); for ( unsigned int i = 0; i < (${message.register_data})->size(); ++i ) { auto r = zeek::val_mgr->Count(${message.register_data[i]}); t->Assign(i, r); } zeek::BifEvent::enqueue_modbus_read_fifo_queue_response(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), std::move(t)); } return true; %} # REQUEST FC=2B function deliver_EncapInterfaceTransportRequest(header: ModbusTCP_TransportHeader, message: EncapInterfaceTransportRequest): bool %{ if ( ::modbus_encap_interface_transport_request ) { zeek::BifEvent::enqueue_modbus_encap_interface_transport_request( connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.mei_type}, to_stringval(${message.data})); } return true; %} # RESPONSE FC=2B function deliver_EncapInterfaceTransportResponse(header: ModbusTCP_TransportHeader, message: EncapInterfaceTransportResponse): bool %{ if ( ::modbus_encap_interface_transport_response ) { zeek::BifEvent::enqueue_modbus_encap_interface_transport_response( connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), HeaderToVal(header), ${message.mei_type}, to_stringval(${message.data})); } return true; %} };