Check if request has response in Zeek script

Good Morning,

I have a Zeek machine generating logs on a Modbus traffic.

My logs look like this :

1342774501.072580       32      10.2.2.2      51411   10.2.2.3      502     255    READ_HOLDING_REGISTERS  request 
1342774501.087014       32      10.2.2.2      51411   10.2.2.3      502     255     READ_HOLDING_REGISTERS  response

‘tid’ is the transaction id to identify a request/response couple. I want to know if a robot hasn’t responded to the Controller by logging only requests that did not have a response within 1 second.

My code is :

module Modbus_Extended;

export {
    redef enum Log::ID += { LOG_DETAILED,
                            LOG_MASK_WRITE_REGISTER,
                            LOG_READ_WRITE_MULTIPLE_REGISTERS};

    type Modbus_Detailed: record {
        ts                      : time              &log;             # Timestamp of event
        tid                     : count            &log;             # Zeek unique ID for connection
        id                      : conn_id           &log;             # Zeek connection struct (addresses and ports)
        unit_id                 : count             &log;             # Modbus unit-id
        func                    : string            &log &optional;   # Modbus Function
        network_direction       : string            &log &optional;   # Message direction (request or response)
        address                 : count             &log &optional;   # Starting address for value(s) field
        quantity                : count             &log &optional;   # Number of addresses/values read or written to
        values                  : string            &log &optional;   # Coils, discrete_inputs, or registers read/written to
    };
    global log_modbus_detailed: event(rec: Modbus_Detailed);


global transaction_ids: set[string, string] = {};

event modbus_message(c: connection,
                     headers: ModbusHeaders,
                     is_orig: bool) &priority=-5 {

    local modbus_detailed_rec: Modbus_Detailed;

    if(headers$tid !in transaction_ids[count]){
        add transaction_ids[headers$tid, c$modbus$ts]
    }else{
        delete transaction_ids[headers$tid, c$modbus$ts]
    }
    for(i in transaction_ids[timestamp]){
        if(c$modbus$ts > transactions_ids[headers$tid, i] +1)
        {
            Log::write(LOG_DETAILED, modbus_detailed_rec);
        }
    }
}
}

My guess is that I have to store transaction ids and check if I get only one occurence within this timelapse and then log it into a file, but I can figure out how to do it. Currently I can only generate logs with all the modbus traffic.

Thank you for your help

One way is to add a &create_expire attribute to your transaction_ids table and register an expiration function (&expire_func) See the docs for an explanation. The other “expire” attributes you can use follow in the docs.

Within the expiration function you can then log the just to be expired record.

You may want to key transactions by the connection identifier c$id as well as different connections may use the same tid values.

In the Zeek base code, examples of this approach are in the Tunnels module using a global table and the MQTT module, tracking publish/subscribes on the connection objects directly, for example.

Hope this helps,
Arne

Thank you for your answer,

I tried what you suggested and now my code looks like this :

function expire (transac: set[time,count,count,string,string], ts: time, tid: count, unit_id: count, func: string, direction: string): interval
{
        local modbus_detailed_rec: Modbus_Detailed;

        modbus_detailed_rec$ts                 = ts;
        modbus_detailed_rec$tid                 = tid;

        modbus_detailed_rec$unit_id            = unit_id;
        modbus_detailed_rec$func               = func;
        modbus_detailed_rec$network_direction  = direction;
        Log::write(LOG_DETAILED, modbus_detailed_rec);
}

global transaction_ids: set[time,count,count,string,string] &create_expire=1sec &expire_func=expire;

event modbus_message(c: connection,
                     headers: ModbusHeaders,
                     is_orig: bool) &priority=-5 {

    local modbus_detailed_rec: Modbus_Detailed;

    if (( headers$function_code < 0x81 || headers$function_code > 0x98 )) {
        if (is_orig)
            modbus_detailed_rec$network_direction = request_str_g;
        else
            modbus_detailed_rec$network_direction = response_str_g;

        }

        modbus_detailed_rec$ts                 = network_time();
        modbus_detailed_rec$tid                = headers$tid;
        modbus_detailed_rec$id                 = c$id;

        modbus_detailed_rec$unit_id            = headers$uid;
        modbus_detailed_rec$func               = Modbus::function_codes[headers$function_code];

        #if (headers$tid !in transaction_ids || c$modbus$id !in transaction_ids){
                add transaction_ids[modbus_detailed_rec$ts,modbus_detailed_rec$tid, modbus_detailed_rec$unit_id,modbus_detailed_rec$func,modbus_detailed_rec$network_direction];
        #}
        #else{
                delete transaction_ids[modbus_detailed_rec$ts,modbus_detailed_rec$tid,modbus_detailed_rec$unit_id,modbus_detailed_rec$func,modbus_detailed_rec$network_direction];
        #}
}

I would need to log a lot of data from the event (timestamp, tid, cid, source_ip, dest_ip, unit_id, func) so I added fields to my set to store it.

The problem is that I can’t set a condition to find if the tid has already been seen in order to “refresh” the timer. In the end all my entries will be logged after 1 sec. I can’t look for a specific value in a multiple fields set. How should I do it ?

Hello Lev,

instead of keeping the individual values in as set, use a table of [conn_id, count] representing connection and transaction id, pointing to the record you want to log.

Something like:

global transactions: table[conn_id, count] of Modbus_Detailed &create_expire=1sec &expire_func=expire;

You should then be able to use

if ( [c$id, headers$tid] !in transactions ) {
   local rec = Create a new Modbus_Detailed record...
   transactions[c$id, headers$tid] = rec;
} else {
   Log::write(transactions[c$id, headers$tid]);
   delete transactions[c$id, headers$tid];
}

…and have a corresponding Log::write() in the expire() function.

Hope that helps.

1 Like

Thank you, it worked perfectly for me :slight_smile: