From bc5ecd6da7f068a12b9ee5397178723481c7a3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Lindh=C3=A9?= Date: Tue, 31 Oct 2017 08:33:46 +0100 Subject: Move all files one level down --- script/README | 9 +++ script/investigate.sh | 53 ++++++++++++++++ script/livegraph.sh | 38 ++++++++++++ script/measure-packets.sh | 119 ++++++++++++++++++++++++++++++++++++ script/modbus.bro | 150 ++++++++++++++++++++++++++++++++++++++++++++++ script/pasad-parsed.bro | 96 +++++++++++++++++++++++++++++ script/pasad-simple.bro | 56 +++++++++++++++++ script/run-midbro.sh | 40 +++++++++++++ 8 files changed, 561 insertions(+) create mode 100644 script/README create mode 100755 script/investigate.sh create mode 100755 script/livegraph.sh create mode 100644 script/measure-packets.sh create mode 100644 script/modbus.bro create mode 100644 script/pasad-parsed.bro create mode 100644 script/pasad-simple.bro create mode 100644 script/run-midbro.sh (limited to 'script') diff --git a/script/README b/script/README new file mode 100644 index 0000000..15f121e --- /dev/null +++ b/script/README @@ -0,0 +1,9 @@ +This directory contains a baseline implementation of the package parser +implemented as a Bro script. A .bro file contains a script that can be +executed on a Modbus pcap dump. A .log file contains an example for an +output file generated by this script. By convention, the sample log file +should contain the first 100 lines of a real log file obtained from running +the script on packets_00014_20161128135616.cap. + +Currently, the scripts only handle the read_holding_registers event. Other +events can handled by simply copying and adapting the existing handlers. diff --git a/script/investigate.sh b/script/investigate.sh new file mode 100755 index 0000000..9f67949 --- /dev/null +++ b/script/investigate.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +if [ $# -ne 3 ] +then + echo "Extracts the data for one machine and one register from a Modbus dump" + echo "and stores both the data and a plot in the current directory." + echo + echo "Usage: $0 DUMP IP ADDR" + echo "Example: $0 packets_00014_20161128135616.cap 192.168.215.66 64" + exit +fi + +if [[ ! -f "$1" || ! -r "$1" ]] +then + echo "Dump file $1 does not exist or cannot be read." + exit +fi + +CAPTURE_FILE=$(realpath "$1") +FILTER_MACHINE=$2 +FILTER_REGISTER=$3 + +BRODIR=$(realpath "$(dirname "$0")/../..") +BROSCRIPT_BASE=${BRODIR}/broccoli/script/modbus.bro + +TMPDIR=$(mktemp --tmpdir --directory pasad.XXXX) +TMPDIR_BRO=${TMPDIR}/bro +BROSCRIPT_MOD=${TMPDIR}/modbus.bro + +OUTDIR=$(pwd) +OUTFILE_DAT=${OUTDIR}/${FILTER_MACHINE}-${FILTER_REGISTER}.dat +OUTFILE_PNG=${OUTDIR}/${FILTER_MACHINE}-${FILTER_REGISTER}.png + +echo " * Preparing Bro script ..." +cp "${BROSCRIPT_BASE}" "${BROSCRIPT_MOD}" +sed -ie "s/\(const enable_filtering : bool = \).*;/\1T;/g" "${BROSCRIPT_MOD}" +sed -ie "s/\(const filter_ip_addr : addr = \).*;/\1${FILTER_MACHINE};/g" "${BROSCRIPT_MOD}" +sed -ie "s/\(const filter_mem_addr : count = \).*;/\1${FILTER_REGISTER};/g" "${BROSCRIPT_MOD}" + +echo " * Running Bro ..." +mkdir "${TMPDIR_BRO}" +cd "${TMPDIR_BRO}" +bro -r "${CAPTURE_FILE}" "${BROSCRIPT_MOD}" > /dev/null + +echo " * Extracting data ..." +tail -n +9 "${TMPDIR_BRO}/pasad-parsed.log" | cut -f 5 > "${OUTFILE_DAT}" +echo "${OUTFILE_DAT}" + +echo " * Generating graph ..." +echo "set terminal png; plot '${OUTFILE_DAT}' using 0:1 title '${FILTER_MACHINE} ${FILTER_REGISTER}'" | gnuplot > "${OUTFILE_PNG}" +echo "${OUTFILE_PNG}" + +rm -r "${TMPDIR}" diff --git a/script/livegraph.sh b/script/livegraph.sh new file mode 100755 index 0000000..67111bc --- /dev/null +++ b/script/livegraph.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +if [[ $# -ne 1 ]] +then + echo "Reads the sensor.dat and distance.dat from a running Pasad" + echo "instance and draws a graph from them." + echo + echo "Usage:" + echo " $0 SOURCE" + echo "Arguments:" + echo " SOURCE an expression such that SOURCE/sensor.dat and" + echo " SOURCE/distance.dat can be used as arguments for" + echo " scp (e. g. user@host:/path/to/files)" + echo + echo "Note: Use ssh-add to avoid typing your SSH passphrase every second" + exit 1 +fi + +function plot() { + scp -i /home/andreas/.ssh/pasadpi_rsa -P 8022 "${SCP_EXPR}/sensor.dat" "${SCP_EXPR}/distance.dat" . + tail -1000 sensor.dat > sensor-1000.dat + tail -1000 distance.dat > distance-1000.dat + echo "set terminal png; set yrange [17000:17300]; set y2range [0:300]; set ytics nomirror; set y2tics nomirror; set title 'Midbro/PASAD demo'; set ylabel 'sensor value'; set y2label 'distance'; plot 'sensor-1000.dat' using 0:1 with line title 'sensor value', 'distance-1000.dat' using 0:1 axis x1y2 with line title 'distance'" | gnuplot > live-tmp.png + mv live-tmp.png live.png +} + +SCP_EXPR=$1 + +echo 0 > sensor.dat +echo 0 > distance.dat +plot +feh -x --reload 0.1 live.png & + +while true +do + sleep 0.1 + plot +done diff --git a/script/measure-packets.sh b/script/measure-packets.sh new file mode 100644 index 0000000..b3df4be --- /dev/null +++ b/script/measure-packets.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +# This function has to execute the given arguments on the target machine. +# For local execution, this could look like: +# sudo bash -c "$@" +# Or for remote execution: +# ssh -i ~/.ssh/id_rsa root@remote "$@" +# Make sure that the command is executed by root, and that root has +# ~/.ssh/id_rsa. +# Also note that the remote tests assumes no sudo password needed. + +function execute_command { + # bash -c "$@" + ssh -i ~/.ssh/pasadpi_rsa pi@pasadpi2 "sudo bash -c '$@'" +} + +function measure_packets { + TCPREPLAY_SPEED=$1 + TCPREPLAY_COUNT=$2 + + BRO_PID=$(execute_command "bro -i \"${BRO_INTERFACE}\" -C -b Log::default_writer=Log::WRITER_NONE \"${BRO_SCRIPT}\" > ${BRO_DIR}/bro-out.txt 2> ${BRO_DIR}/bro-err.txt & echo \$!") + + PASAD_PID="" + if [[ -n "${PASAD}" ]] + then + # We also want to execute a Pasad instance + # Wait for Bro to be ready + execute_command "tail -f ${BRO_DIR}/bro-err.txt | while read LOGLINE ; do [[ \"\${LOGLINE}\" == *\"listening on \"* ]] && pkill -P \$\$ tail ; done" + # Start Pasad + PASAD_PID=$(execute_command "${PASAD} > ${BRO_DIR}/pasad-out.txt 2> ${BRO_DIR}/pasad-err.txt & echo \$!") + fi + + tcpreplay -i ${TCPREPLAY_INTERFACE} -M ${TCPREPLAY_SPEED} -L ${TCPREPLAY_COUNT} ${TCPREPLAY_DUMP} > /dev/null 2> /dev/null + + PCPU="100.0" + while [[ $(echo "${PCPU}>${IDLE}" | bc) -eq 1 ]] + do + sleep 1 + PCPU=$(execute_command "ps -q ${BRO_PID} -o pcpu --no-headers") + done + + if [[ -n "${PASAD_PID}" ]] + then + execute_command "kill -SIGINT \"${PASAD_PID}\"" + fi + execute_command "kill -SIGINT \"${BRO_PID}\"" + execute_command "while kill -0 ${BRO_PID} 2>/dev/null ; do sleep 0.1 ; done" + + execute_command "tail -1 ${BRO_DIR}/bro-err.txt" | sed 's/.* \([0-9]\+\) packets received.*/\1/' +} + +if [[ $# -lt 4 || $# -gt 5 ]] +then + echo "Executes Bro and tcpreplay and measures the number of packages" + echo "received and handled by Bro." + echo + echo "Usage:" + echo " $0 SCRIPT BIFACE DUMP TIFACE [PASAD]" + echo "Arguments:" + echo " SCRIPT the Bro script to execute" + echo " BIFACE the interface for Bro to listen on" + echo " DUMP the network dump to replay" + echo " TIFACE the interface for tcpreplay to replay to" + echo " PASAD the Pasad command to execute (optional)" + exit 1 +fi + +BRO_SCRIPT=$1 +BRO_INTERFACE=$2 +TCPREPLAY_DUMP=$3 +TCPREPLAY_INTERFACE=$4 +PASAD="" +if [[ $# -eq 5 ]] +then + PASAD=$5 +fi + +SPEEDS=(100 50 25) +COUNTS=(1000000 2000000 4000000) + +if [[ ! -r "${TCPREPLAY_DUMP}" ]] +then + echo "The network dump '${TCPREPLAY_DUMP}' does not exist. Aborting." + exit 1 +fi + +TCPREPLAY_DUMP=$(realpath "${TCPREPLAY_DUMP}") + +BRO_DIR=$(execute_command "mktemp --directory --tmpdir bro.XXX") + +# First run a test to measure what CPU base load to wait for +BRO_PID=$(execute_command "bro -i \"${BRO_INTERFACE}\" -C -b Log::default_writer=Log::WRITER_NONE \"${BRO_SCRIPT}\" > ${BRO_DIR}/bro-out.txt 2> ${BRO_DIR}/bro-err.txt & echo \$!") +sleep 10 +IDLECPU=$(execute_command "ps -q ${BRO_PID} -o pcpu --no-headers") +IDLE=$(echo "${IDLECPU}+10" | bc); +echo "Idle baseload is: $IDLE"; +execute_command "killall bro" + +echo "Starting time: $(date +'%F_%T')" + +echo -ne "sent\t" +for SPEED in ${SPEEDS[@]} +do + echo -ne "${SPEED}\t" +done +echo "time" + +for COUNT in ${COUNTS[@]} +do + echo -ne "${COUNT}\t" + for SPEED in ${SPEEDS[@]} + do + COUNT_RECEIVED=$(measure_packets ${SPEED} ${COUNT}) + echo -ne "${COUNT_RECEIVED}\t" + done + echo "$(date +'%F_%T')" +done + +execute_command "rm -rf \"${BRO_DIR}\"" diff --git a/script/modbus.bro b/script/modbus.bro new file mode 100644 index 0000000..d258de3 --- /dev/null +++ b/script/modbus.bro @@ -0,0 +1,150 @@ +# Example usage: +# bro -b -C -i eth0 modbus.bro Log::default_writer=Log::WRITER_NONE +@load frameworks/communication/listen +@load base/protocols/modbus + +module Pasad; + +redef Pcap::bufsize = 256; + +redef Communication::listen_port = 47760/tcp; + +redef Communication::listen_ssl = F; + +## Global variables +global verbose=F; + +## DATA STRUCTURES + +export { + redef enum Log::ID += { LOG }; + + type Transaction: record { + start_address: count; + quantity: count; + }; + + type TransactionTable: table[count] of Transaction; + + type Info: record { + transactions: TransactionTable &default=TransactionTable(); + }; + + type RegisterData: record { + ip: addr &log; + uid: count &log; + regtype: string &log; + address: count &log; + register: count &log; + }; + + const enable_filtering : bool = T; + const filter_ip_addr : addr = 192.168.215.66; + const filter_mem_addr : count = 64; +} + +redef record connection += { + pasad: Info &default=Info(); +}; + +redef Communication::nodes += { + ["pasad"] = [$host = 127.0.0.1, $events = /pasad/, $connect=F, $ssl=F] +}; + +## CUSTOM EVENTS + +event pasad_register_received(data: RegisterData) { + Log::write(Pasad::LOG, data); + if(verbose) + print fmt("Received address=%d, register=%d", data$address, data$register); +} + +event pasad_unmatched_response(tid: count) { + if(verbose) + print fmt("Unmatched response: tid=%d", tid); +} + +## CUSTOM FUNCTIONS + +function pasad_check_filter(ip: addr, start_address: count, quantity: count) : bool { + if (!enable_filtering) + return T; + if (ip != filter_ip_addr) + return F; + + if (start_address == 0 && quantity == 0) + return T; + if (start_address > filter_mem_addr) + return F; + return filter_mem_addr < start_address + quantity; +} + +function pasad_generate_event(transaction: Transaction, c: connection, + headers: ModbusHeaders, registers: ModbusRegisters, regtype: string, + i: count) { + local data = RegisterData( + $ip=c$id$resp_h, + $uid=headers$uid, + $regtype=regtype, + $address=transaction$start_address + i, + $register=registers[i] + ); + event pasad_register_received(data); +} + +function pasad_generate_events(transaction: Transaction, c: connection, + headers: ModbusHeaders, registers: ModbusRegisters, regtype: string) { + # TODO: check registers size + if (enable_filtering) { + if(verbose) + print fmt("%d %d %d", filter_mem_addr, transaction$start_address, transaction$quantity); + pasad_generate_event(transaction, c, headers, registers, regtype, + filter_mem_addr - transaction$start_address); + } else { + local i = 0; + while (i < transaction$quantity) { + pasad_generate_event(transaction, c, headers, registers, regtype, i); + ++i; + } + } +} + +## EVENT HANDLERS + +event bro_init() &priority=5 { + Log::create_stream(Pasad::LOG, [$columns=RegisterData, $path="pasad-parsed"]); +} + +event modbus_read_holding_registers_request(c: connection, + headers: ModbusHeaders, start_address: count, quantity: count) { + if (!pasad_check_filter(c$id$resp_h, start_address, quantity)) { + if(verbose) + print fmt("Filtered %s/%d/%d", c$id$resp_h, start_address, quantity); + return; + } + + local tid = headers$tid; + local transaction = Transaction( + $start_address=start_address, + $quantity=quantity + ); + c$pasad$transactions[tid] = transaction; +} + +event modbus_read_holding_registers_response(c: connection, + headers: ModbusHeaders, registers: ModbusRegisters) { + if (!pasad_check_filter(c$id$resp_h, 0, 0)) { + if(verbose) + print fmt("Filtered %s", c$id$resp_h); + return; + } + + local tid = headers$tid; + if (tid !in c$pasad$transactions) { + event pasad_unmatched_response(tid); + return; + } + local transaction = c$pasad$transactions[tid]; + delete c$pasad$transactions[tid]; + pasad_generate_events(transaction, c, headers, registers, "h"); +} diff --git a/script/pasad-parsed.bro b/script/pasad-parsed.bro new file mode 100644 index 0000000..88b1be1 --- /dev/null +++ b/script/pasad-parsed.bro @@ -0,0 +1,96 @@ +## Implementation that outputs the register identification and the register +## value. The correct register count is not checked and might lead to indexing +## errors. + +module Pasad; + +## DATA STRUCTURES + +export { + redef enum Log::ID += { LOG }; + + type Transaction: record { + start_address: count; + quantity: count; + }; + + type TransactionTable: table[count] of Transaction; + + type Info: record { + transactions: TransactionTable &default=TransactionTable(); + }; + + type Entry: record { + ip: addr &log; + uid: count &log; + regtype: string &log; + address: count &log; + register: count &log; + }; +} + +redef record connection += { + pasad: Info &default=Info(); +}; + +## CUSTOM EVENTS + +event pasad_entry(entry: Entry) + { + Log::write(Pasad::LOG, entry); + } + +event pasad_unmatched(tid: count) + { + print fmt("Unmatched response: tid=%d", tid); + } + +## CUSTOM FUNCTIONS + +function pasad_generate_events(transaction: Transaction, c: connection, headers: ModbusHeaders, registers: ModbusRegisters, regtype: string) + { + # TODO: check registers size + local i = 0; + while ( i < transaction$quantity ) + { + local entry = Entry( + $ip=c$id$orig_h, + $uid=headers$uid, + $regtype=regtype, + $address=transaction$start_address + i, + $register=registers[i] + ); + event pasad_entry(entry); + ++i; + } + } + +## EVENT HANDLERS + +event bro_init() &priority=5 + { + Log::create_stream(Pasad::LOG, [$columns=Entry, $path="pasad-parsed"]); + } + +event modbus_read_holding_registers_request(c: connection, headers: ModbusHeaders, start_address: count, quantity: count) + { + local tid = headers$tid; + local transaction = Transaction( + $start_address=start_address, + $quantity=quantity + ); + c$pasad$transactions[tid] = transaction; + } + +event modbus_read_holding_registers_response(c: connection, headers: ModbusHeaders, registers: ModbusRegisters) + { + local tid = headers$tid; + if ( tid !in c$pasad$transactions ) + { + event pasad_unmatched(tid); + return; + } + local transaction = c$pasad$transactions[tid]; + delete c$pasad$transactions[tid]; + pasad_generate_events(transaction, c, headers, registers, "h"); + } diff --git a/script/pasad-simple.bro b/script/pasad-simple.bro new file mode 100644 index 0000000..db3b4be --- /dev/null +++ b/script/pasad-simple.bro @@ -0,0 +1,56 @@ +## Simple implementation that outputs the raw request and response data +## to a log file. +## Currently, this only handles the read_holding_registers event. Other +## events can be handled similarily. This implementation assumes that +## requests and responses are exchanged within the same connection. I am not +## sure whether this really holds. + +module Pasad; + +export { + redef enum Log::ID += { LOG }; + + type Info: record { + ts_request: time &log; + ts_response: time &log &optional; + rtype: string &log; + tid_request: count &log; + tid_response: count &log &optional; + ip_orig: addr &log; + ip_resp: addr &log; + start_address: count &log; + quantity: count &log; + registers: ModbusRegisters &log &optional; + }; +} + +redef record connection += { + pasad: Info &optional; +}; + +event bro_init() &priority=5 + { + Log::create_stream(Pasad::LOG, [$columns=Info, $path="pasad-simple"]); + } + +event modbus_read_holding_registers_request(c: connection, headers: ModbusHeaders, start_address: count, quantity: count) + { + local rec: Info = [ + $ts_request=network_time(), + $rtype="holding", + $tid_request=headers$tid, + $start_address=start_address, + $quantity=quantity, + $ip_orig=c$id$orig_h, + $ip_resp=c$id$resp_h + ]; + c$pasad = rec; + } + +event modbus_read_holding_registers_response(c: connection, headers: ModbusHeaders, registers: ModbusRegisters) + { + c$pasad$tid_response = headers$tid; + c$pasad$ts_response = network_time(); + c$pasad$registers = registers; + Log::write(Pasad::LOG, c$pasad); + } diff --git a/script/run-midbro.sh b/script/run-midbro.sh new file mode 100644 index 0000000..a9bb008 --- /dev/null +++ b/script/run-midbro.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +if [ $# -ne 2 ] +then + echo "Starts Bro with the given arguments in the background and, when" + echo "it’s ready, starts Midbro." + echo + echo "Usage: $0 INTERFACE SCRIPT" + echo "Example: $0 lo modbus.bro" + exit +fi + +INTERFACE=$1 +SCRIPT=$(realpath $2) + +BRODIR=$(realpath "$(dirname "$0")/../..") +BROLOG=$(realpath bro.log) + +MIDBRO=${BRODIR}/broccoli/bin/midbropasad +MIDBROLOG=$(realpath midbro.log) + +TMPDIR=$(mktemp --directory --tmpdir pasad.XXXX) + +echo "* Starting Bro in background ..." +cd "${TMPDIR}" && sudo bro -i "${INTERFACE}" "${SCRIPT}" > ${BROLOG} 2>&1 & +BROPID=$! + +echo "* Waiting for Bro to listen ..." +sleep 1 +tail -f ${BROLOG} | while read LOGLINE +do + [[ "${LOGLINE}" == "listening on "* ]] && pkill -P $$ tail +done + +echo "* Starting Midbro ..." +${MIDBRO} + +kill $BROPID + +rm -r "${TMPDIR}" -- cgit v1.2.1