zeek/scripts/base/protocols/modbus/main.zeek
Emmanuele Zambon 792aacc8e5 Fix issues with Modbus message logging
This commit fixes three issues with Zeek's Modbus message logging:

1 - Some exception responses (e.g., READ_COILS_EXCEPTION) are logged
    twice: once without and once with the exception message.
2 - Some exception responses (e.g., PROGRAM_484_EXCEPTION) are not
    logged.
3 - Some known but reserved function codes (e.g., PROGRAM_UNITY) are
    logged as unk-xxx (e.g., unk-90), while it would be possible to
    log their known name.

To address these inconsistencies, the modbus parser has been updated
to parse all exception responses (i.e., all responses where the MSB
of the function code is set) using the already defined Exception
message.
Also, the Modbus main.zeek script has been updated to consistently
demand logging exception responses to the specialized
modbus_exception event, rather than logging some exception responses
in the modbus_message event and others in the modbus_exception event.
Finally, the main.zeek script has been updated to make sure that
for every known function code, the corresponding exception code was
also present, and the enumeration of known function codes in
consts.zeek has been expanded.

Closes #3984
2024-10-30 13:12:27 +01:00

100 lines
2.7 KiB
Text

##! Base Modbus analysis script.
module Modbus;
@load ./consts
export {
redef enum Log::ID += { LOG };
global log_policy: Log::PolicyHook;
type Info: record {
## Time of the request.
ts: time &log;
## Unique identifier for the connection.
uid: string &log;
## Identifier for the connection.
id: conn_id &log;
## Modbus transaction ID
tid: count &log &optional;
## The terminal unit identifier for the message
unit: count &log &optional;
## The name of the function message that was sent.
func: string &log &optional;
## Whether this PDU was a response ("RESP") or request ("REQ")
pdu_type: string &log &optional;
## The exception if the response was a failure.
exception: string &log &optional;
};
## Event that can be handled to access the Modbus record as it is sent
## on to the logging framework.
global log_modbus: event(rec: Info);
}
redef record connection += {
modbus: Info &optional;
};
const ports = { 502/tcp };
redef likely_server_ports += { ports };
event zeek_init() &priority=5
{
Log::create_stream(Modbus::LOG, [$columns=Info, $ev=log_modbus, $path="modbus", $policy=log_policy]);
Analyzer::register_for_ports(Analyzer::ANALYZER_MODBUS, ports);
}
function build_func(func: count): string
{
local masked = func & ~0x80;
# If the function code is in function_codes, use it. Also,
# if the masked value isn't in function_codes, use function_codes
# &default functionality.
if ( func in function_codes || masked !in function_codes )
return function_codes[func];
local s = function_codes[masked];
# Suffix exceptions with _EXCEPTION.
if ( func & 0x80 == 0x80 )
s += "_EXCEPTION";
return s;
}
event modbus_message(c: connection, headers: ModbusHeaders, is_orig: bool) &priority=5
{
if ( ! c?$modbus )
{
c$modbus = [$ts=network_time(), $uid=c$uid, $id=c$id];
}
c$modbus$ts = network_time();
c$modbus$tid = headers$tid;
c$modbus$unit = headers$uid;
c$modbus$func = build_func(headers$function_code);
## If this message is from the TCP originator, it is a request. Otherwise,
## it is a response.
c$modbus$pdu_type = is_orig ? "REQ" : "RESP";
}
event modbus_message(c: connection, headers: ModbusHeaders, is_orig: bool) &priority=-5
{
# Don't log now if this is an exception (log in the exception event handler)
if ( headers$function_code < 0x80 )
Log::write(LOG, c$modbus);
}
event modbus_exception(c: connection, headers: ModbusHeaders, code: count) &priority=5
{
c$modbus$exception = exception_codes[code];
}
event modbus_exception(c: connection, headers: ModbusHeaders, code: count) &priority=-5
{
Log::write(LOG, c$modbus);
delete c$modbus$exception;
}