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.
Image Source: ICS Cheat Sheets | It’s Not Cheating If You Have an Effective and Safe Approach!
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.
Følgende bilde viser «en sesjon» for protokollen.
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:
Hvis det er behov for å simulere kontrollsystemer så kan man bruke en kombinasjon av OpenPLC og Factory.io
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.
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.
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:
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.
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
##!
## 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";
}
Og det vil se slik ut:
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.
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.