Sicra Header Logo
  • Karriere
  • Om oss
  • Folk
NorskEnglish
Kontakt oss
  1. Kunnskap
  2. Innsikter
  3. Blogg
Blogg
03.06.2025
min tid å lese

Når normalen brytes: Slik avsløres digitale trusler i OT-nettverk

Malware utnytter Modbus for å sende farlige instruksjoner til kontrollsystemer. Zeek-script gjør det mulig å oppdage unormal aktivitet som følge av malware.
<span id="hs_cos_wrapper_name" class="hs_cos_wrapper hs_cos_wrapper_meta_field hs_cos_wrapper_type_text" style="" data-hs-cos-general-type="meta_field" data-hs-cos-type="text" >Når normalen brytes: Slik avsløres digitale trusler i OT-nettverk</span>
Sicra_Portrait_Crop_1200x1500px_9141
Filip FogSikkerhetsanalytiker
Filip har en lidenskap for sikkerhets- og hendelseshåndtering. Han har også utviklet og ledet en hendelsesoperasjonssentral for private og offentlige kunder. I Sicra vil han spesielt bruke Palo Alto Networks sin verktøykasse.

I forrige artikkel, «Malware i maskinrommet: Avvik fra normaltilstand», ble det gitt et overblikk over et forenklet industrielt miljø som ble utsatt for angrep. Denne artikkelen tar et videre dypdykk i de tekniske detaljene rundt dette miljøet.

Industriell kontrollsystemer bruker forskjellige protokoller for å kommunisere mellom seg. De protokollene kjører på ulik port og støtter ulike funksjoner. Det er viktig å bli kjent med dem hvis man operer med kontrollsystmerer.

ICS_Protocols

Image Source: ICS Cheat Sheets | It’s Not Cheating If You Have an Effective and Safe Approach!

Modbus TCP/IP

Modbus TCP er en av de mest brukte protokollene som blir brukt mot PLC, RTU eller andre kontrollsystemer.

Ved å ta en nærmere titt på definisjonen av Modbus TCP ser vi at alle pakkene er som regel like lange.

image (3)

Følgende bilde viser «en sesjon» for protokollen.

Picture2

Bildet viser at klienten ønsker å lese (0x03) fra en enhet som har id 9 (0x09). Og klienten ønsker å vite verdien på Register 1 (0x0001).
Serveren svarer med samme funksjonskode (0x03) og verdien (0x005). Dette kan for eksempel være trykket i en tank.

Hvis vi ønsker å skrive til en enhets register kan vi bruke funksjonskode 0x06, og da med en verdi av 0x0001 for eksempel. Dette kunne vært sendt for å starte en pumpe.

Slik sender kontrollsystemene pakker fram og tilbake for å styre et komplekst anlegg med pumper, tanker, eller maskineri.

DRAGOS skriver at over 46000 enheter er offentlig tilgjengelig via denne protokollen. Og det gjør det veldig attraktivt for trusselaktører når de kan nå industrielle kontrollsystemer ved å sende noen Modbus pakker.

Dragos-FrostyGoop-ICS-Malware-Intel-Brief-0724_r2.pdf

Hvis det er behov, på en enkel måte, å simulere Modbus trafikk mellom «master» og «slaves» så ligger det et Github repo tilgjengelig for dette:

reverseame/MOSTO-Modbus-simulator: MOSTO is a SCADA network device simulator based on ModbusTCP communications. Based on Python3

Hvis det er behov for å simulere kontrollsystemer så kan man bruke en kombinasjon av OpenPLC og Factory.io

OT malware

Hvordan bruker så skadevare Modbus protokollen for å angripe industrielle kontrollsystemer og anlegg? Det finnes flere metoder og varianter av skadevare. Jeg skal bruke FrostyGoop skadevaren som utgangspunkt. Det er mulig å laste den ned her: https://bazaar.abuse.ch/browse/tag/FrostyGoop/

Etterforskningsrapporter viser at FrostyGoop brukte en JSON fil for få instruksjoner på hva den skulle utføre. Her er det to JSON filer med to ulike instruksjoner.

Bildet til venstre viser at funksjonskode 1, «Read Coils», skal kjøre mot 127.0.0.1.
Bildet til høyre viser at instruksjonskode 15, «Write Multiple Coils», skal kjøre med en verdi av 0x11110000.

Picture3Screenshot 2025-05-27 224155

Det skjer ofte i rekkefølge, men nødvendigvis ikke innenfor samme tidsrom. Som angriper ønsker man å vite noe om kontrollsystemet og sender derfor «Read» instruksjoner mot systemet. Deretter må verdiene tydes og forstås for så å sende en «Write» instruks.

Hvis vi ikke ønsker å gjenbruke skadevare kan vi kode vårt eget i Golang ved hjelp av biblioteket “rolfl\modbus". Det er ganske enkelt å lage et program som kommuniserer med kontrollsystemer, siden protokollen er enkel. Og det bare en nettverkspakke som skal sendes for å få det svaret man er ute etter.

Her er et forenklet eksempel basert på tidligere bildet til høyre. Legg merke til at variabelen vals blir initiert med lik verdi som på bildet, og client utfører samme funksjonskode som på bildet.

Kode kopiert

package main
import (
       "fmt";"log";"time";
       "github.com/rolfl/modbus"
)

func main() {
       mb, err := modbus.NewTCP("127.0.0.1:502")
       if err != nil {
               log.Fatalf("Failed to connect: %v", err)
       }
       client := mb.GetClient(254)
       timeout := 5 * time.Second
       vals := []int{1, 1, 1, 1, 0, 0, 0, 0}
       _, err = client.WriteMultipleCoils (0, vals, timeout)
       if err != nil {
               fmt.Println("WriteMultipleCoils failed:", err)
       }
}

Se for deg nå at det programmet nå kjører kontinuerlig uten stans.

Hva skjer her, i bildet under, som kan være unormalt? Muligens vanskelig å se og gjette:

OT-controlbox

Svaret er at her blir både pumpen slått på og avløpet (Discharge valve) åpnet. Og dette skal simulerer en oppførsel som egentlig burde være umulig å få til. Når det kommer til vann så skjer det muligens ingen skade, utenom skade på utstyret. Men det kan være andre miljøer der to ting ikke burde skje samtidig.

Zeek deteksjon

Et maskineri eller anlegg operer i et mønster slik at flere moduler kan operere med hverandre. Det er dette som er normaltilstanden. Men når det kommer en ukjent applikasjon som begynner å sende instruksjoner så oppstår det en unormal tilstand som vi burde klare å detektere.

Nedenfor har jeg skrevet et Zeek script som generer varsler om det er en optikk av en spesifikk instruksjon, eller om det plutselig blir en stor mengde Modbus pakker.

Scriptet fungerer på denne måten: Hver gang det kommer en Modbus pakke vil zeek ha kontroll på hvem som sendte den og hvilken instruksjon som ble sendt. Det vil så måles til en threshold verdi som er satt i toppen av scriptet. De verdiene er knyttet opp til tidsvinduet som det måles i (epoch). Om det er ett sekund eller ett minutt, er det mulig å justeres.

Filename: modbus-thresholds.zeek

Kode kopiert

##!
## Zeek Script: Modbus Multi-Function & Total Packet Flood Alert
##
## This script monitors multiple Modbus function codes and total packet counts,
## generating notices when thresholds are exceeded within a time window.
##

@load base/protocols/modbus/main

module ModbusThreshold;

export {
    redef enum Notice::Type += { 
        Excessive_Modbus_Function,
        Excessive_Total_Modbus_Packets
    };
}

# --- Customizable parameters ---
# Function-specific thresholds: [function_name] = threshold_count
const function_thresholds: table[string] of count = {
    ["WRITE_SINGLE_REGISTER"] = 1,           # <-- Threshold for Write Single Register
    ["READ_HOLDING_REGISTERS"] = 20,         # <-- Threshold for Read Holding Registers  
    ["WRITE_MULTIPLE_REGISTERS"] = 20,       # <-- Threshold for Write Multiple Registers
    ["WRITE_MULTIPLE_COILS"] = 20,           # <-- Threshold for Write Multiple Registers
    ["READ_COILS"] = 15,                     # <-- Threshold for Read Coils
    ["MASK_WRITE_REGISTER"] = 1              # <-- Threshold for Mask Write Register
};

const total_packet_threshold = 170;           # <-- Total Modbus packets threshold per IP
const epoch = 1sec;                         # <-- Time window for counting
# --------------------------------

# Per-IP counters for each function type
global func_counts: table[addr, string] of count &default=0;
# Per-IP total packet counters
global total_counts: table[addr] of count &default=0;

event Modbus::log_modbus(rec: Modbus::Info) {
    local source_ip = rec$id$orig_h;
    
    # Count total packets per IP
    if ( source_ip !in total_counts ) {
        total_counts[source_ip] = 0;
    }
    total_counts[source_ip] += 1;
    
    # Count function-specific packets per IP
    if ( rec?$func ) {
        local func_name = rec$func;
        
        # Check if this function is monitored
        if ( func_name in function_thresholds ) {
            if ( [source_ip, func_name] !in func_counts ) {
                func_counts[source_ip, func_name] = 0;
            }
            func_counts[source_ip, func_name] += 1;
        }
    }
}

event modbus_check_thresholds() {
    # Check function-specific thresholds
    for ( [source_ip, func_name] in func_counts ) {
        local cnt = func_counts[source_ip, func_name];
        local threshold = function_thresholds[func_name];
        
        if ( cnt > threshold ) {
            NOTICE([$note=Excessive_Modbus_Function,
                    $msg=fmt("Excessive Modbus Function '%s': %d packets in %s from %s (threshold: %d)",
                             func_name, cnt, epoch, source_ip, threshold),
                    $sub=fmt("Function '%s' threshold exceeded", func_name),
                    $src=source_ip]);
        }
    }
    
    # Check total packet thresholds
    for ( source_ip in total_counts ) {
        local total_cnt = total_counts[source_ip];
        
        if ( total_cnt > total_packet_threshold ) {
            NOTICE([$note=Excessive_Total_Modbus_Packets,
                    $msg=fmt("Excessive Total Modbus Packets: %d packets in %s from %s (threshold: %d)",
                             total_cnt, epoch, source_ip, total_packet_threshold),
                    $sub="Total Modbus packet threshold exceeded",
                    $src=source_ip]);
        }
    }
    
    # Reset all counters
    func_counts = table();
    total_counts = table();
    
    # Reschedule for next epoch
    schedule epoch { modbus_check_thresholds() };
}

event zeek_init() {
    print fmt("DEBUG: Multi-function Modbus monitoring started");
    print fmt("DEBUG: Monitoring %d function types with total threshold %d over %s windows", 
              |function_thresholds|, total_packet_threshold, epoch);
    
    # Print monitored functions
    for ( func_name in function_thresholds ) {
        print fmt("DEBUG: Monitoring '%s' with threshold %d", 
                  func_name, function_thresholds[func_name]);
    }
    
    schedule epoch { modbus_check_thresholds() };
}

event zeek_done() {
    print "DEBUG: Multi-function Modbus monitoring finished";
}

Scriptet kan testes med følgende kommando:

Kode kopiert

/opt/zeek/bin/zeek -C -r ./attack.pcap modbus-thresholds.zeek
Og resultatet kan hentes med denne kommandoen:

Kode kopiert

   cat notice.log | /opt/zeek/bin/zeek-cut -d ts note msg

Og det vil se slik ut:

Screenshot 2025-05-27 231032

Denne loggfilen kan så mates inn i en SIEM-løsning for deretter å generere varsler, og ikke minst en rask respons og etterforskning av dette avviket.

Oppsummering

Vi må bli kjent med OT-miljøet for å ha en riktig deteksjon og respons. Vi må vite hvilke protokoller som blir brukt, og hvordan normal tilstanden ser ut. Det hjelper også å vite hvordan anlegget er strukturert og hvilket komponenter som er i drift.

Uten denne informasjonen så har vi ingenting der å gjøre!
Men når vi har den den informasjon kan vi se etter avvik og bygge opp alarmer rundt normaltilstanden, på et plan som ligger nært kontrollsystemene.

Les mer

Når er det riktig å leie inn en CISO?
Blogg

Når er det riktig å leie inn en CISO?

Fagblogg
Cybersikkerhet
Å spre sikkerhetsansvaret mellom IT-avdelingen og ansatte i helt andre roller kan være forståelig, men er sjelden effektivt – og ofte risikabelt.
Kritisk tenkning i sikkerhetsanalyse: Redusere subjektiv tolkning
Blogg

Kritisk tenkning i sikkerhetsanalyse: Redusere subjektiv tolkning

Fagblogg
Cybersikkerhet
ACH-metodikk for kritisk tenkning i sikkerhetsanalyse reduserer bias.
Malware i maskinrommet: Avvik fra normaltilstand
Blogg

Malware i maskinrommet: Avvik fra normaltilstand

Fagblogg
Cybersikkerhet
Malware i maskinrommet krever tidlig oppdagelse av unormale hendelser i OT
Azure Policy – En grunnleggende innføring
Blogg

Azure Policy – En grunnleggende innføring

Fagblogg
Cybersikkerhet
Azure Policy gir kontroll, men feilbruk kan medføre risiko.

Skreddersydd cybersikkerhet for virksomheter og institusjoner – så dere kan vokse, innovere og jobbe uten bekymringer.

Ta kontaktRing oss +47 648 08 488
Hold deg oppdatert
Motta siste nytt fra Sicra

Linker
BærekraftFAQPartnereSertifiseringerKarrierePresse
Kontakt

Tel: +47 648 08 488
E-post: firmapost@sicra.no

Rosenholmveien 25, 1414
Trollåsen. Norge

Følg oss på LinkedIn
Sertifiseringer
iso27001-white
ISO 27001
miljofyrtarnlogo-hvit-rgb
Miljøfyrtårn
Sicra Footer Logo
Sicra © 2025
Personvern