aboutsummaryrefslogtreecommitdiff
path: root/script
diff options
context:
space:
mode:
authorAndreas Lindhé <andreas@lindhe.io>2017-10-31 08:33:46 +0100
committerAndreas Lindhé <andreas@lindhe.io>2017-10-31 08:41:40 +0100
commitbc5ecd6da7f068a12b9ee5397178723481c7a3ea (patch)
tree6ac5bb33df7c3aacde8eb254c4aee1ce1df9dd29 /script
parent2d5d5be5702867a7a719312a5a148489c3b68f31 (diff)
downloadmidbro-bc5ecd6da7f068a12b9ee5397178723481c7a3ea.tar.gz
midbro-bc5ecd6da7f068a12b9ee5397178723481c7a3ea.tar.bz2
Move all files one level down
Diffstat (limited to 'script')
-rw-r--r--script/README9
-rwxr-xr-xscript/investigate.sh53
-rwxr-xr-xscript/livegraph.sh38
-rw-r--r--script/measure-packets.sh119
-rw-r--r--script/modbus.bro150
-rw-r--r--script/pasad-parsed.bro96
-rw-r--r--script/pasad-simple.bro56
-rw-r--r--script/run-midbro.sh40
8 files changed, 561 insertions, 0 deletions
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}"