Skip to content

Commit 3bb74b1

Browse files
authored
Merge pull request #37 from afmurillo/dev-paper
Merge to dev
2 parents 1591791 + 2390512 commit 3bb74b1

File tree

73 files changed

+5952
-114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+5952
-114
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import time
2+
3+
from tensorflow.keras.layers import Input, Dense, Activation, BatchNormalization, Lambda
4+
from tensorflow.keras.models import Model, load_model
5+
from tensorflow.keras.optimizers import Adam
6+
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
7+
from sklearn.preprocessing import MinMaxScaler
8+
from tensorflow.keras.initializers import glorot_normal
9+
10+
from sklearn.model_selection import train_test_split
11+
import numpy as np
12+
import pandas as pd
13+
14+
15+
# This module was developed by Alessandro Erba, the original is found here:
16+
# https://github.com/scy-phy/ICS-Evasion-Attacks/blob/master/Adversarial_Attacks/Black_Box_Attack/adversarial_AE.py
17+
18+
class Adversarial_AE:
19+
20+
def __init__(self, feature_dims, hide_layers):
21+
# define parameters
22+
self.attacker_scaler = MinMaxScaler()
23+
self.feature_dims = feature_dims
24+
self.hide_layers = hide_layers
25+
self.generator_layers = [self.feature_dims, int(self.hide_layers /
26+
2), self.hide_layers, int(self.hide_layers/2), self.feature_dims]
27+
optimizer = Adam(lr=0.001)
28+
29+
# Build the generator
30+
self.generator = self.build_generator()
31+
self.generator.compile(optimizer=optimizer, loss='mean_squared_error')
32+
33+
def build_generator(self):
34+
input = Input(shape=(self.feature_dims,))
35+
x = input
36+
for dim in self.generator_layers[1:]:
37+
x = Dense(dim, activation='sigmoid',
38+
kernel_initializer=glorot_normal(seed=12345))(x)
39+
generator = Model(input, x, name='generator')
40+
41+
return generator
42+
43+
def train_advAE(self, ben_data, xset):
44+
ben_data[xset] = self.attacker_scaler.transform(
45+
ben_data[xset])
46+
x_ben = pd.DataFrame(index=ben_data.index,
47+
columns=xset, data=ben_data[xset])
48+
x_ben_train, x_ben_test, _, _ = train_test_split(
49+
x_ben, x_ben, test_size=0.33, random_state=42)
50+
earlyStopping = EarlyStopping(
51+
monitor='val_loss', patience=3, verbose=0, min_delta=1e-4, mode='auto')
52+
lr_reduced = ReduceLROnPlateau(
53+
monitor='val_loss', factor=0.5, patience=1, verbose=0, min_delta=1e-4, mode='min')
54+
print(self.generator.summary())
55+
self.generator.fit(x_ben_train, x_ben_train,
56+
epochs=500,
57+
batch_size=64,
58+
shuffle=False,
59+
callbacks=[earlyStopping, lr_reduced],
60+
verbose=2,
61+
validation_data=(x_ben_test, x_ben_test))
62+
63+
64+
def fix_sample(self, gen_examples, dataset):
65+
"""
66+
Adjust discrete actuators values to the nearest allowed value
67+
Parameters
68+
----------
69+
gen_examples : Pandas Dataframe
70+
adversarial examples that needs to be adjusted
71+
dataset : string
72+
name of the dataset the data come from to select the correct strategy
73+
Returns
74+
-------
75+
pandas DataFrame
76+
adversarial examples with distrete values adjusted
77+
"""
78+
if dataset == 'BATADAL':
79+
list_pump_status = list(gen_examples.filter(
80+
regex='STATUS_PU[0-9]|STATUS_V[0-9]').columns)
81+
82+
for j, _ in gen_examples.iterrows():
83+
for i in list_pump_status: #list(gen_examples.columns[31:43]):
84+
if gen_examples.at[j, i] > 0.5:
85+
gen_examples.at[j, i] = 1
86+
else:
87+
gen_examples.at[j, i] = 0
88+
gen_examples.at[j, i.replace('STATUS', 'FLOW')] = 0 #gen_examples.columns[(
89+
# gen_examples.columns.get_loc(i)) - 12]] = 0
90+
91+
return gen_examples
92+
93+
def decide_concealment(self, n, binary_dataframe, gen_examples, original_examples, xset):
94+
"""
95+
Conceal only n variables among the modified ones by the autoencoder
96+
computes the squared error between original and concealed sample and forward only the first n wrongly reconstructed
97+
Parameters
98+
----------
99+
n : int
100+
number of variables to be forwarded concealed
101+
gen_examples : Pandas Dataframe
102+
concealed tuples by the autoencoder
103+
original_examples : Pandas Dataframe
104+
original tuples
105+
Returns
106+
-------
107+
pandas series
108+
concealed tuple with exactly n concealed sensor readings
109+
pandas DataFrame
110+
one hot encoded table keeping track of which of the n variables have been manipulated
111+
"""
112+
for j in range(0, len(gen_examples)):
113+
distance = (original_examples.iloc[j] - gen_examples.iloc[j])
114+
distance = np.sqrt(distance**2)
115+
distance = distance.sort_values(ascending=False)
116+
distance = distance.drop(distance.index[n:])
117+
binary_row = pd.DataFrame(
118+
index=[distance.name], columns=xset, data=0)
119+
for elem in distance.keys():
120+
binary_row.loc[distance.name, elem] = 1
121+
binary_dataframe = binary_dataframe.append(binary_row)
122+
for col, _ in distance.iteritems():
123+
original_examples.at[j, col] = gen_examples.at[j, col]
124+
125+
return original_examples.values, binary_dataframe
126+
127+
def conceal_fixed(self, constraints, gen_examples, original_examples):
128+
"""
129+
Conceal only n variables according to the list of allowed ones.
130+
131+
Parameters
132+
----------
133+
constraints : list
134+
list of sensor values that can be changed
135+
gen_examples : Pandas Dataframe
136+
concealed tuples by the autoencoder
137+
original_examples : Pandas Dataframe
138+
original tuples
139+
Returns
140+
-------
141+
pandas series
142+
adversarial examples with the allowed concealed sensor readings
143+
"""
144+
for j in range(0, len(gen_examples)):
145+
#print(constraints)
146+
#print(original_examples.iloc[j])
147+
for col in constraints:
148+
original_examples.at[j, col] = gen_examples.at[j, col]
149+
#print(original_examples.iloc[j])
150+
return original_examples.values
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import numpy as np
2+
import pandas as pd
3+
4+
import argparse
5+
import os
6+
from pathlib import Path
7+
import subprocess
8+
import sys
9+
import signal
10+
import os
11+
import time
12+
13+
from dhalsim.network_attacks.utilities import launch_arp_poison, restore_arp
14+
from dhalsim.network_attacks.synced_attack import SyncedAttack
15+
from evasion_attacks.Adversarial_Attacks.Black_Box_Attack import adversarial_AE
16+
17+
from tensorflow.keras.models import Model, load_model
18+
19+
20+
class Error(Exception):
21+
"""Base class for exceptions in this module."""
22+
23+
24+
class DirectionError(Error):
25+
"""Raised when the optional parameter direction does not have source or destination as value"""
26+
27+
28+
class UnconstrainedBlackBox(SyncedAttack):
29+
"""
30+
todo
31+
32+
:param intermediate_yaml_path: The path to the intermediate YAML file
33+
:param yaml_index: The index of the attack in the intermediate YAML
34+
"""
35+
36+
NETFILTERQUEUE_SUBPROCESS_TIMEOUT = 3
37+
""" Timeout to wait for the netfilter subprocess to be finished"""
38+
39+
def __init__(self, intermediate_yaml_path: Path, yaml_index: int):
40+
41+
# sync in this attack needs to be hanlded by the netfilterqueue. This value will be changed after we
42+
# launch the netfilterqueue process
43+
sync = True
44+
super().__init__(intermediate_yaml_path, yaml_index, sync)
45+
os.system('sysctl net.ipv4.ip_forward=1')
46+
47+
if self.intermediate_attack['persistent'] == 'True':
48+
self.persistent = True
49+
else:
50+
self.persistent = False
51+
52+
# Process object to handle nfqueue
53+
self.nfqueue_process = None
54+
55+
56+
def setup(self):
57+
"""
58+
This function start the network attack.
59+
60+
It first sets up the iptables on the attacker node to capture the tcp packets coming from
61+
the target PLC. It also drops the icmp packets, to avoid network packets skipping the
62+
attacker node.
63+
64+
Afterwards it launches the ARP poison, which basically tells the network that the attacker
65+
is the PLC, and it tells the PLC that the attacker is the router.
66+
67+
Finally, it launches the thread that will examine all captured packets.
68+
"""
69+
self.modify_ip_tables(True)
70+
71+
queue_number = 1
72+
nfqueue_path = Path(__file__).parent.absolute() / "unconstrained_blackbox_netfilter_queue.py"
73+
cmd = ["python3", str(nfqueue_path), str(self.intermediate_yaml_path), str(self.yaml_index), str(queue_number)]
74+
75+
self.sync = False
76+
self.nfqueue_process = subprocess.Popen(cmd, shell=False, stderr=sys.stderr, stdout=sys.stdout)
77+
78+
# Launch the ARP poison by sending the required ARP network packets
79+
launch_arp_poison(self.target_plc_ip, self.intermediate_attack['gateway_ip'])
80+
if self.intermediate_yaml['network_topology_type'] == "simple":
81+
for plc in self.intermediate_yaml['plcs']:
82+
if plc['name'] != self.intermediate_plc['name']:
83+
launch_arp_poison(self.target_plc_ip, plc['local_ip'])
84+
85+
self.logger.debug(f"Concealment MITM Attack ARP Poison between {self.target_plc_ip} and "
86+
f"{self.intermediate_attack['gateway_ip']}")
87+
88+
def interrupt(self):
89+
"""
90+
This function will be called when we want to stop the attacker. It calls the teardown
91+
function if the attacker is in state 1 (running)
92+
"""
93+
if self.state == 1:
94+
self.teardown()
95+
96+
def teardown(self):
97+
"""
98+
This function will undo the actions done by the setup function.
99+
100+
It first restores the arp poison, to point to the original router and PLC again. Afterwards
101+
it will delete the iptable rules and stop the thread.
102+
"""
103+
restore_arp(self.target_plc_ip, self.intermediate_attack['gateway_ip'])
104+
if self.intermediate_yaml['network_topology_type'] == "simple":
105+
for plc in self.intermediate_yaml['plcs']:
106+
if plc['name'] != self.intermediate_plc['name']:
107+
restore_arp(self.target_plc_ip, plc['local_ip'])
108+
109+
self.logger.debug(f"MITM Attack ARP Restore between {self.target_plc_ip} and "
110+
f"{self.intermediate_attack['gateway_ip']}")
111+
112+
self.modify_ip_tables(False)
113+
self.logger.debug(f"Restored ARP")
114+
115+
self.logger.debug("Stopping nfqueue subprocess...")
116+
self.nfqueue_process.send_signal(signal.SIGINT)
117+
118+
try:
119+
self.nfqueue_process.wait(self.NETFILTERQUEUE_SUBPROCESS_TIMEOUT)
120+
except subprocess.TimeoutExpired as e:
121+
self.logger.debug('TimeoutExpire: ' + str(e))
122+
if self.nfqueue_process.poll() is None:
123+
self.nfqueue_process.terminate()
124+
if self.nfqueue_process.poll() is None:
125+
self.nfqueue_process.kill()
126+
127+
self.logger.debug("Stopped nfqueue subprocess")
128+
129+
def attack_step(self):
130+
"""Polls the NetFilterQueue subprocess and sends a signal to stop it when teardown is called"""
131+
#todo: Here we would connect to the adversarial model?
132+
pass
133+
134+
135+
@staticmethod
136+
def modify_ip_tables(append=True):
137+
138+
if append:
139+
os.system(f'iptables -t mangle -A PREROUTING -p tcp -j NFQUEUE --queue-num 1')
140+
141+
os.system('iptables -A FORWARD -p icmp -j DROP')
142+
os.system('iptables -A INPUT -p icmp -j DROP')
143+
os.system('iptables -A OUTPUT -p icmp -j DROP')
144+
else:
145+
146+
os.system(f'iptables -t mangle -D INPUT -p tcp -j NFQUEUE --queue-num 1')
147+
os.system(f'iptables -t mangle -D FORWARD -p tcp -j NFQUEUE --queue-num 1')
148+
149+
os.system('iptables -D FORWARD -p icmp -j DROP')
150+
os.system('iptables -D INPUT -p icmp -j DROP')
151+
os.system('iptables -D OUTPUT -p icmp -j DROP')
152+
153+
def is_valid_file(parser_instance, arg):
154+
"""Verifies whether the intermediate yaml path is valid."""
155+
if not os.path.exists(arg):
156+
parser_instance.error(arg + " does not exist")
157+
else:
158+
return arg
159+
160+
161+
if __name__ == "__main__":
162+
parser = argparse.ArgumentParser(description='Start an unconstrained black box attack ')
163+
parser.add_argument(dest="intermediate_yaml",
164+
help="intermediate yaml file", metavar="FILE",
165+
type=lambda x: is_valid_file(parser, x))
166+
parser.add_argument(dest="index", help="Index of the network attack in intermediate yaml",
167+
type=int,
168+
metavar="N")
169+
170+
args = parser.parse_args()
171+
172+
attack = UnconstrainedBlackBox(
173+
intermediate_yaml_path=Path(args.intermediate_yaml),
174+
yaml_index=args.index)
175+
176+
attack.main_loop(attack.persistent)

0 commit comments

Comments
 (0)